31 Commits

Author SHA1 Message Date
l-jonas f9b91f6ef8 Update whatsnew 2018-12-13 10:34:10 +01:00
l-jonas 01fb92e2c9 Release 0.3.9 2018-12-13 10:33:21 +01:00
l-jonas 4b519e84e3 Update Releasing.md 2018-12-13 10:32:42 +01:00
l-jonas f3d51f0cb9 Add temp repository encryption (#131) 2018-12-13 10:27:07 +01:00
Jonas L fa30beb9d5 Update translations 2018-12-13 10:15:21 +01:00
l-jonas 919fdc31bd Fix crash when accessing closed connections (#129)
* Fix crash when accessing closed connections
2018-12-12 17:43:42 +01:00
l-jonas b3f2af0ee7 Add option to convert file extension to lower case for mime type (#127)
* Add option to convert file extension to lower case for mime type

* Always convert file extension to lower case
2018-12-12 17:20:55 +01:00
l-jonas f33364939b Ignore index updates without matching index info (#125) 2018-12-09 15:39:41 +01:00
l-jonas 1a773daf24 Clear cache handling (#124)
* Remove folder stats from cache when the data is deleted

* Move database deletion off the UI thread

* Remove index info from cache when data is deleted

* Allow observing configuration for changes

* Update folder list on config changes
2018-12-09 15:32:40 +01:00
l-jonas b115a99907 Path validation (#122)
* Remove isTrimmed path validation rule

* Include exact reason in path validation exceptions
2018-12-09 12:00:40 +01:00
l-jonas 5d09d011b1 Update whatsnew 2018-12-07 10:28:46 +01:00
l-jonas 71a433edf6 Release 0.3.8 2018-12-07 10:28:30 +01:00
l-jonas 974817b7a3 Update Releasing.md
Add link to build.gradle which specified the version
2018-12-07 10:27:59 +01:00
Jonas L f00760bddd Update translations 2018-12-07 10:22:32 +01:00
l-jonas 5f539c4149 Fix release builds (#120)
* Fix creating release builds
2018-12-07 10:07:59 +01:00
l-jonas 1869a49c2c Partial crashes (#119)
* Add partial exceptions

* Save last (caught) exception

* Add link to GitHub issues from the settings screen

* Ignore cancellation exceptions

* Show exception message toast longer

* Add more details to exceptions in LocalDiscoveryUtil

* Ensure that the error toast is always shown
2018-12-07 09:57:57 +01:00
l-jonas 91289b05ce Settings locking (#117)
* Add lock for configuration object changes

* Add lock for saving config file
2018-12-07 07:47:59 +01:00
l-jonas 98bc67939f Fix crash in intro activity after screen rotation (#118) 2018-12-07 07:45:07 +01:00
l-jonas fcb31ae9fa Remove connection change callback (#115) 2018-12-06 17:16:52 +01:00
l-jonas 147ad6abcc Fix crash if mime type can not be determined (#114) 2018-12-06 16:56:55 +01:00
l-jonas 4c13af3662 Remove things from the Roadmap which are done 2018-12-06 16:49:09 +01:00
l-jonas 17f9ad336c Selective folder sharing (#113)
* Remove obsolete isConnected from DeviceInfo

* Implement selective folder sharing in the backend

* Fix building

* Add ignored devices to folder info

* Add UI to ask for sync for new devices which share a folder

* Add adding new devices which share a folder to the blacklist

* Add saving the config when the user chose to (not) share a folder

* Fix sync question text + reconnect after decision

* Add UI to configure folder sharing

* Fix applying share config change
2018-12-06 16:46:02 +01:00
l-jonas 852fc0d230 Show status in ui (#110)
* Add showing connection status in the device list

* Show if trying to find device at the last step of the setup wizard
2018-12-04 14:44:03 +01:00
l-jonas 0032726e3e Update dependencies (#109)
* Update build tools and support library

* Remove specifying build tools version

* Remove obsolete comment
2018-12-04 12:52:31 +01:00
l-jonas c70211bc24 Update whatsnew 2018-12-03 17:21:45 +01:00
l-jonas a7f80fa45c Release 0.3.7 2018-12-03 17:20:39 +01:00
l-jonas 461d64950b Fix detecting the main dispatcher
This closes https://github.com/syncthing/syncthing-lite/issues/106
2018-12-03 17:19:51 +01:00
l-jonas c37832d084 Create Roadmap.md
This closes https://github.com/syncthing/syncthing-lite/issues/85
2018-12-03 09:39:27 +01:00
l-jonas f336a2932f Update whatsnew 2018-12-02 10:49:49 +01:00
l-jonas 0b3e2bf914 Release 0.3.6 2018-12-02 10:49:08 +01:00
l-jonas 6d9009daff Refactor index handler (#104)
* Add base for new database API

* Implement new database API for the android db implementation

* Implement new API for the default database implementation

* Fix compilation errors

* Move IndexHandler to own package

* Move some code out of the IndexHandler

* Fix compilation errors

* Make IndexInfo a data class

* Make FolderStats a data class

* Make FolderInfo a data class

* Move code out of IndexHandler

* Use one transaction per index message

* Start replacing callbacks by BroadcastChannels

* Fix compilation errors

* Replace callbacks by BroadcastChannels

* Remove IndexHandler.folderInfoByFolder

* Move code out of IndexHandler

* Use index update events to notify IndexBrowsers

* Remove preloading from the IndexBrowser

* Use channels to handle index update messages

* Remove the old ExecutorUtils

* Remove functions from the IndexHandler

* Refactor FolderBrowser

* Remove writeAccessLock

* Remove the indexWaitLock

* Send index change events after the transaction

* Remove markActive from the IndexHandler

* Refactor the IndexBrowser

* Fix showing folder content

* Fix document provider integration

* Fix index sequence handling

* Use a LinearLayout as base for folder entries

* Add theoretically showing of the missing index updates

* Move folder stats update events out of the database

* Send index update events when receiving a cluster config

* Fix counting missing index updates

* Send events after the db transaction at handle cluster config

* Handle index updates in batches

* Add logging of time for index processing

* Deduplicate index updates

* Read old records in bulk

* Update folder stats in bulk

* Fix typo

* Modularize IndexElementProcessor

* Prepare bulk FileInfo updates

* Update FileInfo in bulk

* Make logger private

* Use IO dispatcher

* Reconnect better

* Fix detecting new folders

* Dispatch crashes from background threads to the main thread

* Fix random crash on library shutdown

* Add option for more detailed crash reports

* Fix transaction running check and remove old code at DB abstraction

* Sort directory listings
2018-12-02 10:47:09 +01:00
109 changed files with 3974 additions and 2464 deletions
+4 -3
View File
@@ -1,8 +1,9 @@
# Releasing
- do tests
- update translations using ``tx pull -a -af`` (as extra merge request or branch for the case it does not build correctly)
- update the version name and version code of the app
- update translations using ``tx pull -af`` (as extra merge request or branch for the case it does not build correctly)
- update the version name and version code of the app [here](https://github.com/syncthing/syncthing-lite/blob/master/app/build.gradle)
- update the changelog at [app/src/main/play/en-GB/whatsnew](https://github.com/syncthing/syncthing-lite/blob/master/app/src/main/play/en-GB/whatsnew)
- create a tag/ release in GitHub with an changelog; The tag name should be the version number
- F-Droid picks up the release by the tag; additonally, the tag triggers a CI build which uploads the generated APK to Google Play
- trigger a release at <https://build.syncthing.net/> to publish the release to google play
- F-Droid picks up the release by the tag
+20
View File
@@ -0,0 +1,20 @@
# Roadmap
## What should happen
- fixing bugs and crashs
- just create a issue WITH a detailed crash report (not: it does not work)
- search if there is an other issue for it before creating a new one
- add option to manually select the IP address of an device (<https://github.com/syncthing/syncthing-lite/issues/25>)
- allow custom discovery servers or disabling device discovery (<https://github.com/syncthing/syncthing-lite/issues/105>)
- downloading all files of an folder (<https://github.com/syncthing/syncthing-lite/issues/34>)
- better server offline handling (<https://github.com/syncthing/syncthing-lite/issues/63>)
- file uploading support (it currently does not work) <https://github.com/syncthing/syncthing-lite/issues/70>
## What could happen
- thumbnails (<https://github.com/syncthing/syncthing-lite/issues/37>)
## What will not happen
- additional encryption within the App (see <https://github.com/syncthing/syncthing-lite/issues/85>)
+3 -13
View File
@@ -6,7 +6,6 @@ apply plugin: 'com.github.triplet.play'
android {
compileSdkVersion 27
buildToolsVersion "28.0.2"
dataBinding.enabled = true
playAccountConfigs {
@@ -19,8 +18,8 @@ android {
applicationId "net.syncthing.lite"
minSdkVersion 21
targetSdkVersion 26
versionCode 15
versionName "0.3.5"
versionCode 19
versionName "0.3.9"
multiDexEnabled true
playAccountConfig = playAccountConfigs.defaultAccountConfig
}
@@ -77,16 +76,6 @@ dependencies {
implementation "com.android.support:support-v4:$support_version"
implementation 'android.arch.lifecycle:extensions:1.1.1'
/**
* syncthing-java depends on the Apache HTTP Client
* https://github.com/syncthing/syncthing-java/blob/dd020737ba5fc6a7c681a1d258025b8ddb2e8f67/core/build.gradle#L9
*
* Android itself contains an older version of this HTTP Client. Due to that, there is an
* extra version of it which does not cause conflicts with the builtin client of Android.
*
* This extra implementation is included below. As this other version is used,
* it's ignored as dependency of syncthing-java.
*/
implementation(project(':syncthing-client')) {
exclude group: 'commons-logging', module: 'commons-logging'
exclude group: 'org.slf4j'
@@ -99,4 +88,5 @@ dependencies {
implementation 'com.github.apl-devs:appintro:v4.2.3'
implementation project(':syncthing-repository-android')
implementation project(':syncthing-temp-repository-encryption')
}
+3
View File
@@ -28,6 +28,9 @@
volatile <fields>;
}
# fix detecting the main dispatcher
-keepnames class kotlinx.coroutines.internal.MainDispatcherFactory {}
# disable warnings
-dontwarn com.google.protobuf.UnsafeUtil
-dontwarn com.google.protobuf.UnsafeUtil$1
@@ -4,48 +4,61 @@ import android.app.Activity
import android.content.Intent
import android.databinding.DataBindingUtil
import android.os.Bundle
import android.util.Log
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import net.syncthing.java.bep.IndexBrowser
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.ConflatedBroadcastChannel
import kotlinx.coroutines.channels.consumeEach
import net.syncthing.java.bep.index.browser.DirectoryContentListing
import net.syncthing.java.bep.index.browser.DirectoryListing
import net.syncthing.java.bep.index.browser.DirectoryNotFoundListing
import net.syncthing.java.bep.index.browser.IndexBrowser
import net.syncthing.java.core.beans.FileInfo
import net.syncthing.java.core.beans.FolderInfo
import net.syncthing.java.core.utils.PathUtils
import net.syncthing.lite.BuildConfig
import net.syncthing.lite.R
import net.syncthing.lite.adapters.FolderContentsAdapter
import net.syncthing.lite.adapters.FolderContentsListener
import net.syncthing.lite.databinding.ActivityFolderBrowserBinding
import net.syncthing.lite.dialogs.EnableFolderSyncForNewDeviceDialog
import net.syncthing.lite.dialogs.FileMenuDialogFragment
import net.syncthing.lite.dialogs.FileUploadDialog
import net.syncthing.lite.dialogs.ReconnectIssueDialogFragment
import net.syncthing.lite.dialogs.downloadfile.DownloadFileDialogFragment
import org.jetbrains.anko.doAsync
class FolderBrowserActivity : SyncthingActivity() {
companion object {
private const val TAG = "FolderBrowserActivity"
private const val REQUEST_SELECT_UPLOAD_FILE = 171
private const val STATUS_PATH = "path"
const val EXTRA_FOLDER_NAME = "folder_name"
}
private lateinit var binding: ActivityFolderBrowserBinding
private lateinit var indexBrowser: IndexBrowser
private val adapter = FolderContentsAdapter()
private lateinit var folder: String
private val path = ConflatedBroadcastChannel<String>()
private val listing = ConflatedBroadcastChannel<DirectoryListing?>(null)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.activity_folder_browser)
binding.mainListViewUploadHereButton.setOnClickListener { showUploadHereDialog() }
val binding: ActivityFolderBrowserBinding = DataBindingUtil.setContentView(this, R.layout.activity_folder_browser)
val adapter = FolderContentsAdapter()
binding.listView.adapter = adapter
binding.mainListViewUploadHereButton.setOnClickListener {
startActivityForResult(
Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = "*/*"
},
REQUEST_SELECT_UPLOAD_FILE
)
}
adapter.listener = object: FolderContentsListener {
override fun onItemClicked(fileInfo: FileInfo) {
navigateToFolder(fileInfo)
if (fileInfo.isDirectory()) {
path.offer(fileInfo.path)
} else {
DownloadFileDialogFragment.newInstance(fileInfo).show(supportFragmentManager)
}
}
override fun onItemLongClicked(fileInfo: FileInfo): Boolean {
@@ -58,26 +71,98 @@ class FolderBrowserActivity : SyncthingActivity() {
}
}
}
val folder = intent.getStringExtra(EXTRA_FOLDER_NAME)
libraryHandler.syncthingClient {
indexBrowser = it.indexHandler.newIndexBrowser(folder, true, true)
indexBrowser.setOnFolderChangedListener(this::onFolderChanged)
}
ReconnectIssueDialogFragment.showIfNeeded(this)
folder = intent.getStringExtra(EXTRA_FOLDER_NAME)
path.offer(if (savedInstanceState == null) IndexBrowser.ROOT_PATH else savedInstanceState.getString(STATUS_PATH))
launch {
var job = Job()
path.consumeEach { path ->
job.cancel()
job = Job()
binding.listView.scrollToPosition(0)
listing.send(null)
async(job) {
libraryHandler.libraryManager.streamDirectoryListing(folder, path).consumeEach {
listing.send(it)
}
}
}
}
launch {
listing.openSubscription().consumeEach { listing ->
if (listing == null) {
binding.isLoading = true
} else {
supportActionBar?.title = if (PathUtils.isRoot(listing.path)) folder else PathUtils.getFileName(listing.path)
binding.isLoading = false
adapter.data = if (listing is DirectoryContentListing)
listing.entries.sortedWith(IndexBrowser.sortAlphabeticallyDirectoriesFirst)
else
emptyList()
}
}
}
if (savedInstanceState == null) {
launch {
val devicesToAskFor = libraryHandler.libraryManager.withLibrary {
val folderInfo = it.configuration.folders.find { it.folderId == folder }
val notIgnoredBlacklistEntries = folderInfo?.notIgnoredBlacklistEntries ?: emptySet()
notIgnoredBlacklistEntries.mapNotNull { deviceId ->
it.configuration.peers.find { peer -> peer.deviceId == deviceId }
}
}
if (devicesToAskFor.isNotEmpty()) {
EnableFolderSyncForNewDeviceDialog.newInstance(
folderId = folder,
devices = devicesToAskFor,
folderName = libraryHandler.libraryManager.withLibrary {
it.configuration.folders.find { it.folderId == folder }?.label ?: folder
}
).show(supportFragmentManager)
}
}
}
}
override fun onDestroy() {
super.onDestroy()
Thread {
indexBrowser.setOnFolderChangedListener(null)
indexBrowser.close()
}.start()
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putString(STATUS_PATH, path.value)
}
private fun goUp(): Boolean {
val currentListing = listing.value
val parentPath = when (currentListing) {
is DirectoryContentListing -> currentListing.parentEntry?.path
is DirectoryNotFoundListing -> currentListing.theoreticalParentPath
else -> null
}
return if (parentPath == null) {
false
} else {
path.offer(parentPath)
true
}
}
override fun onBackPressed() {
//click item '0', ie '..' (go to parent)
navigateToFolder(adapter.data[0])
if (!goUp()) {
super.onBackPressed()
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, intent: Intent?) {
@@ -85,85 +170,18 @@ class FolderBrowserActivity : SyncthingActivity() {
libraryHandler.syncthingClient { syncthingClient ->
GlobalScope.launch (Dispatchers.Main) {
// FIXME: it would be better if the dialog would use the library handler
FileUploadDialog(this@FolderBrowserActivity, syncthingClient, intent!!.data,
indexBrowser.folder, indexBrowser.currentPath,
{ showFolderListView(indexBrowser.currentPath) }).show()
FileUploadDialog(
this@FolderBrowserActivity,
syncthingClient,
intent!!.data,
folder,
path.value,
{ /* nothing to do on success */ }
).show()
}
}
} else {
super.onActivityResult(requestCode, resultCode, intent)
}
}
private fun showFolderListView(path: String) {
indexBrowser.navigateToNearestPath(path)
navigateToFolder(indexBrowser.currentPathInfo())
}
private fun navigateToFolder(fileInfo: FileInfo) {
Log.d(TAG, "navigate to path = '" + fileInfo.path + "' from path = '" + indexBrowser.currentPath + "'")
if (indexBrowser.isRoot() && PathUtils.isParent(fileInfo.path)) {
finish()
} else {
if (fileInfo.isDirectory()) {
doAsync {
indexBrowser.navigateTo(fileInfo)
}
Log.d(TAG, "load folder cache bg")
binding.isLoading = true
} else {
if (BuildConfig.DEBUG) {
Log.i(TAG, "pulling file = " + fileInfo)
}
DownloadFileDialogFragment.newInstance(fileInfo).show(supportFragmentManager)
}
}
}
private fun onFolderChanged() {
GlobalScope.launch {
val list = indexBrowser.listFiles()
Log.i("navigateToFolder", "list for path = '" + indexBrowser.currentPath + "' list = " + list.size + " records")
Log.d("navigateToFolder", "list for path = '" + indexBrowser.currentPath + "' list = " + list)
assert(!list.isEmpty())//list must contain at least the 'parent' path
val title = if (indexBrowser.isRoot()) {
val result = CompletableDeferred<String?>()
libraryHandler.folderBrowser {
result.complete(it.getFolderInfo(indexBrowser.folder)?.label)
}
result.await()
} else {
indexBrowser.currentPathInfo().fileName
}
runOnUiThread {
binding.isLoading = false
adapter.data = list
binding.listView.scrollToPosition(0)
supportActionBar?.title = title
}
}
}
private fun updateFolderListView() {
showFolderListView(indexBrowser.currentPath)
}
private fun showUploadHereDialog() {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT)
intent.addCategory(Intent.CATEGORY_OPENABLE)
intent.type = "*/*"
startActivityForResult(intent, REQUEST_SELECT_UPLOAD_FILE)
}
override fun onIndexUpdateComplete(folderInfo: FolderInfo) {
super.onIndexUpdateComplete(folderInfo)
updateFolderListView()
}
}
@@ -12,9 +12,9 @@ import android.view.View
import android.view.ViewGroup
import android.widget.Button
import com.github.paolorotolo.appintro.AppIntro
import com.github.paolorotolo.appintro.ISlidePolicy
import com.google.zxing.integration.android.IntentIntegrator
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.channels.consumeEach
import kotlinx.coroutines.launch
import net.syncthing.java.core.beans.DeviceId
import net.syncthing.lite.R
@@ -40,14 +40,6 @@ class IntroActivity : AppIntro() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Disable continue button on second slide until a valid device ID is entered.
nextButton.setOnClickListener {
val fragment = fragments[pager.currentItem]
if (fragment !is IntroFragmentTwo || fragment.isDeviceIdValid()) {
pager.goToNextSlide()
}
}
addSlide(IntroFragmentOne())
addSlide(IntroFragmentTwo())
addSlide(IntroFragmentThree())
@@ -72,6 +64,19 @@ class IntroActivity : AppIntro() {
* Display some simple welcome text.
*/
class IntroFragmentOne : SyncthingFragment() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
launch {
libraryHandler.libraryManager.withLibrary { library ->
library.configuration.update { oldConfig ->
oldConfig.copy(localDeviceName = Util.getDeviceName())
}
library.configuration.persistLater()
}
}
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
val binding = FragmentIntroOneBinding.inflate(inflater, container, false)
@@ -80,21 +85,12 @@ class IntroActivity : AppIntro() {
return binding.root
}
override fun onLibraryLoaded() {
super.onLibraryLoaded()
libraryHandler.configuration { config ->
config.localDeviceName = Util.getDeviceName()
config.persistLater()
}
}
}
/**
* Display device ID entry field and QR scanner option.
*/
class IntroFragmentTwo : SyncthingFragment() {
class IntroFragmentTwo : SyncthingFragment(), ISlidePolicy {
private lateinit var binding: FragmentIntroTwoBinding
@@ -122,7 +118,7 @@ class IntroActivity : AppIntro() {
fun isDeviceIdValid(): Boolean {
return try {
val deviceId = binding.enterDeviceId.deviceId.text.toString()
Util.importDeviceId(libraryHandler, context, deviceId, { })
Util.importDeviceId(libraryHandler.libraryManager, context!!, deviceId, { })
true
} catch (e: IOException) {
binding.enterDeviceId.deviceId.error = getString(R.string.invalid_device_id)
@@ -130,6 +126,12 @@ class IntroActivity : AppIntro() {
}
}
override fun isPolicyRespected() = isDeviceIdValid()
override fun onUserIllegallyRequestedNextPage() {
// nothing to do, but some user feedback would be nice
}
private val addedDeviceIds = HashSet<DeviceId>()
override fun onResume() {
@@ -176,32 +178,31 @@ class IntroActivity : AppIntro() {
*/
class IntroFragmentThree : SyncthingFragment() {
private lateinit var binding: FragmentIntroThreeBinding
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
binding = DataBindingUtil.inflate(inflater, R.layout.fragment_intro_three, container, false)
val binding = FragmentIntroThreeBinding.inflate(inflater, container, false)
libraryHandler.library { config, client, _ ->
GlobalScope.launch (Dispatchers.Main) {
client.addOnConnectionChangedListener(this@IntroFragmentThree::onConnectionChanged)
val deviceId = config.localDeviceId.deviceId
val desc = activity?.getString(R.string.intro_page_three_description, "<b>$deviceId</b>")
binding.description.text = Html.fromHtml(desc)
launch {
val ownDeviceId = libraryHandler.libraryManager.withLibrary { it.configuration.localDeviceId }
libraryHandler.subscribeToConnectionStatus().consumeEach {
if (it.values.find { it.addresses.isNotEmpty() } != null) {
val desc = activity?.getString(R.string.intro_page_three_description, "<b>$ownDeviceId</b>")
binding.description.text = Html.fromHtml(desc)
} else {
binding.description.text = getString(R.string.intro_page_three_searching_device)
}
}
}
launch {
libraryHandler.subscribeToFolderStatusList().consumeEach {
if (it.isNotEmpty()) {
(activity as IntroActivity?)?.onDonePressed(this@IntroFragmentThree)
}
}
}
return binding.root
}
private fun onConnectionChanged(deviceId: DeviceId) {
libraryHandler.library { config, client, _ ->
GlobalScope.launch (Dispatchers.Main) {
if (config.folders.isNotEmpty()) {
client.removeOnConnectionChangedListener(this@IntroFragmentThree::onConnectionChanged)
(activity as IntroActivity?)?.onDonePressed(this@IntroFragmentThree)
}
}
}
}
}
}
@@ -102,9 +102,10 @@ class MainActivity : SyncthingActivity() {
}
private fun cleanCacheAndIndex() {
GlobalScope.launch (Dispatchers.Main) {
libraryHandler.syncthingClient { it.clearCacheAndIndex() }
recreate()
launch {
libraryHandler.libraryManager.withLibrary {
it.syncthingClient.clearCacheAndIndex()
}
}
}
}
@@ -4,22 +4,18 @@ import android.app.AlertDialog
import android.databinding.DataBindingUtil
import android.os.Bundle
import android.support.design.widget.Snackbar
import android.support.v7.app.AppCompatActivity
import android.view.LayoutInflater
import net.syncthing.java.core.beans.FolderInfo
import net.syncthing.lite.BuildConfig
import net.syncthing.lite.R
import net.syncthing.lite.async.CoroutineActivity
import net.syncthing.lite.databinding.DialogLoadingBinding
import net.syncthing.lite.library.LibraryHandler
import org.jetbrains.anko.contentView
import org.slf4j.impl.HandroidLoggerAdapter
abstract class SyncthingActivity : AppCompatActivity() {
abstract class SyncthingActivity : CoroutineActivity() {
val libraryHandler: LibraryHandler by lazy {
LibraryHandler(
context = this@SyncthingActivity,
onIndexUpdateProgressListener = this::onIndexUpdateProgress,
onIndexUpdateCompleteListener = this::onIndexUpdateComplete
context = this@SyncthingActivity
)
}
private var loadingDialog: AlertDialog? = null
@@ -58,19 +54,6 @@ abstract class SyncthingActivity : AppCompatActivity() {
loadingDialog?.dismiss()
}
open fun onIndexUpdateProgress(folderInfo: FolderInfo, percentage: Int) {
val message = getString(R.string.index_update_progress_label, folderInfo.label, percentage)
snackBar?.setText(message) ?: run {
snackBar = Snackbar.make(contentView!!, message, Snackbar.LENGTH_INDEFINITE)
snackBar?.show()
}
}
open fun onIndexUpdateComplete(folderInfo: FolderInfo) {
snackBar?.dismiss()
snackBar = null
}
open fun onLibraryLoaded() {
// nothing to do
}
@@ -3,12 +3,15 @@ package net.syncthing.lite.adapters
import android.support.v7.widget.RecyclerView
import android.view.LayoutInflater
import android.view.ViewGroup
import net.syncthing.java.bep.connectionactor.ConnectionInfo
import net.syncthing.java.bep.connectionactor.ConnectionStatus
import net.syncthing.java.core.beans.DeviceInfo
import net.syncthing.lite.R
import net.syncthing.lite.databinding.ListviewDeviceBinding
import kotlin.properties.Delegates
class DevicesAdapter: RecyclerView.Adapter<DeviceViewHolder>() {
var data: List<DeviceInfo> by Delegates.observable(listOf()) {
var data: List<Pair<DeviceInfo, ConnectionInfo>> by Delegates.observable(listOf()) {
_, _, _ -> notifyDataSetChanged()
}
@@ -19,7 +22,7 @@ class DevicesAdapter: RecyclerView.Adapter<DeviceViewHolder>() {
}
override fun getItemCount() = data.size
override fun getItemId(position: Int) = data[position].deviceId.deviceId.hashCode().toLong()
override fun getItemId(position: Int) = data[position].first.deviceId.deviceId.hashCode().toLong()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = DeviceViewHolder(
ListviewDeviceBinding.inflate(
@@ -28,13 +31,23 @@ class DevicesAdapter: RecyclerView.Adapter<DeviceViewHolder>() {
)
override fun onBindViewHolder(holder: DeviceViewHolder, position: Int) {
val deviceStats = data[position]
val binding = holder.binding
val context = binding.root.context
val (deviceInfo, connectionInfo) = data[position]
binding.name = deviceStats.name
binding.isConnected = deviceStats.isConnected
binding.name = deviceInfo.name
binding.isConnected = connectionInfo.status == ConnectionStatus.Connected
binding.root.setOnLongClickListener { listener?.onDeviceLongClicked(deviceStats) ?: false }
binding.status = when (connectionInfo.status) {
ConnectionStatus.Connected -> context.getString(R.string.device_status_connected, connectionInfo.currentAddress?.address)
ConnectionStatus.Connecting -> context.getString(R.string.device_status_connecting, connectionInfo.currentAddress?.address)
ConnectionStatus.Disconnected -> if (connectionInfo.addresses.isEmpty())
context.getString(R.string.device_status_no_address)
else
context.getString(R.string.device_status_disconnected, connectionInfo.addresses.size)
}
binding.root.setOnLongClickListener { listener?.onDeviceLongClicked(deviceInfo) ?: false }
binding.executePendingBindings()
}
@@ -44,4 +57,4 @@ interface DeviceAdapterListener {
fun onDeviceLongClicked(deviceInfo: DeviceInfo): Boolean
}
class DeviceViewHolder(val binding: ListviewDeviceBinding): RecyclerView.ViewHolder(binding.root)
class DeviceViewHolder(val binding: ListviewDeviceBinding): RecyclerView.ViewHolder(binding.root)
@@ -2,8 +2,10 @@ package net.syncthing.lite.adapters
import android.support.v7.widget.RecyclerView
import android.text.format.DateUtils
import android.util.Log
import android.view.LayoutInflater
import android.view.ViewGroup
import net.syncthing.java.bep.folder.FolderStatus
import net.syncthing.java.core.beans.FolderInfo
import net.syncthing.java.core.beans.FolderStats
import net.syncthing.lite.R
@@ -11,7 +13,7 @@ import net.syncthing.lite.databinding.ListviewFolderBinding
import kotlin.properties.Delegates
class FoldersListAdapter: RecyclerView.Adapter<FolderListViewHolder>() {
var data: List<Pair<FolderInfo, FolderStats>> by Delegates.observable(listOf()) {
var data: List<FolderStatus> by Delegates.observable(listOf()) {
_, _, _ -> notifyDataSetChanged()
}
@@ -22,7 +24,7 @@ class FoldersListAdapter: RecyclerView.Adapter<FolderListViewHolder>() {
}
override fun getItemCount() = data.size
override fun getItemId(position: Int) = data[position].first.folderId.hashCode().toLong()
override fun getItemId(position: Int) = data[position].info.folderId.hashCode().toLong()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = FolderListViewHolder (
ListviewFolderBinding.inflate(
@@ -32,19 +34,31 @@ class FoldersListAdapter: RecyclerView.Adapter<FolderListViewHolder>() {
override fun onBindViewHolder(holder: FolderListViewHolder, position: Int) {
val binding = holder.binding
val (folderInfo, folderStats) = data[position]
val item = data[position]
val (folderInfo, folderStats) = item
val context = holder.itemView.context
Log.d("FolderListAdapter", "$item")
binding.folderName = context.getString(R.string.folder_label_format, folderInfo.label, folderInfo.folderId)
binding.lastModification = context.getString(R.string.last_modified_time,
DateUtils.getRelativeDateTimeString(context, folderStats.lastUpdate.time, DateUtils.MINUTE_IN_MILLIS, DateUtils.WEEK_IN_MILLIS, 0))
binding.info = context.getString(R.string.folder_content_info, folderStats.describeSize(), folderStats.fileCount, folderStats.dirCount)
binding.info = context.getString(R.string.folder_content_info, folderStats.sizeDescription, folderStats.fileCount, folderStats.dirCount)
binding.info2 = if (item.missingIndexUpdates == 0L)
null
else
context.getString(R.string.pending_index_updates, item.missingIndexUpdates)
binding.root.setOnClickListener {
listener?.onFolderClicked(folderInfo, folderStats)
}
binding.root.setOnLongClickListener {
listener?.onFolderLongClicked(folderInfo) ?: false
}
}
}
@@ -52,4 +66,5 @@ class FolderListViewHolder(val binding: ListviewFolderBinding): RecyclerView.Vie
interface FolderListAdapterListener {
fun onFolderClicked(folderInfo: FolderInfo, folderStats: FolderStats)
}
fun onFolderLongClicked(folderInfo: FolderInfo): Boolean
}
@@ -1,52 +1,48 @@
package net.syncthing.lite.android
import android.app.Application
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.os.Handler
import android.os.Looper
import android.util.Log
import net.syncthing.lite.BuildConfig
import org.jetbrains.anko.defaultSharedPreferences
import java.io.PrintWriter
import java.io.StringWriter
import net.syncthing.lite.error.ErrorStorage
class Application: Application() {
companion object {
private const val LOG_TAG = "Application"
private const val PREF_ENABLE_CRASH_HANDLER = "crash_handler"
private val handler = Handler(Looper.getMainLooper())
}
override fun onCreate() {
super.onCreate()
val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
val defaultHandler = Thread.getDefaultUncaughtExceptionHandler()
val mainThread = Thread.currentThread()
if (defaultHandler == null) {
Log.w(LOG_TAG, "could not get default crash handler")
}
Thread.setDefaultUncaughtExceptionHandler { thread, ex ->
fun handleCrash(ex: Throwable) {
Log.w(LOG_TAG, "app crashed", ex)
val enableCustomCrashHandling = defaultSharedPreferences.getBoolean(PREF_ENABLE_CRASH_HANDLER, false)
if (enableCustomCrashHandling) {
clipboard.primaryClip = ClipData.newPlainText(
"stacktrace",
StringWriter().apply {
append("Version: ").append(BuildConfig.VERSION_NAME).append('\n')
append(Log.getStackTraceString(ex)).append('\n')
ex.printStackTrace(PrintWriter(this))
}.buffer.toString()
)
}
ErrorStorage.reportError(
this,
Log.getStackTraceString(ex)
)
if (defaultHandler != null) {
defaultHandler.uncaughtException(thread, ex)
defaultHandler.uncaughtException(mainThread, ex)
} else {
System.exit(1)
}
}
Thread.setDefaultUncaughtExceptionHandler { thread, ex ->
if (Looper.getMainLooper() === Looper.myLooper()) {
handleCrash(ex)
} else {
handler.post { handleCrash(ex) }
}
}
}
}
@@ -0,0 +1,19 @@
package net.syncthing.lite.async
import android.support.v7.app.AppCompatActivity
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlin.coroutines.CoroutineContext
abstract class CoroutineActivity: AppCompatActivity(), CoroutineScope {
val job = Job()
override val coroutineContext: CoroutineContext
get() = job + Dispatchers.Main
override fun onDestroy() {
super.onDestroy()
job.cancel()
}
}
@@ -0,0 +1,19 @@
package net.syncthing.lite.async
import android.support.v4.app.DialogFragment
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlin.coroutines.CoroutineContext
abstract class CoroutineDialogFragment: DialogFragment(), CoroutineScope {
val job = Job()
override val coroutineContext: CoroutineContext
get() = job + Dispatchers.Main
override fun onDestroy() {
super.onDestroy()
job.cancel()
}
}
@@ -0,0 +1,19 @@
package net.syncthing.lite.async
import android.support.v4.app.Fragment
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlin.coroutines.CoroutineContext
abstract class CoroutineFragment: Fragment(), CoroutineScope {
val job = Job()
override val coroutineContext: CoroutineContext
get() = job + Dispatchers.Main
override fun onDestroy() {
super.onDestroy()
job.cancel()
}
}
@@ -0,0 +1,125 @@
package net.syncthing.lite.dialogs
import android.app.AlertDialog
import android.app.Dialog
import android.os.Bundle
import android.support.v4.app.FragmentManager
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import net.syncthing.java.core.beans.DeviceInfo
import net.syncthing.lite.R
import net.syncthing.lite.fragments.SyncthingDialogFragment
class EnableFolderSyncForNewDeviceDialog: SyncthingDialogFragment() {
companion object {
private const val FOLDER_ID = "folderId"
private const val FOLDER_NAME = "folderName"
private const val DEVICES = "devices"
private const val STATUS_CURRENT_DEVICE_ID = "currentDeviceId"
private const val TAG = "EnableFolderSyncForNewDeviceDialog"
fun newInstance(folderId: String, folderName: String, devices: List<DeviceInfo>) = EnableFolderSyncForNewDeviceDialog().apply {
arguments = Bundle().apply {
putString(FOLDER_ID, folderId)
putString(FOLDER_NAME, folderName)
putSerializable(DEVICES, ArrayList<DeviceInfo>(devices))
}
}
}
private var currentDeviceId = 0
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val folderId = arguments!!.getString(FOLDER_ID)
val folderName = arguments!!.getString(FOLDER_NAME)
val devices = arguments!!.getSerializable(DEVICES) as ArrayList<DeviceInfo>
if (savedInstanceState != null) {
currentDeviceId = savedInstanceState.getInt(STATUS_CURRENT_DEVICE_ID)
}
val dialog = AlertDialog.Builder(context!!)
.setTitle(R.string.dialog_enable_folder_sync_for_new_device_title)
.setMessage(R.string.dialog_enable_folder_sync_for_new_device_text)
.setPositiveButton(R.string.dialog_enable_folder_sync_for_new_device_positive, null)
.setNegativeButton(R.string.dialog_enable_folder_sync_for_new_device_negative, null)
.create()
fun bindDeviceId() {
if (currentDeviceId >= devices.size) {
dismissAllowingStateLoss()
} else {
val device = devices[currentDeviceId]
dialog.setMessage(getString(
R.string.dialog_enable_folder_sync_for_new_device_text,
folderName,
device.name,
device.deviceId.deviceId
))
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener {
GlobalScope.launch {
libraryHandler.libraryManager.withLibrary {
val oldFolderEntry = it.configuration.folders.find { it.folderId == folderId }!!
it.configuration.update { oldConfig ->
oldConfig.copy(
folders = oldConfig.folders.filter { it != oldFolderEntry }.toSet() + setOf(
oldFolderEntry.copy(
deviceIdWhitelist = oldFolderEntry.deviceIdWhitelist + setOf(device.deviceId),
deviceIdBlacklist = oldFolderEntry.deviceIdBlacklist - setOf(device.deviceId)
)
)
)
}
it.syncthingClient.reconnect(device.deviceId)
it.configuration.persistLater()
}
}
currentDeviceId++
bindDeviceId()
}
dialog.getButton(AlertDialog.BUTTON_NEGATIVE).setOnClickListener {
GlobalScope.launch {
libraryHandler.libraryManager.withLibrary {
val oldFolderEntry = it.configuration.folders.find { it.folderId == folderId }!!
it.configuration.update { oldConfig ->
oldConfig.copy(
folders = oldConfig.folders.filter { it != oldFolderEntry }.toSet() + setOf(
oldFolderEntry.copy(
ignoredDeviceIdList = oldFolderEntry.deviceIdWhitelist + setOf(device.deviceId)
)
)
)
}
it.syncthingClient.reconnect(device.deviceId)
it.configuration.persistLater()
}
}
currentDeviceId++
bindDeviceId()
}
}
}
dialog.setOnShowListener { bindDeviceId() }
return dialog
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putInt(STATUS_CURRENT_DEVICE_ID, currentDeviceId)
}
fun show(fragmentManager: FragmentManager) = show(fragmentManager, TAG)
}
@@ -0,0 +1,52 @@
package net.syncthing.lite.dialogs
import android.app.Dialog
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.os.Bundle
import android.support.v4.app.DialogFragment
import android.support.v4.app.FragmentManager
import android.support.v7.app.AlertDialog
import android.widget.Toast
import net.syncthing.lite.R
class ErrorReportDialog: DialogFragment() {
companion object {
private const val REPORT = "report"
private const val TAG = "ErrorReportDialog"
fun newInstance(report: String) = ErrorReportDialog().apply {
arguments = Bundle().apply {
putString(REPORT, report)
}
}
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val report = arguments!!.getString(REPORT)
val clipboard = context!!.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
return AlertDialog.Builder(context!!)
.setTitle(R.string.settings_last_error_title)
.setMessage(report)
.setPositiveButton(android.R.string.ok, null)
.setNeutralButton(R.string.copy_to_clipboard, null)
.create()
.apply {
setOnShowListener {
getButton(AlertDialog.BUTTON_NEUTRAL).setOnClickListener {
clipboard.primaryClip = ClipData.newPlainText(
context!!.getString(R.string.settings_last_error_title),
report
)
Toast.makeText(context, context!!.getString(R.string.copied_to_clipboard), Toast.LENGTH_SHORT)
.show()
}
}
}
}
fun show(fragmentManager: FragmentManager) = show(fragmentManager, TAG)
}
@@ -8,12 +8,11 @@ import android.support.v4.app.FragmentManager
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.webkit.MimeTypeMap
import net.syncthing.java.core.beans.FileInfo
import net.syncthing.lite.databinding.DialogFileBinding
import net.syncthing.lite.dialogs.downloadfile.DownloadFileDialogFragment
import net.syncthing.lite.dialogs.downloadfile.DownloadFileSpec
import org.apache.commons.io.FilenameUtils
import net.syncthing.lite.utils.MimeType
class FileMenuDialogFragment: BottomSheetDialogFragment() {
companion object {
@@ -48,9 +47,7 @@ class FileMenuDialogFragment: BottomSheetDialogFragment() {
Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = MimeTypeMap.getSingleton().getMimeTypeFromExtension(
FilenameUtils.getExtension(fileSpec.fileName)
)
type = MimeType.getFromUrl(fileSpec.fileName)
putExtra(Intent.EXTRA_TITLE, fileSpec.fileName)
},
@@ -0,0 +1,105 @@
package net.syncthing.lite.dialogs
import android.app.Dialog
import android.os.Bundle
import android.support.v4.app.FragmentManager
import android.support.v7.app.AlertDialog
import android.support.v7.widget.AppCompatCheckBox
import android.view.LayoutInflater
import kotlinx.coroutines.launch
import net.syncthing.lite.R
import net.syncthing.lite.databinding.DialogFolderInfoBinding
import net.syncthing.lite.fragments.SyncthingDialogFragment
class FolderInfoDialog: SyncthingDialogFragment() {
companion object {
fun newInstance(folderId: String) = FolderInfoDialog().apply {
arguments = Bundle().apply {
putString(FOLDER_ID, folderId)
}
}
private const val FOLDER_ID = "folderId"
private const val TAG = "FolderInfoDialog"
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val folderId = arguments!!.getString(FOLDER_ID)
val binding = DialogFolderInfoBinding.inflate(LayoutInflater.from(context))
val dialog = AlertDialog.Builder(context!!)
.setTitle(folderId)
.setView(binding.root)
.create()
launch {
val configuration = libraryHandler.libraryManager.withLibrary { it.configuration }
val folderInfo = configuration.folders.find { it.folderId == folderId }
if (folderInfo == null) {
dismissAllowingStateLoss()
return@launch
}
dialog.setTitle(folderInfo.label)
binding.deviceCheckboxesContainer.removeAllViews()
val allRelatedDevices = (folderInfo.deviceIdWhitelist + folderInfo.deviceIdBlacklist).toSet()
allRelatedDevices.forEach { deviceId ->
val deviceInfo = configuration.peers.find { it.deviceId == deviceId }
val deviceLabel = if (deviceInfo == null)
deviceId.deviceId
else
context!!.getString(R.string.dialog_folder_info_device_list_item, deviceInfo.name, deviceId.deviceId)
binding.deviceCheckboxesContainer.addView(
AppCompatCheckBox(context!!).apply {
text = deviceLabel
isChecked = folderInfo.deviceIdWhitelist.contains(deviceId)
setOnCheckedChangeListener { _, isShared ->
this@FolderInfoDialog.launch {
libraryHandler.libraryManager.withLibrary { library ->
// update the config
library.configuration.update { oldConfig ->
val oldFolders = oldConfig.folders
var folderToChange = oldFolders.find { it.folderId == folderId }!!
val foldersNotToChange = oldFolders.filterNot { it.folderId == folderId }.toSet()
if (isShared) {
folderToChange = folderToChange.copy(
ignoredDeviceIdList = folderToChange.ignoredDeviceIdList.filterNot { it == deviceId }.toSet(),
deviceIdBlacklist = folderToChange.deviceIdBlacklist.filterNot { it == deviceId }.toSet(),
deviceIdWhitelist = folderToChange.deviceIdWhitelist + setOf(deviceId)
)
} else {
folderToChange = folderToChange.copy(
deviceIdWhitelist = folderToChange.deviceIdWhitelist.filterNot { it == deviceId }.toSet(),
deviceIdBlacklist = folderToChange.deviceIdBlacklist + setOf(deviceId),
ignoredDeviceIdList = folderToChange.ignoredDeviceIdList + setOf(deviceId)
)
}
oldConfig.copy(folders = foldersNotToChange + folderToChange)
}
library.configuration.persistLater()
// apply the change
library.syncthingClient.reconnect(deviceId)
}
}
}
}
)
}
}
return dialog
}
fun show(fragmentManager: FragmentManager) = show(fragmentManager, TAG)
}
@@ -12,13 +12,12 @@ import android.os.Bundle
import android.support.v4.app.DialogFragment
import android.support.v4.app.FragmentManager
import android.util.Log
import android.webkit.MimeTypeMap
import net.syncthing.java.core.beans.FileInfo
import net.syncthing.lite.BuildConfig
import net.syncthing.lite.R
import net.syncthing.lite.library.CacheFileProviderUrl
import net.syncthing.lite.library.LibraryHandler
import org.apache.commons.io.FilenameUtils
import net.syncthing.lite.utils.MimeType
import org.jetbrains.anko.newTask
import org.jetbrains.anko.toast
@@ -89,7 +88,7 @@ class DownloadFileDialogFragment : DialogFragment() {
dismissAllowingStateLoss()
if (outputUri == null) {
val mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(FilenameUtils.getExtension(fileSpec.fileName))
val mimeType = MimeType.getFromUrl(fileSpec.fileName)
try {
context!!.startActivity(
@@ -0,0 +1,17 @@
package net.syncthing.lite.error
import android.content.Context
import org.jetbrains.anko.defaultSharedPreferences
object ErrorStorage {
private const val PREF_KEY = "last_error"
fun reportError(context: Context, error: String) {
// this uses commit because the App could be quit directly after that
context.defaultSharedPreferences.edit()
.putString(PREF_KEY, error)
.commit()
}
fun getLastErrorReport(context: Context) = context.defaultSharedPreferences.getString(PREF_KEY, "there is no saved report")
}
@@ -10,9 +10,9 @@ import android.view.View
import android.view.ViewGroup
import android.view.inputmethod.InputMethodManager
import com.google.zxing.integration.android.IntentIntegrator
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.channels.consumeEach
import kotlinx.coroutines.launch
import net.syncthing.java.bep.connectionactor.ConnectionInfo
import net.syncthing.java.core.beans.DeviceInfo
import net.syncthing.lite.R
import net.syncthing.lite.adapters.DeviceAdapterListener
@@ -34,25 +34,7 @@ class DevicesFragment : SyncthingFragment() {
savedInstanceState: Bundle?): View? {
binding = DataBindingUtil.inflate(layoutInflater, R.layout.fragment_devices, container, false)
binding.addDevice.setOnClickListener { showDialog() }
return binding.root
}
override fun onResume() {
super.onResume()
libraryHandler.syncthingClient { it.addOnConnectionChangedListener { _ -> updateDeviceList() } }
}
override fun onPause() {
super.onPause()
libraryHandler.syncthingClient { it.removeOnConnectionChangedListener{ _ -> updateDeviceList() } }
}
override fun onLibraryLoaded() {
initDeviceList()
updateDeviceList()
}
private fun initDeviceList() {
binding.list.adapter = adapter
adapter.listener = object: DeviceAdapterListener {
@@ -61,12 +43,21 @@ class DevicesFragment : SyncthingFragment() {
.setTitle(getString(R.string.remove_device_title, deviceInfo.name))
.setMessage(getString(R.string.remove_device_message, deviceInfo.deviceId.deviceId.substring(0, 7)))
.setPositiveButton(android.R.string.yes) { _, _ ->
libraryHandler.library { config, syncthingClient, _ ->
config.peers = config.peers.filterNot { it.deviceId == deviceInfo.deviceId }.toSet()
config.persistLater()
updateDeviceList()
launch {
libraryHandler.libraryManager.withLibrary { library ->
library.configuration.update { oldConfig ->
oldConfig.copy(
peers = oldConfig.peers
.filterNot { it.deviceId == deviceInfo.deviceId }
.toSet()
)
}
syncthingClient.disconnectFromRemovedDevices()
library.configuration.persistLater()
// TODO: update the device list (should become a side effect of the call below)
library.syncthingClient.disconnectFromRemovedDevices()
}
}
}
.setNegativeButton(android.R.string.no, null)
@@ -75,15 +66,17 @@ class DevicesFragment : SyncthingFragment() {
return false
}
}
}
private fun updateDeviceList() {
libraryHandler.syncthingClient { syncthingClient ->
GlobalScope.launch (Dispatchers.Main) {
adapter.data = syncthingClient.getPeerStatus()
binding.isEmpty = adapter.data.isEmpty()
launch {
libraryHandler.subscribeToConnectionStatus().consumeEach { connectionInfo ->
val devices = libraryHandler.libraryManager.withLibrary { it.configuration.peers }
adapter.data = devices.map { device -> device to (connectionInfo[device.deviceId] ?: ConnectionInfo.empty) }
binding.isEmpty = devices.isEmpty()
}
}
return binding.root
}
override fun onActivityResult(requestCode: Int, resultCode: Int, intent: Intent?) {
@@ -94,35 +87,38 @@ class DevicesFragment : SyncthingFragment() {
}
private fun showDialog() {
addDeviceDialogBinding = DataBindingUtil.inflate(LayoutInflater.from(context), R.layout.view_enter_device_id, null, false)
addDeviceDialogBinding?.let { binding ->
binding.scanQrCode.setOnClickListener {
FragmentIntentIntegrator(this@DevicesFragment).initiateScan()
}
binding.deviceId.post {
val imm = context!!.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
imm.showSoftInput(binding.deviceId, InputMethodManager.SHOW_IMPLICIT)
}
val binding = ViewEnterDeviceIdBinding.inflate(LayoutInflater.from(context), null, false)
addDeviceDialogBinding = binding
addDeviceDialog = AlertDialog.Builder(context)
.setTitle(R.string.device_id_dialog_title)
.setView(binding.root)
.setPositiveButton(android.R.string.ok, null)
.setNegativeButton(android.R.string.cancel, null)
.show()
// Use different listener to keep dialog open after button click.
// https://stackoverflow.com/a/15619098
addDeviceDialog?.getButton(AlertDialog.BUTTON_POSITIVE)
?.setOnClickListener {
try {
val deviceId = binding.deviceId.text.toString()
Util.importDeviceId(libraryHandler, context, deviceId, { updateDeviceList() })
addDeviceDialog?.dismiss()
} catch (e: IOException) {
binding.deviceId.error = getString(R.string.invalid_device_id)
}
}
binding.scanQrCode.setOnClickListener {
FragmentIntentIntegrator(this@DevicesFragment).initiateScan()
}
binding.deviceId.post {
val imm = context!!.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
imm.showSoftInput(binding.deviceId, InputMethodManager.SHOW_IMPLICIT)
}
val dialog = AlertDialog.Builder(context)
.setTitle(R.string.device_id_dialog_title)
.setView(binding.root)
.setPositiveButton(android.R.string.ok, null)
.setNegativeButton(android.R.string.cancel, null)
.show()
addDeviceDialog = dialog
fun handleAddClick() {
try {
val deviceId = binding.deviceId.text.toString()
Util.importDeviceId(libraryHandler.libraryManager, context!!, deviceId, { /* TODO: Is updateDeviceList() still required? */ })
dialog.dismiss()
} catch (e: IOException) {
binding.deviceId.error = getString(R.string.invalid_device_id)
}
}
// Use different listener to keep dialog open after button click.
// https://stackoverflow.com/a/15619098
dialog.getButton(AlertDialog.BUTTON_POSITIVE)!!.setOnClickListener { handleAddClick() }
}
}
@@ -2,12 +2,10 @@ package net.syncthing.lite.fragments
import android.arch.lifecycle.Observer
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.channels.consumeEach
import kotlinx.coroutines.launch
import net.syncthing.java.core.beans.FolderInfo
import net.syncthing.java.core.beans.FolderStats
@@ -15,52 +13,42 @@ import net.syncthing.lite.activities.FolderBrowserActivity
import net.syncthing.lite.adapters.FolderListAdapterListener
import net.syncthing.lite.adapters.FoldersListAdapter
import net.syncthing.lite.databinding.FragmentFoldersBinding
import net.syncthing.lite.dialogs.FolderInfoDialog
import org.jetbrains.anko.intentFor
class FoldersFragment : SyncthingFragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val adapter = FoldersListAdapter()
private val TAG = "FoldersFragment"
adapter.listener = object : FolderListAdapterListener {
override fun onFolderClicked(folderInfo: FolderInfo, folderStats: FolderStats) {
startActivity(
activity!!.intentFor<FolderBrowserActivity>(
FolderBrowserActivity.EXTRA_FOLDER_NAME to folderInfo.folderId
)
)
}
private lateinit var binding: FragmentFoldersBinding
override fun onFolderLongClicked(folderInfo: FolderInfo): Boolean {
FolderInfoDialog
.newInstance(folderId = folderInfo.folderId)
.show(fragmentManager!!)
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
binding = FragmentFoldersBinding.inflate(layoutInflater, container, false)
return true
}
}
val binding = FragmentFoldersBinding.inflate(layoutInflater, container, false)
binding.list.adapter = adapter
libraryHandler.isListeningPortTaken.observe(this, Observer { binding.listeningPortTaken = it })
launch {
libraryHandler.subscribeToFolderStatusList().consumeEach {
adapter.data = it
binding.isEmpty = it.isEmpty()
}
}
return binding.root
}
override fun onLibraryLoaded() {
showAllFoldersListView()
}
private fun showAllFoldersListView() {
libraryHandler.folderBrowser { folderBrowser ->
val list = folderBrowser.folderInfoAndStatsList()
GlobalScope.launch (Dispatchers.Main) {
Log.i(TAG, "list folders = " + list + " (" + list.size + " records)")
val adapter = FoldersListAdapter().apply { data = list }
binding.list.adapter = adapter
adapter.listener = object : FolderListAdapterListener {
override fun onFolderClicked(folderInfo: FolderInfo, folderStats: FolderStats) {
startActivity(
activity!!.intentFor<FolderBrowserActivity>(
FolderBrowserActivity.EXTRA_FOLDER_NAME to folderInfo.folderId
)
)
}
}
binding.isEmpty = list.isEmpty()
}
}
}
override fun onIndexUpdateComplete(folderInfo: FolderInfo) {
super.onIndexUpdateComplete(folderInfo)
showAllFoldersListView()
}
}
@@ -1,10 +1,17 @@
package net.syncthing.lite.fragments
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.support.v7.preference.EditTextPreference
import android.support.v7.preference.PreferenceFragmentCompat
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import net.syncthing.lite.R
import net.syncthing.lite.activities.SyncthingActivity
import net.syncthing.lite.dialogs.ErrorReportDialog
import net.syncthing.lite.error.ErrorStorage
import net.syncthing.lite.library.DefaultLibraryManager
class SettingsFragment : PreferenceFragmentCompat() {
@@ -13,19 +20,48 @@ class SettingsFragment : PreferenceFragmentCompat() {
val localDeviceName = findPreference("local_device_name") as EditTextPreference
val appVersion = findPreference("app_version")
val forceStop = findPreference("force_stop")
val lastCrash = findPreference("last_crash")
val reportBug = findPreference("report_bug")
val libraryManager = DefaultLibraryManager.with(context!!)
(activity as SyncthingActivity?)?.let { activity ->
val versionName = activity.packageManager.getPackageInfo(activity.packageName, 0)?.versionName
appVersion.summary = versionName
activity.libraryHandler.configuration { localDeviceName.text = it.localDeviceName }
localDeviceName.setOnPreferenceChangeListener { _, _ ->
activity.libraryHandler.configuration { conf ->
conf.localDeviceName = localDeviceName.text
conf.persistLater()
}
true
GlobalScope.launch (Dispatchers.Main) {
libraryManager.withLibrary { library ->
localDeviceName.text = library.configuration.localDeviceName
}
}
appVersion.summary = context!!.packageManager.getPackageInfo(context!!.packageName, 0)?.versionName
localDeviceName.setOnPreferenceChangeListener { _, _ ->
val newDeviceName = localDeviceName.text
GlobalScope.launch {
libraryManager.withLibrary { library ->
library.configuration.update { it.copy(localDeviceName = newDeviceName) }
library.configuration.persistLater()
}
}
true
}
forceStop.setOnPreferenceClickListener {
System.exit(0)
true
}
lastCrash.setOnPreferenceClickListener {
ErrorReportDialog.newInstance(ErrorStorage.getLastErrorReport(context!!)).show(fragmentManager!!)
true
}
reportBug.setOnPreferenceClickListener {
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse("https://github.com/syncthing/syncthing-lite/issues")))
true
}
}
}
@@ -1,9 +1,9 @@
package net.syncthing.lite.fragments
import android.support.v4.app.DialogFragment
import net.syncthing.lite.async.CoroutineDialogFragment
import net.syncthing.lite.library.LibraryHandler
abstract class SyncthingDialogFragment : DialogFragment() {
abstract class SyncthingDialogFragment : CoroutineDialogFragment() {
val libraryHandler: LibraryHandler by lazy { LibraryHandler(
context = context!!
)}
@@ -1,15 +1,10 @@
package net.syncthing.lite.fragments
import android.support.v4.app.Fragment
import net.syncthing.java.core.beans.FolderInfo
import net.syncthing.lite.async.CoroutineFragment
import net.syncthing.lite.library.LibraryHandler
abstract class SyncthingFragment : Fragment() {
val libraryHandler: LibraryHandler by lazy { LibraryHandler(
context = context!!,
onIndexUpdateProgressListener = this::onIndexUpdateProgress,
onIndexUpdateCompleteListener = this::onIndexUpdateComplete
)}
abstract class SyncthingFragment : CoroutineFragment() {
val libraryHandler: LibraryHandler by lazy { LibraryHandler(context = context!!)}
override fun onStart() {
super.onStart()
@@ -27,8 +22,4 @@ abstract class SyncthingFragment : Fragment() {
}
open fun onLibraryLoaded() {}
open fun onIndexUpdateProgress(folderInfo: FolderInfo, percentage: Int) {}
open fun onIndexUpdateComplete(folderInfo: FolderInfo) {}
}
}
@@ -4,8 +4,10 @@ import android.content.Context
import android.os.Handler
import android.os.Looper
import android.util.Log
import android.widget.Toast
import net.syncthing.lite.BuildConfig
import net.syncthing.lite.R
import net.syncthing.lite.error.ErrorStorage
import org.jetbrains.anko.defaultSharedPreferences
object DefaultLibraryManager {
@@ -39,7 +41,16 @@ object DefaultLibraryManager {
}
instance = LibraryManager(
synchronousInstanceCreator = { LibraryInstance(context) },
synchronousInstanceCreator = {
LibraryInstance(context) { ex ->
// this delay ensures that the toast is shown even if the UI thread is busy
handler.postDelayed({
Toast.makeText(context, R.string.toast_error, Toast.LENGTH_LONG).show()
}, 100L)
ErrorStorage.reportError(context, "${ex.component}\n${ex.detailsReadableString}\n${Log.getStackTraceString(ex.exception)}")
}
},
userCounterListener = {
newUserCounter ->
@@ -5,16 +5,18 @@ import android.arch.lifecycle.MutableLiveData
import android.content.Context
import android.os.Handler
import android.os.Looper
import android.util.Log
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.channels.BroadcastChannel
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.ConflatedBroadcastChannel
import kotlinx.coroutines.channels.consumeEach
import kotlinx.coroutines.launch
import net.syncthing.java.bep.FolderBrowser
import net.syncthing.java.bep.connectionactor.ConnectionInfo
import net.syncthing.java.bep.folder.FolderBrowser
import net.syncthing.java.bep.folder.FolderStatus
import net.syncthing.java.client.SyncthingClient
import net.syncthing.java.core.beans.DeviceId
import net.syncthing.java.core.beans.FileInfo
import net.syncthing.java.core.beans.FolderInfo
import net.syncthing.java.core.beans.IndexInfo
import net.syncthing.java.core.configuration.Configuration
import org.jetbrains.anko.doAsync
import java.util.concurrent.atomic.AtomicBoolean
@@ -25,18 +27,20 @@ import java.util.concurrent.atomic.AtomicBoolean
*
* It's possible to do multiple start and stop cycles with one instance of this class.
*/
class LibraryHandler(context: Context,
private val onIndexUpdateProgressListener: (FolderInfo, Int) -> Unit = {_, _ -> },
private val onIndexUpdateCompleteListener: (FolderInfo) -> Unit = {}) {
class LibraryHandler(context: Context) {
companion object {
private const val TAG = "LibraryHandler"
private val handler = Handler(Looper.getMainLooper())
}
private val libraryManager = DefaultLibraryManager.with(context)
val libraryManager = DefaultLibraryManager.with(context)
private val isStarted = AtomicBoolean(false)
private val isListeningPortTakenInternal = MutableLiveData<Boolean>().apply { postValue(false) }
private val indexUpdateCompleteMessages = BroadcastChannel<String>(capacity = 16)
private val folderStatusList = BroadcastChannel<List<FolderStatus>>(capacity = Channel.CONFLATED)
private val connectionStatus = ConflatedBroadcastChannel<Map<DeviceId, ConnectionInfo>>()
private var job: Job = Job()
val isListeningPortTaken: LiveData<Boolean> = isListeningPortTakenInternal
@@ -62,9 +66,27 @@ class LibraryHandler(context: Context,
val client = libraryInstance.syncthingClient
client.indexHandler.registerOnIndexRecordAcquiredListener(this::onIndexRecordAcquired)
client.indexHandler.registerOnFullIndexAcquiredListenersListener(this::onRemoteIndexAcquired)
client.discoveryHandler.registerMessageFromUnknownDeviceListener(internalMessageFromUnknownDeviceListener)
job = Job()
GlobalScope.launch (job) {
libraryInstance.syncthingClient.indexHandler.subscribeToOnFullIndexAcquiredEvents().consumeEach {
indexUpdateCompleteMessages.send(it)
}
}
GlobalScope.launch (job) {
libraryInstance.folderBrowser.folderInfoAndStatusStream().consumeEach {
folderStatusList.send(it)
}
}
GlobalScope.launch (job) {
libraryInstance.syncthingClient.subscribeToConnectionStatus().consumeEach {
connectionStatus.send(it)
}
}
}
}
@@ -73,10 +95,10 @@ class LibraryHandler(context: Context,
throw IllegalStateException("already stopped")
}
job!!.cancel()
syncthingClient {
try {
it.indexHandler.unregisterOnIndexRecordAcquiredListener(this::onIndexRecordAcquired)
it.indexHandler.unregisterOnFullIndexAcquiredListenersListener(this::onRemoteIndexAcquired)
it.discoveryHandler.unregisterMessageFromUnknownDeviceListener(internalMessageFromUnknownDeviceListener)
} catch (e: IllegalArgumentException) {
// ignored, no idea why this is thrown
@@ -86,22 +108,6 @@ class LibraryHandler(context: Context,
libraryManager.stopLibraryUsage()
}
private fun onIndexRecordAcquired(folderInfo: FolderInfo, newRecords: List<FileInfo>, indexInfo: IndexInfo) {
Log.i(TAG, "handleIndexRecordEvent trigger folder list update from index record acquired")
GlobalScope.launch (Dispatchers.Main) {
onIndexUpdateProgressListener(folderInfo, (indexInfo.getCompleted() * 100).toInt())
}
}
private fun onRemoteIndexAcquired(folderInfo: FolderInfo) {
Log.i(TAG, "handleIndexAcquiredEvent trigger folder list update from index acquired")
GlobalScope.launch (Dispatchers.Main) {
onIndexUpdateCompleteListener(folderInfo)
}
}
/*
* The callback is executed asynchronously.
* As soon as it returns, there is no guarantee about the availability of the library
@@ -139,4 +145,8 @@ class LibraryHandler(context: Context,
fun unregisterMessageFromUnknownDeviceListener(listener: (DeviceId) -> Unit) {
messageFromUnknownDeviceListeners.remove(listener)
}
fun subscribeToOnFullIndexAcquiredEvents() = indexUpdateCompleteMessages.openSubscription()
fun subscribeToFolderStatusList() = folderStatusList.openSubscription()
fun subscribeToConnectionStatus() = connectionStatus.openSubscription()
}
@@ -2,8 +2,13 @@ package net.syncthing.lite.library
import android.content.Context
import android.util.Log
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import net.syncthing.java.client.SyncthingClient
import net.syncthing.java.core.configuration.Configuration
import net.syncthing.java.core.exception.ExceptionReport
import net.syncthing.java.repository.EncryptedTempRepository
import net.syncthing.repository.android.SqliteIndexRepository
import net.syncthing.repository.android.TempDirectoryLocalRepository
import net.syncthing.repository.android.database.RepositoryDatabase
@@ -21,7 +26,10 @@ import java.net.SocketException
*
* The creation and the shutdown are synchronous, so keep them out of the UI Thread
*/
class LibraryInstance (context: Context) {
class LibraryInstance (
context: Context,
private val exceptionReportHandler: (ExceptionReport) -> Unit
) {
companion object {
private const val LOG_TAG = "LibraryInstance"
@@ -42,7 +50,11 @@ class LibraryInstance (context: Context) {
}
}
private val tempRepository = TempDirectoryLocalRepository(File(context.filesDir, "temp_repository"))
private val tempRepository = EncryptedTempRepository(
TempDirectoryLocalRepository(
File(context.filesDir, "temp_repository")
)
)
val isListeningPortTaken = checkIsListeningPortTaken() // this must come first to work correctly
val configuration = Configuration(configFolder = context.filesDir)
@@ -51,14 +63,21 @@ class LibraryInstance (context: Context) {
repository = SqliteIndexRepository(
database = RepositoryDatabase.with(context),
closeDatabaseOnClose = false,
clearTempStorageHook = { tempRepository.deleteAllData() }
clearTempStorageHook = { tempRepository.deleteAllTempData() }
),
tempRepository = tempRepository
)
val folderBrowser = syncthingClient.indexHandler.newFolderBrowser()
tempRepository = tempRepository,
exceptionReportHandler = { ex ->
Log.w(LOG_TAG, "${ex.component}\n${ex.detailsReadableString}\n${Log.getStackTraceString(ex.exception)}")
fun shutdown() {
folderBrowser.close()
GlobalScope.launch (Dispatchers.Main) {
exceptionReportHandler(ex)
}
}
)
val folderBrowser = syncthingClient.indexHandler.folderBrowser
val indexBrowser = syncthingClient.indexHandler.indexBrowser
suspend fun shutdown() {
syncthingClient.close()
configuration.persistNow()
}
@@ -2,6 +2,13 @@ package net.syncthing.lite.library
import android.os.Handler
import android.os.Looper
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.async
import kotlinx.coroutines.channels.ConflatedBroadcastChannel
import kotlinx.coroutines.channels.consumeEach
import kotlinx.coroutines.channels.produce
import kotlinx.coroutines.runBlocking
import java.util.concurrent.Executors
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
@@ -35,7 +42,7 @@ class LibraryManager (
// only this Thread should access instance and userCounter
private val startStopExecutor = Executors.newSingleThreadExecutor()
private var instance: LibraryInstance? = null
private val instanceStream = ConflatedBroadcastChannel<LibraryInstance?>(null)
private var userCounter = 0
fun startLibraryUsage(callback: (LibraryInstance) -> Unit) {
@@ -43,12 +50,12 @@ class LibraryManager (
val newUserCounter = ++userCounter
handler.post { userCounterListener(newUserCounter) }
if (instance == null) {
instance = synchronousInstanceCreator()
if (instanceStream.value == null) {
instanceStream.offer(synchronousInstanceCreator())
handler.post { isRunningListener(true) }
}
handler.post { callback(instance!!) }
handler.post { callback(instanceStream.value!!) }
}
}
@@ -60,6 +67,16 @@ class LibraryManager (
}
}
suspend fun <T> withLibrary(action: suspend (LibraryInstance) -> T): T {
val instance = startLibraryUsageCoroutine()
return try {
action(instance)
} finally {
stopLibraryUsage()
}
}
fun stopLibraryUsage() {
startStopExecutor.submit {
val newUserCounter = --userCounter
@@ -77,8 +94,8 @@ class LibraryManager (
fun shutdownIfThereAreZeroUsers(listener: (wasShutdownPerformed: Boolean) -> Unit = {}) {
startStopExecutor.submit {
if (userCounter == 0) {
instance?.shutdown()
instance = null
runBlocking { instanceStream.value?.shutdown() }
instanceStream.offer(null)
handler.post { isRunningListener(false) }
handler.post { listener(true) }
@@ -87,4 +104,21 @@ class LibraryManager (
}
}
}
fun streamDirectoryListing(folder: String, path: String) = GlobalScope.produce {
var job = Job()
instanceStream.openSubscription().consumeEach { instance ->
job.cancel()
job = Job()
if (instance != null) {
async (job) {
instance.indexBrowser.streamDirectoryListing(folder, path).consumeEach {
send(it)
}
}
}
}
}
}
@@ -10,33 +10,36 @@ import android.provider.DocumentsProvider
import android.util.Log
import kotlinx.coroutines.cancel
import kotlinx.coroutines.runBlocking
import net.syncthing.java.bep.IndexBrowser
import net.syncthing.java.bep.index.browser.DirectoryContentListing
import net.syncthing.java.bep.index.browser.DirectoryNotFoundListing
import net.syncthing.java.core.beans.FileInfo
import net.syncthing.java.core.beans.FolderInfo
import net.syncthing.java.core.beans.FolderStats
import net.syncthing.lite.R
import net.syncthing.lite.utils.MimeType
import java.io.FileNotFoundException
import java.net.URLConnection
import java.util.concurrent.CountDownLatch
class SyncthingProvider : DocumentsProvider() {
companion object {
private const val Tag = "SyncthingProvider"
private val DefaultRootProjection = arrayOf(
Root.COLUMN_ROOT_ID,
Root.COLUMN_FLAGS,
Root.COLUMN_TITLE,
Root.COLUMN_SUMMARY,
Root.COLUMN_DOCUMENT_ID,
Root.COLUMN_ICON)
Root.COLUMN_ICON
)
private val DefaultDocumentProjection = arrayOf(
Document.COLUMN_DOCUMENT_ID,
Document.COLUMN_DISPLAY_NAME,
Document.COLUMN_SIZE,
Document.COLUMN_MIME_TYPE,
Document.COLUMN_LAST_MODIFIED,
Document.COLUMN_FLAGS)
Document.COLUMN_FLAGS
)
}
override fun onCreate(): Boolean {
@@ -45,96 +48,122 @@ class SyncthingProvider : DocumentsProvider() {
}
// this instance is not started -> it connects and disconnects on demand
private val libraryHandler: LibraryHandler by lazy { LibraryHandler(context) }
private val libraryManager: LibraryManager by lazy { DefaultLibraryManager.with(context) }
override fun queryRoots(projection: Array<String>?): Cursor {
Log.d(Tag, "queryRoots($projection)")
val latch = CountDownLatch(1)
var folders: List<Pair<FolderInfo, FolderStats>>? = null
libraryHandler.folderBrowser { folderBrowser ->
folders = folderBrowser.folderInfoAndStatsList()
latch.countDown()
}
latch.await()
val result = MatrixCursor(projection ?: DefaultRootProjection)
folders!!.forEach { folder ->
val row = result.newRow()
row.add(Root.COLUMN_ROOT_ID, folder.first.folderId)
row.add(Root.COLUMN_SUMMARY, folder.first.label)
row.add(Root.COLUMN_FLAGS, 0)
row.add(Root.COLUMN_TITLE, context.getString(R.string.app_name))
row.add(Root.COLUMN_DOCUMENT_ID, getDocIdForFile(folder.first))
row.add(Root.COLUMN_ICON, R.mipmap.ic_launcher)
return runBlocking {
libraryManager.withLibrary { instance ->
MatrixCursor(projection ?: DefaultRootProjection).apply {
instance.folderBrowser.folderInfoAndStatusList().forEach { folder ->
newRow().apply {
add(Root.COLUMN_ROOT_ID, folder.info.folderId)
add(Root.COLUMN_SUMMARY, folder.info.label)
add(Root.COLUMN_FLAGS, 0)
add(Root.COLUMN_TITLE, context.getString(R.string.app_name))
add(Root.COLUMN_DOCUMENT_ID, getDocIdForFile(folder.info))
add(Root.COLUMN_ICON, R.mipmap.ic_launcher)
}
}
}
}
}
return result
}
override fun queryChildDocuments(parentDocumentId: String, projection: Array<String>?,
sortOrder: String?): Cursor {
Log.d(Tag, "queryChildDocuments($parentDocumentId, $projection, $sortOrder)")
val result = MatrixCursor(projection ?: DefaultDocumentProjection)
getIndexBrowser(getFolderIdForDocId(parentDocumentId))
.listFiles(getPathForDocId(parentDocumentId))
.forEach { fileInfo ->
includeFile(result, fileInfo)
return runBlocking {
libraryManager.withLibrary { instance ->
val listing = instance.indexBrowser.getDirectoryListing(
folder = getFolderIdForDocId(parentDocumentId),
path = getPathForDocId(parentDocumentId)
)
when (listing) {
is DirectoryNotFoundListing -> throw FileNotFoundException()
is DirectoryContentListing -> {
val result = MatrixCursor(projection ?: DefaultDocumentProjection)
listing.entries.forEach { entry ->
includeFile(result, entry)
}
result
}
}
return result
}
}
}
override fun queryDocument(documentId: String, projection: Array<String>?): Cursor {
Log.d(Tag, "queryDocument($documentId, $projection)")
val result = MatrixCursor(projection ?: DefaultDocumentProjection)
val fileInfo = getIndexBrowser(getFolderIdForDocId(documentId))
.getFileInfoByAbsolutePath(getPathForDocId(documentId))
includeFile(result, fileInfo)
return result
return runBlocking {
libraryManager.withLibrary { instance ->
val fileInfo = instance.indexBrowser.getFileInfoByAbsolutePathAllowNull(
folder = getFolderIdForDocId(documentId),
path = getPathForDocId(documentId)
) ?: throw FileNotFoundException()
MatrixCursor(projection ?: DefaultDocumentProjection).apply {
includeFile(this, fileInfo)
}
}
}
}
@Throws(FileNotFoundException::class)
override fun openDocument(documentId: String, mode: String, signal: CancellationSignal?):
ParcelFileDescriptor {
Log.d(Tag, "openDocument($documentId, $mode, $signal)")
val fileInfo = getIndexBrowser(getFolderIdForDocId(documentId))
.getFileInfoByAbsolutePath(getPathForDocId(documentId))
val accessMode = ParcelFileDescriptor.parseMode(mode)
if (accessMode != ParcelFileDescriptor.MODE_READ_ONLY) {
throw NotImplementedError()
}
val outputFile = runBlocking {
signal?.setOnCancelListener {
this.coroutineContext.cancel()
}
return runBlocking {
libraryManager.withLibrary { instance ->
val fileInfo = instance.indexBrowser.getFileInfoByAbsolutePathAllowNull(
folder = getFolderIdForDocId(documentId),
path = getPathForDocId(documentId)
) ?: throw FileNotFoundException()
val libraryInstance = libraryManager.startLibraryUsageCoroutine()
signal?.setOnCancelListener {
this.coroutineContext.cancel()
}
try {
DownloadFileTask.downloadFileCoroutine(
val outputFile = DownloadFileTask.downloadFileCoroutine(
externalCacheDir = context.externalCacheDir,
syncthingClient = libraryInstance.syncthingClient,
syncthingClient = instance.syncthingClient,
fileInfo = fileInfo,
onProgress = { /* ignore the progress */ }
)
} finally {
libraryManager.stopLibraryUsage()
ParcelFileDescriptor.open(outputFile, ParcelFileDescriptor.MODE_READ_ONLY)
}
}
return ParcelFileDescriptor.open(outputFile, ParcelFileDescriptor.MODE_READ_ONLY)
}
private fun includeFile(result: MatrixCursor, fileInfo: FileInfo) {
val row = result.newRow()
row.add(Document.COLUMN_DOCUMENT_ID, getDocIdForFile(fileInfo))
row.add(Document.COLUMN_DISPLAY_NAME, fileInfo.fileName)
row.add(Document.COLUMN_SIZE, fileInfo.size)
val mime = if (fileInfo.isDirectory()) Document.MIME_TYPE_DIR
else URLConnection.guessContentTypeFromName(fileInfo.fileName)
row.add(Document.COLUMN_MIME_TYPE, mime)
row.add(Document.COLUMN_LAST_MODIFIED, fileInfo.lastModified)
row.add(Document.COLUMN_FLAGS, 0)
result.newRow().apply {
add(Document.COLUMN_DOCUMENT_ID, getDocIdForFile(fileInfo))
add(Document.COLUMN_DISPLAY_NAME, fileInfo.fileName)
add(Document.COLUMN_SIZE, fileInfo.size)
add(
Document.COLUMN_MIME_TYPE,
if (fileInfo.isDirectory())
Document.MIME_TYPE_DIR
else
MimeType.getFromUrl(fileInfo.fileName)
)
add(Document.COLUMN_LAST_MODIFIED, fileInfo.lastModified)
add(Document.COLUMN_FLAGS, 0)
}
}
private fun getFolderIdForDocId(docId: String) = docId.split(":")[0]
@@ -144,15 +173,4 @@ class SyncthingProvider : DocumentsProvider() {
private fun getDocIdForFile(folderInfo: FolderInfo) = folderInfo.folderId + ":"
private fun getDocIdForFile(fileInfo: FileInfo) = fileInfo.folder + ":" + fileInfo.path
private fun getIndexBrowser(folderId: String): IndexBrowser {
val latch = CountDownLatch(1)
var indexBrowser: IndexBrowser? = null
libraryHandler.syncthingClient {
indexBrowser = it.indexHandler.newIndexBrowser(folderId)
latch.countDown()
}
latch.await()
return indexBrowser!!
}
}
@@ -0,0 +1,17 @@
package net.syncthing.lite.utils
import android.webkit.MimeTypeMap
object MimeType {
private const val DEFAULT_MIME_TYPE = "application/octet-stream"
private fun getFromExtension(extension: String): String {
val mimeType: String? = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension)
return mimeType ?: DEFAULT_MIME_TYPE
}
fun getFromUrl(url: String) = getFromExtension(
MimeTypeMap.getFileExtensionFromUrl(url).toLowerCase()
)
}
@@ -10,7 +10,7 @@ import kotlinx.coroutines.launch
import net.syncthing.java.core.beans.DeviceId
import net.syncthing.java.core.beans.DeviceInfo
import net.syncthing.lite.R
import net.syncthing.lite.library.LibraryHandler
import net.syncthing.lite.library.LibraryManager
import org.apache.commons.lang3.StringUtils.capitalize
import org.jetbrains.anko.toast
import java.io.IOException
@@ -23,11 +23,11 @@ object Util {
val manufacturer = Build.MANUFACTURER ?: ""
val model = Build.MODEL ?: ""
val deviceName =
if (model.startsWith(manufacturer)) {
capitalize(model)
} else {
capitalize(manufacturer) + " " + model
}
if (model.startsWith(manufacturer)) {
capitalize(model)
} else {
capitalize(manufacturer) + " " + model
}
return deviceName ?: "android"
}
@@ -41,22 +41,34 @@ object Util {
}
@Throws(IOException::class)
fun importDeviceId(libraryHandler: LibraryHandler?, context: Context?, deviceId: String,
onComplete: () -> Unit) {
val deviceId2 = DeviceId(deviceId.toUpperCase(Locale.US))
libraryHandler?.library { configuration, syncthingClient, _ ->
if (!configuration.peerIds.contains(deviceId2)) {
configuration.peers = configuration.peers + DeviceInfo(deviceId2, null)
configuration.persistLater()
syncthingClient.connectToNewlyAddedDevices()
GlobalScope.launch (Dispatchers.Main) {
context?.toast(context.getString(R.string.device_import_success, deviceId2.shortId))
fun importDeviceId(libraryManager: LibraryManager, context: Context, deviceId: String, onComplete: () -> Unit) {
val newDeviceId = DeviceId(deviceId.toUpperCase(Locale.US))
GlobalScope.launch (Dispatchers.Main) {
libraryManager.withLibrary { library ->
val didAddDevice = library.configuration.update { oldConfig ->
if (oldConfig.peers.find { it.deviceId == newDeviceId } != null) {
// already known
oldConfig
} else {
oldConfig.copy(
peers = oldConfig.peers + DeviceInfo(newDeviceId, newDeviceId.shortId)
)
}
}
if (didAddDevice) {
library.configuration.persistLater()
library.syncthingClient.connectToNewlyAddedDevices()
context.toast(context.getString(R.string.device_import_success, newDeviceId.shortId))
onComplete()
} else {
context.toast(context.getString(R.string.device_already_known, newDeviceId.shortId))
}
} else {
GlobalScope.launch (Dispatchers.Main) {
context?.toast(context.getString(R.string.device_already_known, deviceId2.shortId))
}
null
}
}
}
+7 -3
View File
@@ -1,3 +1,7 @@
- new connection handling
- option for users to get detailed crash reports
- bugfixes
- update translations
- encrypt some temporarily data which is stored on disk
- update path validation
- fix crash when cleaning the cache
- fix crash of the index handler after cleaning the cache
- convert file extensions to lower case for detecting Apps to open it
- fix crash when accessing (e.g. trying to close) closed connections
@@ -0,0 +1,36 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<LinearLayout
android:padding="8dp"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:textAppearance="?android:textAppearanceMedium"
android:text="@string/dialog_folder_info_device_list"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<LinearLayout
android:id="@+id/device_checkboxes_container"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<!--
<CheckBox
android:text="Test device 1 (the very very very very very very very very very very very long id)"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<CheckBox
android:text="Test device 2 (the very very very very very very very very very very very long id)"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
-->
</LinearLayout>
</LinearLayout>
</layout>
+38 -25
View File
@@ -7,46 +7,59 @@
name="name"
type="String" />
<variable
name="status"
type="String" />
<variable
name="isConnected"
type="Boolean" />
</data>
<RelativeLayout
<LinearLayout
android:orientation="horizontal"
android:background="?selectableItemBackground"
android:padding="8dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingBottom="12dp"
android:paddingLeft="24dp"
android:paddingRight="24dp"
android:paddingTop="12dp">
android:layout_height="wrap_content">
<ImageView
android:layout_gravity="center_vertical"
android:id="@+id/device_icon"
android:layout_width="32dp"
android:layout_height="32dp"
tools:src="@drawable/ic_laptop_green_24dp"
android:src="@{safeUnbox(isConnected) ? @drawable/ic_laptop_green_24dp : @drawable/ic_laptop_red_24dp}"
android:layout_alignParentStart="true"
android:layout_alignParentTop="true"/>
android:src="@{safeUnbox(isConnected) ? @drawable/ic_laptop_green_24dp : @drawable/ic_laptop_red_24dp}" />
<TextView
tools:text="Computer"
android:text="@{name}"
android:id="@+id/device_name"
<View
android:layout_width="16dp"
android:layout_height="0dp" />
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_alignParentEnd="true"
android:layout_height="wrap_content"
android:ellipsize="end"
android:maxLines="1"
android:layout_alignParentTop="true"
android:gravity="top"
android:textAlignment="gravity"
android:paddingStart="40dp"
android:paddingEnd="40dp"
android:textSize="18sp"
android:textStyle="bold"/>
android:layout_height="wrap_content">
</RelativeLayout>
<TextView
tools:text="Computer"
android:text="@{name}"
android:id="@+id/device_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ellipsize="end"
android:maxLines="1"
android:textAlignment="gravity"
android:textSize="18sp"
android:textStyle="bold"/>
<TextView
android:text="@{status}"
tools:text="Trying to connect to 127.0.0.1 ..."
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>
</LinearLayout>
</layout>
+22 -13
View File
@@ -13,9 +13,17 @@
<variable
name="info"
type="String" />
<variable
name="info2"
type="String" />
<import type="android.view.View" />
<import type="android.text.TextUtils" />
</data>
<RelativeLayout
<LinearLayout
android:orientation="vertical"
android:background="?selectableItemBackground"
android:layout_width="match_parent"
android:layout_height="wrap_content"
@@ -29,13 +37,9 @@
android:text="@{folderName}"
android:id="@+id/folder_name_view"
android:layout_width="match_parent"
android:layout_alignParentEnd="true"
android:layout_height="wrap_content"
android:ellipsize="end"
android:maxLines="1"
android:layout_alignParentTop="true"
android:gravity="top"
android:textAlignment="gravity"
android:textSize="20sp"
android:textStyle="bold"/>
@@ -47,9 +51,7 @@
android:layout_height="wrap_content"
android:ellipsize="end"
android:maxLines="1"
android:layout_below="@id/folder_name_view"
android:textSize="14sp"
android:layout_alignParentStart="true" />
android:textSize="14sp" />
<TextView
tools:text="Additional information"
@@ -59,11 +61,18 @@
android:layout_height="wrap_content"
android:ellipsize="end"
android:maxLines="1"
android:layout_below="@id/folder_lastmod_info"
android:textSize="14sp"
android:layout_alignParentStart="true" />
android:textSize="14sp" />
<TextView
android:visibility="@{TextUtils.isEmpty(info2) ? View.GONE : View.VISIBLE}"
tools:text="Index Update Progress"
android:text="@{info2}"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ellipsize="end"
android:maxLines="1"
android:textSize="14sp" />
</RelativeLayout>
</LinearLayout>
</layout>
</layout>
+25 -1
View File
@@ -36,9 +36,33 @@
<string name="intro_page_three_title">Ordner teilen</string>
<string name="intro_page_two_description">Eine Syncthing Geräte ID eingeben oder QR Code einer Geräte ID scannen.</string>
<string name="intro_page_three_description">Akzeptieren Sie nun das Gerät mit der ID %1$s und geben Sie einen Ordner mit ihm frei. Es kann einige Minuten dauern, bis sich die Geräte verbinden.</string>
<string name="intro_page_three_searching_device">Versuche das andere Gerät zu finden. Dies kann einen Moment dauern.</string>
<string name="settings">Einstellungen</string>
<string name="settings_app_version_title">App Version</string>
<string name="settings_local_device_name">Lokaler Geräte Namen</string>
<string name="settings_local_device_summary">Name, den das andere Gerät für dieses Gerät sehen wird</string>
<string name="settings_force_stop">Beenden dieser App erzwingen</string>
<string name="settings_last_error_title">Letzter Fehler</string>
<string name="settings_last_error_summary">Details des letzten Fehlers anzeigen</string>
<string name="settings_report_bug_title">Einen Fehler melden</string>
<string name="settings_report_bug_summary">Den Bugtracker bei GitHub für diese App öffnen</string>
<string name="copy_to_clipboard">In die Zwischenablage kopieren</string>
<string name="copied_to_clipboard">In die Zwischenablage kopiert</string>
<string name="device_id_dialog_title">Geräte ID eingeben</string>
</resources>
<string name="dialog_warning_reconnect_problem">
Aufgrund des Verhaltens dieser App und des Verhaltens des Syncthing-Servers können Sie sich für einige Minuten nicht verbinden, wenn die App erzwungen beendet wurde (durch das Entfernen aus der Liste der aktiven Apps) oder die Verbindung unterbrochen wurde.
Dies gilt nicht für Verbindungen, die per lokaler Gerätesuche hergestellt wurden.</string>
<string name="dialog_file_save_as">Speichern unter</string>
<string name="pending_index_updates">%d Index-Updates verbleibend</string>
<string name="device_status_connecting">Verbinden mit %s</string>
<string name="device_status_connected">Mit %s verbunden</string>
<string name="device_status_disconnected">Verbinden wird bald erneut versucht - es sind %d Adressen bekannt</string>
<string name="device_status_no_address">Keine bekannte Adresse für dieses Gerät</string>
<string name="dialog_enable_folder_sync_for_new_device_title">Ordnersynchronisation für neues Gerät aktivieren</string>
<string name="dialog_enable_folder_sync_for_new_device_text">Möchten Sie den Ordner %1$s mit %2$s (%3$s) synchronisieren?</string>
<string name="dialog_enable_folder_sync_for_new_device_positive">synchronisieren</string>
<string name="dialog_enable_folder_sync_for_new_device_negative">nicht synchronisieren</string>
<string name="dialog_folder_info_device_list">Ordner teilen mit:</string>
<string name="dialog_folder_info_device_list_item">%1$s (%2$s)</string>
<string name="toast_error">Etwas in Syncthing Lite hat nicht funktioniert. Sie können die Details in den Einstellungen von Syncthing Lite anzeigen.</string>
</resources>
+2 -1
View File
@@ -49,4 +49,5 @@
<string name="settings_shutdown_delay_30_seconds">30 segundos</string>
<string name="settings_shutdown_delay_1_minute">1 minuto</string>
<string name="settings_shutdown_delay_5_minutes">5 minutos</string>
</resources>
<string name="dialog_folder_info_device_list_item">%1$s (%2$s)</string>
</resources>
+2 -1
View File
@@ -47,4 +47,5 @@
<string name="settings_shutdown_delay_30_seconds">30 secondes</string>
<string name="settings_shutdown_delay_1_minute">1 minute</string>
<string name="settings_shutdown_delay_5_minutes">5 minutes</string>
</resources>
<string name="dialog_folder_info_device_list_item">%1$s (%2$s)</string>
</resources>
+2 -1
View File
@@ -47,4 +47,5 @@
<string name="settings_shutdown_delay_30_seconds">30 másodperc</string>
<string name="settings_shutdown_delay_1_minute">1 perc</string>
<string name="settings_shutdown_delay_5_minutes">5 perc</string>
</resources>
<string name="dialog_folder_info_device_list_item">%1$s (%2$s)</string>
</resources>
+2 -1
View File
@@ -47,4 +47,5 @@
<string name="settings_shutdown_delay_30_seconds">30 secondi</string>
<string name="settings_shutdown_delay_1_minute">1 minuto</string>
<string name="settings_shutdown_delay_5_minutes">5 minuti</string>
</resources>
<string name="dialog_folder_info_device_list_item">%1$s (%2$s)</string>
</resources>
+1
View File
@@ -41,4 +41,5 @@
<string name="settings_local_device_name">ローカルのデバイス名</string>
<string name="settings_local_device_summary">他のデバイスがこのデバイスを表示する名前</string>
<string name="device_id_dialog_title">デバイス ID を入力</string>
<string name="dialog_folder_info_device_list_item">%1$s (%2$s)</string>
</resources>
@@ -41,4 +41,5 @@
<string name="settings_local_device_name">Naam van lokaal apparaat</string>
<string name="settings_local_device_summary">De naam die dat andere apparaten voor dit apparaat gaan zien</string>
<string name="device_id_dialog_title">Voert nen apparaats-ID in</string>
<string name="dialog_folder_info_device_list_item">%1$s (%2$s)</string>
</resources>
+1
View File
@@ -41,4 +41,5 @@
<string name="settings_local_device_name">Naam van lokaal apparaat</string>
<string name="settings_local_device_summary">De naam die andere apparaten voor dit apparaat zullen zien</string>
<string name="device_id_dialog_title">Voer een apparaats-ID in</string>
<string name="dialog_folder_info_device_list_item">%1$s (%2$s)</string>
</resources>
+21
View File
@@ -40,15 +40,36 @@ fi stocate, dacă vor fi partajate cu terțe entități precum și cum vor fi
<string name="intro_page_three_title">Partajați-vă directoarele</string>
<string name="intro_page_two_description">Introduceți ID-ul Syncthing al unui dispozitiv sau scanați ID-ul unui dispozitiv dintr-un cod QR</string>
<string name="intro_page_three_description">Acceptați acum dispozitivul cu ID-ul %1$s, și partajați un director cu el. S-ar putea să dureze câteva minute până când dispozitivele se vor conecta.</string>
<string name="intro_page_three_searching_device">Se încearcă găsirea celuilalt dispozitiv. Această operație poate dura un moment.</string>
<string name="settings">Setări</string>
<string name="settings_app_version_title">Versiune aplicație</string>
<string name="settings_local_device_name">Nume local dispozitiv</string>
<string name="settings_local_device_summary">Numele pe care celălalt dispozitiv îl va vedea pentru acest dispozitiv</string>
<string name="settings_shutdown_delay_title">Temporizare oprire</string>
<string name="settings_shutdown_delay_summary">După cât timp se va închide clientul Syncthing în funcție de ultima utilizare</string>
<string name="settings_force_stop">Forțează oprirea acestei aplicații</string>
<string name="settings_last_error_title">Ultima eroare</string>
<string name="settings_last_error_summary">Arată detaliile ultimei erori</string>
<string name="settings_report_bug_title">Raportează o eroare</string>
<string name="settings_report_bug_summary">Deschideți un raport de eroare pentru această aplicație pe GitHub</string>
<string name="copy_to_clipboard">Copiază în memorie</string>
<string name="copied_to_clipboard">Copiat în memorie</string>
<string name="device_id_dialog_title">Introduceți ID dispozitiv</string>
<string name="settings_shutdown_delay_10_seconds">10 secunde</string>
<string name="settings_shutdown_delay_30_seconds">30 secunde</string>
<string name="settings_shutdown_delay_1_minute">1 minut</string>
<string name="settings_shutdown_delay_5_minutes">5 minute</string>
<string name="dialog_file_save_as">Salvează ca</string>
<string name="pending_index_updates">%d actualizări de index în așteptare</string>
<string name="device_status_connecting">Conectare la %s</string>
<string name="device_status_connected">Conectat la %s</string>
<string name="device_status_disconnected">Se va încerca conectarea în curând - există %d adrese cunoscute</string>
<string name="device_status_no_address">Nici o adresă cunoscută pentru acest dispozitiv</string>
<string name="dialog_enable_folder_sync_for_new_device_title">Activați sincronizarea directorului pentru un dispozitiv nou</string>
<string name="dialog_enable_folder_sync_for_new_device_text">Doriți să sincronizați %1$s cu %2$s (%3$s)?</string>
<string name="dialog_enable_folder_sync_for_new_device_positive">Se sincronizează</string>
<string name="dialog_enable_folder_sync_for_new_device_negative">Nu se sincronizează</string>
<string name="dialog_folder_info_device_list">Partajează directorul cu:</string>
<string name="dialog_folder_info_device_list_item">%1$s (%2$s)</string>
<string name="toast_error">O eroare s-a produs in Syncthing Lite. Puteți vedea detalii în setările Syncthing Lite.</string>
</resources>
+27
View File
@@ -36,15 +36,42 @@
<string name="intro_page_three_title">Dela dina mappar</string>
<string name="intro_page_two_description">Ange ett Syncthing enhets-ID, eller skanna ett enhets-ID-nummer från en QR-kod</string>
<string name="intro_page_three_description">Acceptera nu enheten med ID %1$s och dela en mapp med den. Det kan ta några minuter tills enheterna ansluter.</string>
<string name="intro_page_three_searching_device">Försöker hitta den andra enheten. Det kan ta ett ögonblick.</string>
<string name="settings">Inställningar</string>
<string name="settings_app_version_title">Appversion</string>
<string name="settings_local_device_name">Lokala enhetens namn</string>
<string name="settings_local_device_summary">Namnet som andra enheter kommer att se för den här enheten</string>
<string name="settings_shutdown_delay_title">Avstängningsfördröjning</string>
<string name="settings_shutdown_delay_summary">Tid innan du stänger av Syncthing-klienten efter den senaste användningen</string>
<string name="settings_force_stop">Tvinga stoppa denna App</string>
<string name="settings_last_error_title">Senaste felet</string>
<string name="settings_last_error_summary">Visa detaljerna för det senaste felet</string>
<string name="settings_report_bug_title">Rapportera ett fel</string>
<string name="settings_report_bug_summary">Öppna problemen för den här appen på GitHub</string>
<string name="copy_to_clipboard">Kopiera till urklipp</string>
<string name="copied_to_clipboard">Kopieras till urklippet</string>
<string name="device_id_dialog_title">Ange enhets-ID</string>
<string name="settings_shutdown_delay_10_seconds">10 sekunder</string>
<string name="settings_shutdown_delay_30_seconds">30 sekunder</string>
<string name="settings_shutdown_delay_1_minute">1 minut</string>
<string name="settings_shutdown_delay_5_minutes">5 minuter</string>
<string name="dialog_warning_reconnect_problem">
På grund av beteendet hos denna App och beteendet hos Syncthing-servern,
du kan inte återansluta i några minuter om appen dödades (på grund av att du tog bort från den senaste applistan)
eller anslutningen avbröts.
Detta gäller inte lokala upptäcktsanslutningar.
</string>
<string name="dialog_file_save_as">Spara som</string>
<string name="pending_index_updates">%d indexuppdateringar som väntar</string>
<string name="device_status_connecting">Ansluter till %s</string>
<string name="device_status_connected">Ansluten till %s</string>
<string name="device_status_disconnected">Kommer att försöka ansluta snart - det finns%d kända adresser</string>
<string name="device_status_no_address">Ingen känd adress för enheten</string>
<string name="dialog_enable_folder_sync_for_new_device_title">Aktivera mappsynkronisering för ny enhet</string>
<string name="dialog_enable_folder_sync_for_new_device_text">Vill du synkronisera %1$s med %2$s (%3$s)?</string>
<string name="dialog_enable_folder_sync_for_new_device_positive">Synkronisera</string>
<string name="dialog_enable_folder_sync_for_new_device_negative">Synkronisera inte</string>
<string name="dialog_folder_info_device_list">Dela mapp med:</string>
<string name="dialog_folder_info_device_list_item">%1$s (%2$s)</string>
<string name="toast_error">Något gick fel i Syncthing Lite. Du kan visa detaljerna från inställningarna för Syncthing Lite.</string>
</resources>
+2 -1
View File
@@ -46,4 +46,5 @@
<string name="settings_shutdown_delay_30_seconds">30 秒</string>
<string name="settings_shutdown_delay_1_minute">1 分钟</string>
<string name="settings_shutdown_delay_5_minutes">5 分钟</string>
</resources>
<string name="dialog_folder_info_device_list_item">%1$s%2$s</string>
</resources>
+20 -2
View File
@@ -36,14 +36,20 @@
<string name="intro_page_three_title">Share your folders</string>
<string name="intro_page_two_description">Enter a Syncthing device ID, or scan a device ID from a QR code</string>
<string name="intro_page_three_description">Now accept the device with ID %1$s, and share a folder with it. It may take a few minutes until the devices connect.</string>
<string name="intro_page_three_searching_device">Trying to find the other device. This may take a moment.</string>
<string name="settings">Settings</string>
<string name="settings_app_version_title">App version</string>
<string name="settings_local_device_name">Local device name</string>
<string name="settings_local_device_summary">The name that other devices will see for this device</string>
<string name="settings_shutdown_delay_title">Shutdown delay</string>
<string name="settings_shutdown_delay_summary">Time before shuting down the Syncthing client after its last usage</string>
<string name="settings_crash_handler_title">Custom Crash-Handler</string>
<string name="settings_crash_handler_summary">Copy the error message to the clipboard when the App crashes</string>
<string name="settings_force_stop">Force stop this App</string>
<string name="settings_last_error_title">Last error</string>
<string name="settings_last_error_summary">View the details of the last error</string>
<string name="settings_report_bug_title">Report a bug</string>
<string name="settings_report_bug_summary">Open the issues for this App at GitHub</string>
<string name="copy_to_clipboard">Copy to clipboard</string>
<string name="copied_to_clipboard">Copied to the clipboard</string>
<string name="device_id_dialog_title">Enter Device ID</string>
<string name="settings_shutdown_delay_10_seconds">10 seconds</string>
<string name="settings_shutdown_delay_30_seconds">30 seconds</string>
@@ -56,4 +62,16 @@
This does not apply to local discovery connections.
</string>
<string name="dialog_file_save_as">Save as</string>
<string name="pending_index_updates">%d index updates pending</string>
<string name="device_status_connecting">Connecting to %s</string>
<string name="device_status_connected">Connected to %s</string>
<string name="device_status_disconnected">Will retry connecting soon - there are %d known addresses</string>
<string name="device_status_no_address">No known address for the device</string>
<string name="dialog_enable_folder_sync_for_new_device_title">Enable folder sync for new device</string>
<string name="dialog_enable_folder_sync_for_new_device_text">Do you want to sync %1$s with %2$s (%3$s)?</string>
<string name="dialog_enable_folder_sync_for_new_device_positive">Sync</string>
<string name="dialog_enable_folder_sync_for_new_device_negative">Do not sync</string>
<string name="dialog_folder_info_device_list">Share folder with:</string>
<string name="dialog_folder_info_device_list_item">%1$s (%2$s)</string>
<string name="toast_error">Something went wrong in Syncthing Lite. You can view the details from the settings of Syncthing Lite.</string>
</resources>
+13 -4
View File
@@ -24,15 +24,24 @@
-->
<CheckBoxPreference
android:key="crash_handler"
android:title="@string/settings_crash_handler_title"
android:summary="@string/settings_crash_handler_summary" />
<Preference
android:key="last_crash"
android:title="@string/settings_last_error_title"
android:summary="@string/settings_last_error_summary" />
<Preference
android:key="app_version"
android:title="@string/settings_app_version_title"/>
<Preference
android:key="report_bug"
android:title="@string/settings_report_bug_title"
android:summary="@string/settings_report_bug_summary" />
<Preference
android:key="force_stop"
android:title="@string/settings_force_stop" />
</PreferenceCategory>
</PreferenceScreen>
+2 -2
View File
@@ -2,8 +2,8 @@
buildscript {
ext.kotlin_version = '1.3.0'
ext.support_version = '27.0.2'
ext.build_tools_version = '3.2.0'
ext.support_version = '27.1.1'
ext.build_tools_version = '3.2.1'
ext.anko_version = '0.10.8'
ext.protobuf_lite_version = '3.0.1'
repositories {
+1 -1
View File
@@ -1 +1 @@
include ':app', ':syncthing-repository-android', ':syncthing-repository-default', ':syncthing-relay-client', ':syncthing-bep', ':syncthing-core', ':syncthing-client', ':syncthing-discovery', ':syncthing-client-cli'
include ':app', ':syncthing-repository-android', ':syncthing-repository-default', ':syncthing-relay-client', ':syncthing-bep', ':syncthing-core', ':syncthing-client', ':syncthing-discovery', ':syncthing-client-cli', ':syncthing-temp-repository-encryption'
@@ -18,6 +18,7 @@ import com.google.protobuf.ByteString
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.Channel
import net.syncthing.java.bep.connectionactor.ConnectionActorWrapper
import net.syncthing.java.bep.index.IndexHandler
import net.syncthing.java.bep.utils.longSumBy
import net.syncthing.java.core.beans.BlockInfo
import net.syncthing.java.core.beans.FileBlocks
@@ -17,10 +17,16 @@ package net.syncthing.java.bep
import com.google.protobuf.ByteString
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.async
import kotlinx.coroutines.channels.consumeEach
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import net.syncthing.java.bep.BlockExchangeProtos.Vector
import net.syncthing.java.bep.connectionactor.ConnectionActorWrapper
import net.syncthing.java.core.beans.*
import net.syncthing.java.bep.index.*
import net.syncthing.java.core.beans.BlockInfo
import net.syncthing.java.core.beans.DeviceId
import net.syncthing.java.core.beans.FileInfo.Version
import net.syncthing.java.core.beans.FolderStats
import net.syncthing.java.core.utils.BlockUtils
import net.syncthing.java.core.utils.NetworkUtils
import org.apache.commons.io.IOUtils
@@ -46,7 +52,7 @@ class BlockPusher(private val localDeviceId: DeviceId,
private val logger = LoggerFactory.getLogger(javaClass)
suspend fun pushDelete(folderId: String, targetPath: String): BlockExchangeProtos.IndexUpdate {
val fileInfo = indexHandler.waitForRemoteIndexAcquired(connectionHandler).getFileInfoByPath(folderId, targetPath)!!
val fileInfo = indexHandler.waitForRemoteIndexAcquiredWithTimeout(connectionHandler).getFileInfoByPath(folderId, targetPath)!!
NetworkUtils.assertProtocol(connectionHandler.hasFolder(fileInfo.folder), {"supplied connection handler $connectionHandler will not share folder ${fileInfo.folder}"})
return sendIndexUpdate(folderId, BlockExchangeProtos.FileInfo.newBuilder()
.setName(targetPath)
@@ -62,7 +68,7 @@ class BlockPusher(private val localDeviceId: DeviceId,
}
suspend fun pushFile(inputStream: InputStream, folderId: String, targetPath: String): FileUploadObserver {
val fileInfo = indexHandler.waitForRemoteIndexAcquired(connectionHandler).getFileInfoByPath(folderId, targetPath)
val fileInfo = indexHandler.waitForRemoteIndexAcquiredWithTimeout(connectionHandler).getFileInfoByPath(folderId, targetPath)
NetworkUtils.assertProtocol(connectionHandler.hasFolder(folderId), {"supplied connection handler $connectionHandler will not share folder $folderId"})
assert(fileInfo == null || fileInfo.folder == folderId)
assert(fileInfo == null || fileInfo.path == targetPath)
@@ -99,20 +105,26 @@ class BlockPusher(private val localDeviceId: DeviceId,
}
logger.debug("send index update for file = {}", targetPath)
val indexListener = { folderInfo: FolderInfo, newRecords: List<FileInfo>, _: IndexInfo ->
if (folderInfo.folderId == folderId) {
for (fileInfo2 in newRecords) {
if (fileInfo2.path == targetPath && fileInfo2.hash == dataSource.getHash()) { //TODO check not invalid
// sentBlocks.addAll(dataSource.getHashes());
isCompleted.set(true)
synchronized(updateLock) {
updateLock.notifyAll()
val indexListenerStream = indexHandler.subscribeToOnIndexUpdateEvents()
GlobalScope.launch {
indexListenerStream.consumeEach { event ->
if (event is IndexRecordAcquiredEvent) {
val (indexFolderId, newRecords, _) = event
if (indexFolderId == folderId) {
for (fileInfo2 in newRecords) {
if (fileInfo2.path == targetPath && fileInfo2.hash == dataSource.getHash()) { //TODO check not invalid
// sentBlocks.addAll(dataSource.getHashes());
isCompleted.set(true)
synchronized(updateLock) {
updateLock.notifyAll()
}
}
}
}
}
}
}
indexHandler.registerOnIndexRecordAcquiredListener(indexListener)
val indexUpdate = sendIndexUpdate(folderId, BlockExchangeProtos.FileInfo.newBuilder()
.setName(targetPath)
.setSize(fileSize)
@@ -128,9 +140,27 @@ class BlockPusher(private val localDeviceId: DeviceId,
override fun close() {
logger.debug("closing upload process")
monitoringProcessExecutorService.shutdown()
indexHandler.unregisterOnIndexRecordAcquiredListener(indexListener)
indexListenerStream.cancel()
requestHandlerRegistry.unregisterListener(requestFilter)
val fileInfo1 = indexHandler.pushRecord(indexUpdate.folder, indexUpdate.filesList.single())
val (fileInfo1, folderStatsUpdate) = indexHandler.indexRepository.runInTransaction {
val folderStatsUpdateCollector = FolderStatsUpdateCollector(folderId)
// TODO: notify the IndexBrowsers again (as it was earlier)
val fileInfo = IndexElementProcessor.pushRecord(
it,
indexUpdate.folder,
indexUpdate.filesList.single(),
folderStatsUpdateCollector,
it.findFileInfo(folderId, indexUpdate.filesList.single().name)
)
IndexMessageProcessor.handleFolderStatsUpdate(it, folderStatsUpdateCollector)
val folderStatsUpdate = it.findFolderStats(folderId) ?: FolderStats.createDummy(folderId)
fileInfo to folderStatsUpdate
}
runBlocking { indexHandler.sendFolderStatsUpdate(folderStatsUpdate) }
logger.info("sent file info record = {}", fileInfo1)
}
@@ -151,7 +181,7 @@ class BlockPusher(private val localDeviceId: DeviceId,
private suspend fun sendIndexUpdate(folderId: String, fileInfoBuilder: BlockExchangeProtos.FileInfo.Builder,
oldVersions: Iterable<Version>?): BlockExchangeProtos.IndexUpdate {
run {
val nextSequence = indexHandler.sequencer().nextSequence()
val nextSequence = indexHandler.getNextSequenceNumber()
val list = oldVersions ?: emptyList()
logger.debug("version list = {}", list)
val id = ByteBuffer.wrap(localDeviceId.toHashData()).long
@@ -1,58 +0,0 @@
/*
* Copyright (C) 2016 Davide Imbriaco
*
* This Java file is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.syncthing.java.bep
import net.syncthing.java.core.beans.FolderInfo
import net.syncthing.java.core.beans.FolderStats
import net.syncthing.java.core.interfaces.IndexRepository
import java.io.Closeable
class FolderBrowser internal constructor(private val indexHandler: IndexHandler) : Closeable {
private val folderStatsCache = mutableMapOf<String, FolderStats>()
private val indexRepositoryEventListener = { event: IndexRepository.FolderStatsUpdatedEvent ->
addFolderStats(event.getFolderStats())
}
fun folderInfoAndStatsList(): List<Pair<FolderInfo, FolderStats>> =
indexHandler.folderInfoList()
.map { folderInfo -> Pair(folderInfo, getFolderStats(folderInfo.folderId)) }
.sortedBy { it.first.label }
init {
indexHandler.indexRepository.setOnFolderStatsUpdatedListener(indexRepositoryEventListener)
addFolderStats(indexHandler.indexRepository.findAllFolderStats())
}
private fun addFolderStats(folderStatsList: List<FolderStats>) {
for (folderStats in folderStatsList) {
folderStatsCache.put(folderStats.folderId, folderStats)
}
}
fun getFolderStats(folder: String): FolderStats {
return folderStatsCache[folder] ?: let {
FolderStats.Builder()
.setFolder(folder)
.build()
}
}
fun getFolderInfo(folder: String): FolderInfo? {
return indexHandler.getFolderInfo(folder)
}
override fun close() {
indexHandler.indexRepository.setOnFolderStatsUpdatedListener(null)
}
}
@@ -1,184 +0,0 @@
/*
* Copyright (C) 2016 Davide Imbriaco
*
* This Java file is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.syncthing.java.bep
import net.syncthing.java.core.beans.FileInfo
import net.syncthing.java.core.interfaces.IndexRepository
import net.syncthing.java.core.utils.PathUtils
import net.syncthing.java.core.utils.awaitTerminationSafe
import net.syncthing.java.core.utils.submitLogging
import org.apache.commons.lang3.StringUtils
import org.slf4j.LoggerFactory
import java.io.Closeable
import java.util.*
import java.util.concurrent.Executors
class IndexBrowser internal constructor(private val indexRepository: IndexRepository, private val indexHandler: IndexHandler,
val folder: String, private val includeParentInList: Boolean = false,
private val allowParentInRoot: Boolean = false, ordering: Comparator<FileInfo>?) : Closeable {
private fun isParent(fileInfo: FileInfo) = PathUtils.isParent(fileInfo.path)
val ALPHA_ASC_DIR_FIRST: Comparator<FileInfo> =
compareBy<FileInfo>({!isParent(it)}, {!it.isDirectory()})
.thenBy { it.fileName.toLowerCase() }
val LAST_MOD_DESC: Comparator<FileInfo> =
compareBy<FileInfo>({!isParent(it)}, {it.lastModified})
.thenBy { it.fileName.toLowerCase() }
private val ordering = ordering ?: ALPHA_ASC_DIR_FIRST
private val logger = LoggerFactory.getLogger(javaClass)
var currentPath: String = PathUtils.ROOT_PATH
private set
private val PARENT_FILE_INFO: FileInfo
private val ROOT_FILE_INFO: FileInfo
private val executorService = Executors.newSingleThreadScheduledExecutor()
private val preloadJobs = mutableSetOf<String>()
private val preloadJobsLock = Any()
private var mOnPathChangedListener: (() -> Unit)? = null
private fun isCacheReady(): Boolean {
synchronized(preloadJobsLock) {
return preloadJobs.isEmpty()
}
}
internal fun onIndexChangedevent(folder: String) {
if (folder == this.folder) {
preloadFileInfoForCurrentPath()
}
}
fun currentPathInfo(): FileInfo = getFileInfoByAbsolutePath(currentPath)
fun currentPathFileName(): String? = PathUtils.getFileName(currentPath)
fun isRoot(): Boolean = PathUtils.isRoot(currentPath)
init {
assert(folder.isNotEmpty())
PARENT_FILE_INFO = FileInfo(folder = folder, type = FileInfo.FileType.DIRECTORY, path = PathUtils.PARENT_PATH)
ROOT_FILE_INFO = FileInfo(folder = folder, type = FileInfo.FileType.DIRECTORY, path = PathUtils.ROOT_PATH)
navigateToAbsolutePath(PathUtils.ROOT_PATH)
}
fun setOnFolderChangedListener(onPathChangedListener: (() -> Unit)?) {
mOnPathChangedListener = onPathChangedListener
}
private fun preloadFileInfoForCurrentPath() {
logger.debug("trigger preload for folder = '{}'", folder)
synchronized(preloadJobsLock) {
currentPath.let<String, Any> { currentPath ->
if (preloadJobs.contains(currentPath)) {
preloadJobs.remove(currentPath)
preloadJobs.add(currentPath) ///add last
} else {
preloadJobs.add(currentPath)
executorService.submitLogging(object : Runnable {
override fun run() {
val preloadPath =
synchronized(preloadJobsLock) {
assert(!preloadJobs.isEmpty())
preloadJobs.last() //pop last job
}
logger.info("folder preload BEGIN for folder = '{}' path = '{}'", folder, preloadPath)
getFileInfoByAbsolutePath(preloadPath)
if (!PathUtils.isRoot(preloadPath)) {
val parent = PathUtils.getParentPath(preloadPath)
getFileInfoByAbsolutePath(parent)
listFiles(parent)
}
for (record in listFiles(preloadPath)) {
if (record.path == PARENT_FILE_INFO.path && record.isDirectory()) {
listFiles(record.path)
}
}
logger.info("folder preload END for folder = '{}' path = '{}'", folder, preloadPath)
synchronized(preloadJobsLock) {
preloadJobs.remove(preloadPath)
if (isCacheReady()) {
logger.info("cache ready, notify listeners")
mOnPathChangedListener?.invoke()
} else {
logger.info("still {} job[s] left in cache loader", preloadJobs.size)
executorService.submitLogging(this)
}
}
}
})
}
}
}
}
fun listFiles(path: String = currentPath): List<FileInfo> {
logger.debug("doListFiles for path = '{}' BEGIN", path)
val list = ArrayList(indexRepository.findNotDeletedFilesByFolderAndParent(folder, path))
logger.debug("doListFiles for path = '{}' : {} records loaded)", path, list.size)
if (includeParentInList && (!PathUtils.isRoot(path) || allowParentInRoot)) {
list.add(0, PARENT_FILE_INFO)
}
return list.sortedWith(ordering)
}
fun getFileInfoByAbsolutePath(path: String): FileInfo {
return if (PathUtils.isRoot(path)) {
ROOT_FILE_INFO
} else {
logger.debug("doGetFileInfoByAbsolutePath for path = '{}' BEGIN", path)
val fileInfo = indexRepository.findNotDeletedFileInfo(folder, path) ?: error("file not found for path = $path")
logger.debug("doGetFileInfoByAbsolutePath for path = '{}' END", path)
fileInfo
}
}
fun navigateTo(fileInfo: FileInfo) {
assert(fileInfo.isDirectory())
assert(fileInfo.folder == folder)
return if (fileInfo.path == PARENT_FILE_INFO.path)
navigateToAbsolutePath(PathUtils.getParentPath(currentPath))
else
navigateToAbsolutePath(fileInfo.path)
}
fun navigateToNearestPath(oldPath: String) {
if (!StringUtils.isBlank(oldPath)) {
navigateToAbsolutePath(oldPath)
}
}
private fun navigateToAbsolutePath(newPath: String) {
if (PathUtils.isRoot(newPath)) {
currentPath = PathUtils.ROOT_PATH
} else {
val fileInfo = getFileInfoByAbsolutePath(newPath)
assert(fileInfo.isDirectory(), {"cannot navigate to path ${fileInfo.path}: not a directory"})
currentPath = fileInfo.path
}
logger.info("navigate to path = '{}'", currentPath)
preloadFileInfoForCurrentPath()
}
override fun close() {
logger.info("closing")
indexHandler.unregisterIndexBrowser(this)
executorService.shutdown()
executorService.awaitTerminationSafe()
}
}
@@ -1,451 +0,0 @@
/*
* Copyright (C) 2016 Davide Imbriaco
*
* This Java file is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.syncthing.java.bep
import net.syncthing.java.bep.connectionactor.ClusterConfigInfo
import net.syncthing.java.bep.connectionactor.ConnectionActorWrapper
import net.syncthing.java.core.beans.*
import net.syncthing.java.core.beans.FileInfo.Version
import net.syncthing.java.core.configuration.Configuration
import net.syncthing.java.core.interfaces.IndexRepository
import net.syncthing.java.core.interfaces.Sequencer
import net.syncthing.java.core.interfaces.TempRepository
import net.syncthing.java.core.utils.NetworkUtils
import net.syncthing.java.core.utils.awaitTerminationSafe
import net.syncthing.java.core.utils.trySubmitLogging
import org.apache.commons.lang3.tuple.Pair
import org.bouncycastle.util.encoders.Hex
import org.slf4j.LoggerFactory
import java.io.Closeable
import java.io.IOException
import java.util.*
import java.util.concurrent.Executors
class IndexHandler(private val configuration: Configuration, val indexRepository: IndexRepository,
private val tempRepository: TempRepository) : Closeable {
private val logger = LoggerFactory.getLogger(javaClass)
private val folderInfoByFolder = mutableMapOf<String, FolderInfo>()
private val indexMessageProcessor = IndexMessageProcessor()
private var lastIndexActivity: Long = 0
private val writeAccessLock = Object()
private val indexWaitLock = Object()
private val indexBrowsers = mutableSetOf<IndexBrowser>()
private val onIndexRecordAcquiredListeners = mutableSetOf<(FolderInfo, List<FileInfo>, IndexInfo) -> Unit>()
private val onFullIndexAcquiredListeners = mutableSetOf<(FolderInfo) -> Unit>()
private fun lastActive(): Long = System.currentTimeMillis() - lastIndexActivity
fun sequencer(): Sequencer = indexRepository.getSequencer()
fun folderList(): List<String> = folderInfoByFolder.keys.toList()
fun folderInfoList(): List<FolderInfo> = folderInfoByFolder.values.toList()
private fun markActive() {
lastIndexActivity = System.currentTimeMillis()
}
fun registerOnIndexRecordAcquiredListener(listener: (FolderInfo, List<FileInfo>, IndexInfo) -> Unit) {
onIndexRecordAcquiredListeners.add(listener)
}
fun unregisterOnIndexRecordAcquiredListener(listener: (FolderInfo, List<FileInfo>, IndexInfo) -> Unit) {
assert(onIndexRecordAcquiredListeners.contains(listener))
onIndexRecordAcquiredListeners.remove(listener)
}
fun registerOnFullIndexAcquiredListenersListener(listener: (FolderInfo) -> Unit) {
onFullIndexAcquiredListeners.add(listener)
}
fun unregisterOnFullIndexAcquiredListenersListener(listener: (FolderInfo) -> Unit) {
assert(onFullIndexAcquiredListeners.contains(listener))
onFullIndexAcquiredListeners.remove(listener)
}
init {
loadFolderInfoFromConfig()
}
private fun loadFolderInfoFromConfig() {
synchronized(writeAccessLock) {
for (folderInfo in configuration.folders) {
folderInfoByFolder.put(folderInfo.folderId, folderInfo) //TODO reference 'folder info' repository
}
}
}
@Synchronized
fun clearIndex() {
synchronized(writeAccessLock) {
indexRepository.clearIndex()
folderInfoByFolder.clear()
loadFolderInfoFromConfig()
}
}
internal fun isRemoteIndexAcquired(clusterConfigInfo: ClusterConfigInfo, peerDeviceId: DeviceId): Boolean {
var ready = true
for (folder in clusterConfigInfo.sharedFolderIds) {
val indexSequenceInfo = indexRepository.findIndexInfoByDeviceAndFolder(peerDeviceId, folder)
if (indexSequenceInfo == null || indexSequenceInfo.localSequence < indexSequenceInfo.maxSequence) {
logger.debug("waiting for index on folder = {} sequenceInfo = {}", folder, indexSequenceInfo)
ready = false
}
}
return ready
}
@Throws(InterruptedException::class)
fun waitForRemoteIndexAcquired(connectionHandler: ConnectionActorWrapper, timeoutSecs: Long? = null): IndexHandler {
val timeoutMillis = (timeoutSecs ?: DEFAULT_INDEX_TIMEOUT) * 1000
synchronized(indexWaitLock) {
while (!isRemoteIndexAcquired(connectionHandler.getClusterConfig(), connectionHandler.deviceId)) {
indexWaitLock.wait(timeoutMillis)
NetworkUtils.assertProtocol(/* TODO connectionHandler.getLastActive() < timeoutMillis || */ lastActive() < timeoutMillis,
{"unable to acquire index from connection $connectionHandler, timeout reached!"})
}
}
logger.debug("acquired all indexes on connection {}", connectionHandler)
return this
}
fun handleClusterConfigMessageProcessedEvent(clusterConfig: BlockExchangeProtos.ClusterConfig) {
synchronized(writeAccessLock) {
for (folderRecord in clusterConfig.foldersList) {
val folder = folderRecord.id
val folderInfo = updateFolderInfo(folder, folderRecord.label)
logger.debug("acquired folder info from cluster config = {}", folderInfo)
for (deviceRecord in folderRecord.devicesList) {
val deviceId = DeviceId.fromHashData(deviceRecord.id.toByteArray())
if (deviceRecord.hasIndexId() && deviceRecord.hasMaxSequence()) {
val folderIndexInfo = updateIndexInfo(folder, deviceId, deviceRecord.indexId, deviceRecord.maxSequence, null)
logger.debug("acquired folder index info from cluster config = {}", folderIndexInfo)
}
}
}
}
}
internal fun handleIndexMessageReceivedEvent(folderId: String, filesList: List<BlockExchangeProtos.FileInfo>, clusterConfigInfo: ClusterConfigInfo, peerDeviceId: DeviceId) {
indexMessageProcessor.handleIndexMessageReceivedEvent(folderId, filesList, clusterConfigInfo, peerDeviceId)
}
fun pushRecord(folder: String, bepFileInfo: BlockExchangeProtos.FileInfo): FileInfo? {
var fileBlocks: FileBlocks? = null
val builder = FileInfo.Builder()
.setFolder(folder)
.setPath(bepFileInfo.name)
.setLastModified(Date(bepFileInfo.modifiedS * 1000 + bepFileInfo.modifiedNs / 1000000))
.setVersionList((if (bepFileInfo.hasVersion()) bepFileInfo.version.countersList else null ?: emptyList()).map { record -> Version(record.id, record.value) })
.setDeleted(bepFileInfo.deleted)
when (bepFileInfo.type) {
BlockExchangeProtos.FileInfoType.FILE -> {
fileBlocks = FileBlocks(folder, builder.getPath()!!, ((bepFileInfo.blocksList ?: emptyList())).map { record ->
BlockInfo(record.offset, record.size, Hex.toHexString(record.hash.toByteArray()))
})
builder
.setTypeFile()
.setHash(fileBlocks.hash)
.setSize(bepFileInfo.size)
}
BlockExchangeProtos.FileInfoType.DIRECTORY -> builder.setTypeDir()
else -> {
logger.warn("unsupported file type = {}, discarding file info", bepFileInfo.type)
return null
}
}
return addRecord(builder.build(), fileBlocks)
}
private fun updateIndexInfo(folder: String, deviceId: DeviceId, indexId: Long?, maxSequence: Long?, localSequence: Long?): IndexInfo {
synchronized(writeAccessLock) {
var indexSequenceInfo = indexRepository.findIndexInfoByDeviceAndFolder(deviceId, folder)
var shouldUpdate = false
val builder: IndexInfo.Builder
if (indexSequenceInfo == null) {
shouldUpdate = true
assert(indexId != null, {"index sequence info not found, and supplied null index id (folder = $folder, device = $deviceId)"})
builder = IndexInfo.newBuilder()
.setFolder(folder)
.setDeviceId(deviceId.deviceId)
.setIndexId(indexId!!)
.setLocalSequence(0)
.setMaxSequence(-1)
} else {
builder = indexSequenceInfo.copyBuilder()
}
if (indexId != null && indexId != builder.getIndexId()) {
shouldUpdate = true
builder.setIndexId(indexId)
}
if (maxSequence != null && maxSequence > builder.getMaxSequence()) {
shouldUpdate = true
builder.setMaxSequence(maxSequence)
}
if (localSequence != null && localSequence > builder.getLocalSequence()) {
shouldUpdate = true
builder.setLocalSequence(localSequence)
}
if (shouldUpdate) {
indexSequenceInfo = builder.build()
indexRepository.updateIndexInfo(indexSequenceInfo)
}
return indexSequenceInfo!!
}
}
private fun addRecord(record: FileInfo, fileBlocks: FileBlocks?): FileInfo? {
synchronized(writeAccessLock) {
val lastModified = indexRepository.findFileInfoLastModified(record.folder, record.path)
return if (lastModified != null && record.lastModified < lastModified) {
logger.trace("discarding record = {}, modified before local record", record)
null
} else {
indexRepository.updateFileInfo(record, fileBlocks)
logger.trace("loaded new record = {}", record)
indexBrowsers.forEach {
it.onIndexChangedevent(record.folder)
}
record
}
}
}
fun getFileInfoByPath(folder: String, path: String): FileInfo? {
return indexRepository.findFileInfo(folder, path)
}
fun getFileInfoAndBlocksByPath(folder: String, path: String): Pair<FileInfo, FileBlocks>? {
val fileInfo = getFileInfoByPath(folder, path)
return if (fileInfo == null) {
null
} else {
assert(fileInfo.isFile())
val fileBlocks = indexRepository.findFileBlocks(folder, path)
checkNotNull(fileBlocks) {"file blocks not found for file info = $fileInfo"}
FileInfo.checkBlocks(fileInfo, fileBlocks)
Pair.of(fileInfo, fileBlocks)
}
}
private fun updateFolderInfo(folder: String, label: String?): FolderInfo {
var folderInfo: FolderInfo? = folderInfoByFolder[folder]
if (folderInfo == null || label.isNullOrEmpty()) {
folderInfo = FolderInfo(folder, label)
folderInfoByFolder.put(folderInfo.folderId, folderInfo)
}
return folderInfo
}
fun getFolderInfo(folder: String): FolderInfo? {
return folderInfoByFolder[folder]
}
fun getIndexInfo(device: DeviceId, folder: String): IndexInfo? {
return indexRepository.findIndexInfoByDeviceAndFolder(device, folder)
}
fun newFolderBrowser(): FolderBrowser {
return FolderBrowser(this)
}
fun newIndexBrowser(folder: String, includeParentInList: Boolean = false, allowParentInRoot: Boolean = false,
ordering: Comparator<FileInfo>? = null): IndexBrowser {
val indexBrowser = IndexBrowser(indexRepository, this, folder, includeParentInList, allowParentInRoot, ordering)
indexBrowsers.add(indexBrowser)
return indexBrowser
}
internal fun unregisterIndexBrowser(indexBrowser: IndexBrowser) {
assert(indexBrowsers.contains(indexBrowser))
indexBrowsers.remove(indexBrowser)
}
override fun close() {
assert(indexBrowsers.isEmpty())
assert(onIndexRecordAcquiredListeners.isEmpty())
assert(onFullIndexAcquiredListeners.isEmpty())
indexMessageProcessor.stop()
}
private inner class IndexMessageProcessor {
private val executorService = Executors.newSingleThreadExecutor()
private var queuedMessages = 0
private var queuedRecords: Long = 0
// private long lastRecordProcessingTime = 0;
// , delay = 0;
// private boolean addProcessingDelayForInterface = true;
// private final int MIN_DELAY = 0, MAX_DELAY = 5000, MAX_RECORD_PER_PROCESS = 16, DELAY_FACTOR = 1;
private var startTime: Long? = null
fun handleIndexMessageReceivedEvent(folderId: String, filesList: List<BlockExchangeProtos.FileInfo>, clusterConfigInfo: ClusterConfigInfo, peerDeviceId: DeviceId) {
logger.info("received index message event, preparing (queued records = {} event record count = {})", queuedRecords, filesList.size)
markActive()
// List<BlockExchangeProtos.FileInfo> fileList = event.getFilesList();
// for (int index = 0; index < fileList.size(); index += MAX_RECORD_PER_PROCESS) {
// BlockExchangeProtos.IndexUpdate data = BlockExchangeProtos.IndexUpdate.newBuilder()
// .addAllFiles(Iterables.limit(Iterables.skip(fileList, index), MAX_RECORD_PER_PROCESS))
// .setFolder(event.getFolder())
// .build();
// if (queuedMessages > 0) {
// storeAndProcessBg(data, clusterConfigInfo, peerDeviceId);
// } else {
// processBg(data, clusterConfigInfo, peerDeviceId);
// }
// }
val data = BlockExchangeProtos.IndexUpdate.newBuilder()
.addAllFiles(filesList)
.setFolder(folderId)
.build()
if (queuedMessages > 0) {
storeAndProcessBg(data, clusterConfigInfo, peerDeviceId)
} else {
processBg(data, clusterConfigInfo, peerDeviceId)
}
}
private fun processBg(data: BlockExchangeProtos.IndexUpdate, clusterConfigInfo: ClusterConfigInfo?, peerDeviceId: DeviceId) {
logger.debug("received index message event, queuing for processing")
queuedMessages++
queuedRecords += data.filesCount.toLong()
executorService.trySubmitLogging(object : ProcessingRunnable() {
override fun runProcess() {
doHandleIndexMessageReceivedEvent(data, clusterConfigInfo, peerDeviceId)
}
})
}
private fun storeAndProcessBg(data: BlockExchangeProtos.IndexUpdate, clusterConfigInfo: ClusterConfigInfo?, peerDeviceId: DeviceId) {
val key = tempRepository.pushTempData(data.toByteArray())
logger.debug("received index message event, stored to temp record {}, queuing for processing", key)
queuedMessages++
queuedRecords += data.filesCount.toLong()
executorService.trySubmitLogging(object : ProcessingRunnable() {
override fun runProcess() {
try {
doHandleIndexMessageReceivedEvent(key, clusterConfigInfo, peerDeviceId)
} catch (ex: IOException) {
logger.error("error processing index message", ex)
}
}
})
}
private abstract inner class ProcessingRunnable : Runnable {
override fun run() {
startTime = System.currentTimeMillis()
runProcess()
queuedMessages--
// lastRecordProcessingTime = stopwatch.elapsed(TimeUnit.MILLISECONDS) - delay;
// logger.info("processed a bunch of records, {}*{} remaining", queuedMessages, MAX_RECORD_PER_PROCESS);
// logger.debug("processed index message in {} secs", lastRecordProcessingTime / 1000d);
startTime = null
}
protected abstract fun runProcess()
// private boolean isVersionOlderThanSequence(BlockExchangeProtos.FileInfo fileInfo, long localSequence) {
// long fileSequence = fileInfo.getSequence();
// //TODO should we check last version instead of sequence? verify
// return fileSequence < localSequence;
// }
@Throws(IOException::class)
protected fun doHandleIndexMessageReceivedEvent(key: String, clusterConfigInfo: ClusterConfigInfo?, peerDeviceId: DeviceId) {
logger.debug("processing index message event from temp record {}", key)
markActive()
val data = tempRepository.popTempData(key)
val message = BlockExchangeProtos.IndexUpdate.parseFrom(data)
doHandleIndexMessageReceivedEvent(message, clusterConfigInfo, peerDeviceId)
}
protected fun doHandleIndexMessageReceivedEvent(message: BlockExchangeProtos.IndexUpdate, clusterConfigInfo: ClusterConfigInfo?, peerDeviceId: DeviceId) {
// synchronized (writeAccessLock) {
// if (addProcessingDelayForInterface) {
// delay = Math.min(MAX_DELAY, Math.max(MIN_DELAY, lastRecordProcessingTime * DELAY_FACTOR));
// logger.info("add delay of {} secs before processing index message (to allow UI to process)", delay / 1000d);
// try {
// Thread.sleep(delay);
// } catch (InterruptedException ex) {
// logger.warn("interrupted", ex);
// }
// } else {
// delay = 0;
// }
logger.info("processing index message with {} records (queue size: messages = {} records = {})", message.filesCount, queuedMessages, queuedRecords)
// String deviceId = connectionHandler.getDeviceId();
val folderId = message.folder
var sequence: Long = -1
val newRecords = mutableListOf<FileInfo>()
// IndexInfo oldIndexInfo = indexRepository.findIndexInfoByDeviceAndFolder(deviceId, folder);
// Stopwatch stopwatch = Stopwatch.createStarted();
logger.debug("processing {} index records for folder {}", message.filesList.size, folderId)
for (fileInfo in message.filesList) {
markActive()
// if (oldIndexInfo != null && isVersionOlderThanSequence(fileInfo, oldIndexInfo.getLocalSequence())) {
// logger.trace("skipping file {}, version older than sequence {}", fileInfo, oldIndexInfo.getLocalSequence());
// } else {
val newRecord = pushRecord(folderId, fileInfo)
if (newRecord != null) {
newRecords.add(newRecord)
}
sequence = Math.max(fileInfo.sequence, sequence)
markActive()
// }
}
val newIndexInfo = updateIndexInfo(folderId, peerDeviceId, null, null, sequence)
val elap = System.currentTimeMillis() - startTime!!
queuedRecords -= message.filesCount.toLong()
logger.info("processed {} index records, acquired {} ({} secs, {} record/sec)", message.filesCount, newRecords.size, elap / 1000.0, Math.round(message.filesCount / (elap / 1000.0) * 100) / 100.0)
if (logger.isInfoEnabled && newRecords.size <= 10) {
for (fileInfo in newRecords) {
logger.info("acquired record = {}", fileInfo)
}
}
val folderInfo = folderInfoByFolder[folderId]
if (!newRecords.isEmpty()) {
onIndexRecordAcquiredListeners.forEach { it(folderInfo!!, newRecords, newIndexInfo) }
}
logger.debug("index info = {}", newIndexInfo)
if (isRemoteIndexAcquired(clusterConfigInfo!!, peerDeviceId)) {
logger.debug("index acquired")
onFullIndexAcquiredListeners.forEach { it(folderInfo!!)}
}
// IndexHandler.this.notifyAll();
markActive()
synchronized(indexWaitLock) {
indexWaitLock.notifyAll()
}
}
}
fun stop() {
logger.info("stopping index record processor")
executorService.shutdown()
executorService.awaitTerminationSafe()
}
}
companion object {
private const val DEFAULT_INDEX_TIMEOUT: Long = 30
}
}
@@ -16,7 +16,7 @@ package net.syncthing.java.bep.connectionactor
import com.google.protobuf.ByteString
import net.syncthing.java.bep.BlockExchangeProtos
import net.syncthing.java.bep.IndexHandler
import net.syncthing.java.bep.index.IndexHandler
import net.syncthing.java.core.beans.DeviceId
import net.syncthing.java.core.beans.FolderInfo
import net.syncthing.java.core.configuration.Configuration
@@ -32,48 +32,52 @@ object ClusterConfigHandler {
): BlockExchangeProtos.ClusterConfig {
val builder = BlockExchangeProtos.ClusterConfig.newBuilder()
for (folder in configuration.folders) {
val folderBuilder = BlockExchangeProtos.Folder.newBuilder()
.setId(folder.folderId)
.setLabel(folder.label)
indexHandler.indexRepository.runInTransaction { indexTransaction ->
configuration.folders
.filter { it.deviceIdWhitelist.contains(deviceId) }
.forEach { folder ->
val folderBuilder = BlockExchangeProtos.Folder.newBuilder()
.setId(folder.folderId)
.setLabel(folder.label)
// add this device
folderBuilder.addDevices(
BlockExchangeProtos.Device.newBuilder()
.setId(ByteString.copyFrom(configuration.localDeviceId.toHashData()))
.setIndexId(indexHandler.sequencer().indexId())
.setMaxSequence(indexHandler.sequencer().currentSequence())
)
// add this device
folderBuilder.addDevices(
BlockExchangeProtos.Device.newBuilder()
.setId(ByteString.copyFrom(configuration.localDeviceId.toHashData()))
.setIndexId(indexTransaction.getSequencer().indexId())
.setMaxSequence(indexTransaction.getSequencer().currentSequence())
)
// add other device
val indexSequenceInfo = indexHandler.indexRepository.findIndexInfoByDeviceAndFolder(deviceId, folder.folderId)
// add other device
val indexSequenceInfo = indexTransaction.findIndexInfoByDeviceAndFolder(deviceId, folder.folderId)
folderBuilder.addDevices(
BlockExchangeProtos.Device.newBuilder()
.setId(ByteString.copyFrom(deviceId.toHashData()))
.apply {
indexSequenceInfo?.let {
setIndexId(indexSequenceInfo.indexId)
setMaxSequence(indexSequenceInfo.localSequence)
folderBuilder.addDevices(
BlockExchangeProtos.Device.newBuilder()
.setId(ByteString.copyFrom(deviceId.toHashData()))
.apply {
indexSequenceInfo?.let {
indexId = indexSequenceInfo.indexId
maxSequence = indexSequenceInfo.localSequence
logger.info("send delta index info device = {} index = {} max (local) sequence = {}",
indexSequenceInfo.deviceId,
indexSequenceInfo.indexId,
indexSequenceInfo.localSequence)
}
}
)
logger.info("send delta index info device = {} index = {} max (local) sequence = {}",
indexSequenceInfo.deviceId,
indexSequenceInfo.indexId,
indexSequenceInfo.localSequence)
}
}
)
builder.addFolders(folderBuilder)
builder.addFolders(folderBuilder)
// TODO: add the other devices to the cluster config
// TODO: add the other devices to the cluster config
}
}
return builder.build()
}
// TODO: understand this
internal fun handleReceivedClusterConfig(
internal suspend fun handleReceivedClusterConfig(
clusterConfig: BlockExchangeProtos.ClusterConfig,
configuration: Configuration,
otherDeviceId: DeviceId,
@@ -82,33 +86,69 @@ object ClusterConfigHandler {
val folderInfoList = mutableListOf<ClusterConfigFolderInfo>()
val newSharedFolders = mutableListOf<FolderInfo>()
for (folder in clusterConfig.foldersList ?: emptyList()) {
var folderInfo = ClusterConfigFolderInfo(folder.id, folder.label)
val devicesById = (folder.devicesList ?: emptyList())
.associateBy { input ->
DeviceId.fromHashData(input.id!!.toByteArray())
}
val otherDevice = devicesById[otherDeviceId]
val ourDevice = devicesById[configuration.localDeviceId]
if (otherDevice != null) {
folderInfo = folderInfo.copy(isAnnounced = true)
}
if (ourDevice != null) {
folderInfo = folderInfo.copy(isShared = true)
logger.info("folder shared from device = {} folder = {}", otherDeviceId, folderInfo)
val folderIds = configuration.folders.map { it.folderId }
if (!folderIds.contains(folderInfo.folderId)) {
val fi = FolderInfo(folderInfo.folderId, folderInfo.label)
configuration.folders = configuration.folders + fi
newSharedFolders.add(fi)
logger.info("new folder shared = {}", folderInfo)
configuration.update { oldConfig ->
val configFolders = oldConfig.folders.toMutableSet()
for (folder in clusterConfig.foldersList ?: emptyList()) {
var folderInfo = ClusterConfigFolderInfo(folder.id, folder.label, isDeviceInSharedFolderWhitelist = false)
val devicesById = (folder.devicesList ?: emptyList())
.associateBy { input ->
DeviceId.fromHashData(input.id!!.toByteArray())
}
val otherDevice = devicesById[otherDeviceId]
val ourDevice = devicesById[configuration.localDeviceId]
if (otherDevice != null) {
folderInfo = folderInfo.copy(isAnnounced = true)
}
} else {
logger.info("folder not shared from device = {} folder = {}", otherDeviceId, folderInfo)
if (ourDevice != null) {
folderInfo = folderInfo.copy(isShared = true)
logger.info("folder shared from device = {} folder = {}", otherDeviceId, folderInfo)
val oldFolderEntry = configFolders.find { it.folderId == folderInfo.folderId }
if (oldFolderEntry == null) {
folderInfo = folderInfo.copy(isDeviceInSharedFolderWhitelist = true)
val newFolderInfo = FolderInfo(
folderId = folderInfo.folderId,
label = folderInfo.label,
deviceIdWhitelist = setOf(otherDeviceId),
deviceIdBlacklist = emptySet(),
ignoredDeviceIdList = emptySet()
)
configFolders.add(newFolderInfo)
newSharedFolders.add(newFolderInfo)
logger.info("new folder shared = {}", folderInfo)
} else {
if (oldFolderEntry.deviceIdWhitelist.contains(otherDeviceId)) {
folderInfo = folderInfo.copy(isDeviceInSharedFolderWhitelist = true)
if (oldFolderEntry.label != folderInfo.label) {
configFolders.remove(oldFolderEntry)
configFolders.add(oldFolderEntry.copy(label = folderInfo.label))
}
} else {
if (!oldFolderEntry.deviceIdBlacklist.contains(otherDeviceId)) {
configFolders.remove(oldFolderEntry)
configFolders.add(
oldFolderEntry.copy(
deviceIdBlacklist = oldFolderEntry.deviceIdBlacklist + setOf(otherDeviceId)
)
)
}
}
}
} else {
logger.info("folder not shared from device = {} folder = {}", otherDeviceId, folderInfo)
}
folderInfoList.add(folderInfo)
}
folderInfoList.add(folderInfo)
oldConfig.copy(folders = configFolders)
}
configuration.persistLater()
indexHandler.handleClusterConfigMessageProcessedEvent(clusterConfig)
@@ -123,7 +163,7 @@ class ClusterConfigInfo (val folderInfo: List<ClusterConfigFolderInfo>, val newS
val folderInfoById = folderInfo.associateBy { it.folderId }
val sharedFolderIds: Set<String> by lazy {
folderInfo.filter { it.isShared }.map { it.folderId }.toSet()
folderInfo.filter { it.isShared && it.isDeviceInSharedFolderWhitelist }.map { it.folderId }.toSet()
}
}
@@ -131,7 +171,8 @@ data class ClusterConfigFolderInfo(
val folderId: String,
val label: String = folderId,
val isAnnounced: Boolean = false,
val isShared: Boolean = false
val isShared: Boolean = false,
val isDeviceInSharedFolderWhitelist: Boolean
) {
init {
assert(folderId.isNotEmpty())
@@ -17,12 +17,17 @@ package net.syncthing.java.bep.connectionactor
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.*
import net.syncthing.java.bep.BlockExchangeProtos
import net.syncthing.java.bep.IndexHandler
import net.syncthing.java.bep.index.IndexHandler
import net.syncthing.java.core.beans.DeviceAddress
import net.syncthing.java.core.configuration.Configuration
import org.slf4j.LoggerFactory
import java.io.IOException
data class Connection (
val actor: SendChannel<ConnectionAction>,
val clusterConfigInfo: ClusterConfigInfo
)
object ConnectionActorGenerator {
private val closed = Channel<ConnectionAction>().apply { cancel() }
private val logger = LoggerFactory.getLogger(ConnectionActorGenerator::class.java)
@@ -89,18 +94,42 @@ object ConnectionActorGenerator {
configuration: Configuration,
indexHandler: IndexHandler,
requestHandler: (BlockExchangeProtos.Request) -> Deferred<BlockExchangeProtos.Response>
) = GlobalScope.produce<Pair<SendChannel<ConnectionAction>, ClusterConfigInfo>> {
) = GlobalScope.produce<Pair<Connection, ConnectionInfo>> {
var currentActor: SendChannel<ConnectionAction> = closed
var currentClusterConfig = ClusterConfigInfo.dummy
var currentDeviceAddress: DeviceAddress? = null
var currentStatus = ConnectionInfo.empty
suspend fun dispatchStatus() {
send(Connection(currentActor, currentClusterConfig) to currentStatus)
}
suspend fun closeCurrent() {
if (currentActor != closed) {
currentActor.close()
currentActor = closed
send(currentActor to ClusterConfigInfo.dummy)
currentClusterConfig = ClusterConfigInfo.dummy
if (currentStatus.status != ConnectionStatus.Disconnected) {
currentStatus = currentStatus.copy(status = ConnectionStatus.Disconnected)
}
dispatchStatus()
}
}
suspend fun dispatchConnection(
connection: SendChannel<ConnectionAction>,
clusterConfig: ClusterConfigInfo,
deviceAddress: DeviceAddress
) {
currentActor = connection
currentDeviceAddress = deviceAddress
currentClusterConfig = clusterConfig
dispatchStatus()
}
suspend fun tryConnectingToAddressHandleBaseErrors(deviceAddress: DeviceAddress) = try {
val newActor = ConnectionActor.createInstance(deviceAddress, configuration, indexHandler, requestHandler)
val clusterConfig = ConnectionActorUtil.waitUntilConnected(newActor)
@@ -118,31 +147,37 @@ object ConnectionActorGenerator {
null
}
suspend fun dispatchConnection(
connection: SendChannel<ConnectionAction>,
clusterConfig: ClusterConfigInfo,
deviceAddress: DeviceAddress
) {
currentActor = connection
currentDeviceAddress = deviceAddress
send(connection to clusterConfig)
}
suspend fun tryConnectingToAddress(deviceAddress: DeviceAddress): Boolean {
closeCurrent()
var connection = tryConnectingToAddressHandleBaseErrors(deviceAddress) ?: return false
suspend fun handleCancel() {
currentStatus = currentStatus.copy(
status = ConnectionStatus.Disconnected
)
dispatchStatus()
}
currentStatus = currentStatus.copy(
status = ConnectionStatus.Connecting,
currentAddress = deviceAddress
)
dispatchStatus()
var connection = tryConnectingToAddressHandleBaseErrors(deviceAddress) ?: return run {handleCancel(); false}
if (connection.second.newSharedFolders.isNotEmpty()) {
logger.debug("connected to $deviceAddress with new folders -> reconnect")
// reconnect to send new cluster config
connection.first.close()
connection = tryConnectingToAddressHandleBaseErrors(deviceAddress) ?: return false
connection = tryConnectingToAddressHandleBaseErrors(deviceAddress) ?: return run {handleCancel(); false}
}
logger.debug("connected to $deviceAddress")
currentStatus = currentStatus.copy(
status = ConnectionStatus.Connected,
currentAddress = deviceAddress
)
dispatchConnection(connection.first, connection.second, deviceAddress)
return true
@@ -157,18 +192,26 @@ object ConnectionActorGenerator {
val reconnectTicker = ticker(delayMillis = 30 * 1000, initialDelayMillis = 0)
deviceAddressSource.consume {
var lastDeviceAddressList: List<DeviceAddress> = emptyList()
while (true) {
if (isConnected()) {
lastDeviceAddressList = deviceAddressSource.poll() ?: lastDeviceAddressList
run {
// get the new list version if there is any
val newDeviceAddressList = deviceAddressSource.poll()
if (lastDeviceAddressList.isNotEmpty()) {
if (newDeviceAddressList != null) {
currentStatus = currentStatus.copy(addresses = newDeviceAddressList)
dispatchStatus()
}
}
if (isConnected()) {
val deviceAddressList = currentStatus.addresses
if (deviceAddressList.isNotEmpty()) {
if (reconnectTicker.poll() != null) {
if (currentDeviceAddress != lastDeviceAddressList.first()) {
if (currentDeviceAddress != deviceAddressList.first()) {
val oldDeviceAddress = currentDeviceAddress!!
if (!tryConnectingToAddress(lastDeviceAddressList.first())) {
if (!tryConnectingToAddress(deviceAddressList.first())) {
tryConnectingToAddress(oldDeviceAddress)
}
}
@@ -179,11 +222,15 @@ object ConnectionActorGenerator {
delay(500) // don't take too much CPU
} else /* is not connected */ {
// get the new list version if there is any
lastDeviceAddressList = deviceAddressSource.poll() ?: lastDeviceAddressList
if (currentStatus.status == ConnectionStatus.Connected) {
currentStatus = currentStatus.copy(status = ConnectionStatus.Disconnected)
dispatchStatus()
}
val deviceAddressList = currentStatus.addresses
// try all addresses
for (address in lastDeviceAddressList) {
for (address in deviceAddressList) {
if (tryConnectingToAddress(address)) {
break
}
@@ -194,9 +241,14 @@ object ConnectionActorGenerator {
reconnectTicker.poll()
// wait for new device address list but not more than 15 seconds before the next iteration
lastDeviceAddressList = withTimeoutOrNull(15 * 1000) {
val newDeviceAddressList = withTimeoutOrNull(15 * 1000) {
deviceAddressSource.receive()
} ?: lastDeviceAddressList
}
if (newDeviceAddressList != null) {
currentStatus = currentStatus.copy(addresses = newDeviceAddressList)
dispatchStatus()
}
}
}
}
@@ -16,6 +16,7 @@ package net.syncthing.java.bep.connectionactor
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.channels.SendChannel
import net.syncthing.java.bep.BlockExchangeProtos
import java.io.IOException
object ConnectionActorUtil {
suspend fun waitUntilConnected(actor: SendChannel<ConnectionAction>): ClusterConfigInfo {
@@ -28,18 +29,34 @@ object ConnectionActorUtil {
}
suspend fun sendRequest(request: BlockExchangeProtos.Request, actor: SendChannel<ConnectionAction>): BlockExchangeProtos.Response {
val deferred = CompletableDeferred<BlockExchangeProtos.Response>()
try {
val deferred = CompletableDeferred<BlockExchangeProtos.Response>()
actor.send(SendRequestConnectionAction(request, deferred))
actor.send(SendRequestConnectionAction(request, deferred))
return deferred.await()
return deferred.await()
} catch (ex: Exception) {
throw IOException("not connected", ex)
}
}
suspend fun sendIndexUpdate(update: BlockExchangeProtos.IndexUpdate, actor: SendChannel<ConnectionAction>) {
val deferred = CompletableDeferred<Unit?>()
try {
val deferred = CompletableDeferred<Unit?>()
actor.send(SendIndexUpdateAction(update, deferred))
actor.send(SendIndexUpdateAction(update, deferred))
deferred.await()
deferred.await()
} catch (ex: Exception) {
throw IOException("not connected", ex)
}
}
suspend fun disconnect(actor: SendChannel<ConnectionAction>) {
try {
actor.send(CloseConnectionAction)
} catch (ex: Exception) {
// ignore if the channel is closed already
}
}
}
@@ -14,73 +14,67 @@
package net.syncthing.java.bep.connectionactor
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.ConflatedBroadcastChannel
import kotlinx.coroutines.channels.ReceiveChannel
import kotlinx.coroutines.channels.SendChannel
import kotlinx.coroutines.channels.consumeEach
import net.syncthing.java.bep.BlockExchangeProtos
import net.syncthing.java.core.beans.DeviceId
import net.syncthing.java.core.exception.ExceptionReport
import net.syncthing.java.core.exception.reportExceptions
import java.io.IOException
class ConnectionActorWrapper (
private val source: ReceiveChannel<Pair<SendChannel<ConnectionAction>, ClusterConfigInfo>>,
private val source: ReceiveChannel<Pair<Connection, ConnectionInfo>>,
val deviceId: DeviceId,
val connectivityChangeListener: () -> Unit
private val exceptionReportHandler: (ExceptionReport) -> Unit
) {
private val job = Job()
private var currentConnectionActor: SendChannel<ConnectionAction>? = null
private var clusterConfigInfo: ClusterConfigInfo? = null
private var connection: Connection? = null
private val connectionInfo = ConflatedBroadcastChannel<ConnectionInfo>(ConnectionInfo.empty)
var isConnected = false
get() = currentConnectionActor?.isClosedForSend == false
val isConnected
get() = connectionInfo.valueOrNull?.status == ConnectionStatus.Connected
init {
GlobalScope.launch (job) {
source.consumeEach { (connectionActor, clusterConfig) ->
currentConnectionActor = connectionActor
clusterConfigInfo = clusterConfig
GlobalScope.async (job) {
source.consumeEach { (connection, connectionInfo) ->
this@ConnectionActorWrapper.connection = connection
this@ConnectionActorWrapper.connectionInfo.send(connectionInfo)
}
}
// this is a very simple solution but it does its job
GlobalScope.launch (job) {
var previousConnected = false
while (isActive) {
val nowConnected = isConnected
if (previousConnected != nowConnected) {
previousConnected = nowConnected
connectivityChangeListener()
}
delay(200)
}
}
}.reportExceptions("ConnectionActorWrapper(${deviceId.deviceId})", exceptionReportHandler)
}
suspend fun sendRequest(request: BlockExchangeProtos.Request) = ConnectionActorUtil.sendRequest(
request,
currentConnectionActor ?: throw IOException("not connected")
connection?.actor ?: throw IOException("not connected")
)
suspend fun sendIndexUpdate(update: BlockExchangeProtos.IndexUpdate) = ConnectionActorUtil.sendIndexUpdate(
update,
currentConnectionActor ?: throw IOException("not connected")
connection?.actor ?: throw IOException("not connected")
)
fun hasFolder(folderId: String) = clusterConfigInfo?.sharedFolderIds?.contains(folderId) ?: false
fun hasFolder(folderId: String) = connection?.clusterConfigInfo?.sharedFolderIds?.contains(folderId) ?: false
fun getClusterConfig() = clusterConfigInfo ?: throw IOException("not connected")
fun getClusterConfig() = connection?.clusterConfigInfo ?: throw IOException("not connected")
fun shutdown() {
job.cancel()
connectionInfo.close()
}
// this triggers a disconnection
// the ConnectionActorGenerator will reconnect soon
fun reconnect() {
currentConnectionActor?.close()
val actor = connection?.actor
GlobalScope.launch {
if (actor != null) {
ConnectionActorUtil.disconnect(actor)
}
}
}
fun subscribeToConnectionInfo() = connectionInfo.openSubscription()
}
@@ -0,0 +1,21 @@
package net.syncthing.java.bep.connectionactor
import net.syncthing.java.core.beans.DeviceAddress
data class ConnectionInfo (
val addresses: List<DeviceAddress>,
val currentAddress: DeviceAddress?,
val status: ConnectionStatus
) {
companion object {
val empty = ConnectionInfo(
addresses = emptyList(),
currentAddress = null,
status = ConnectionStatus.Disconnected
)
}
}
enum class ConnectionStatus {
Disconnected, Connecting, Connected
}
@@ -73,7 +73,7 @@ object HelloMessageHandler {
)
}
fun processHelloMessage(
suspend fun processHelloMessage(
hello: BlockExchangeProtos.Hello,
configuration: Configuration,
deviceId: DeviceId
@@ -81,14 +81,17 @@ object HelloMessageHandler {
logger.info("Received hello message, deviceName=${hello.deviceName}, clientName=${hello.clientName}, clientVersion=${hello.clientVersion}")
// update the local device name
// TODO: this could need some locking
configuration.peers = configuration.peers.map { peer ->
if (peer.deviceId == deviceId) {
DeviceInfo(deviceId, hello.deviceName)
} else {
peer
}
}.toSet()
configuration.update { oldConfig ->
oldConfig.copy(
peers = oldConfig.peers.map { peer ->
if (peer.deviceId == deviceId) {
DeviceInfo(deviceId, hello.deviceName)
} else {
peer
}
}.toSet()
)
}
configuration.persistLater()
}
@@ -22,7 +22,7 @@ import kotlinx.coroutines.channels.consumeEach
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import net.syncthing.java.bep.BlockExchangeProtos
import net.syncthing.java.bep.IndexHandler
import net.syncthing.java.bep.index.IndexHandler
import net.syncthing.java.core.beans.DeviceAddress
import net.syncthing.java.core.configuration.Configuration
import net.syncthing.java.core.security.KeystoreHandler
@@ -0,0 +1,128 @@
/*
* Copyright (C) 2016 Davide Imbriaco
* Copyright (C) 2018 Jonas Lochmann
*
* This Java file is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.syncthing.java.bep.folder
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.ConflatedBroadcastChannel
import kotlinx.coroutines.channels.consumeEach
import kotlinx.coroutines.channels.first
import kotlinx.coroutines.channels.produce
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import net.syncthing.java.bep.index.*
import net.syncthing.java.core.beans.FolderStats
import net.syncthing.java.core.configuration.Configuration
import java.io.Closeable
class FolderBrowser internal constructor(private val indexHandler: IndexHandler, private val configuration: Configuration) : Closeable {
private val job = Job()
private val foldersStatus = ConflatedBroadcastChannel<Map<String, FolderStatus>>()
init {
GlobalScope.launch (job) {
// get initial status
val currentFolderStats = mutableMapOf<String, FolderStats>()
val currentIndexInfo = withContext(Dispatchers.IO) {
indexHandler.indexRepository.runInTransaction { indexTransaction ->
configuration.folders.map { it.folderId }.forEach { folderId ->
currentFolderStats[folderId] = indexTransaction.findFolderStats(folderId) ?: FolderStats.createDummy(folderId)
}
indexTransaction.findAllIndexInfos().groupBy { it.folderId }.toMutableMap()
}
}
// send status
suspend fun dispatch() {
foldersStatus.send(
configuration.folders.map { info ->
FolderStatus(
info = info,
stats = currentFolderStats[info.folderId] ?: FolderStats.createDummy(info.folderId),
indexInfo = currentIndexInfo[info.folderId] ?: emptyList()
)
}.associateBy { it.info.folderId }
)
}
dispatch()
// handle changes
val updateLock = Mutex()
async {
indexHandler.subscribeFolderStatsUpdatedEvents().consumeEach { event ->
updateLock.withLock {
when (event) {
is FolderStatsUpdatedEvent -> currentFolderStats[event.folderStats.folderId] = event.folderStats
FolderStatsResetEvent -> currentFolderStats.clear()
}.let { /* require that all cases are handled */ }
dispatch()
}
}
}
async {
indexHandler.subscribeToOnIndexUpdateEvents().consumeEach { event ->
updateLock.withLock {
when (event) {
is IndexRecordAcquiredEvent -> {
val oldList = currentIndexInfo[event.folderId] ?: emptyList()
val newList = oldList.filter { it.deviceId != event.indexInfo.deviceId } + event.indexInfo
currentIndexInfo[event.folderId] = newList
}
IndexInfoClearedEvent -> currentIndexInfo.clear()
}.let { /* require that all cases are handled */ }
dispatch()
}
}
}
async {
configuration.subscribe().consumeEach {
dispatch()
}
}
}
}
fun folderInfoAndStatusStream() = GlobalScope.produce {
foldersStatus.openSubscription().consumeEach { folderStats ->
send(
folderStats
.values
.sortedBy { it.info.label }
)
}
}
suspend fun folderInfoAndStatusList(): List<FolderStatus> = folderInfoAndStatusStream().first()
suspend fun getFolderStatus(folder: String): FolderStatus {
return getFolderStatus(folder, foldersStatus.openSubscription().first())
}
fun getFolderStatusSync(folder: String) = runBlocking { getFolderStatus(folder) }
private fun getFolderStatus(folder: String, folderStatus: Map<String, FolderStatus>) = folderStatus[folder] ?: FolderStatus.createDummy(folder)
override fun close() {
job.cancel()
}
}
@@ -0,0 +1,35 @@
package net.syncthing.java.bep.folder
import net.syncthing.java.bep.utils.longMaxBy
import net.syncthing.java.core.beans.FolderInfo
import net.syncthing.java.core.beans.FolderStats
import net.syncthing.java.core.beans.IndexInfo
data class FolderStatus(
val info: FolderInfo,
val stats: FolderStats,
val indexInfo: List<IndexInfo>
) {
companion object {
fun createDummy(folder: String) = FolderStatus(
info = FolderInfo(
folder,
folder,
deviceIdBlacklist = emptySet(),
deviceIdWhitelist = emptySet(),
ignoredDeviceIdList = emptySet()
),
stats = FolderStats.createDummy(folder),
indexInfo = emptyList()
)
}
val missingIndexUpdates: Long by lazy {
Math.max(
0,
indexInfo.longMaxBy ({ it.maxSequence }, 0) -
indexInfo.longMaxBy ({ it.localSequence }, 0)
)
}
}
@@ -0,0 +1,7 @@
package net.syncthing.java.bep.index
import net.syncthing.java.core.beans.FolderStats
sealed class FolderStatsChangedEvent
data class FolderStatsUpdatedEvent(val folderStats: FolderStats): FolderStatsChangedEvent()
object FolderStatsResetEvent: FolderStatsChangedEvent()
@@ -0,0 +1,17 @@
package net.syncthing.java.bep.index
import java.util.*
class FolderStatsUpdateCollector (val folderId: String) {
var deltaFileCount = 0L
var deltaDirCount = 0L
var deltaSize = 0L
var lastModified = Date(0)
fun isEmpty() = (
deltaFileCount == 0L &&
deltaDirCount == 0L &&
deltaSize == 0L &&
lastModified.time == 0L
)
}
@@ -0,0 +1,164 @@
package net.syncthing.java.bep.index
import net.syncthing.java.bep.BlockExchangeProtos
import net.syncthing.java.core.beans.BlockInfo
import net.syncthing.java.core.beans.FileBlocks
import net.syncthing.java.core.beans.FileInfo
import net.syncthing.java.core.exception.ExceptionDetailException
import net.syncthing.java.core.exception.ExceptionDetails
import net.syncthing.java.core.interfaces.IndexTransaction
import org.bouncycastle.util.encoders.Hex
import org.slf4j.LoggerFactory
import java.io.IOException
import java.util.*
object IndexElementProcessor {
private val logger = LoggerFactory.getLogger(IndexElementProcessor::class.java)
fun pushRecords(
transaction: IndexTransaction,
folder: String,
updates: List<BlockExchangeProtos.FileInfo>,
oldRecords: Map<String, FileInfo>,
folderStatsUpdateCollector: FolderStatsUpdateCollector
): List<FileInfo> {
// this always keeps the last version per path
val filesToProcess = updates
.sortedBy { it.sequence }
.reversed()
.distinctBy { it.name /* this is the whole path */ }
.reversed()
val preparedUpdates = filesToProcess.mapNotNull { prepareUpdate(folder, it) }
val updatesToApply = preparedUpdates.filter { shouldUpdateRecord(oldRecords[it.first.path], it.first) }
transaction.updateFileInfoAndBlocks(
fileInfos = updatesToApply.map { it.first },
fileBlocks = updatesToApply.mapNotNull { it.second }
)
for ((newRecord) in updatesToApply) {
updateFolderStatsCollector(oldRecords[newRecord.path], newRecord, folderStatsUpdateCollector)
}
return updatesToApply.map { it.first }
}
fun pushRecord(
transaction: IndexTransaction,
folder: String,
bepFileInfo: BlockExchangeProtos.FileInfo,
folderStatsUpdateCollector: FolderStatsUpdateCollector,
oldRecord: FileInfo?
): FileInfo? {
val update = prepareUpdate(folder, bepFileInfo)
return if (update != null) {
addRecord(
transaction = transaction,
newRecord = update.first,
fileBlocks = update.second,
folderStatsUpdateCollector = folderStatsUpdateCollector,
oldRecord = oldRecord
)
} else {
null
}
}
private fun prepareUpdate(
folder: String,
bepFileInfo: BlockExchangeProtos.FileInfo
): Pair<FileInfo, FileBlocks?>? {
val builder = FileInfo.Builder()
.setFolder(folder)
.setPath(bepFileInfo.name)
.setLastModified(Date(bepFileInfo.modifiedS * 1000 + bepFileInfo.modifiedNs / 1000000))
.setVersionList((if (bepFileInfo.hasVersion()) bepFileInfo.version.countersList else null ?: emptyList()).map { record -> FileInfo.Version(record.id, record.value) })
.setDeleted(bepFileInfo.deleted)
var fileBlocks: FileBlocks? = null
when (bepFileInfo.type) {
BlockExchangeProtos.FileInfoType.FILE -> {
fileBlocks = FileBlocks(folder, builder.getPath()!!, ((bepFileInfo.blocksList ?: emptyList())).map { record ->
BlockInfo(record.offset, record.size, Hex.toHexString(record.hash.toByteArray()))
})
builder
.setTypeFile()
.setHash(fileBlocks.hash)
.setSize(bepFileInfo.size)
}
BlockExchangeProtos.FileInfoType.DIRECTORY -> builder.setTypeDir()
else -> {
logger.warn("unsupported file type = {}, discarding file info", bepFileInfo.type)
return null
}
}
return builder.build() to fileBlocks
}
private fun shouldUpdateRecord(
oldRecord: FileInfo?,
newRecord: FileInfo
) = oldRecord == null || newRecord.lastModified >= oldRecord.lastModified
private fun addRecord(
transaction: IndexTransaction,
newRecord: FileInfo,
oldRecord: FileInfo?,
fileBlocks: FileBlocks?,
folderStatsUpdateCollector: FolderStatsUpdateCollector
): FileInfo? {
return if (shouldUpdateRecord(oldRecord, newRecord)) {
logger.trace("discarding record = {}, modified before local record", newRecord)
null
} else {
logger.trace("loaded new record = {}", newRecord)
transaction.updateFileInfo(newRecord, fileBlocks)
updateFolderStatsCollector(oldRecord, newRecord, folderStatsUpdateCollector)
newRecord
}
}
private fun updateFolderStatsCollector(
oldRecord: FileInfo?,
newRecord: FileInfo,
folderStatsUpdateCollector: FolderStatsUpdateCollector
) {
val oldMissing = oldRecord == null || oldRecord.isDeleted
val newMissing = newRecord.isDeleted
val oldSizeMissing = oldMissing || !oldRecord!!.isFile()
val newSizeMissing = newMissing || !newRecord.isFile()
if (!oldSizeMissing) {
folderStatsUpdateCollector.deltaSize -= oldRecord!!.size!!
}
if (!newSizeMissing) {
folderStatsUpdateCollector.deltaSize += newRecord.size!!
}
if (!oldMissing) {
if (oldRecord!!.isFile()) {
folderStatsUpdateCollector.deltaFileCount--
} else if (oldRecord.isDirectory()) {
folderStatsUpdateCollector.deltaDirCount--
}
}
if (!newMissing) {
if (newRecord.isFile()) {
folderStatsUpdateCollector.deltaFileCount++
} else if (newRecord.isDirectory()) {
folderStatsUpdateCollector.deltaDirCount++
}
}
folderStatsUpdateCollector.lastModified = newRecord.lastModified
}
}
@@ -0,0 +1,197 @@
/*
* Copyright (C) 2016 Davide Imbriaco
* Copyright (C) 2018 Jonas Lochmann
*
* This Java file is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.syncthing.java.bep.index
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.BroadcastChannel
import kotlinx.coroutines.channels.consume
import kotlinx.coroutines.withContext
import kotlinx.coroutines.withTimeoutOrNull
import net.syncthing.java.bep.BlockExchangeProtos
import net.syncthing.java.bep.connectionactor.ClusterConfigInfo
import net.syncthing.java.bep.connectionactor.ConnectionActorWrapper
import net.syncthing.java.bep.folder.FolderBrowser
import net.syncthing.java.bep.index.browser.IndexBrowser
import net.syncthing.java.core.beans.*
import net.syncthing.java.core.configuration.Configuration
import net.syncthing.java.core.exception.ExceptionReport
import net.syncthing.java.core.interfaces.IndexRepository
import net.syncthing.java.core.interfaces.IndexTransaction
import net.syncthing.java.core.interfaces.TempRepository
import org.apache.commons.lang3.tuple.Pair
import org.slf4j.LoggerFactory
import java.io.Closeable
import java.io.IOException
class IndexHandler(
configuration: Configuration,
val indexRepository: IndexRepository,
tempRepository: TempRepository,
exceptionReportHandler: (ExceptionReport) -> Unit
) : Closeable {
private val logger = LoggerFactory.getLogger(javaClass)
private val indexInfoUpdateEvents = BroadcastChannel<IndexInfoUpdateEvent>(capacity = 16)
private val onFullIndexAcquiredEvents = BroadcastChannel<String>(capacity = 16)
private val onFolderStatsUpdatedEvents = BroadcastChannel<FolderStatsChangedEvent>(capacity = 16)
private val indexMessageProcessor = IndexMessageQueueProcessor(
indexRepository = indexRepository,
tempRepository = tempRepository,
isRemoteIndexAcquired = ::isRemoteIndexAcquired,
onIndexRecordAcquiredEvents = indexInfoUpdateEvents,
onFullIndexAcquiredEvents = onFullIndexAcquiredEvents,
onFolderStatsUpdatedEvents = onFolderStatsUpdatedEvents,
exceptionReportHandler = exceptionReportHandler
)
fun subscribeToOnFullIndexAcquiredEvents() = onFullIndexAcquiredEvents.openSubscription()
fun subscribeToOnIndexUpdateEvents() = indexInfoUpdateEvents.openSubscription()
fun subscribeFolderStatsUpdatedEvents() = onFolderStatsUpdatedEvents.openSubscription()
fun getNextSequenceNumber() = indexRepository.runInTransaction { it.getSequencer().nextSequence() }
suspend fun clearIndex() {
withContext(Dispatchers.IO) {
indexRepository.runInTransaction { it.clearIndex() }
}
onFolderStatsUpdatedEvents.send(FolderStatsResetEvent)
indexInfoUpdateEvents.send(IndexInfoClearedEvent)
}
private fun isRemoteIndexAcquiredWithoutTransaction(clusterConfigInfo: ClusterConfigInfo, peerDeviceId: DeviceId): Boolean {
return indexRepository.runInTransaction { transaction -> isRemoteIndexAcquired(clusterConfigInfo, peerDeviceId, transaction) }
}
private fun isRemoteIndexAcquired(clusterConfigInfo: ClusterConfigInfo, peerDeviceId: DeviceId, transaction: IndexTransaction): Boolean {
return clusterConfigInfo.sharedFolderIds.find { sharedFolderId ->
// try to find one folder which is not yet ready
val indexSequenceInfo = transaction.findIndexInfoByDeviceAndFolder(peerDeviceId, sharedFolderId)
indexSequenceInfo == null || indexSequenceInfo.localSequence < indexSequenceInfo.maxSequence
} == null
}
// the old implementation kept waiting when index updates were still happening, but waiting 30 seconds should be enough
suspend fun waitForRemoteIndexAcquiredWithTimeout(connectionHandler: ConnectionActorWrapper, timeoutSecs: Long? = null): IndexHandler {
val timeoutMillis = (timeoutSecs ?: DEFAULT_INDEX_TIMEOUT) * 1000
val ok = withTimeoutOrNull(timeoutMillis) {
waitForRemoteIndexAcquiredWithoutTimeout(connectionHandler)
true
} ?: false
if (!ok) {
throw IOException("unable to acquire index from connection $connectionHandler, timeout reached!")
}
return this
}
suspend fun waitForRemoteIndexAcquiredWithoutTimeout(connectionHandler: ConnectionActorWrapper) {
val events = onFullIndexAcquiredEvents.openSubscription()
events.consume {
fun isDone() = isRemoteIndexAcquiredWithoutTransaction(connectionHandler.getClusterConfig(), connectionHandler.deviceId)
if (isDone()) {
return
}
for (event in events) {
if (isDone()) {
return
}
}
}
}
suspend fun handleClusterConfigMessageProcessedEvent(clusterConfig: BlockExchangeProtos.ClusterConfig) {
val updatedIndexInfos = indexRepository.runInTransaction { transaction ->
val updatedIndexInfos = mutableListOf<IndexInfo>()
for (folderRecord in clusterConfig.foldersList) {
val folder = folderRecord.id
logger.debug("acquired folder info from cluster config = {}", folder)
for (deviceRecord in folderRecord.devicesList) {
val deviceId = DeviceId.fromHashData(deviceRecord.id.toByteArray())
if (deviceRecord.hasIndexId() && deviceRecord.hasMaxSequence()) {
val folderIndexInfo = UpdateIndexInfo.updateIndexInfoFromClusterConfig(transaction, folder, deviceId, deviceRecord.indexId, deviceRecord.maxSequence)
logger.debug("acquired folder index info from cluster config = {}", folderIndexInfo)
updatedIndexInfos.add(folderIndexInfo)
}
}
}
updatedIndexInfos
}
updatedIndexInfos.forEach {
indexInfoUpdateEvents.send(
IndexRecordAcquiredEvent(
folderId = it.folderId,
indexInfo = it,
files = emptyList()
)
)
}
}
internal suspend fun handleIndexMessageReceivedEvent(folderId: String, filesList: List<BlockExchangeProtos.FileInfo>, clusterConfigInfo: ClusterConfigInfo, peerDeviceId: DeviceId) {
indexMessageProcessor.handleIndexMessageReceivedEvent(folderId, filesList, clusterConfigInfo, peerDeviceId)
}
fun getFileInfoByPath(folder: String, path: String): FileInfo? {
return indexRepository.runInTransaction { it.findFileInfo(folder, path) }
}
fun getFileInfoAndBlocksByPath(folder: String, path: String): Pair<FileInfo, FileBlocks>? {
return indexRepository.runInTransaction { transaction ->
val fileInfo = transaction.findFileInfo(folder, path)
if (fileInfo == null) {
null
} else {
val fileBlocks = transaction.findFileBlocks(folder, path)
assert(fileInfo.isFile())
checkNotNull(fileBlocks) {"file blocks not found for file info = $fileInfo"}
FileInfo.checkBlocks(fileInfo, fileBlocks)
Pair.of(fileInfo, fileBlocks)
}
}
}
val folderBrowser = FolderBrowser(this, configuration)
val indexBrowser = IndexBrowser(indexRepository, this)
suspend fun sendFolderStatsUpdate(event: FolderStats) {
onFolderStatsUpdatedEvents.send(FolderStatsUpdatedEvent(event))
}
override fun close() {
indexInfoUpdateEvents.close()
onFullIndexAcquiredEvents.close()
indexMessageProcessor.stop()
}
companion object {
private const val DEFAULT_INDEX_TIMEOUT: Long = 30
}
}
@@ -0,0 +1,8 @@
package net.syncthing.java.bep.index
import net.syncthing.java.core.beans.FileInfo
import net.syncthing.java.core.beans.IndexInfo
sealed class IndexInfoUpdateEvent
data class IndexRecordAcquiredEvent(val folderId: String, val files: List<FileInfo>, val indexInfo: IndexInfo): IndexInfoUpdateEvent()
object IndexInfoClearedEvent: IndexInfoUpdateEvent()
@@ -0,0 +1,70 @@
package net.syncthing.java.bep.index
import net.syncthing.java.bep.BlockExchangeProtos
import net.syncthing.java.core.beans.DeviceId
import net.syncthing.java.core.beans.FileInfo
import net.syncthing.java.core.beans.FolderStats
import net.syncthing.java.core.beans.IndexInfo
import net.syncthing.java.core.interfaces.IndexTransaction
import org.slf4j.LoggerFactory
import java.lang.RuntimeException
object IndexMessageProcessor {
private val logger = LoggerFactory.getLogger(IndexMessageProcessor::class.java)
fun doHandleIndexMessageReceivedEvent(
message: BlockExchangeProtos.IndexUpdate,
peerDeviceId: DeviceId,
transaction: IndexTransaction
): Result {
val folderId = message.folder
val oldIndexInfo = transaction.findIndexInfoByDeviceAndFolder(peerDeviceId, folderId)
?: throw IndexInfoNotFoundException()
logger.debug("processing {} index records for folder {}", message.filesList.size, folderId)
val oldRecords = transaction.findFileInfo(folderId, message.filesList.map { it.name })
val folderStatsUpdateCollector = FolderStatsUpdateCollector(message.folder)
val newRecords = IndexElementProcessor.pushRecords(
transaction = transaction,
oldRecords = oldRecords,
folder = folderId,
folderStatsUpdateCollector = folderStatsUpdateCollector,
updates = message.filesList
)
val newIndexInfo = if (message.filesList.isEmpty()) {
oldIndexInfo
} else {
var sequence: Long = -1
for (newRecord in message.filesList) {
sequence = Math.max(newRecord.sequence, sequence)
}
handleFolderStatsUpdate(transaction, folderStatsUpdateCollector)
UpdateIndexInfo.updateIndexInfoFromIndexElementProcessor(transaction, oldIndexInfo, sequence)
}
return Result(newIndexInfo, newRecords.toList(), transaction.findFolderStats(folderId) ?: FolderStats.createDummy(folderId))
}
fun handleFolderStatsUpdate(transaction: IndexTransaction, folderStatsUpdateCollector: FolderStatsUpdateCollector) {
if (folderStatsUpdateCollector.isEmpty()) {
return
}
transaction.updateOrInsertFolderStats(
folder = folderStatsUpdateCollector.folderId,
deltaSize = folderStatsUpdateCollector.deltaSize,
deltaFileCount = folderStatsUpdateCollector.deltaFileCount,
deltaDirCount = folderStatsUpdateCollector.deltaDirCount,
lastUpdate = folderStatsUpdateCollector.lastModified
)
}
data class Result(val newIndexInfo: IndexInfo, val updatedFiles: List<FileInfo>, val newFolderStats: FolderStats)
class IndexInfoNotFoundException: RuntimeException()
}
@@ -0,0 +1,159 @@
/*
* Copyright (C) 2016 Davide Imbriaco
* Copyright (C) 2018 Jonas Lochmann
*
* This Java file is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.syncthing.java.bep.index
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.BroadcastChannel
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.consumeEach
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import net.syncthing.java.bep.BlockExchangeProtos
import net.syncthing.java.bep.connectionactor.ClusterConfigInfo
import net.syncthing.java.core.beans.DeviceId
import net.syncthing.java.core.exception.ExceptionReport
import net.syncthing.java.core.exception.reportExceptions
import net.syncthing.java.core.interfaces.IndexRepository
import net.syncthing.java.core.interfaces.IndexTransaction
import net.syncthing.java.core.interfaces.TempRepository
import org.slf4j.LoggerFactory
class IndexMessageQueueProcessor (
private val indexRepository: IndexRepository,
private val tempRepository: TempRepository,
private val onIndexRecordAcquiredEvents: BroadcastChannel<IndexInfoUpdateEvent>,
private val onFullIndexAcquiredEvents: BroadcastChannel<String>,
private val onFolderStatsUpdatedEvents: BroadcastChannel<FolderStatsChangedEvent>,
private val isRemoteIndexAcquired: (ClusterConfigInfo, DeviceId, IndexTransaction) -> Boolean,
exceptionReportHandler: (ExceptionReport) -> Unit
) {
private data class IndexUpdateAction(val update: BlockExchangeProtos.IndexUpdate, val clusterConfigInfo: ClusterConfigInfo, val peerDeviceId: DeviceId)
private data class StoredIndexUpdateAction(val updateId: String, val clusterConfigInfo: ClusterConfigInfo, val peerDeviceId: DeviceId)
companion object {
private val logger = LoggerFactory.getLogger(IndexMessageQueueProcessor::class.java)
private const val BATCH_SIZE = 128
}
private val job = Job()
private val indexUpdateIncomingLock = Mutex()
private val indexUpdateProcessStoredQueue = Channel<StoredIndexUpdateAction>(capacity = Channel.UNLIMITED)
private val indexUpdateProcessingQueue = Channel<IndexUpdateAction>(capacity = Channel.RENDEZVOUS)
suspend fun handleIndexMessageReceivedEvent(folderId: String, filesList: List<BlockExchangeProtos.FileInfo>, clusterConfigInfo: ClusterConfigInfo, peerDeviceId: DeviceId) {
filesList.chunked(BATCH_SIZE).forEach { chunck ->
handleIndexMessageReceivedEventWithoutChuncking(folderId, chunck, clusterConfigInfo, peerDeviceId)
}
}
suspend fun handleIndexMessageReceivedEventWithoutChuncking(folderId: String, filesList: List<BlockExchangeProtos.FileInfo>, clusterConfigInfo: ClusterConfigInfo, peerDeviceId: DeviceId) {
indexUpdateIncomingLock.withLock {
logger.info("received index message event, preparing")
val data = BlockExchangeProtos.IndexUpdate.newBuilder()
.addAllFiles(filesList)
.setFolder(folderId)
.build()
if (indexUpdateProcessingQueue.offer(IndexUpdateAction(data, clusterConfigInfo, peerDeviceId))) {
// message is beeing processed now
} else {
val key = tempRepository.pushTempData(data.toByteArray())
logger.debug("received index message event, stored to temp record {}, queuing for processing", key)
indexUpdateProcessStoredQueue.send(StoredIndexUpdateAction(key, clusterConfigInfo, peerDeviceId))
}
}
}
init {
GlobalScope.async(Dispatchers.IO + job) {
indexUpdateProcessingQueue.consumeEach {
try {
doHandleIndexMessageReceivedEvent(it)
} catch (ex: IndexMessageProcessor.IndexInfoNotFoundException) {
// ignored
// this is expected when the data is deleted but some index updates are still in the queue
logger.warn("could not find index info for index update")
}
}
}.reportExceptions("IndexMessageQueueProcessor.indexUpdateProcessingQueue", exceptionReportHandler)
GlobalScope.async(Dispatchers.IO + job) {
indexUpdateProcessStoredQueue.consumeEach { action ->
logger.debug("processing index message event from temp record {}", action.updateId)
val data = tempRepository.popTempData(action.updateId)
val message = BlockExchangeProtos.IndexUpdate.parseFrom(data)
indexUpdateProcessingQueue.send(IndexUpdateAction(
message,
action.clusterConfigInfo,
action.peerDeviceId
))
}
}.reportExceptions("IndexMessageQueueProcessor.indexUpdateProcessStoredQueue", exceptionReportHandler)
}
private suspend fun doHandleIndexMessageReceivedEvent(action: IndexUpdateAction) {
val (message, clusterConfigInfo, peerDeviceId) = action
val folderInfo = clusterConfigInfo.folderInfoById[message.folder]
?: throw IllegalStateException("got folder info for folder without known folder info")
if (!folderInfo.isDeviceInSharedFolderWhitelist) {
throw IllegalStateException("received index update for folder which is not shared")
}
logger.info("processing index message with {} records", message.filesCount)
val (indexResult, wasIndexAcquired) = indexRepository.runInTransaction { indexTransaction ->
val wasIndexAcquiredBefore = isRemoteIndexAcquired(clusterConfigInfo, peerDeviceId, indexTransaction)
val startTime = System.currentTimeMillis()
val indexResult = IndexMessageProcessor.doHandleIndexMessageReceivedEvent(
message = message,
peerDeviceId = peerDeviceId,
transaction = indexTransaction
)
val endTime = System.currentTimeMillis()
logger.info("processed {} index records, acquired {} in ${endTime - startTime} ms", message.filesCount, indexResult.updatedFiles.size)
logger.debug("index info = {}", indexResult.newIndexInfo)
indexResult to ((!wasIndexAcquiredBefore) && isRemoteIndexAcquired(clusterConfigInfo, peerDeviceId, indexTransaction))
}
if (indexResult.updatedFiles.isNotEmpty()) {
onIndexRecordAcquiredEvents.send(IndexRecordAcquiredEvent(message.folder, indexResult.updatedFiles, indexResult.newIndexInfo))
}
onFolderStatsUpdatedEvents.send(FolderStatsUpdatedEvent(indexResult.newFolderStats))
if (wasIndexAcquired) {
logger.debug("index acquired")
onFullIndexAcquiredEvents.send(message.folder)
}
}
fun stop() {
logger.info("stopping index record processor")
job.cancel()
}
}
@@ -0,0 +1,57 @@
package net.syncthing.java.bep.index
import net.syncthing.java.core.beans.DeviceId
import net.syncthing.java.core.beans.IndexInfo
import net.syncthing.java.core.interfaces.IndexTransaction
object UpdateIndexInfo {
fun updateIndexInfoFromClusterConfig(
transaction: IndexTransaction,
folder: String,
deviceId: DeviceId,
indexId: Long,
maxSequence: Long
): IndexInfo {
val oldIndexSequenceInfo = transaction.findIndexInfoByDeviceAndFolder(deviceId, folder)
var newIndexSequenceInfo = oldIndexSequenceInfo ?: IndexInfo(
folderId = folder,
deviceId = deviceId.deviceId,
indexId = indexId,
localSequence = 0,
maxSequence = -1
)
if (indexId != newIndexSequenceInfo.indexId) {
newIndexSequenceInfo = newIndexSequenceInfo.copy(indexId = indexId)
}
if (maxSequence > newIndexSequenceInfo.maxSequence) {
newIndexSequenceInfo = newIndexSequenceInfo.copy(maxSequence = maxSequence)
}
if (oldIndexSequenceInfo != newIndexSequenceInfo) {
transaction.updateIndexInfo(newIndexSequenceInfo)
}
return newIndexSequenceInfo
}
fun updateIndexInfoFromIndexElementProcessor(
transaction: IndexTransaction,
oldIndexInfo: IndexInfo,
localSequence: Long?
): IndexInfo {
var newIndexSequenceInfo = oldIndexInfo
if (localSequence != null && localSequence > newIndexSequenceInfo.localSequence) {
newIndexSequenceInfo = newIndexSequenceInfo.copy(localSequence = localSequence)
}
if (oldIndexInfo != newIndexSequenceInfo) {
transaction.updateIndexInfo(newIndexSequenceInfo)
}
return newIndexSequenceInfo
}
}
@@ -0,0 +1,25 @@
package net.syncthing.java.bep.index.browser
import net.syncthing.java.core.beans.FileInfo
import net.syncthing.java.core.utils.PathUtils
sealed class DirectoryListing {
abstract val folder: String
abstract val path: String
}
data class DirectoryContentListing(
val directoryInfo: FileInfo,
val parentEntry: FileInfo?,
val entries: List<FileInfo>
): DirectoryListing() {
override val folder = directoryInfo.folder
override val path = directoryInfo.path
}
data class DirectoryNotFoundListing(
override val folder: String,
override val path: String
): DirectoryListing() {
val theoreticalParentPath = if (PathUtils.isRoot(path)) null else PathUtils.getParentPath(path)
}
@@ -0,0 +1,172 @@
/*
* Copyright (C) 2016 Davide Imbriaco
* Copyright (C) 2018 Jonas Lochmann
*
* This Java file is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.syncthing.java.bep.index.browser
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.channels.consume
import kotlinx.coroutines.channels.produce
import kotlinx.coroutines.withContext
import net.syncthing.java.bep.index.FolderStatsUpdatedEvent
import net.syncthing.java.bep.index.IndexHandler
import net.syncthing.java.bep.index.IndexInfoUpdateEvent
import net.syncthing.java.bep.index.IndexRecordAcquiredEvent
import net.syncthing.java.core.beans.FileInfo
import net.syncthing.java.core.interfaces.IndexRepository
import net.syncthing.java.core.interfaces.IndexTransaction
import net.syncthing.java.core.utils.PathUtils
import java.util.*
class IndexBrowser internal constructor(
private val indexRepository: IndexRepository,
private val indexHandler: IndexHandler
) {
companion object {
val sortAlphabeticallyDirectoriesFirst: Comparator<FileInfo> =
compareBy<FileInfo>({!isParent(it) }, {!it.isDirectory()})
.thenBy { it.fileName.toLowerCase() }
val sortByLastModification: Comparator<FileInfo> =
compareBy<FileInfo>({!isParent(it) }, {it.lastModified})
.thenBy { it.fileName.toLowerCase() }
private fun isParent(fileInfo: FileInfo) = PathUtils.isParent(fileInfo.path)
fun getPathFileName(path: String) = PathUtils.getFileName(path)
const val ROOT_PATH = PathUtils.ROOT_PATH
}
fun getDirectoryListing(folder: String, path: String): DirectoryListing = indexRepository.runInTransaction { indexTransaction ->
val entries = indexTransaction.findNotDeletedFilesByFolderAndParent(folder, path)
val parentPath = if (PathUtils.isRoot(path)) null else PathUtils.getParentPath(path)
val parentEntry = if (PathUtils.isRoot(path)) null else getFileInfoByPathAllowNull(folder, PathUtils.getParentPath(path), indexTransaction)
val directoryInfo = getFileInfoByPathAllowNull(folder, path, indexTransaction)
if ((parentPath != null && parentEntry == null) || directoryInfo == null || directoryInfo.type != FileInfo.FileType.DIRECTORY) {
DirectoryNotFoundListing(folder, path)
} else {
DirectoryContentListing(
entries = entries,
parentEntry = parentEntry,
directoryInfo = directoryInfo
)
}
}
fun streamDirectoryListing(folder: String, path: String) = GlobalScope.produce {
indexHandler.subscribeToOnIndexUpdateEvents().consume {
val directoryName = PathUtils.getFileName(path)
val parentPath = if (PathUtils.isRoot(path)) null else PathUtils.getParentPath(path)
val parentDirectoryName = if (parentPath != null) PathUtils.getFileName(parentPath) else null
val parentParentPath = if (parentPath == null || PathUtils.isRoot(parentPath)) null else PathUtils.getParentPath(parentPath)
// get the initial state
var (entries, parentEntry, directoryInfo) = withContext (Dispatchers.IO) {
indexRepository.runInTransaction { indexTransaction ->
val entries = indexTransaction.findNotDeletedFilesByFolderAndParent(folder, path)
val parentEntry = if (PathUtils.isRoot(path)) null else getFileInfoByPathAllowNull(folder, PathUtils.getParentPath(path), indexTransaction)
val directoryInfo = getFileInfoByPathAllowNull(folder, path, indexTransaction)
Triple(entries, parentEntry, directoryInfo)
}
}
var previousStatus: DirectoryListing? = null
suspend fun dispatch() {
// let Kotlin understand that the value does not change during running this
val directoryInfo = directoryInfo
val newStatus = if ((parentPath != null && parentEntry == null) || directoryInfo == null || directoryInfo.type != FileInfo.FileType.DIRECTORY) {
DirectoryNotFoundListing(folder, path)
} else {
DirectoryContentListing(
entries = entries,
parentEntry = parentEntry,
directoryInfo = directoryInfo
)
}
if (newStatus != previousStatus) {
previousStatus = newStatus
send(newStatus)
}
}
dispatch()
// handle updates
for (event in this) {
if (event is IndexRecordAcquiredEvent) {
var hadChanges = false
if (event.folderId == folder) {
event.files.forEach { fileUpdate ->
// entry change
if (fileUpdate.parent == path) {
hadChanges = true
entries = entries.filter { it.fileName != fileUpdate.fileName }
if (!fileUpdate.isDeleted) {
entries += listOf(fileUpdate)
}
}
// handle directory info changes
if (fileUpdate.parent == parentPath && fileUpdate.fileName == directoryName) {
directoryInfo = if (fileUpdate.isDeleted) null else fileUpdate
hadChanges = true
}
// handle parent directory info changes
if (fileUpdate.parent == parentParentPath && fileUpdate.fileName == parentDirectoryName) {
parentEntry = if (fileUpdate.isDeleted) null else fileUpdate
hadChanges = true
}
}
}
if (hadChanges) {
dispatch()
}
}
}
}
}
fun getFileInfoByAbsolutePath(folder: String, path: String): FileInfo = getFileInfoByAbsolutePathAllowNull(folder, path)
?: error("file not found for path = $path")
fun getFileInfoByAbsolutePathAllowNull(folder: String, path: String): FileInfo? {
return if (PathUtils.isRoot(path)) {
FileInfo(folder = folder, type = FileInfo.FileType.DIRECTORY, path = PathUtils.ROOT_PATH)
} else {
indexRepository.runInTransaction { it.findNotDeletedFileInfo(folder, path) }
}
}
fun getFileInfoByPath(folder: String, path: String, transaction: IndexTransaction) = getFileInfoByPathAllowNull(folder, path, transaction)
?: error("file not found for path = $path")
fun getFileInfoByPathAllowNull(folder: String, path: String, transaction: IndexTransaction): FileInfo? {
return if (PathUtils.isRoot(path)) {
FileInfo(folder = folder, type = FileInfo.FileType.DIRECTORY, path = PathUtils.ROOT_PATH)
} else {
transaction.findNotDeletedFileInfo(folder, path)
}
}
}
@@ -0,0 +1,11 @@
package net.syncthing.java.bep.utils
inline fun <T> Iterable<T>.longMaxBy(selector: (T) -> Long, defaultValue: Long): Long {
var max = defaultValue
this.forEach {
max = Math.max(max, selector(it))
}
return max
}
@@ -13,7 +13,10 @@
*/
package net.syncthing.java.client.cli
import kotlinx.coroutines.channels.consume
import kotlinx.coroutines.runBlocking
import net.syncthing.java.bep.index.browser.DirectoryContentListing
import net.syncthing.java.bep.index.browser.IndexBrowser
import net.syncthing.java.client.SyncthingClient
import net.syncthing.java.core.beans.DeviceId
import net.syncthing.java.core.beans.DeviceInfo
@@ -26,7 +29,6 @@ import org.slf4j.LoggerFactory
import java.io.File
import java.io.FileInputStream
import java.io.IOException
import java.util.concurrent.CountDownLatch
class Main(private val commandLine: CommandLine) {
@@ -46,7 +48,13 @@ class Main(private val commandLine: CommandLine) {
val repository = SqlRepository(configuration.databaseFolder)
SyncthingClient(configuration, repository, repository).use { syncthingClient ->
SyncthingClient(
configuration,
repository,
repository
) { ex ->
throw ex.exception
}.use { syncthingClient ->
val main = Main(cmd)
cmd.options.forEach { main.handleOption(it, configuration, syncthingClient) }
}
@@ -84,8 +92,16 @@ class Main(private val commandLine: CommandLine) {
.map { DeviceId(it.trim()) }
.toList()
System.out.println("set peers = $peers")
configuration.peers = peers.map { DeviceInfo(it, null) }.toSet()
configuration.persistNow()
runBlocking {
configuration.update { oldConfig ->
oldConfig.copy(
peers = peers.map { DeviceInfo(it, it.shortId) }.toSet()
)
}
}
runBlocking { configuration.persistNow() }
}
"p" -> {
val folderAndPath = option.value
@@ -117,7 +133,6 @@ class Main(private val commandLine: CommandLine) {
System.out.println("file path = $path")
val folder = path.split(":".toRegex()).dropLastWhile({ it.isEmpty() }).toTypedArray()[0]
path = path.split(":".toRegex()).dropLastWhile({ it.isEmpty() }).toTypedArray()[1]
val latch = CountDownLatch(1)
val blockPusher = syncthingClient.getBlockPusher(folder)
val observer = runBlocking {
@@ -169,33 +184,35 @@ class Main(private val commandLine: CommandLine) {
System.out.println("uploaded dir to network")
}
"L" -> {
waitForIndexUpdate(syncthingClient, configuration)
for (folder in syncthingClient.indexHandler.folderList()) {
syncthingClient.indexHandler.newIndexBrowser(folder).use { indexBrowser ->
System.out.println("list folder = ${indexBrowser.folder}")
for (fileInfo in indexBrowser.listFiles()) {
waitForIndexUpdate(syncthingClient)
for (folder in configuration.folders) {
System.out.println("list folder = ${folder}")
val listing = syncthingClient.indexHandler.indexBrowser.getDirectoryListing(folder.folderId, IndexBrowser.ROOT_PATH)
if (listing is DirectoryContentListing) {
for (fileInfo in listing.entries) {
System.out.println("${fileInfo.type.name.substring(0, 1)}\t${fileInfo.describeSize()}\t${fileInfo.path}")
}
}
}
}
"I" -> {
waitForIndexUpdate(syncthingClient, configuration)
waitForIndexUpdate(syncthingClient)
val folderInfo = StringBuilder()
for (folder in syncthingClient.indexHandler.folderList()) {
for (folder in configuration.folders) {
folderInfo.append("\nfolder info: ")
.append(syncthingClient.indexHandler.getFolderInfo(folder))
.append(folder)
folderInfo.append("\nfolder stats: ")
.append(syncthingClient.indexHandler.newFolderBrowser().getFolderStats(folder).dumpInfo())
.append(syncthingClient.indexHandler.folderBrowser.getFolderStatusSync(folder.folderId).stats.infoDump)
.append("\n")
}
System.out.println("folders:\n$folderInfo\n")
}
"l" -> {
var folderInfo = ""
for (folder in syncthingClient.indexHandler.folderList()) {
folderInfo += "\nfolder info: " + syncthingClient.indexHandler.getFolderInfo(folder)
folderInfo += "\nfolder stats: " + syncthingClient.indexHandler.newFolderBrowser().getFolderStats(folder).dumpInfo() + "\n"
for (folder in configuration.folders) {
folderInfo += "\nfolder info: " + folder
folderInfo += "\nfolder stats: " + syncthingClient.indexHandler.folderBrowser.getFolderStatusSync(folder.folderId).stats.infoDump + "\n"
}
System.out.println("folders:\n$folderInfo\n")
}
@@ -211,11 +228,12 @@ class Main(private val commandLine: CommandLine) {
}
@Throws(InterruptedException::class)
private fun waitForIndexUpdate(client: SyncthingClient, configuration: Configuration) {
val latch = CountDownLatch(configuration.peers.size)
client.indexHandler.registerOnFullIndexAcquiredListenersListener {
latch.countDown()
private fun waitForIndexUpdate(client: SyncthingClient) {
// FIXME: what happens if the index update happened already?
runBlocking {
client.indexHandler.subscribeToOnFullIndexAcquiredEvents().consume {
this.receive() // wait until there is one event
}
}
latch.await()
}
}
@@ -13,14 +13,25 @@
*/
package net.syncthing.java.client
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.channels.ConflatedBroadcastChannel
import kotlinx.coroutines.channels.consumeEach
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import net.syncthing.java.bep.connectionactor.ConnectionActorWrapper
import net.syncthing.java.bep.connectionactor.ConnectionInfo
import net.syncthing.java.core.beans.DeviceId
class Connections (val generate: (DeviceId) -> ConnectionActorWrapper) {
private val map = mutableMapOf<DeviceId, ConnectionActorWrapper>()
private val connectionStatus = ConflatedBroadcastChannel<Map<DeviceId, ConnectionInfo>>(emptyMap())
private val connectionStatusLock = Mutex()
private val job = Job()
fun getByDeviceId(deviceId: DeviceId): ConnectionActorWrapper {
return synchronized(map) {
synchronized(map) {
val oldEntry = map[deviceId]
if (oldEntry != null) {
@@ -30,6 +41,17 @@ class Connections (val generate: (DeviceId) -> ConnectionActorWrapper) {
map[deviceId] = newEntry
GlobalScope.launch (job) {
newEntry.subscribeToConnectionInfo().consumeEach { status ->
connectionStatusLock.withLock {
connectionStatus.send(
connectionStatus.value +
mapOf(deviceId to status)
)
}
}
}
return newEntry
}
}
@@ -39,6 +61,8 @@ class Connections (val generate: (DeviceId) -> ConnectionActorWrapper) {
synchronized(map) {
map.values.forEach { it.shutdown() }
}
job.cancel()
}
fun reconnectAllConnections() {
@@ -46,4 +70,12 @@ class Connections (val generate: (DeviceId) -> ConnectionActorWrapper) {
map.values.forEach { it.reconnect() }
}
}
fun reconnect(deviceId: DeviceId) {
synchronized(map) {
map[deviceId]?.reconnect()
}
}
fun subscribeToConnectionStatusMap() = connectionStatus.openSubscription()
}
@@ -16,27 +16,31 @@ package net.syncthing.java.client
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.async
import kotlinx.coroutines.runBlocking
import net.syncthing.java.bep.*
import net.syncthing.java.bep.BlockPuller
import net.syncthing.java.bep.BlockPullerStatus
import net.syncthing.java.bep.BlockPusher
import net.syncthing.java.bep.RequestHandlerRegistry
import net.syncthing.java.bep.connectionactor.ConnectionActorGenerator
import net.syncthing.java.bep.connectionactor.ConnectionActorWrapper
import net.syncthing.java.bep.index.IndexHandler
import net.syncthing.java.core.beans.DeviceId
import net.syncthing.java.core.beans.FileInfo
import net.syncthing.java.core.configuration.Configuration
import net.syncthing.java.core.exception.ExceptionReport
import net.syncthing.java.core.interfaces.IndexRepository
import net.syncthing.java.core.interfaces.TempRepository
import net.syncthing.java.discovery.DiscoveryHandler
import java.io.Closeable
import java.io.InputStream
import java.util.*
class SyncthingClient(
private val configuration: Configuration,
private val repository: IndexRepository,
private val tempRepository: TempRepository
private val tempRepository: TempRepository,
exceptionReportHandler: (ExceptionReport) -> Unit
) : Closeable {
val indexHandler = IndexHandler(configuration, repository, tempRepository)
val discoveryHandler = DiscoveryHandler(configuration)
private val onConnectionChangedListeners = Collections.synchronizedList(mutableListOf<(DeviceId) -> Unit>())
val indexHandler = IndexHandler(configuration, repository, tempRepository, exceptionReportHandler)
val discoveryHandler = DiscoveryHandler(configuration, exceptionReportHandler)
private val requestHandlerRegistry = RequestHandlerRegistry()
private val connections = Connections(
@@ -56,31 +60,20 @@ class SyncthingClient(
configuration = configuration
),
deviceId = deviceId,
connectivityChangeListener = {
synchronized(onConnectionChangedListeners) {
onConnectionChangedListeners.forEach { it(deviceId) }
}
}
exceptionReportHandler = exceptionReportHandler
)
}
)
fun clearCacheAndIndex() {
suspend fun clearCacheAndIndex() {
indexHandler.clearIndex()
configuration.folders = emptySet()
configuration.update {
it.copy(folders = emptySet())
}
configuration.persistLater()
connections.reconnectAllConnections()
}
fun addOnConnectionChangedListener(listener: (DeviceId) -> Unit) {
onConnectionChangedListeners.add(listener)
}
fun removeOnConnectionChangedListener(listener: (DeviceId) -> Unit) {
assert(onConnectionChangedListeners.contains(listener))
onConnectionChangedListeners.remove(listener)
}
private fun getConnections() = configuration.peerIds.map { connections.getByDeviceId(it) }
init {
@@ -88,6 +81,10 @@ class SyncthingClient(
getConnections()
}
fun reconnect(deviceId: DeviceId) {
connections.reconnect(deviceId)
}
fun connectToNewlyAddedDevices() {
getConnections()
}
@@ -124,11 +121,7 @@ class SyncthingClient(
)
}
fun getPeerStatus() = configuration.peers.map { device ->
device.copy(
isConnected = connections.getByDeviceId(device.deviceId).isConnected
)
}
fun subscribeToConnectionStatus() = connections.subscribeToConnectionStatusMap()
override fun close() {
discoveryHandler.close()
@@ -136,6 +129,5 @@ class SyncthingClient(
repository.close()
tempRepository.close()
connections.shutdown()
assert(onConnectionChangedListeners.isEmpty())
}
}
+1
View File
@@ -11,4 +11,5 @@ dependencies {
compile "com.google.code.gson:gson:2.8.2"
compile "org.bouncycastle:bcmail-jdk15on:1.59"
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.0.0'
}
@@ -5,8 +5,9 @@ import com.google.gson.stream.JsonWriter
import net.syncthing.java.core.utils.NetworkUtils
import org.apache.commons.codec.binary.Base32
import java.io.IOException
import java.io.Serializable
data class DeviceId @Throws(IOException::class) constructor(val deviceId: String) {
data class DeviceId @Throws(IOException::class) constructor(val deviceId: String): Serializable {
init {
val withoutDashes = this.deviceId.replace("-", "")
@@ -16,8 +16,9 @@ package net.syncthing.java.core.beans
import com.google.gson.stream.JsonReader
import com.google.gson.stream.JsonWriter
import java.io.Serializable
data class DeviceInfo(val deviceId: DeviceId, val name: String, val isConnected: Boolean? = null) {
data class DeviceInfo(val deviceId: DeviceId, val name: String): Serializable {
companion object {
private const val DEVICE_ID = "deviceId"
@@ -44,9 +45,6 @@ data class DeviceInfo(val deviceId: DeviceId, val name: String, val isConnected:
}
}
constructor(deviceId: DeviceId, name: String?) :
this(deviceId, if (name != null && !name.isBlank()) name else deviceId.shortId, null)
fun serialize(writer: JsonWriter) {
writer.beginObject()
@@ -1,4 +1,4 @@
/*
/*
* Copyright (C) 2016 Davide Imbriaco
* Copyright (C) 2018 Jonas Lochmann
*
@@ -17,20 +17,72 @@ package net.syncthing.java.core.beans
import com.google.gson.stream.JsonReader
import com.google.gson.stream.JsonWriter
open class FolderInfo(val folderId: String, label: String? = null) {
// the whitelist are device ids with which the folder should be shared
// the blacklist are device ids with which the folder should not be shared
// the ignored device ids are devices for which the user confirmed the blacklist entry so that
// there should not be any question
data class FolderInfo(
val folderId: String,
val label: String,
val deviceIdWhitelist: Set<DeviceId>,
val deviceIdBlacklist: Set<DeviceId>,
val ignoredDeviceIdList: Set<DeviceId>
) {
companion object {
private const val FOLDER_ID = "folderId"
private const val LABEL = "label"
private const val DEVICE_ID_WHITELIST = "deviceWhitelist"
private const val DEVICE_ID_BLACKLIST = "deviceBlacklist"
private const val IGNORED_DEVICE_ID_LIST = "ignoredDeviceIdList"
fun parse(reader: JsonReader): FolderInfo {
var folderId: String? = null
var label: String? = null
// the following fields were added later and thus have got a default value
var deviceIdWhitelist = emptySet<DeviceId>()
var deviceIdBlacklist = emptySet<DeviceId>()
var ignoredDeviceIdList = emptySet<DeviceId>()
reader.beginObject()
while (reader.hasNext()) {
when (reader.nextName()) {
FOLDER_ID -> folderId = reader.nextString()
LABEL -> label = reader.nextString()
DEVICE_ID_WHITELIST -> {
reader.beginArray()
deviceIdWhitelist = mutableSetOf<DeviceId>().apply {
while (reader.hasNext()) {
add(DeviceId(reader.nextString()))
}
}
reader.endArray()
}
DEVICE_ID_BLACKLIST -> {
reader.beginArray()
deviceIdBlacklist = mutableSetOf<DeviceId>().apply {
while (reader.hasNext()) {
add(DeviceId(reader.nextString()))
}
}
reader.endArray()
}
IGNORED_DEVICE_ID_LIST -> {
reader.beginArray()
ignoredDeviceIdList = mutableSetOf<DeviceId>().apply {
while (reader.hasNext()) {
add(DeviceId(reader.nextString()))
}
}
reader.endArray()
}
else -> reader.skipValue()
}
}
@@ -38,16 +90,23 @@ open class FolderInfo(val folderId: String, label: String? = null) {
return FolderInfo(
folderId = folderId!!,
label = label!!
label = label!!,
deviceIdBlacklist = deviceIdBlacklist,
deviceIdWhitelist = deviceIdWhitelist,
ignoredDeviceIdList = ignoredDeviceIdList
)
}
}
val label: String
init {
assert(!folderId.isEmpty())
this.label = if (label != null && !label.isEmpty()) label else folderId
assert(deviceIdWhitelist.find { deviceIdBlacklist.contains(it) } == null)
}
val notIgnoredBlacklistEntries: Set<DeviceId> by lazy {
deviceIdBlacklist
.filterNot { ignoredDeviceIdList.contains(it) }
.toSet()
}
override fun toString(): String {
@@ -60,7 +119,18 @@ open class FolderInfo(val folderId: String, label: String? = null) {
writer.name(FOLDER_ID).value(folderId)
writer.name(LABEL).value(label)
writer.name(DEVICE_ID_WHITELIST).beginArray()
deviceIdWhitelist.forEach { writer.value(it.deviceId) }
writer.endArray()
writer.name(DEVICE_ID_BLACKLIST).beginArray()
deviceIdBlacklist.forEach { writer.value(it.deviceId) }
writer.endArray()
writer.name(IGNORED_DEVICE_ID_LIST).beginArray()
ignoredDeviceIdList.forEach { writer.value(it.deviceId) }
writer.endArray()
writer.endObject()
}
}
@@ -1,5 +1,6 @@
/*
* Copyright (C) 2016 Davide Imbriaco
* Copyright (C) 2018 Jonas Lochmann
*
* This Java file is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
@@ -16,87 +17,22 @@ package net.syncthing.java.core.beans
import org.apache.commons.io.FileUtils
import java.util.*
class FolderStats private constructor(val fileCount: Long, val dirCount: Long, val size: Long, val lastUpdate: Date, folder: String, label: String?) : FolderInfo(folder, label) {
fun getRecordCount(): Long = dirCount + fileCount
fun describeSize(): String = FileUtils.byteCountToDisplaySize(size)
fun dumpInfo(): String {
return ("folder " + label + " (" + folderId + ") file count = " + fileCount
+ " dir count = " + dirCount + " folder size = " + describeSize() + " last update = " + lastUpdate)
data class FolderStats(val fileCount: Long, val dirCount: Long, val size: Long, val lastUpdate: Date, val folderId: String) {
companion object {
fun createDummy(folderId: String) = FolderStats(
fileCount = 0,
dirCount = 0,
size = 0,
lastUpdate = Date(0),
folderId = folderId
)
}
override fun toString(): String {
return "FolderStats{folder=$folderId, fileCount=$fileCount, dirCount=$dirCount, size=$size, lastUpdate=$lastUpdate}"
}
val recordCount: Long = dirCount + fileCount
fun copyBuilder(): Builder = Builder(fileCount, dirCount, size, folderId, label)
class Builder {
private var fileCount: Long = 0
private var dirCount: Long = 0
private var size: Long = 0
private var lastUpdate = Date(0)
private var folder: String? = null
private var label: String? = null
constructor()
constructor(fileCount: Long, dirCount: Long, size: Long, folder: String, label: String) {
this.fileCount = fileCount
this.dirCount = dirCount
this.size = size
this.folder = folder
this.label = label
}
fun getFileCount(): Long = fileCount
fun setFileCount(fileCount: Long): Builder {
this.fileCount = fileCount
return this
}
fun getDirCount(): Long = dirCount
fun setDirCount(dirCount: Long): Builder {
this.dirCount = dirCount
return this
}
fun getSize(): Long = size
fun setSize(size: Long): Builder {
this.size = size
return this
}
fun getLastUpdate(): Date = lastUpdate
fun setLastUpdate(lastUpdate: Date): Builder {
this.lastUpdate = lastUpdate
return this
}
fun getFolder(): String? = folder
fun setFolder(folder: String): Builder {
this.folder = folder
return this
}
fun getLabel(): String? = label
fun setLabel(label: String): Builder {
this.label = label
return this
}
fun build(): FolderStats {
return FolderStats(fileCount, dirCount, size, lastUpdate, folder!!, label)
}
val sizeDescription: String by lazy { FileUtils.byteCountToDisplaySize(size) }
val infoDump: String by lazy {
"folder $folderId file count = $fileCount dir count = $dirCount folder size = $sizeDescription last update = $lastUpdate"
}
}
@@ -1,5 +1,6 @@
/*
* Copyright (C) 2016 Davide Imbriaco
* Copyright (C) 2018 Jonas Lochmann
*
* This Java file is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
@@ -14,98 +15,11 @@
package net.syncthing.java.core.beans
class IndexInfo private constructor(folder: String, val deviceId: String, val indexId: Long, val localSequence: Long, val maxSequence: Long) : FolderInfo(folder) {
data class IndexInfo(val folderId: String, val deviceId: String, val indexId: Long, val localSequence: Long, val maxSequence: Long) {
fun getCompleted(): Double = if (maxSequence > 0) localSequence.toDouble() / maxSequence else 0.0
val completed: Double = if (maxSequence > 0) localSequence.toDouble() / maxSequence else 0.0
init {
assert(!deviceId.isEmpty())
assert(deviceId.isNotEmpty())
}
fun copyBuilder(): Builder {
return Builder(folderId, indexId, deviceId, localSequence, maxSequence)
}
override fun toString(): String {
return "FolderIndexInfo{indexId=$indexId, folder=$folderId, deviceId=$deviceId, localSequence=$localSequence, maxSequence=$maxSequence}"
}
class Builder {
private var indexId: Long = 0
private var deviceId: String? = null
private var folder: String? = null
private var localSequence: Long = 0
private var maxSequence: Long = 0
internal constructor()
internal constructor(folder: String, indexId: Long, deviceId: String, localSequence: Long, maxSequence: Long) {
assert(!folder.isEmpty())
assert(!deviceId.isEmpty())
this.folder = folder
this.indexId = indexId
this.deviceId = deviceId
this.localSequence = localSequence
this.maxSequence = maxSequence
}
fun getIndexId(): Long {
return indexId
}
fun getDeviceId(): String? {
return deviceId
}
fun getFolder(): String? {
return folder
}
fun getLocalSequence(): Long {
return localSequence
}
fun getMaxSequence(): Long {
return maxSequence
}
fun setIndexId(indexId: Long): Builder {
this.indexId = indexId
return this
}
fun setDeviceId(deviceId: String): Builder {
this.deviceId = deviceId
return this
}
fun setFolder(folder: String): Builder {
this.folder = folder
return this
}
fun setLocalSequence(localSequence: Long): Builder {
this.localSequence = localSequence
return this
}
fun setMaxSequence(maxSequence: Long): Builder {
this.maxSequence = maxSequence
return this
}
fun build(): IndexInfo {
return IndexInfo(folder!!, deviceId!!, indexId, localSequence, maxSequence)
}
}
companion object {
fun newBuilder(): Builder {
return Builder()
}
}
}
@@ -2,6 +2,14 @@ package net.syncthing.java.core.configuration
import com.google.gson.stream.JsonReader
import com.google.gson.stream.JsonWriter
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.channels.ConflatedBroadcastChannel
import kotlinx.coroutines.channels.sendBlocking
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import net.syncthing.java.core.beans.DeviceId
import net.syncthing.java.core.beans.DeviceInfo
import net.syncthing.java.core.beans.FolderInfo
@@ -17,12 +25,14 @@ import java.util.*
class Configuration(configFolder: File = DefaultConfigFolder) {
private val logger = LoggerFactory.getLogger(javaClass)
private val modifyLock = Mutex()
private val saveLock = Mutex()
private val configChannel = ConflatedBroadcastChannel<Config>()
private val configFile = File(configFolder, ConfigFileName)
val databaseFolder = File(configFolder, DatabaseFolderName)
private var isSaved = true
private var config: Config
init {
configFolder.mkdirs()
@@ -36,19 +46,23 @@ class Configuration(configFolder: File = DefaultConfigFolder) {
}
val keystoreData = KeystoreHandler.Loader().generateKeystore()
isSaved = false
config = Config(peers = setOf(), folders = setOf(),
localDeviceName = localDeviceName,
localDeviceId = keystoreData.first.deviceId,
keystoreData = Base64.toBase64String(keystoreData.second),
keystoreAlgorithm = keystoreData.third,
customDiscoveryServers = emptySet(),
useDefaultDiscoveryServers = true
configChannel.sendBlocking(
Config(peers = setOf(), folders = setOf(),
localDeviceName = localDeviceName,
localDeviceId = keystoreData.first.deviceId,
keystoreData = Base64.toBase64String(keystoreData.second),
keystoreAlgorithm = keystoreData.third,
customDiscoveryServers = emptySet(),
useDefaultDiscoveryServers = true
)
)
persistNow()
runBlocking { persistNow() }
} else {
config = Config.parse(JsonReader(StringReader(configFile.readText())))
configChannel.sendBlocking(
Config.parse(JsonReader(StringReader(configFile.readText())))
)
}
logger.debug("Loaded config = $config")
logger.debug("Loaded config = ${configChannel.value}")
}
companion object {
@@ -60,72 +74,89 @@ class Configuration(configFolder: File = DefaultConfigFolder) {
val instanceId = Math.abs(Random().nextLong())
val localDeviceId: DeviceId
get() = DeviceId(config.localDeviceId)
get() = DeviceId(configChannel.value.localDeviceId)
val discoveryServers: Set<DiscoveryServer>
get() = config.customDiscoveryServers + (if (config.useDefaultDiscoveryServers) DiscoveryServer.defaultDiscoveryServers else emptySet())
get() = configChannel.value.let { config ->
config.customDiscoveryServers + (if (config.useDefaultDiscoveryServers) DiscoveryServer.defaultDiscoveryServers else emptySet())
}
val keystoreData: ByteArray
get() = Base64.decode(config.keystoreData)
get() = Base64.decode(configChannel.value.keystoreData)
val keystoreAlgorithm: String
get() = config.keystoreAlgorithm
get() = configChannel.value.keystoreAlgorithm
val clientName = "syncthing-java"
val clientVersion = javaClass.`package`.implementationVersion ?: "0.0.0"
val peerIds: Set<DeviceId>
get() = config.peers.map { it.deviceId }.toSet()
get() = configChannel.value.peers.map { it.deviceId }.toSet()
var localDeviceName: String
get() = config.localDeviceName
set(localDeviceName) {
config = config.copy(localDeviceName = localDeviceName)
isSaved = false
val localDeviceName: String
get() = configChannel.value.localDeviceName
val folders: Set<FolderInfo>
get() = configChannel.value.folders
val peers: Set<DeviceInfo>
get() = configChannel.value.peers
suspend fun update(operation: suspend (Config) -> Config): Boolean {
modifyLock.withLock {
val oldConfig = configChannel.value
val newConfig = operation(oldConfig)
if (oldConfig != newConfig) {
configChannel.send(newConfig)
isSaved = false
return true
} else {
return false
}
}
}
var folders: Set<FolderInfo>
get() = config.folders
set(folders) {
config = config.copy(folders = folders)
isSaved = false
}
var peers: Set<DeviceInfo>
get() = config.peers
set(peers) {
config = config.copy(peers = peers)
isSaved = false
}
fun persistNow() {
suspend fun persistNow() {
persist()
}
fun persistLater() {
Thread { persist() }.start()
GlobalScope.launch (Dispatchers.IO) { persist() }
}
private fun persist() {
if (isSaved)
return
private suspend fun persist() {
saveLock.withLock {
val (config1, isConfig1Saved) = modifyLock.withLock { configChannel.value to isSaved }
if (isConfig1Saved) {
return
}
config.let {
System.out.println("writing config to $configFile")
configFile.writeText(
StringWriter().apply {
JsonWriter(this).apply {
setIndent(" ")
config.serialize(this)
config1.serialize(this)
}
}.toString()
)
isSaved = true
modifyLock.withLock {
if (config1 === configChannel.value) {
isSaved = true
}
}
}
}
fun subscribe() = configChannel.openSubscription()
override fun toString() = "Configuration(peers=$peers, folders=$folders, localDeviceName=$localDeviceName, " +
"localDeviceId=${localDeviceId.deviceId}, discoveryServers=$discoveryServers, instanceId=$instanceId, " +
"configFile=$configFile, databaseFolder=$databaseFolder)"
@@ -0,0 +1,82 @@
/*
* Copyright 2018 Jonas Lochmann
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.syncthing.java.core.exception
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Job
import kotlin.Exception
data class ExceptionReport(
val component: String,
val exception: Exception,
val details: List<ExceptionDetails>
) {
companion object {
fun fromException(exception: Exception, component: String) = ExceptionReport(
component,
exception,
ExceptionDetailException.getExceptionReportDetails(exception)
)
}
val detailsReadableString: String by lazy {
details.map { it.readableString }.joinToString("\n")
}
}
data class ExceptionDetails(
val component: String,
val details: String
) {
val readableString: String by lazy { component + "\n" + details + "\n" }
}
class ExceptionDetailException(
cause: Throwable,
val details: ExceptionDetails
): Exception(cause) {
companion object {
fun getExceptionReportDetails(exception: Exception): List<ExceptionDetails> {
val result = mutableListOf<ExceptionDetails>()
var ex: Throwable? = exception
while (ex != null) {
if (ex is ExceptionDetailException) {
result.add(ex.details)
}
ex = ex.cause
}
return result.reversed()
}
}
}
fun Job.reportExceptions(component: String, exceptionReportHandler: (ExceptionReport) -> Unit) {
invokeOnCompletion {
if (it != null) {
if (it is Exception) {
if (it is CancellationException) {
// ignore
} else {
exceptionReportHandler(ExceptionReport.fromException(it, component))
}
} else {
throw it
}
}
}
}
@@ -1,5 +1,6 @@
/*
* Copyright (C) 2016 Davide Imbriaco
* Copyright (C) 2018 Jonas Lochmann
*
* This Java file is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
@@ -13,46 +14,8 @@
*/
package net.syncthing.java.core.interfaces
import net.syncthing.java.core.beans.*
import java.io.Closeable
import java.util.*
interface IndexRepository: Closeable {
fun setOnFolderStatsUpdatedListener(listener: ((IndexRepository.FolderStatsUpdatedEvent) -> Unit)?)
fun getSequencer(): Sequencer
fun updateIndexInfo(indexInfo: IndexInfo)
fun findIndexInfoByDeviceAndFolder(deviceId: DeviceId, folder: String): IndexInfo?
fun findFileInfo(folder: String, path: String): FileInfo?
fun findFileInfoLastModified(folder: String, path: String): Date?
fun findNotDeletedFileInfo(folder: String, path: String): FileInfo?
fun findFileBlocks(folder: String, path: String): FileBlocks?
fun updateFileInfo(fileInfo: FileInfo, fileBlocks: FileBlocks?)
fun findNotDeletedFilesByFolderAndParent(folder: String, parentPath: String): List<FileInfo>
fun clearIndex()
fun findFolderStats(folder: String): FolderStats?
fun findAllFolderStats(): List<FolderStats>
fun findFileInfoBySearchTerm(query: String): List<FileInfo>
fun countFileInfoBySearchTerm(query: String): Long
abstract class FolderStatsUpdatedEvent {
abstract fun getFolderStats(): List<FolderStats>
}
fun <T> runInTransaction(action: (IndexTransaction) -> T): T
}
@@ -0,0 +1,57 @@
/*
* Copyright (C) 2016 Davide Imbriaco
* Copyright (C) 2018 Jonas Lochmann
*
* This Java file is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.syncthing.java.core.interfaces
import net.syncthing.java.core.beans.*
import java.util.*
interface IndexTransaction {
fun getSequencer(): Sequencer
fun updateIndexInfo(indexInfo: IndexInfo)
fun findAllIndexInfos(): List<IndexInfo>
fun findIndexInfoByDeviceAndFolder(deviceId: DeviceId, folder: String): IndexInfo?
fun findFileInfo(folder: String, path: String): FileInfo?
// path to FileInfo
fun findFileInfo(folder: String, path: List<String>): Map<String, FileInfo>
fun findFileInfoLastModified(folder: String, path: String): Date?
fun findNotDeletedFileInfo(folder: String, path: String): FileInfo?
fun findFileBlocks(folder: String, path: String): FileBlocks?
fun updateFileInfo(fileInfo: FileInfo, fileBlocks: FileBlocks?)
fun updateFileInfoAndBlocks(fileInfos: List<FileInfo>, fileBlocks: List<FileBlocks>)
fun findNotDeletedFilesByFolderAndParent(folder: String, parentPath: String): List<FileInfo>
fun clearIndex()
fun findFolderStats(folder: String): FolderStats?
fun findAllFolderStats(): List<FolderStats>
fun updateOrInsertFolderStats(folder: String, deltaFileCount: Long, deltaDirCount: Long, deltaSize: Long, lastUpdate: Date)
fun findFileInfoBySearchTerm(query: String): List<FileInfo>
fun countFileInfoBySearchTerm(query: String): Long
}
@@ -1,5 +1,6 @@
/*
* Copyright (C) 2016 Davide Imbriaco
* Copyright (C) 2018 Jonas Lochmann
*
* This Java file is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
@@ -22,4 +23,6 @@ interface TempRepository: Closeable {
fun popTempData(key: String): ByteArray
fun deleteTempData(keys: List<String>)
fun deleteAllTempData()
}
@@ -1,56 +0,0 @@
/*
* Copyright (C) 2016 Davide Imbriaco
*
* This Java file is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.syncthing.java.core.utils
import org.slf4j.LoggerFactory
import java.util.concurrent.ExecutorService
import java.util.concurrent.Future
import java.util.concurrent.RejectedExecutionException
import java.util.concurrent.TimeUnit
private val logger = LoggerFactory.getLogger(ExecutorService::class.java)
fun ExecutorService.awaitTerminationSafe() {
try {
awaitTermination(2, TimeUnit.SECONDS)
} catch (ex: InterruptedException) {
logger.warn("", ex)
}
}
fun ExecutorService.submitLogging(runnable: Runnable) = submitLogging { runnable.run() }
/**
* Wrapper method for [[ExecutorService.submit]], which silently swallows exceptions. If an exception is thrown in
* [[runnable]], logs the exception and force crashes
*/
fun <T> ExecutorService.submitLogging(runnable: () -> T): Future<T> {
return submit<T>({
try {
runnable()
} catch (e: Exception) {
logger.error("", e)
System.exit(1)
null
}
})
}
fun ExecutorService.trySubmitLogging(runnable: Runnable) {
try {
submitLogging(runnable)
} catch (ex: RejectedExecutionException) {
logger.warn("could not submit task", ex)
}
}
@@ -14,6 +14,9 @@
*/
package net.syncthing.java.core.utils
import net.syncthing.java.core.exception.ExceptionDetailException
import net.syncthing.java.core.exception.ExceptionDetails
object PathUtils {
const val ROOT_PATH = ""
const val PATH_SEPARATOR = "/"
@@ -31,29 +34,56 @@ object PathUtils {
return pathSegments.contains(PARENT_PATH) or pathSegments.contains(CURRENT_PATH)
}
private fun isTrimmed(value: String) = value.trim() == value
private fun containsWindowsPathSeparator(path: String) = path.contains(PATH_SEPARATOR_WIN)
private fun startsWithPathSeperator(path: String) = path.startsWith(PATH_SEPARATOR)
private fun isValidPath(path: String) = (!containsRelativeElements(path)) and
(!containsWindowsPathSeparator(path)) and
path.isNotEmpty() and
(!startsWithPathSeperator(path)) and
isTrimmed(path)
private fun startsWithPathSeparator(path: String) = path.startsWith(PATH_SEPARATOR)
private fun containsPathSeparator(file: String) = file.contains(PATH_SEPARATOR) or file.contains(PATH_SEPARATOR_WIN)
private fun isFilenameValid(file: String) = file.isNotBlank() and
(!containsPathSeparator(file)) and
isTrimmed(file)
private fun assertPathValid(path: String) {
if (!isValidPath(path)) {
throw IllegalArgumentException("provided path is invalid")
fun throwException(reason: String) {
throw ExceptionDetailException(
IllegalArgumentException("provided path is invalid because it $reason"),
ExceptionDetails(
component = "PathUtils",
details = "processed path: \"$path\""
)
)
}
if (containsRelativeElements(path)) {
throwException("contains relative path elements")
}
if (containsWindowsPathSeparator(path)) {
throwException("contains windows path separators")
}
if (path.isEmpty()) {
throwException("is empty")
}
if (startsWithPathSeparator(path)) {
throwException("starts with a path separator")
}
}
private fun assertFilenameValid(filename: String) {
if (!isFilenameValid(filename)) {
throw IllegalArgumentException("provided filename is invalid")
fun throwException(reason: String) {
throw ExceptionDetailException(
IllegalArgumentException("provided filename is invalid because the filename $reason"),
ExceptionDetails(
component = "PathUtils",
details = "processed filename: \"$filename\""
)
)
}
if (filename.isBlank()) {
throwException("is blank")
}
if (containsPathSeparator(filename)) {
throwException("contains a path separator")
}
}
@@ -20,6 +20,7 @@ import kotlinx.coroutines.launch
import net.syncthing.java.core.beans.DeviceAddress
import net.syncthing.java.core.beans.DeviceId
import net.syncthing.java.core.configuration.Configuration
import net.syncthing.java.core.exception.ExceptionReport
import net.syncthing.java.discovery.protocol.GlobalDiscoveryHandler
import net.syncthing.java.discovery.protocol.LocalDiscoveryHandler
import net.syncthing.java.discovery.utils.AddressRanker
@@ -27,19 +28,27 @@ import org.slf4j.LoggerFactory
import java.io.Closeable
import java.util.*
class DiscoveryHandler(private val configuration: Configuration) : Closeable {
class DiscoveryHandler(
private val configuration: Configuration,
exceptionReportHandler: (ExceptionReport) -> Unit
) : Closeable {
private val logger = LoggerFactory.getLogger(javaClass)
private val globalDiscoveryHandler = GlobalDiscoveryHandler(configuration)
private val localDiscoveryHandler = LocalDiscoveryHandler(configuration, { message ->
logger.info("received device address list from local discovery")
private val localDiscoveryHandler = LocalDiscoveryHandler(
configuration,
exceptionReportHandler,
{ message ->
logger.info("received device address list from local discovery")
GlobalScope.launch {
processDeviceAddressBg(message.addresses)
}
}, { deviceId ->
onMessageFromUnknownDeviceListeners.forEach { listener -> listener(deviceId) }
})
GlobalScope.launch {
processDeviceAddressBg(message.addresses)
}
},
{ deviceId ->
onMessageFromUnknownDeviceListeners.forEach { listener -> listener(deviceId) }
}
)
val devicesAddressesManager = DevicesAddressesManager()
private var isClosed = false
private val onMessageFromUnknownDeviceListeners = Collections.synchronizedSet(HashSet<(DeviceId) -> Unit>())
@@ -83,14 +83,20 @@ class Main {
private fun queryLocalDiscovery(configuration: Configuration, deviceId: DeviceId): Collection<DeviceAddress> {
val lock = Object()
val discoveredAddresses = mutableListOf<DeviceAddress>()
val handler = LocalDiscoveryHandler(configuration, { message ->
synchronized(lock) {
if (message.deviceId == deviceId) {
discoveredAddresses.addAll(message.addresses)
lock.notify()
val handler = LocalDiscoveryHandler(
configuration,
{
throw it.exception
},
{ message ->
synchronized(lock) {
if (message.deviceId == deviceId) {
discoveredAddresses.addAll(message.addresses)
lock.notify()
}
}
}
}
})
)
handler.startListener()
handler.sendAnnounceMessage()
synchronized(lock) {
@@ -14,34 +14,37 @@
*/
package net.syncthing.java.discovery.protocol
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.consumeEach
import kotlinx.coroutines.launch
import net.syncthing.java.core.beans.DeviceId
import net.syncthing.java.core.configuration.Configuration
import net.syncthing.java.core.exception.ExceptionReport
import net.syncthing.java.core.exception.reportExceptions
import org.slf4j.LoggerFactory
import java.io.Closeable
import java.io.IOException
internal class LocalDiscoveryHandler(private val configuration: Configuration,
private val onMessageReceivedListener: (LocalDiscoveryMessage) -> Unit,
private val onMessageFromUnknownDeviceListener: (DeviceId) -> Unit = {}) : Closeable {
internal class LocalDiscoveryHandler(
private val configuration: Configuration,
private val exceptionReportHandler: (ExceptionReport) -> Unit,
private val onMessageReceivedListener: (LocalDiscoveryMessage) -> Unit,
private val onMessageFromUnknownDeviceListener: (DeviceId) -> Unit = {}
) : Closeable {
private val logger = LoggerFactory.getLogger(javaClass)
private val job = Job()
fun sendAnnounceMessage() {
GlobalScope.launch {
GlobalScope.async (Dispatchers.IO) {
LocalDiscoveryUtil.sendAnnounceMessage(
ownDeviceId = configuration.localDeviceId,
instanceId = configuration.instanceId
)
}
}.reportExceptions("LocalDiscoveryHandler.sendAnnounceMessage", exceptionReportHandler)
}
fun startListener() {
GlobalScope.launch (job) {
GlobalScope.async (job) {
try {
LocalDiscoveryUtil.listenForAnnounceMessages().consumeEach { message ->
if (message.deviceId == configuration.localDeviceId) {
@@ -59,7 +62,7 @@ internal class LocalDiscoveryHandler(private val configuration: Configuration,
} catch (ex: IOException) {
logger.warn("Failed to listen for announcement messages", ex)
}
}
}.reportExceptions("LocalDiscoveryHandler.startListener", exceptionReportHandler)
}
override fun close() {
@@ -22,11 +22,14 @@ import kotlinx.coroutines.channels.produce
import kotlinx.coroutines.withContext
import net.syncthing.java.core.beans.DeviceAddress
import net.syncthing.java.core.beans.DeviceId
import net.syncthing.java.core.exception.ExceptionDetailException
import net.syncthing.java.core.exception.ExceptionDetails
import net.syncthing.java.core.utils.NetworkUtils
import org.slf4j.LoggerFactory
import java.io.ByteArrayOutputStream
import java.io.DataOutputStream
import java.io.IOException
import java.lang.Exception
import java.net.*
import java.nio.ByteBuffer
@@ -117,14 +120,24 @@ object LocalDiscoveryUtil {
if (broadcastAddress != null) {
logger.debug("sending broadcast announce on {}", broadcastAddress)
DatagramSocket().use { broadcastSocket ->
broadcastSocket.broadcast = true
try {
DatagramSocket().use { broadcastSocket ->
broadcastSocket.broadcast = true
broadcastSocket.send(DatagramPacket(
discoveryMessage,
discoveryMessage.size,
broadcastAddress,
LISTENING_PORT))
broadcastSocket.send(DatagramPacket(
discoveryMessage,
discoveryMessage.size,
broadcastAddress,
LISTENING_PORT))
}
} catch (ex: Exception) {
throw ExceptionDetailException(
ex,
ExceptionDetails(
component = "LocalDiscoveryUtil.sendAnnounceMessage",
details = "interface: $networkInterface\naddress: $interfaceAddress\nbroadcast address: $broadcastAddress"
)
)
}
}
}
@@ -1,124 +1,32 @@
package net.syncthing.repository.android
import net.syncthing.java.core.beans.*
import net.syncthing.java.core.interfaces.IndexRepository
import net.syncthing.java.core.interfaces.Sequencer
import net.syncthing.java.core.interfaces.IndexTransaction
import net.syncthing.repository.android.database.RepositoryDatabase
import net.syncthing.repository.android.database.item.*
import java.util.*
import java.util.concurrent.Callable
class SqliteIndexRepository(
private val database: RepositoryDatabase,
private val closeDatabaseOnClose: Boolean,
private val clearTempStorageHook: () -> Unit
): IndexRepository {
private var folderStatsChangeListener: ((IndexRepository.FolderStatsUpdatedEvent) -> Unit)? = null
// FileInfo
override fun findFileInfo(folder: String, path: String) = database.fileInfo().findFileInfo(folder, path)?.native
override fun findFileInfoBySearchTerm(query: String) = database.fileInfo().findFileInfoBySearchTerm(query).map { it.native }
override fun findFileInfoLastModified(folder: String, path: String): Date? = database.fileInfo().findFileInfoLastModified(folder, path)?.lastModified
override fun findNotDeletedFileInfo(folder: String, path: String) = database.fileInfo().findNotDeletedFileInfo(folder, path)?.native
override fun findNotDeletedFilesByFolderAndParent(folder: String, parentPath: String) = database.fileInfo().findNotDeletedFilesByFolderAndParent(folder, parentPath).map { it.native }
override fun countFileInfoBySearchTerm(query: String) = database.fileInfo().countFileInfoBySearchTerm(query)
override fun <T> runInTransaction(action: (IndexTransaction) -> T): T {
return database.runInTransaction (object: Callable<T> {
override fun call(): T {
val transaction = SqliteTransaction(
database = database,
threadId = Thread.currentThread().id,
clearTempStorageHook = clearTempStorageHook
)
override fun updateFileInfo(fileInfo: FileInfo, fileBlocks: FileBlocks?) {
val newFileInfo = fileInfo
val newFileBlocks = fileBlocks
database.runInTransaction {
if (newFileBlocks != null) {
FileInfo.checkBlocks(newFileInfo, newFileBlocks)
database.fileBlocks().mergeBlock(FileBlocksItem.fromNative(newFileBlocks))
}
val oldFileInfo = findFileInfo(newFileInfo.folder, newFileInfo.path)
database.fileInfo().updateFileInfo(FileInfoItem.fromNative(newFileInfo))
//update stats
var deltaFileCount = 0L
var deltaDirCount= 0L
var deltaSize = 0L
val oldMissing = oldFileInfo == null || oldFileInfo.isDeleted
val newMissing = newFileInfo.isDeleted
val oldSizeMissing = oldMissing || !oldFileInfo!!.isFile()
val newSizeMissing = newMissing || !newFileInfo.isFile()
if (!oldSizeMissing) {
deltaSize -= oldFileInfo!!.size!!
}
if (!newSizeMissing) {
deltaSize += newFileInfo.size!!
}
if (!oldMissing) {
if (oldFileInfo!!.isFile()) {
deltaFileCount--
} else if (oldFileInfo.isDirectory()) {
deltaDirCount--
}
}
if (!newMissing) {
if (newFileInfo.isFile()) {
deltaFileCount++
} else if (newFileInfo.isDirectory()) {
deltaDirCount++
}
}
val newFolderStats = kotlin.run {
val updatedRows = database.folderStats().updateFolderStats(
folder = newFileInfo.folder,
deltaDirCount = deltaDirCount,
deltaFileCount = deltaFileCount,
deltaSize = deltaSize,
lastUpdate = newFileInfo.lastModified
)
if (updatedRows == 0L) {
database.folderStats().insertFolderStats(FolderStatsItem(
folder = newFileInfo.folder,
dirCount = deltaDirCount,
fileCount = deltaFileCount,
size = deltaSize,
lastUpdate = newFileInfo.lastModified
))
}
database.folderStats().getFolderStats(newFileInfo.folder)!!
}
folderStatsChangeListener?.invoke(object : IndexRepository.FolderStatsUpdatedEvent() {
override fun getFolderStats(): List<FolderStats> {
return listOf(newFolderStats.native)
return try {
action(transaction)
} finally {
transaction.markFinished()
}
}
})
}
}
// FileBlocks
override fun findFileBlocks(folder: String, path: String) = database.fileBlocks().findFileBlocks(folder, path)?.native
// FolderStats
override fun findAllFolderStats() = database.folderStats().findAllFolderStats().map { it.native }
override fun findFolderStats(folder: String): FolderStats? = database.folderStats().findFolderStats(folder)?.native
// IndexInfo
override fun updateIndexInfo(indexInfo: IndexInfo) {
database.folderIndexInfo().updateIndexInfo(FolderIndexInfoItem.fromNative(indexInfo))
}
override fun findIndexInfoByDeviceAndFolder(deviceId: DeviceId, folder: String): IndexInfo? = database.folderIndexInfo().findIndexInfoByDeviceAndFolder(deviceId, folder)?.native
// managment
override fun clearIndex() {
database.clearAllTables()
clearTempStorageHook()
}
override fun close() {
@@ -126,39 +34,4 @@ class SqliteIndexRepository(
database.close()
}
}
// other
private val sequencer = object: Sequencer {
fun getDatabaseEntry(): IndexSequenceItem {
val entry = database.indexSequence().getItem()
if (entry != null) {
return entry
}
val newEntry = IndexSequenceItem(
indexId = Math.abs(Random().nextLong()) + 1,
currentSequence = Math.abs(Random().nextLong()) + 1
)
database.indexSequence().createItem(newEntry)
return newEntry
}
override fun indexId() = getDatabaseEntry().indexId
override fun currentSequence() = getDatabaseEntry().currentSequence
override fun nextSequence(): Long {
database.indexSequence().incrementSequenceNumber(indexId())
return currentSequence()
}
}
override fun getSequencer() = sequencer
override fun setOnFolderStatsUpdatedListener(listener: ((IndexRepository.FolderStatsUpdatedEvent) -> Unit)?) {
folderStatsChangeListener = listener
}
}
@@ -0,0 +1,169 @@
package net.syncthing.repository.android
import net.syncthing.java.core.beans.*
import net.syncthing.java.core.interfaces.IndexTransaction
import net.syncthing.java.core.interfaces.Sequencer
import net.syncthing.repository.android.database.RepositoryDatabase
import net.syncthing.repository.android.database.item.*
import java.util.*
class SqliteTransaction(
private val database: RepositoryDatabase,
private val threadId: Long,
private val clearTempStorageHook: () -> Unit
): IndexTransaction {
private var finished = false
private fun assertAllowed() {
if (finished) {
throw IllegalStateException("tried to use a transaction which is already done")
}
if (Thread.currentThread().id != threadId) {
throw IllegalStateException("tried to access the transaction from an other Thread")
}
}
fun markFinished() {
finished = true
}
private fun <T> runIfAllowed(block: () -> T): T {
assertAllowed()
return block()
}
// FileInfo
override fun findFileInfo(folder: String, path: String) = runIfAllowed {
database.fileInfo().findFileInfo(folder, path)?.native
}
override fun findFileInfo(folder: String, path: List<String>): Map<String, FileInfo> = runIfAllowed {
database.fileInfo().findFileInfo(folder, path)
.map { it.native }
.associateBy { it.path }
}
override fun findFileInfoBySearchTerm(query: String) = runIfAllowed {
database.fileInfo().findFileInfoBySearchTerm(query).map { it.native }
}
override fun findFileInfoLastModified(folder: String, path: String): Date? = runIfAllowed {
database.fileInfo().findFileInfoLastModified(folder, path)?.lastModified
}
override fun findNotDeletedFileInfo(folder: String, path: String) = runIfAllowed {
database.fileInfo().findNotDeletedFileInfo(folder, path)?.native
}
override fun findNotDeletedFilesByFolderAndParent(folder: String, parentPath: String) = runIfAllowed {
database.fileInfo().findNotDeletedFilesByFolderAndParent(folder, parentPath).map { it.native }
}
override fun countFileInfoBySearchTerm(query: String) = runIfAllowed {
database.fileInfo().countFileInfoBySearchTerm(query)
}
override fun updateFileInfo(fileInfo: FileInfo, fileBlocks: FileBlocks?) = runIfAllowed {
if (fileBlocks != null) {
FileInfo.checkBlocks(fileInfo, fileBlocks)
database.fileBlocks().mergeBlock(FileBlocksItem.fromNative(fileBlocks))
}
database.fileInfo().updateFileInfo(FileInfoItem.fromNative(fileInfo))
}
override fun updateFileInfoAndBlocks(fileInfos: List<FileInfo>, fileBlocks: List<FileBlocks>) = runIfAllowed {
if (fileInfos.isNotEmpty()) {
database.fileInfo().updateFileInfo(fileInfos.map { FileInfoItem.fromNative(it) })
}
if (fileBlocks.isNotEmpty()) {
database.fileBlocks().mergeBlocks(fileBlocks.map { FileBlocksItem.fromNative(it) })
}
}
// FileBlocks
override fun findFileBlocks(folder: String, path: String) = runIfAllowed {
database.fileBlocks().findFileBlocks(folder, path)?.native
}
// FolderStats
override fun findAllFolderStats() = runIfAllowed {
database.folderStats().findAllFolderStats().map { it.native }
}
override fun findFolderStats(folder: String): FolderStats? = runIfAllowed {
database.folderStats().findFolderStats(folder)?.native
}
override fun updateOrInsertFolderStats(
folder: String,
deltaFileCount: Long,
deltaDirCount: Long,
deltaSize: Long,
lastUpdate: Date
) = runIfAllowed {
if (database.folderStats().updateFolderStats(folder, deltaFileCount, deltaDirCount, deltaSize, lastUpdate) == 0L) {
database.folderStats().insertFolderStats(FolderStatsItem(folder, deltaFileCount, deltaDirCount, lastUpdate, deltaSize))
}
}
// IndexInfo
override fun updateIndexInfo(indexInfo: IndexInfo) = runIfAllowed {
database.folderIndexInfo().updateIndexInfo(FolderIndexInfoItem.fromNative(indexInfo))
}
override fun findIndexInfoByDeviceAndFolder(deviceId: DeviceId, folder: String): IndexInfo? = runIfAllowed {
database.folderIndexInfo().findIndexInfoByDeviceAndFolder(deviceId, folder)?.native
}
override fun findAllIndexInfos(): List<IndexInfo> = runIfAllowed {
database.folderIndexInfo().findAllIndexInfo().map { it.native }
}
// managment
override fun clearIndex() {
runIfAllowed {
database.clearAllTables()
clearTempStorageHook()
}
}
// other
private val sequencer = object: Sequencer {
private fun getDatabaseEntry(): IndexSequenceItem {
val entry = database.indexSequence().getItem()
if (entry != null) {
return entry
}
val newEntry = IndexSequenceItem(
indexId = Math.abs(Random().nextLong()) + 1,
currentSequence = Math.abs(Random().nextLong()) + 1
)
database.indexSequence().createItem(newEntry)
return newEntry
}
override fun indexId() = runIfAllowed { getDatabaseEntry().indexId }
override fun currentSequence() = runIfAllowed { getDatabaseEntry().currentSequence }
override fun nextSequence(): Long = runIfAllowed {
database.indexSequence().incrementSequenceNumber(indexId())
currentSequence()
}
}
override fun getSequencer() = sequencer
}
@@ -1,3 +1,16 @@
/*
* Copyright (C) 2018 Jonas Lochmann
*
* This Java file is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.syncthing.repository.android
import net.syncthing.java.core.interfaces.TempRepository
@@ -14,7 +27,7 @@ class TempDirectoryLocalRepository(private val directory: File): TempRepository
directory.mkdirs()
// there could be garbage from the previous session which we don't need anymore
deleteAllData()
deleteAllTempData()
}
override fun pushTempData(data: ByteArray): String {
@@ -59,10 +72,10 @@ class TempDirectoryLocalRepository(private val directory: File): TempRepository
}
override fun close() {
deleteAllData()
deleteAllTempData()
}
fun deleteAllData() {
override fun deleteAllTempData() {
directory.listFiles().forEach { file ->
if (file.isFile) {
file.delete()
@@ -13,4 +13,7 @@ interface FileBlocksDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun mergeBlock(blocksItem: FileBlocksItem)
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun mergeBlocks(blocksItem: List<FileBlocksItem>)
}

Some files were not shown because too many files have changed in this diff Show More