Compare commits
35 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b96c672a8e | |||
| 2a25e9882b | |||
| cb33d8f3e4 | |||
| db8e91eafa | |||
| f9b91f6ef8 | |||
| 01fb92e2c9 | |||
| 4b519e84e3 | |||
| f3d51f0cb9 | |||
| fa30beb9d5 | |||
| 919fdc31bd | |||
| b3f2af0ee7 | |||
| f33364939b | |||
| 1a773daf24 | |||
| b115a99907 | |||
| 5d09d011b1 | |||
| 71a433edf6 | |||
| 974817b7a3 | |||
| f00760bddd | |||
| 5f539c4149 | |||
| 1869a49c2c | |||
| 91289b05ce | |||
| 98bc67939f | |||
| fcb31ae9fa | |||
| 147ad6abcc | |||
| 4c13af3662 | |||
| 17f9ad336c | |||
| 852fc0d230 | |||
| 0032726e3e | |||
| c70211bc24 | |||
| a7f80fa45c | |||
| 461d64950b | |||
| c37832d084 | |||
| f336a2932f | |||
| 0b3e2bf914 | |||
| 6d9009daff |
+4
-3
@@ -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
@@ -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
@@ -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 20
|
||||
versionName "0.3.10"
|
||||
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')
|
||||
}
|
||||
|
||||
Vendored
+3
@@ -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.getFromFilename(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)
|
||||
}
|
||||
+2
-3
@@ -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.getFromFilename(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.getFromFilename(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,18 @@
|
||||
package net.syncthing.lite.utils
|
||||
|
||||
import android.webkit.MimeTypeMap
|
||||
import net.syncthing.java.core.utils.PathUtils
|
||||
|
||||
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 getFromFilename(path: String) = getFromExtension(
|
||||
PathUtils.getFileExtensionFromFilename(path).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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1 @@
|
||||
- new connection handling
|
||||
- option for users to get detailed crash reports
|
||||
- bugfixes
|
||||
- fix file type detection for file names with umlauts and/ or spaces
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -40,15 +40,42 @@ 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_warning_reconnect_problem">
|
||||
Datorită modului în care această aplicație și serverul Syncthing funcționează,
|
||||
nu se poate face reconectarea timp de câteva minute după ce aplicația a fost oprită (ștearsă din lista de aplicații care rulează)
|
||||
sau conexiunea a fost întreruptă.
|
||||
Aceasta limitare nu se aplica la conexiunile descoperite local.
|
||||
</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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
}
|
||||
+97
-56
@@ -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())
|
||||
|
||||
+80
-28
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+23
-6
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+28
-34
@@ -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()
|
||||
}
|
||||
|
||||
+21
@@ -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
|
||||
}
|
||||
+12
-9
@@ -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()
|
||||
+17
@@ -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()
|
||||
}
|
||||
+159
@@ -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
|
||||
}
|
||||
}
|
||||
+25
@@ -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())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+74
-43
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
-39
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,4 +130,12 @@ object PathUtils {
|
||||
|
||||
return dir.removeSuffix(PATH_SEPARATOR) + file
|
||||
}
|
||||
|
||||
fun getFileExtensionFromFilename(filename: String): String {
|
||||
assertFilenameValid(filename)
|
||||
|
||||
val dotIndex = filename.lastIndexOf(".")
|
||||
|
||||
return if (dotIndex != 0) filename.substring(dotIndex + 1) else ""
|
||||
}
|
||||
}
|
||||
|
||||
+18
-9
@@ -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) {
|
||||
|
||||
+13
-10
@@ -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() {
|
||||
|
||||
+20
-7
@@ -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"
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+15
-142
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
+169
@@ -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
|
||||
}
|
||||
+16
-3
@@ -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()
|
||||
|
||||
+3
@@ -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
Reference in New Issue
Block a user