Compare commits
39 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e2a246220e | |||
| 98d6656683 | |||
| c307953fce | |||
| 68f541f00b | |||
| 29c71f1ca9 | |||
| 76ddbdd3b4 | |||
| cae1026f35 | |||
| d07c934ea7 | |||
| d829c18e76 | |||
| e41ed80d05 | |||
| 3e691b61c0 | |||
| 0fb7a9e93d | |||
| 1b4205b04a | |||
| 8e00c8b4a0 | |||
| f3ca98be80 | |||
| 96fc8bfc7b | |||
| 58098aae0f | |||
| c4ad797905 | |||
| a61d8c5c4f | |||
| af579f8311 | |||
| fbdcdbf7ec | |||
| e6870a08d6 | |||
| fbee0ca0e8 | |||
| 65b42475a6 | |||
| af09b763a6 | |||
| 5680c6c554 | |||
| 2caaebfc33 | |||
| 460d421a79 | |||
| 96edbaa240 | |||
| de1566915b | |||
| a294d6f06c | |||
| 7ce017f48c | |||
| 55edc592a0 | |||
| eb5dfcbd46 | |||
| 2b55bd9e76 | |||
| eb8360f276 | |||
| 48f880ee4e | |||
| 9f5072ed3a | |||
| b1743db5af |
@@ -4,19 +4,21 @@
|
||||
[](https://www.mozilla.org/MPL/2.0/)
|
||||
|
||||
This project is an Android app, that works as a client for a [Syncthing][1] share (accessing
|
||||
Syncthing devices in the same way a client-server file sharing app access its proprietary server).
|
||||
Syncthing devices in the same way a client-server file sharing app accesses its proprietary server).
|
||||
|
||||
This is a client-oriented implementation, designed to work online by downloading and
|
||||
uploading files from an active device on the network (instead of synchronizing a local copy of
|
||||
the entire repository). This is quite different from the way the [syncthing-android][2] works,
|
||||
and its useful from those devices that cannot or wish not to download the entire repository (for
|
||||
the entire repository).
|
||||
Due to that, you will see a sync progress of 0% at other devices (and this is expected).
|
||||
This is quite different from the way the [syncthing-android][2] works,
|
||||
and it's useful for those devices that cannot or do not wish to download the entire repository (for
|
||||
example, mobile devices with limited storage available, wishing to access a syncthing share).
|
||||
|
||||
This project is based on [syncthing-java][3], a java implementation of Syncthing protocols.
|
||||
This project is based on syncthing-java (which is in this repository too), a java implementation of Syncthing protocols.
|
||||
|
||||
Due to the behaviour of this App and the [behaviour of the Syncthing Server](https://github.com/syncthing/syncthing/issues/5224),
|
||||
you can't reconnct for some minutes if the App was killed (due to removing from the recent App list) or the connection was interrupted.
|
||||
This does not apply to connections over an WiFi, but to connections over the internet.
|
||||
you can't reconnect for some minutes if the App was killed (due to removing from the recent App list) or the connection was interrupted.
|
||||
This does not apply to local discovery connections.
|
||||
|
||||
[<img alt="Get it on F-Droid" src="https://f-droid.org/badge/get-it-on.png" height="80">](https://f-droid.org/packages/net.syncthing.lite/)
|
||||
[<img alt="Get it on Google Play" src="https://play.google.com/intl/en_us/badges/images/generic/en_badge_web_generic.png" height="80">](https://play.google.com/store/apps/details?id=net.syncthing.lite)
|
||||
@@ -24,21 +26,17 @@ This does not apply to connections over an WiFi, but to connections over the int
|
||||
## Translations
|
||||
|
||||
The project is translated on [Transifex](https://www.transifex.com/syncthing/syncthing-lite/).
|
||||
Requests for new languages are always accepted (but this happens manually because there is no option to accept it automatically).
|
||||
|
||||
## Building
|
||||
|
||||
The project uses a standard Android build, and requires the Android SDK. The easiest option is if
|
||||
you install [Android Studio][4] and import the project.
|
||||
|
||||
To compile with a development version of the [syncthing-java][3] library, you have to install it to
|
||||
the local maven repository. To do this, clone the repo and run `gradle install` in the
|
||||
syncthing-java project folder.
|
||||
The project uses a standard Android build, and requires the Android SDK. The easiest option is to
|
||||
install [Android Studio][3] and import the project.
|
||||
|
||||
## License
|
||||
All code is licensed under the [MPLv2 License][5].
|
||||
All code is licensed under the [MPLv2 License][4].
|
||||
|
||||
[1]: https://syncthing.net/
|
||||
[2]: https://github.com/syncthing/syncthing-android
|
||||
[3]: https://github.com/Nutomic/syncthing-java
|
||||
[4]: https://developer.android.com/studio/index.html
|
||||
[5]: LICENSE
|
||||
[3]: https://developer.android.com/studio/index.html
|
||||
[4]: LICENSE
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
# 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 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
|
||||
+19
-5
@@ -2,18 +2,27 @@ apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply plugin: 'com.github.ben-manes.versions'
|
||||
apply plugin: 'kotlin-android-extensions'
|
||||
apply plugin: 'com.github.triplet.play'
|
||||
|
||||
android {
|
||||
compileSdkVersion 27
|
||||
buildToolsVersion "28.0.2"
|
||||
dataBinding.enabled = true
|
||||
|
||||
playAccountConfigs {
|
||||
defaultAccountConfig {
|
||||
jsonFile = file(System.getenv("SYNCTHING_RELEASE_PLAY_ACCOUNT_CONFIG_FILE") ?: 'keys.json')
|
||||
}
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
applicationId "net.syncthing.lite"
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 26
|
||||
versionCode 10
|
||||
versionName "0.3.0"
|
||||
versionCode 15
|
||||
versionName "0.3.5"
|
||||
multiDexEnabled true
|
||||
playAccountConfig = playAccountConfigs.defaultAccountConfig
|
||||
}
|
||||
sourceSets {
|
||||
main.java.srcDirs += "src/main/kotlin"
|
||||
@@ -51,11 +60,18 @@ android {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
play {
|
||||
jsonFile = file(System.getenv("SYNCTHING_RELEASE_PLAY_ACCOUNT_CONFIG_FILE") ?: 'keys.json')
|
||||
uploadImages = true
|
||||
track = 'production'
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
||||
implementation "org.jetbrains.anko:anko-commons:$anko_version"
|
||||
implementation "org.jetbrains.anko:anko-coroutines:$anko_version"
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:0.30.2'
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.0.0'
|
||||
implementation "com.android.support:design:$support_version"
|
||||
implementation "com.android.support:preference-v14:$support_version"
|
||||
implementation "com.android.support:support-v4:$support_version"
|
||||
@@ -73,11 +89,9 @@ dependencies {
|
||||
*/
|
||||
implementation(project(':syncthing-client')) {
|
||||
exclude group: 'commons-logging', module: 'commons-logging'
|
||||
exclude group: 'org.apache.httpcomponents', module: 'httpclient'
|
||||
exclude group: 'org.slf4j'
|
||||
exclude group: 'ch.qos.logback'
|
||||
}
|
||||
implementation 'org.apache.httpcomponents:httpclient-android:4.3.5.1'
|
||||
|
||||
implementation 'sk.baka.slf4j:slf4j-handroid:1.7.26'
|
||||
implementation 'com.google.zxing:android-integration:3.3.0'
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
|
||||
<application
|
||||
android:name=".android.Application"
|
||||
android:allowBackup="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
@@ -22,14 +23,10 @@
|
||||
<activity android:name=".activities.FolderBrowserActivity"
|
||||
android:parentActivityName=".activities.MainActivity"/>
|
||||
<provider
|
||||
android:name="android.support.v4.content.FileProvider"
|
||||
android:name=".library.CacheFileProvider"
|
||||
android:authorities="net.syncthing.lite.fileprovider"
|
||||
android:grantUriPermissions="true"
|
||||
android:exported="false">
|
||||
<meta-data
|
||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/filepaths" />
|
||||
</provider>
|
||||
android:exported="false" />
|
||||
<provider
|
||||
android:name=".library.SyncthingProvider"
|
||||
android:authorities="net.syncthing.lite.documents"
|
||||
|
||||
@@ -5,8 +5,10 @@ import android.content.Intent
|
||||
import android.databinding.DataBindingUtil
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import kotlinx.coroutines.experimental.android.UI
|
||||
import kotlinx.coroutines.experimental.async
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import net.syncthing.java.bep.IndexBrowser
|
||||
import net.syncthing.java.core.beans.FileInfo
|
||||
import net.syncthing.java.core.beans.FolderInfo
|
||||
@@ -16,10 +18,11 @@ 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.FileMenuDialogFragment
|
||||
import net.syncthing.lite.dialogs.FileUploadDialog
|
||||
import net.syncthing.lite.dialogs.ReconnectIssueDialogFragment
|
||||
import net.syncthing.lite.dialogs.downloadfile.DownloadFileDialogFragment
|
||||
import org.jetbrains.anko.custom.async
|
||||
import org.jetbrains.anko.doAsync
|
||||
|
||||
class FolderBrowserActivity : SyncthingActivity() {
|
||||
|
||||
@@ -44,9 +47,19 @@ class FolderBrowserActivity : SyncthingActivity() {
|
||||
override fun onItemClicked(fileInfo: FileInfo) {
|
||||
navigateToFolder(fileInfo)
|
||||
}
|
||||
|
||||
override fun onItemLongClicked(fileInfo: FileInfo): Boolean {
|
||||
return if (fileInfo.type == FileInfo.FileType.FILE) {
|
||||
FileMenuDialogFragment.newInstance(fileInfo).show(supportFragmentManager)
|
||||
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
val folder = intent.getStringExtra(EXTRA_FOLDER_NAME)
|
||||
libraryHandler?.syncthingClient {
|
||||
libraryHandler.syncthingClient {
|
||||
indexBrowser = it.indexHandler.newIndexBrowser(folder, true, true)
|
||||
indexBrowser.setOnFolderChangedListener(this::onFolderChanged)
|
||||
}
|
||||
@@ -69,14 +82,16 @@ class FolderBrowserActivity : SyncthingActivity() {
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, intent: Intent?) {
|
||||
if (requestCode == REQUEST_SELECT_UPLOAD_FILE && resultCode == Activity.RESULT_OK) {
|
||||
libraryHandler?.syncthingClient { syncthingClient ->
|
||||
async (UI) {
|
||||
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()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
super.onActivityResult(requestCode, resultCode, intent)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,7 +106,7 @@ class FolderBrowserActivity : SyncthingActivity() {
|
||||
finish()
|
||||
} else {
|
||||
if (fileInfo.isDirectory()) {
|
||||
async {
|
||||
doAsync {
|
||||
indexBrowser.navigateTo(fileInfo)
|
||||
}
|
||||
|
||||
@@ -108,32 +123,33 @@ class FolderBrowserActivity : SyncthingActivity() {
|
||||
}
|
||||
|
||||
private fun onFolderChanged() {
|
||||
runOnUiThread {
|
||||
binding.isLoading = false
|
||||
GlobalScope.launch {
|
||||
val list = indexBrowser.listFiles()
|
||||
|
||||
async {
|
||||
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
|
||||
|
||||
async (UI) {
|
||||
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
|
||||
adapter.data = list
|
||||
binding.listView.scrollToPosition(0)
|
||||
if (indexBrowser.isRoot())
|
||||
libraryHandler?.folderBrowser {
|
||||
val title = it.getFolderInfo(indexBrowser.folder)?.label
|
||||
val title = if (indexBrowser.isRoot()) {
|
||||
val result = CompletableDeferred<String?>()
|
||||
|
||||
async(UI) {
|
||||
supportActionBar?.title = title
|
||||
}
|
||||
}
|
||||
else
|
||||
supportActionBar?.title = indexBrowser.currentPathInfo().fileName
|
||||
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)
|
||||
|
||||
@@ -4,8 +4,6 @@ import android.arch.lifecycle.Observer
|
||||
import android.content.Intent
|
||||
import android.databinding.DataBindingUtil
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.support.v4.app.Fragment
|
||||
import android.support.v4.content.ContextCompat
|
||||
import android.text.Html
|
||||
@@ -15,8 +13,9 @@ import android.view.ViewGroup
|
||||
import android.widget.Button
|
||||
import com.github.paolorotolo.appintro.AppIntro
|
||||
import com.google.zxing.integration.android.IntentIntegrator
|
||||
import kotlinx.coroutines.experimental.android.UI
|
||||
import kotlinx.coroutines.experimental.async
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import net.syncthing.java.core.beans.DeviceId
|
||||
import net.syncthing.lite.R
|
||||
import net.syncthing.lite.databinding.FragmentIntroOneBinding
|
||||
@@ -101,18 +100,18 @@ class IntroActivity : AppIntro() {
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
binding = DataBindingUtil.inflate(inflater, R.layout.fragment_intro_two, container, false)
|
||||
binding.enterDeviceId!!.scanQrCode.setOnClickListener {
|
||||
binding.enterDeviceId.scanQrCode.setOnClickListener {
|
||||
FragmentIntentIntegrator(this@IntroFragmentTwo).initiateScan()
|
||||
}
|
||||
binding.enterDeviceId!!.scanQrCode.setImageResource(R.drawable.ic_qr_code_white_24dp)
|
||||
binding.enterDeviceId.scanQrCode.setImageResource(R.drawable.ic_qr_code_white_24dp)
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, intent: Intent?) {
|
||||
val scanResult = IntentIntegrator.parseActivityResult(requestCode, resultCode, intent)
|
||||
if (scanResult?.contents != null && scanResult.contents.isNotBlank()) {
|
||||
binding.enterDeviceId!!.deviceId.setText(scanResult.contents)
|
||||
binding.enterDeviceId!!.deviceIdHolder.isErrorEnabled = false
|
||||
binding.enterDeviceId.deviceId.setText(scanResult.contents)
|
||||
binding.enterDeviceId.deviceIdHolder.isErrorEnabled = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -122,11 +121,11 @@ class IntroActivity : AppIntro() {
|
||||
*/
|
||||
fun isDeviceIdValid(): Boolean {
|
||||
return try {
|
||||
val deviceId = binding.enterDeviceId!!.deviceId.text.toString()
|
||||
val deviceId = binding.enterDeviceId.deviceId.text.toString()
|
||||
Util.importDeviceId(libraryHandler, context, deviceId, { })
|
||||
true
|
||||
} catch (e: IOException) {
|
||||
binding.enterDeviceId!!.deviceId.error = getString(R.string.invalid_device_id)
|
||||
binding.enterDeviceId.deviceId.error = getString(R.string.invalid_device_id)
|
||||
false
|
||||
}
|
||||
}
|
||||
@@ -183,7 +182,7 @@ class IntroActivity : AppIntro() {
|
||||
binding = DataBindingUtil.inflate(inflater, R.layout.fragment_intro_three, container, false)
|
||||
|
||||
libraryHandler.library { config, client, _ ->
|
||||
async(UI) {
|
||||
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>")
|
||||
@@ -196,7 +195,7 @@ class IntroActivity : AppIntro() {
|
||||
|
||||
private fun onConnectionChanged(deviceId: DeviceId) {
|
||||
libraryHandler.library { config, client, _ ->
|
||||
async(UI) {
|
||||
GlobalScope.launch (Dispatchers.Main) {
|
||||
if (config.folders.isNotEmpty()) {
|
||||
client.removeOnConnectionChangedListener(this@IntroFragmentThree::onConnectionChanged)
|
||||
(activity as IntroActivity?)?.onDonePressed(this@IntroFragmentThree)
|
||||
|
||||
@@ -8,8 +8,9 @@ import android.support.v4.app.Fragment
|
||||
import android.support.v7.app.ActionBarDrawerToggle
|
||||
import android.view.Gravity
|
||||
import android.view.MenuItem
|
||||
import kotlinx.coroutines.experimental.android.UI
|
||||
import kotlinx.coroutines.experimental.async
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import net.syncthing.lite.R
|
||||
import net.syncthing.lite.databinding.ActivityMainBinding
|
||||
import net.syncthing.lite.dialogs.DeviceIdDialogFragment
|
||||
@@ -101,7 +102,7 @@ class MainActivity : SyncthingActivity() {
|
||||
}
|
||||
|
||||
private fun cleanCacheAndIndex() {
|
||||
async(UI) {
|
||||
GlobalScope.launch (Dispatchers.Main) {
|
||||
libraryHandler.syncthingClient { it.clearCacheAndIndex() }
|
||||
recreate()
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package net.syncthing.lite.activities
|
||||
|
||||
import android.app.AlertDialog
|
||||
import android.content.Context
|
||||
import android.databinding.DataBindingUtil
|
||||
import android.os.Bundle
|
||||
import android.support.design.widget.Snackbar
|
||||
|
||||
@@ -50,6 +50,10 @@ class FolderContentsAdapter: RecyclerView.Adapter<FolderContentsViewHolder>() {
|
||||
listener?.onItemClicked(fileInfo)
|
||||
}
|
||||
|
||||
binding.root.setOnLongClickListener {
|
||||
listener?.onItemLongClicked(fileInfo) ?: false
|
||||
}
|
||||
|
||||
binding.executePendingBindings()
|
||||
}
|
||||
|
||||
@@ -59,6 +63,7 @@ class FolderContentsAdapter: RecyclerView.Adapter<FolderContentsViewHolder>() {
|
||||
|
||||
interface FolderContentsListener {
|
||||
fun onItemClicked(fileInfo: FileInfo)
|
||||
fun onItemLongClicked(fileInfo: FileInfo): Boolean
|
||||
}
|
||||
|
||||
class FolderContentsViewHolder(val binding: ListviewFileBinding): RecyclerView.ViewHolder(binding.root)
|
||||
class FolderContentsViewHolder(val binding: ListviewFileBinding): RecyclerView.ViewHolder(binding.root)
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
package net.syncthing.lite.android
|
||||
|
||||
import android.app.Application
|
||||
import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import net.syncthing.lite.BuildConfig
|
||||
import org.jetbrains.anko.defaultSharedPreferences
|
||||
import java.io.PrintWriter
|
||||
import java.io.StringWriter
|
||||
|
||||
class Application: Application() {
|
||||
companion object {
|
||||
private const val LOG_TAG = "Application"
|
||||
private const val PREF_ENABLE_CRASH_HANDLER = "crash_handler"
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
|
||||
val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||
val defaultHandler = Thread.getDefaultUncaughtExceptionHandler()
|
||||
|
||||
if (defaultHandler == null) {
|
||||
Log.w(LOG_TAG, "could not get default crash handler")
|
||||
}
|
||||
|
||||
Thread.setDefaultUncaughtExceptionHandler { thread, ex ->
|
||||
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()
|
||||
)
|
||||
}
|
||||
|
||||
if (defaultHandler != null) {
|
||||
defaultHandler.uncaughtException(thread, ex)
|
||||
} else {
|
||||
System.exit(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,8 +17,9 @@ import android.widget.Toast
|
||||
import com.google.zxing.BarcodeFormat
|
||||
import com.google.zxing.WriterException
|
||||
import com.google.zxing.qrcode.QRCodeWriter
|
||||
import kotlinx.coroutines.experimental.android.UI
|
||||
import kotlinx.coroutines.experimental.async
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import net.syncthing.lite.R
|
||||
import net.syncthing.lite.databinding.DialogDeviceIdBinding
|
||||
import net.syncthing.lite.fragments.SyncthingDialogFragment
|
||||
@@ -62,7 +63,7 @@ class DeviceIdDialogFragment: SyncthingDialogFragment() {
|
||||
))
|
||||
}
|
||||
|
||||
async (UI) {
|
||||
GlobalScope.launch (Dispatchers.Main) {
|
||||
binding.deviceId.text = deviceId.deviceId
|
||||
binding.deviceId.visibility = View.VISIBLE
|
||||
|
||||
@@ -83,7 +84,7 @@ class DeviceIdDialogFragment: SyncthingDialogFragment() {
|
||||
}
|
||||
}
|
||||
|
||||
async(UI) {
|
||||
GlobalScope.launch (Dispatchers.Main) {
|
||||
binding.flipper.displayedChild = 1
|
||||
binding.qrCode.setImageBitmap(bmp)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
package net.syncthing.lite.dialogs
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.support.design.widget.BottomSheetDialogFragment
|
||||
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
|
||||
|
||||
class FileMenuDialogFragment: BottomSheetDialogFragment() {
|
||||
companion object {
|
||||
private const val ARG_FILE_SPEC = "file spec"
|
||||
private const val TAG = "DownloadFileDialog"
|
||||
private const val REQ_SAVE_AS = 1
|
||||
|
||||
fun newInstance(fileInfo: FileInfo) = newInstance(DownloadFileSpec(
|
||||
folder = fileInfo.folder,
|
||||
path = fileInfo.path,
|
||||
fileName = fileInfo.fileName
|
||||
))
|
||||
|
||||
fun newInstance(fileSpec: DownloadFileSpec) = FileMenuDialogFragment().apply {
|
||||
arguments = Bundle().apply {
|
||||
putSerializable(ARG_FILE_SPEC, fileSpec)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val fileSpec: DownloadFileSpec by lazy {
|
||||
arguments!!.getSerializable(ARG_FILE_SPEC) as DownloadFileSpec
|
||||
}
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
val binding = DialogFileBinding.inflate(inflater, container, false)
|
||||
|
||||
binding.filename = fileSpec.fileName
|
||||
|
||||
binding.saveAsButton.setOnClickListener {
|
||||
startActivityForResult(
|
||||
Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
|
||||
addCategory(Intent.CATEGORY_OPENABLE)
|
||||
|
||||
type = MimeTypeMap.getSingleton().getMimeTypeFromExtension(
|
||||
FilenameUtils.getExtension(fileSpec.fileName)
|
||||
)
|
||||
|
||||
putExtra(Intent.EXTRA_TITLE, fileSpec.fileName)
|
||||
},
|
||||
REQ_SAVE_AS
|
||||
)
|
||||
}
|
||||
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
when (requestCode) {
|
||||
REQ_SAVE_AS -> {
|
||||
if (resultCode == Activity.RESULT_OK) {
|
||||
DownloadFileDialogFragment.newInstance(fileSpec, data!!.data!!).show(fragmentManager)
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
else -> super.onActivityResult(requestCode, resultCode, data)
|
||||
}
|
||||
}
|
||||
|
||||
fun show(fragmentManager: FragmentManager?) {
|
||||
show(fragmentManager, TAG)
|
||||
}
|
||||
}
|
||||
@@ -3,8 +3,6 @@ package net.syncthing.lite.dialogs
|
||||
import android.app.ProgressDialog
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import kotlinx.coroutines.experimental.android.UI
|
||||
import kotlinx.coroutines.experimental.async
|
||||
import net.syncthing.java.bep.BlockPusher
|
||||
import net.syncthing.java.client.SyncthingClient
|
||||
import net.syncthing.lite.R
|
||||
|
||||
+42
-18
@@ -7,15 +7,16 @@ import android.arch.lifecycle.ViewModelProviders
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.DialogInterface
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.support.v4.app.DialogFragment
|
||||
import android.support.v4.app.FragmentManager
|
||||
import android.support.v4.content.FileProvider
|
||||
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 org.jetbrains.anko.newTask
|
||||
@@ -24,6 +25,7 @@ import org.jetbrains.anko.toast
|
||||
class DownloadFileDialogFragment : DialogFragment() {
|
||||
companion object {
|
||||
private const val ARG_FILE_SPEC = "file spec"
|
||||
private const val ARG_SAVE_AS_URI = "save as"
|
||||
private const val TAG = "DownloadFileDialog"
|
||||
|
||||
fun newInstance(fileInfo: FileInfo) = newInstance(DownloadFileSpec(
|
||||
@@ -32,9 +34,16 @@ class DownloadFileDialogFragment : DialogFragment() {
|
||||
fileName = fileInfo.fileName
|
||||
))
|
||||
|
||||
fun newInstance(fileSpec: DownloadFileSpec) = DownloadFileDialogFragment().apply {
|
||||
fun newInstance(
|
||||
fileSpec: DownloadFileSpec,
|
||||
outputUri: Uri? = null
|
||||
) = DownloadFileDialogFragment().apply {
|
||||
arguments = Bundle().apply {
|
||||
putSerializable(ARG_FILE_SPEC, fileSpec)
|
||||
|
||||
if (outputUri != null) {
|
||||
putParcelable(ARG_SAVE_AS_URI, outputUri)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -45,11 +54,17 @@ class DownloadFileDialogFragment : DialogFragment() {
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
val fileSpec = arguments!!.getSerializable(ARG_FILE_SPEC) as DownloadFileSpec
|
||||
val outputUri = if (arguments!!.containsKey(ARG_SAVE_AS_URI))
|
||||
arguments!!.getParcelable(ARG_SAVE_AS_URI) as Uri
|
||||
else
|
||||
null
|
||||
|
||||
model.init(
|
||||
libraryHandler = LibraryHandler(context!!),
|
||||
fileSpec = fileSpec,
|
||||
externalCacheDir = context!!.externalCacheDir
|
||||
externalCacheDir = context!!.externalCacheDir,
|
||||
outputUri = outputUri,
|
||||
contentResolver = context!!.contentResolver
|
||||
)
|
||||
|
||||
val progressDialog = ProgressDialog(context).apply {
|
||||
@@ -73,22 +88,31 @@ class DownloadFileDialogFragment : DialogFragment() {
|
||||
is DownloadFileStatusDone -> {
|
||||
dismissAllowingStateLoss()
|
||||
|
||||
try {
|
||||
context!!.startActivity(
|
||||
Intent(Intent.ACTION_VIEW)
|
||||
.setDataAndType(
|
||||
FileProvider.getUriForFile(context!!, "net.syncthing.lite.fileprovider", status.file),
|
||||
MimeTypeMap.getSingleton().getMimeTypeFromExtension(FilenameUtils.getExtension(fileSpec.fileName))
|
||||
)
|
||||
.newTask()
|
||||
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
)
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.w(TAG, "No handler found for file " + status.file.name, e)
|
||||
}
|
||||
if (outputUri == null) {
|
||||
val mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(FilenameUtils.getExtension(fileSpec.fileName))
|
||||
|
||||
context!!.toast(R.string.toast_open_file_failed)
|
||||
try {
|
||||
context!!.startActivity(
|
||||
Intent(Intent.ACTION_VIEW)
|
||||
.setDataAndType(
|
||||
CacheFileProviderUrl.fromFile(
|
||||
filename = fileSpec.fileName,
|
||||
mimeType = mimeType,
|
||||
file = status.file,
|
||||
context = context!!
|
||||
).serialized,
|
||||
mimeType
|
||||
)
|
||||
.newTask()
|
||||
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
)
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.w(TAG, "No handler found for file " + status.file.name, e)
|
||||
}
|
||||
|
||||
context!!.toast(R.string.toast_open_file_failed)
|
||||
}
|
||||
}
|
||||
}
|
||||
is DownloadFileStatusFailed -> {
|
||||
|
||||
+32
-5
@@ -2,12 +2,17 @@ package net.syncthing.lite.dialogs.downloadfile
|
||||
|
||||
import android.arch.lifecycle.LiveData
|
||||
import android.arch.lifecycle.MutableLiveData
|
||||
import android.arch.lifecycle.ViewModel;
|
||||
import android.arch.lifecycle.ViewModel
|
||||
import android.content.ContentResolver
|
||||
import android.net.Uri
|
||||
import android.support.v4.os.CancellationSignal
|
||||
import android.util.Log
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import net.syncthing.lite.BuildConfig
|
||||
import net.syncthing.lite.library.DownloadFileTask
|
||||
import net.syncthing.lite.library.LibraryHandler
|
||||
import org.apache.commons.io.FileUtils
|
||||
import java.io.File
|
||||
|
||||
class DownloadFileDialogViewModel : ViewModel() {
|
||||
@@ -20,7 +25,13 @@ class DownloadFileDialogViewModel : ViewModel() {
|
||||
private val cancellationSignal = CancellationSignal()
|
||||
val status: LiveData<DownloadFileStatus> = statusInternal
|
||||
|
||||
fun init(libraryHandler: LibraryHandler, fileSpec: DownloadFileSpec, externalCacheDir: File) {
|
||||
fun init(
|
||||
libraryHandler: LibraryHandler,
|
||||
fileSpec: DownloadFileSpec,
|
||||
externalCacheDir: File,
|
||||
outputUri: Uri?,
|
||||
contentResolver: ContentResolver
|
||||
) {
|
||||
if (isInitialized) {
|
||||
return
|
||||
}
|
||||
@@ -54,10 +65,26 @@ class DownloadFileDialogViewModel : ViewModel() {
|
||||
statusInternal.value = DownloadFileStatusRunning(newProgress)
|
||||
}
|
||||
},
|
||||
onComplete = {
|
||||
statusInternal.value = DownloadFileStatusDone(it)
|
||||
|
||||
onComplete = { file ->
|
||||
libraryHandler.stop()
|
||||
|
||||
GlobalScope.launch {
|
||||
try {
|
||||
if (outputUri != null) {
|
||||
contentResolver.openOutputStream(outputUri).use { outputStream ->
|
||||
FileUtils.copyFile(file, outputStream)
|
||||
}
|
||||
}
|
||||
|
||||
statusInternal.postValue(DownloadFileStatusDone(file))
|
||||
} catch (ex: Exception) {
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.w(TAG, "downloading file failed", ex)
|
||||
}
|
||||
|
||||
statusInternal.postValue(DownloadFileStatusFailed)
|
||||
}
|
||||
}
|
||||
},
|
||||
onError = {
|
||||
statusInternal.value = DownloadFileStatusFailed
|
||||
|
||||
@@ -10,8 +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.experimental.android.UI
|
||||
import kotlinx.coroutines.experimental.async
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import net.syncthing.java.core.beans.DeviceInfo
|
||||
import net.syncthing.lite.R
|
||||
import net.syncthing.lite.adapters.DeviceAdapterListener
|
||||
@@ -38,12 +39,12 @@ class DevicesFragment : SyncthingFragment() {
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
libraryHandler?.syncthingClient { it.addOnConnectionChangedListener { _ -> updateDeviceList() } }
|
||||
libraryHandler.syncthingClient { it.addOnConnectionChangedListener { _ -> updateDeviceList() } }
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
libraryHandler?.syncthingClient { it.removeOnConnectionChangedListener{ _ -> updateDeviceList() } }
|
||||
libraryHandler.syncthingClient { it.removeOnConnectionChangedListener{ _ -> updateDeviceList() } }
|
||||
}
|
||||
|
||||
override fun onLibraryLoaded() {
|
||||
@@ -60,10 +61,12 @@ 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?.configuration { config ->
|
||||
libraryHandler.library { config, syncthingClient, _ ->
|
||||
config.peers = config.peers.filterNot { it.deviceId == deviceInfo.deviceId }.toSet()
|
||||
config.persistLater()
|
||||
updateDeviceList()
|
||||
|
||||
syncthingClient.disconnectFromRemovedDevices()
|
||||
}
|
||||
}
|
||||
.setNegativeButton(android.R.string.no, null)
|
||||
@@ -76,7 +79,7 @@ class DevicesFragment : SyncthingFragment() {
|
||||
|
||||
private fun updateDeviceList() {
|
||||
libraryHandler.syncthingClient { syncthingClient ->
|
||||
async(UI) {
|
||||
GlobalScope.launch (Dispatchers.Main) {
|
||||
adapter.data = syncthingClient.getPeerStatus()
|
||||
binding.isEmpty = adapter.data.isEmpty()
|
||||
}
|
||||
|
||||
@@ -6,8 +6,9 @@ import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import kotlinx.coroutines.experimental.android.UI
|
||||
import kotlinx.coroutines.experimental.async
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import net.syncthing.java.core.beans.FolderInfo
|
||||
import net.syncthing.java.core.beans.FolderStats
|
||||
import net.syncthing.lite.activities.FolderBrowserActivity
|
||||
@@ -39,7 +40,7 @@ class FoldersFragment : SyncthingFragment() {
|
||||
libraryHandler.folderBrowser { folderBrowser ->
|
||||
val list = folderBrowser.folderInfoAndStatsList()
|
||||
|
||||
async (UI) {
|
||||
GlobalScope.launch (Dispatchers.Main) {
|
||||
Log.i(TAG, "list folders = " + list + " (" + list.size + " records)")
|
||||
val adapter = FoldersListAdapter().apply { data = list }
|
||||
binding.list.adapter = adapter
|
||||
|
||||
@@ -18,9 +18,9 @@ class SettingsFragment : PreferenceFragmentCompat() {
|
||||
val versionName = activity.packageManager.getPackageInfo(activity.packageName, 0)?.versionName
|
||||
appVersion.summary = versionName
|
||||
|
||||
activity.libraryHandler?.configuration { localDeviceName.text = it.localDeviceName }
|
||||
activity.libraryHandler.configuration { localDeviceName.text = it.localDeviceName }
|
||||
localDeviceName.setOnPreferenceChangeListener { _, _ ->
|
||||
activity.libraryHandler?.configuration { conf ->
|
||||
activity.libraryHandler.configuration { conf ->
|
||||
conf.localDeviceName = localDeviceName.text
|
||||
conf.persistLater()
|
||||
}
|
||||
@@ -28,4 +28,4 @@ class SettingsFragment : PreferenceFragmentCompat() {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
package net.syncthing.lite.library
|
||||
|
||||
import android.content.ContentProvider
|
||||
import android.content.ContentValues
|
||||
import android.content.Context
|
||||
import android.database.Cursor
|
||||
import android.database.MatrixCursor
|
||||
import android.net.Uri
|
||||
import android.os.ParcelFileDescriptor
|
||||
import android.provider.OpenableColumns
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
|
||||
class CacheFileProvider: ContentProvider() {
|
||||
companion object {
|
||||
const val AUTHORITY = "net.syncthing.lite.fileprovider"
|
||||
}
|
||||
|
||||
override fun onCreate() = true
|
||||
|
||||
override fun insert(uri: Uri?, values: ContentValues?): Uri {
|
||||
throw NotImplementedError()
|
||||
}
|
||||
|
||||
override fun update(uri: Uri?, values: ContentValues?, selection: String?, selectionArgs: Array<out String>?): Int {
|
||||
throw NotImplementedError()
|
||||
}
|
||||
|
||||
override fun delete(uri: Uri?, selection: String?, selectionArgs: Array<out String>?): Int {
|
||||
throw NotImplementedError()
|
||||
}
|
||||
|
||||
override fun query(uri: Uri, projection: Array<out String>?, selection: String?, selectionArgs: Array<out String>?, sortOrder: String?): Cursor {
|
||||
val url = CacheFileProviderUrl.fromUri(uri)
|
||||
val file = url.getFile(context)
|
||||
|
||||
val resultProjection = projection ?: arrayOf(OpenableColumns.DISPLAY_NAME, OpenableColumns.SIZE)
|
||||
val resultCursor = MatrixCursor(resultProjection)
|
||||
|
||||
if (file.exists()) {
|
||||
val builder = resultCursor.newRow()
|
||||
|
||||
for (row in resultProjection) {
|
||||
when (row) {
|
||||
OpenableColumns.DISPLAY_NAME -> builder.add(url.filename)
|
||||
OpenableColumns.SIZE -> builder.add(file.length())
|
||||
else -> builder.add(null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return resultCursor
|
||||
}
|
||||
|
||||
override fun getType(uri: Uri): String = CacheFileProviderUrl.fromUri(uri).mimeType
|
||||
|
||||
override fun openFile(uri: Uri, mode: String): ParcelFileDescriptor {
|
||||
if (mode == "r") {
|
||||
val url = CacheFileProviderUrl.fromUri(uri)
|
||||
val file = url.getFile(context)
|
||||
|
||||
return ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY)
|
||||
} else {
|
||||
throw IOException("illegal mode")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class CacheFileProviderUrl(
|
||||
val pathInCacheDirectory: String,
|
||||
val filename: String,
|
||||
val mimeType: String
|
||||
) {
|
||||
companion object {
|
||||
private const val PATH = "path"
|
||||
private const val FILENAME = "filename"
|
||||
private const val MIME_TYPE = "mimeType"
|
||||
|
||||
fun fromUri(uri: Uri) = CacheFileProviderUrl(
|
||||
pathInCacheDirectory = uri.getQueryParameter(PATH),
|
||||
filename = uri.getQueryParameter(FILENAME),
|
||||
mimeType = uri.getQueryParameter(MIME_TYPE)
|
||||
)
|
||||
|
||||
fun fromFile(file: File, filename: String, mimeType: String, context: Context) = CacheFileProviderUrl(
|
||||
filename = filename,
|
||||
mimeType = mimeType,
|
||||
pathInCacheDirectory = file.toRelativeString(context.externalCacheDir)
|
||||
)
|
||||
}
|
||||
|
||||
val serialized: Uri by lazy {
|
||||
Uri.Builder()
|
||||
.scheme("content")
|
||||
.authority(CacheFileProvider.AUTHORITY)
|
||||
.appendQueryParameter(PATH, pathInCacheDirectory)
|
||||
.appendQueryParameter(FILENAME, filename)
|
||||
.appendQueryParameter(MIME_TYPE, mimeType)
|
||||
.build()
|
||||
}
|
||||
|
||||
fun getFile(context: Context): File {
|
||||
return File(context.externalCacheDir, pathInCacheDirectory)
|
||||
}
|
||||
}
|
||||
@@ -4,8 +4,9 @@ import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.support.v4.os.CancellationSignal
|
||||
import android.util.Log
|
||||
import kotlinx.coroutines.experimental.launch
|
||||
import kotlinx.coroutines.experimental.suspendCancellableCoroutine
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import net.syncthing.java.bep.BlockPullerStatus
|
||||
import net.syncthing.java.client.SyncthingClient
|
||||
import net.syncthing.java.core.beans.FileInfo
|
||||
@@ -13,6 +14,8 @@ import net.syncthing.lite.BuildConfig
|
||||
import org.apache.commons.io.FileUtils
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.resumeWithException
|
||||
|
||||
class DownloadFileTask(private val fileStorageDirectory: File,
|
||||
syncthingClient: SyncthingClient,
|
||||
@@ -58,7 +61,7 @@ class DownloadFileTask(private val fileStorageDirectory: File,
|
||||
init {
|
||||
val file = DownloadFilePath(fileStorageDirectory, fileInfo.hash!!)
|
||||
|
||||
launch {
|
||||
GlobalScope.launch {
|
||||
if (file.targetFile.exists()) {
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.d(TAG, "there is already a file")
|
||||
@@ -69,43 +72,35 @@ class DownloadFileTask(private val fileStorageDirectory: File,
|
||||
return@launch
|
||||
}
|
||||
|
||||
syncthingClient.getBlockPuller(fileInfo.folder, { blockPuller ->
|
||||
val job = launch {
|
||||
try {
|
||||
if (!file.filesDirectory.isDirectory) {
|
||||
if (!file.filesDirectory.mkdirs()) {
|
||||
throw IOException("could not create output directory")
|
||||
}
|
||||
}
|
||||
|
||||
// download the file to a temp location
|
||||
val inputStream = blockPuller.pullFileCoroutine(fileInfo, this@DownloadFileTask::callProgress)
|
||||
|
||||
try {
|
||||
FileUtils.copyInputStreamToFile(inputStream, file.tempFile)
|
||||
file.tempFile.renameTo(file.targetFile)
|
||||
} finally {
|
||||
file.tempFile.delete()
|
||||
}
|
||||
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.i(TAG, "Downloaded file $fileInfo")
|
||||
}
|
||||
|
||||
callComplete(file.targetFile)
|
||||
} catch (e: Exception) {
|
||||
callError(e)
|
||||
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.w(TAG, "Failed to download file $fileInfo", e)
|
||||
}
|
||||
try {
|
||||
if (!file.filesDirectory.isDirectory) {
|
||||
if (!file.filesDirectory.mkdirs()) {
|
||||
throw IOException("could not create output directory")
|
||||
}
|
||||
}
|
||||
|
||||
cancellationSignal.setOnCancelListener {
|
||||
job.cancel()
|
||||
// download the file to a temp location
|
||||
val inputStream = syncthingClient.pullFile(fileInfo, this@DownloadFileTask::callProgress)
|
||||
|
||||
try {
|
||||
FileUtils.copyInputStreamToFile(inputStream, file.tempFile)
|
||||
file.tempFile.renameTo(file.targetFile)
|
||||
} finally {
|
||||
file.tempFile.delete()
|
||||
}
|
||||
}, { callError(IOException("could not get block puller for file")) })
|
||||
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.i(TAG, "Downloaded file $fileInfo")
|
||||
}
|
||||
|
||||
callComplete(file.targetFile)
|
||||
} catch (e: Exception) {
|
||||
callError(e)
|
||||
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.w(TAG, "Failed to download file $fileInfo", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,8 +6,9 @@ import android.content.Context
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.util.Log
|
||||
import kotlinx.coroutines.experimental.android.UI
|
||||
import kotlinx.coroutines.experimental.async
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import net.syncthing.java.bep.FolderBrowser
|
||||
import net.syncthing.java.client.SyncthingClient
|
||||
import net.syncthing.java.core.beans.DeviceId
|
||||
@@ -35,7 +36,7 @@ class LibraryHandler(context: Context,
|
||||
|
||||
private val libraryManager = DefaultLibraryManager.with(context)
|
||||
private val isStarted = AtomicBoolean(false)
|
||||
private val isListeningPortTakenInternal = MutableLiveData<Boolean>().apply { value = false }
|
||||
private val isListeningPortTakenInternal = MutableLiveData<Boolean>().apply { postValue(false) }
|
||||
|
||||
val isListeningPortTaken: LiveData<Boolean> = isListeningPortTakenInternal
|
||||
|
||||
@@ -88,7 +89,7 @@ class LibraryHandler(context: Context,
|
||||
private fun onIndexRecordAcquired(folderInfo: FolderInfo, newRecords: List<FileInfo>, indexInfo: IndexInfo) {
|
||||
Log.i(TAG, "handleIndexRecordEvent trigger folder list update from index record acquired")
|
||||
|
||||
async(UI) {
|
||||
GlobalScope.launch (Dispatchers.Main) {
|
||||
onIndexUpdateProgressListener(folderInfo, (indexInfo.getCompleted() * 100).toInt())
|
||||
}
|
||||
}
|
||||
@@ -96,7 +97,7 @@ class LibraryHandler(context: Context,
|
||||
private fun onRemoteIndexAcquired(folderInfo: FolderInfo) {
|
||||
Log.i(TAG, "handleIndexAcquiredEvent trigger folder list update from index acquired")
|
||||
|
||||
async(UI) {
|
||||
GlobalScope.launch (Dispatchers.Main) {
|
||||
onIndexUpdateCompleteListener(folderInfo)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,8 @@ package net.syncthing.lite.library
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import java.util.concurrent.Executors
|
||||
import kotlin.coroutines.experimental.suspendCoroutine
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
|
||||
/**
|
||||
* This class manages the access to an LibraryInstance
|
||||
|
||||
@@ -8,8 +8,8 @@ import android.provider.DocumentsContract.Document
|
||||
import android.provider.DocumentsContract.Root
|
||||
import android.provider.DocumentsProvider
|
||||
import android.util.Log
|
||||
import kotlinx.coroutines.experimental.cancel
|
||||
import kotlinx.coroutines.experimental.runBlocking
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import net.syncthing.java.bep.IndexBrowser
|
||||
import net.syncthing.java.core.beans.FileInfo
|
||||
import net.syncthing.java.core.beans.FolderInfo
|
||||
|
||||
@@ -5,6 +5,8 @@ import android.net.Uri
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.util.Log
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import net.syncthing.java.bep.BlockPusher
|
||||
import net.syncthing.java.client.SyncthingClient
|
||||
import net.syncthing.java.core.utils.PathUtils
|
||||
@@ -31,22 +33,28 @@ class UploadFileTask(context: Context, syncthingClient: SyncthingClient,
|
||||
|
||||
init {
|
||||
Log.i(TAG, "Uploading file $localFile to folder $syncthingFolder:$syncthingPath")
|
||||
syncthingClient.getBlockPusher(syncthingFolder, { blockPusher ->
|
||||
val observer = blockPusher.pushFile(uploadStream, syncthingFolder, syncthingPath)
|
||||
|
||||
handler.post { onProgress(observer) }
|
||||
GlobalScope.launch {
|
||||
try {
|
||||
val blockPusher = syncthingClient.getBlockPusher(folderId = syncthingFolder)
|
||||
val observer = blockPusher.pushFile(uploadStream, syncthingFolder, syncthingPath)
|
||||
|
||||
while (!observer.isCompleted()) {
|
||||
if (isCancelled)
|
||||
return@getBlockPusher
|
||||
|
||||
observer.waitForProgressUpdate()
|
||||
Log.i(TAG, "upload progress = ${observer.progressPercentage()}%")
|
||||
handler.post { onProgress(observer) }
|
||||
|
||||
while (!observer.isCompleted()) {
|
||||
if (isCancelled)
|
||||
return@launch
|
||||
|
||||
observer.waitForProgressUpdate()
|
||||
Log.i(TAG, "upload progress = ${observer.progressPercentage()}%")
|
||||
handler.post { onProgress(observer) }
|
||||
}
|
||||
IOUtils.closeQuietly(uploadStream)
|
||||
handler.post { onComplete() }
|
||||
} catch (ex: Exception) {
|
||||
handler.post { onError() }
|
||||
}
|
||||
IOUtils.closeQuietly(uploadStream)
|
||||
handler.post { onComplete() }
|
||||
}, { handler.post { onError() } })
|
||||
}
|
||||
}
|
||||
|
||||
fun cancel() {
|
||||
|
||||
@@ -4,8 +4,9 @@ import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.provider.OpenableColumns
|
||||
import kotlinx.coroutines.experimental.android.UI
|
||||
import kotlinx.coroutines.experimental.async
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import net.syncthing.java.core.beans.DeviceId
|
||||
import net.syncthing.java.core.beans.DeviceInfo
|
||||
import net.syncthing.lite.R
|
||||
@@ -43,16 +44,17 @@ object Util {
|
||||
fun importDeviceId(libraryHandler: LibraryHandler?, context: Context?, deviceId: String,
|
||||
onComplete: () -> Unit) {
|
||||
val deviceId2 = DeviceId(deviceId.toUpperCase(Locale.US))
|
||||
libraryHandler?.configuration { configuration ->
|
||||
libraryHandler?.library { configuration, syncthingClient, _ ->
|
||||
if (!configuration.peerIds.contains(deviceId2)) {
|
||||
configuration.peers = configuration.peers + DeviceInfo(deviceId2, null)
|
||||
configuration.persistLater()
|
||||
async(UI) {
|
||||
syncthingClient.connectToNewlyAddedDevices()
|
||||
GlobalScope.launch (Dispatchers.Main) {
|
||||
context?.toast(context.getString(R.string.device_import_success, deviceId2.shortId))
|
||||
onComplete()
|
||||
}
|
||||
} else {
|
||||
async(UI) {
|
||||
GlobalScope.launch (Dispatchers.Main) {
|
||||
context?.toast(context.getString(R.string.device_already_known, deviceId2.shortId))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
googleplay@nutomic.com
|
||||
@@ -0,0 +1 @@
|
||||
https://syncthing.net
|
||||
@@ -0,0 +1 @@
|
||||
en-GB
|
||||
@@ -0,0 +1,5 @@
|
||||
This project is an Android app, that works as a client for a Syncthing share (accessing Syncthing devices in the same way a client-server file sharing app access its proprietary server).
|
||||
|
||||
This is a client-oriented implementation, designed to work online by downloading and uploading files from an active device on the network (instead of synchronizing a local copy of the entire repository). This is quite different from the way the syncthing-android works, and its useful from those devices that cannot or wish not to download the entire repository (for example, mobile devices with limited storage available, wishing to access a syncthing share).
|
||||
|
||||
Source code: https://github.com/syncthing/syncthing-lite
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 73 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 40 KiB |
@@ -0,0 +1 @@
|
||||
A browser app for Syncthing-compatible shares
|
||||
@@ -0,0 +1 @@
|
||||
Syncthing Lite
|
||||
@@ -0,0 +1,3 @@
|
||||
- new connection handling
|
||||
- option for users to get detailed crash reports
|
||||
- bugfixes
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24.0"
|
||||
android:viewportHeight="24.0">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M17,3L5,3c-1.11,0 -2,0.9 -2,2v14c0,1.1 0.89,2 2,2h14c1.1,0 2,-0.9 2,-2L21,7l-4,-4zM12,19c-1.66,0 -3,-1.34 -3,-3s1.34,-3 3,-3 3,1.34 3,3 -1.34,3 -3,3zM15,9L5,9L5,5h10v4z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,35 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<data>
|
||||
<variable
|
||||
name="filename"
|
||||
type="String" />
|
||||
</data>
|
||||
|
||||
<LinearLayout
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<TextView
|
||||
android:padding="8dp"
|
||||
android:textAppearance="?android:textAppearanceMedium"
|
||||
android:text="@{filename}"
|
||||
tools:text="Filename.type"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
<Button
|
||||
android:drawableStart="@drawable/ic_save_black_24dp"
|
||||
android:id="@+id/save_as_button"
|
||||
android:padding="8dp"
|
||||
android:gravity="start|center_vertical"
|
||||
android:text="@string/dialog_file_save_as"
|
||||
android:background="?selectableItemBackground"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
</LinearLayout>
|
||||
</layout>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@mipmap/ic_background"/>
|
||||
<foreground android:drawable="@mipmap/ic_foreground"/>
|
||||
</adaptive-icon>
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 116 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 7.6 KiB |
@@ -1,9 +1,9 @@
|
||||
<resources>
|
||||
<string name="app_name">Syncthing Lite</string>
|
||||
<string name="folder_list_empty_message">Keine Ordner verfügbar</string>
|
||||
<string name="folder_list_empty_message">Kein Ordner verfügbar</string>
|
||||
<string name="clear_local_cache_index_label">Lokalen Index und Cache löschen</string>
|
||||
<string name="devices_list_view_empty_message">Keine Geräte verfügbar</string>
|
||||
<string name="device_id_dialog_title">Geräte ID eingeben</string>
|
||||
<string name="invalid_device_id">Fehler: Ungültige Geräte ID</string>
|
||||
<string name="dialog_downloading_file">Datei %1$s wird heruntergeladen</string>
|
||||
<string name="toast_file_download_failed">Datei konnte nicht heruntergeladen werden</string>
|
||||
<string name="toast_open_file_failed">Keine kompatible app gefunden</string>
|
||||
@@ -12,8 +12,11 @@
|
||||
<string name="dialog_uploading_file">Datei %1$s wird hochgeladen</string>
|
||||
<string name="clear_cache_and_index_title">Lokalen Cache und Index löschen?</string>
|
||||
<string name="clear_cache_and_index_body">Gesamten lokalen Cache und Index löschen?</string>
|
||||
<string name="index_update_progress_label">Index Aktualisierung für Ordner %1$s, %2$d %% synchronisiert</string>
|
||||
<string name="loading_config_starting_syncthing_client">Konfiguartion wird geladen, Syncthing wird gestartet...</string>
|
||||
<string name="last_modified_time">Zuletzt modifiziert: %1$s</string>
|
||||
<string name="remove_device_title">Gerät entfernen: %1$s</string>
|
||||
<string name="remove_device_title">Gerät %1$s entfernen?</string>
|
||||
<string name="remove_device_message">Gerät %1$s von der Liste der bekannten Geräte entfernen?</string>
|
||||
<string name="device_import_success">Gerät %1$s erfolgreich importiert</string>
|
||||
<string name="device_already_known">Gerät ist bereits bekannt $1%s</string>
|
||||
<string name="folders_label">Ordner</string>
|
||||
@@ -21,4 +24,21 @@
|
||||
<string name="folder_label_format">%1$s (%2$s)</string>
|
||||
<string name="folder_content_info">%1$s, %2$d Dateien, %3$d Ordner</string>
|
||||
<string name="file_info">%1$s, zuletzt modifiziert %2$s</string>
|
||||
<string name="show_device_id">Geräte ID anzeigen</string>
|
||||
<string name="device_id">Geräte ID</string>
|
||||
<string name="device_id_copied">Geräte ID in den Zwischenspeicher kopiert</string>
|
||||
<string name="share_device_id_chooser">Teile Geräte ID mit</string>
|
||||
<string name="other_syncthing_instance_title">Eine andere Syncthing Instanz läuft bereits</string>
|
||||
<string name="other_syncthing_instance_message">Lokale Auffindung wird nicht funktionieren. Stoppen Sie die andere Syncthing Instanz, um die lokale Auffindung zu ermöglichen.</string>
|
||||
<string name="intro_page_one_title">Willkommen zu Syncthing Lite</string>
|
||||
<string name="intro_page_one_description">Syncthing ersetzt proprietäre Sync- und Cloud-Services durch etwas Offenes, Vertrauenswürdiges und Dezentrales. Ihre Daten sind allein Ihre Daten, und Sie verdienen es zu wählen, wo sie gespeichert werden, ob sie an Dritte weitergegeben werden und wie sie über das Internet übertragen werden.</string>
|
||||
<string name="intro_page_two_title">Ein Gerät hinzufügen</string>
|
||||
<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="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="device_id_dialog_title">Geräte ID eingeben</string>
|
||||
</resources>
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
<resources>
|
||||
<string name="app_name">Syncthing Lite</string>
|
||||
<string name="folder_list_empty_message">Carpeta no disponible</string>
|
||||
<string name="clear_local_cache_index_label">Limpia caché/índice local</string>
|
||||
<string name="devices_list_view_empty_message">Dispositivos no disponibles</string>
|
||||
<string name="invalid_device_id">Error: identificador de dispositivo inválido</string>
|
||||
<string name="dialog_downloading_file">Descargando archivo%1$s</string>
|
||||
<string name="toast_file_download_failed">Falló al descargar el archivo</string>
|
||||
<string name="toast_open_file_failed">No se ha encontrado una app compatible</string>
|
||||
<string name="toast_file_upload_failed">Error en la carga de archivos
|
||||
</string>
|
||||
<string name="toast_upload_complete">Carga de archivos completa
|
||||
</string>
|
||||
<string name="dialog_uploading_file">Cargando archivo %1$s</string>
|
||||
<string name="clear_cache_and_index_title">¿Borrar la caché local y el índice?</string>
|
||||
<string name="clear_cache_and_index_body">¿Borrar todos los datos de la caché local y los datos de índice?</string>
|
||||
<string name="index_update_progress_label">Actualización del índice de las carpetas %1$s, %2$d%% sincronizado</string>
|
||||
<string name="loading_config_starting_syncthing_client">Cargando configuración, iniciando el cliente de sincronización....</string>
|
||||
<string name="last_modified_time">Última modificación: %1$s</string>
|
||||
<string name="remove_device_title">¿Quitar dispositivo %1$s?</string>
|
||||
<string name="remove_device_message">¿Quitar dispositivo %1$s de la lista de dispositivos conocidos?</string>
|
||||
<string name="device_import_success">Dispositivo %1$s importado con éxito</string>
|
||||
<string name="device_already_known">Dispositivo %1$s ya está presente</string>
|
||||
<string name="folders_label">Carpetas</string>
|
||||
<string name="devices_label">Dispositivos</string>
|
||||
<string name="folder_label_format">%1$s (%2$s)</string>
|
||||
<string name="folder_content_info">%1$s, %2$d archivos, %3$ddirectorios</string>
|
||||
<string name="file_info">%1$s, modificado por última vez %2$s</string>
|
||||
<string name="show_device_id">Mostrar ID de dispositivo </string>
|
||||
<string name="device_id">ID de dispositivo</string>
|
||||
<string name="device_id_copied">ID de dispositivo copiado al portapapeles</string>
|
||||
<string name="share_device_id_chooser">Compartir ID de dispositivo con</string>
|
||||
<string name="other_syncthing_instance_title">Otra instancia de Syncthing está siendo ejecutada</string>
|
||||
<string name="other_syncthing_instance_message">El descubrimiento local no funcionará. Detenga la otra instancia de Syncthing para habilitar la detección local.</string>
|
||||
<string name="intro_page_one_title">Bienvenido a Syncthing Lite</string>
|
||||
<string name="intro_page_one_description">Syncthing reemplaza los servicios de sincronización y nube propietarios por algo abierto, fiable y descentralizado. Sus datos son sólo suyos y usted merece elegir dónde se almacenan, si se comparten con terceros y cómo se transmiten a través de Internet.</string>
|
||||
<string name="intro_page_two_title">Añadir un dispositivo</string>
|
||||
<string name="intro_page_three_title">Compartir tus carpetas</string>
|
||||
<string name="intro_page_two_description">Introduce un ID de dispositivo de Syncthing o escanea un ID de dispositivo desde un código QR</string>
|
||||
<string name="intro_page_three_description">Acepta ahora el dispositivo con ID %1$s, y comparte una carpeta con él. Pueden pasar unos minutos hasta que los dispositivos se conecten.</string>
|
||||
<string name="settings">Configuración</string>
|
||||
<string name="settings_app_version_title">Versión de la aplicación</string>
|
||||
<string name="settings_local_device_name">Nombre del dispositivo local</string>
|
||||
<string name="settings_local_device_summary">El nombre que otros dispositivos verán para este dispositivo</string>
|
||||
<string name="settings_shutdown_delay_title">Retardo en el apagado</string>
|
||||
<string name="settings_shutdown_delay_summary">Tiempo antes de apagar el cliente Syncthing después de su último uso</string>
|
||||
<string name="device_id_dialog_title">Introducir la ID del dispositivo</string>
|
||||
<string name="settings_shutdown_delay_10_seconds">10 segundos</string>
|
||||
<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>
|
||||
@@ -3,7 +3,7 @@
|
||||
<string name="folder_list_empty_message">Aucun dossier disponible</string>
|
||||
<string name="clear_local_cache_index_label">Effacer le cache et l\'index local</string>
|
||||
<string name="devices_list_view_empty_message">Aucun appareil disponible</string>
|
||||
<string name="device_id_dialog_title">Entrer l\'ID de l\'appareil</string>
|
||||
<string name="invalid_device_id">Erreur: ID de l\'appareil invalide</string>
|
||||
<string name="dialog_downloading_file">Téléchargement du fichier %1$s</string>
|
||||
<string name="toast_file_download_failed">Le téléchargement du fichier a échoué</string>
|
||||
<string name="toast_open_file_failed">Aucune appli compatible trouvée</string>
|
||||
@@ -12,8 +12,11 @@
|
||||
<string name="dialog_uploading_file">Upload du fichier %1$s</string>
|
||||
<string name="clear_cache_and_index_title">Effacer le cache local et l\'index?</string>
|
||||
<string name="clear_cache_and_index_body">Effacer toutes les données du cache local et de l\'index ?</string>
|
||||
<string name="index_update_progress_label">Mise à jour de l\'index pour le dossier %1$s, %2$d%% synchronisés</string>
|
||||
<string name="loading_config_starting_syncthing_client">Chargement de la configuration, démarrage du client Syncthing...</string>
|
||||
<string name="last_modified_time">Dernière modification : %1$s</string>
|
||||
<string name="remove_device_title">Supprimer l\'appareil %1$s\?</string>
|
||||
<string name="remove_device_message">Supprimer l\'appareil %1$s de la liste des appareil connus ?</string>
|
||||
<string name="device_import_success">Appareil %1$s importé avec succès</string>
|
||||
<string name="device_already_known">Appareil déjà connu %1$s</string>
|
||||
<string name="folders_label">Dossiers</string>
|
||||
@@ -21,4 +24,27 @@
|
||||
<string name="folder_label_format">%1$s (%2$s)</string>
|
||||
<string name="folder_content_info">%1$s, %2$d fichiers, %3$d dossiers</string>
|
||||
<string name="file_info">%1$s, dernière modification %2$s</string>
|
||||
</resources>
|
||||
<string name="show_device_id">Afficher l\'ID de l\'appareil</string>
|
||||
<string name="device_id">ID de l\'appareil</string>
|
||||
<string name="device_id_copied">ID de l\'appareil copié dans le presse-papier</string>
|
||||
<string name="share_device_id_chooser">Partager l\'ID de l\'appareil avec</string>
|
||||
<string name="other_syncthing_instance_title">Une autre instance de Syncthing fonctionne</string>
|
||||
<string name="other_syncthing_instance_message">La découverte locale ne fonctionnera pas. Arrêtez l\'autre instance de Syncthing pour activer la découverte locale.</string>
|
||||
<string name="intro_page_one_title">Bienvenue dans Syncthing Lite</string>
|
||||
<string name="intro_page_one_description">Syncthing remplace les services de synchronisation et de cloud propriétaires par quelque chose d\'ouvert, fiable et décentralisé. Vos données sont uniquement vos données et vous méritez de choisir où elles sont stockées, si elles sont partagées avec des tiers et comment elles sont transmises sur Internet.</string>
|
||||
<string name="intro_page_two_title">Ajouter un appareil</string>
|
||||
<string name="intro_page_three_title">Partager vos dossiers</string>
|
||||
<string name="intro_page_two_description">Entrer l\'ID Syncthing de l\'appareil, ou scanner le QR code de l\'ID d\'un appareil.</string>
|
||||
<string name="intro_page_three_description">Maintenant, acceptez l\'appareil avec l\'ID %1$s, et partagez un dossier avec lui. Cela peut prendre quelques minutes avant que les appareils ne se connectent.</string>
|
||||
<string name="settings">Réglages</string>
|
||||
<string name="settings_app_version_title">Version d\'application</string>
|
||||
<string name="settings_local_device_name">Nom local de l\'appareil</string>
|
||||
<string name="settings_local_device_summary">Le nom que les autres appareils verront pour cet appareil</string>
|
||||
<string name="settings_shutdown_delay_title">Délai d\'arrêt</string>
|
||||
<string name="settings_shutdown_delay_summary">Délai avant d\'arrêter le client Syncthing après sa dernière utilisation</string>
|
||||
<string name="device_id_dialog_title">Entrer l\'ID de l\'appareil</string>
|
||||
<string name="settings_shutdown_delay_10_seconds">10 secondes</string>
|
||||
<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>
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
<resources>
|
||||
<string name="app_name">Syncthing Lite</string>
|
||||
<string name="folder_list_empty_message">Nincs elérhető mappa</string>
|
||||
<string name="clear_local_cache_index_label">Helyi gyorsítótár/index törlése</string>
|
||||
<string name="devices_list_view_empty_message">Nincs elérhető eszköz</string>
|
||||
<string name="invalid_device_id">Hiba: helytelen eszközazonosítő</string>
|
||||
<string name="dialog_downloading_file">Fájl letöltése %1$s</string>
|
||||
<string name="toast_file_download_failed">Hiba a fájl letöltése közben</string>
|
||||
<string name="toast_open_file_failed">Nem található kompatibilis alkalmazás</string>
|
||||
<string name="toast_file_upload_failed">Hiba fájl feltöltése közben</string>
|
||||
<string name="toast_upload_complete">Feltöltés befejezve</string>
|
||||
<string name="dialog_uploading_file">Fájl feltöltése: %1$s</string>
|
||||
<string name="clear_cache_and_index_title">Törlöd a helyi gyorsítótárat és index-et?</string>
|
||||
<string name="clear_cache_and_index_body">Törlöd az összes helyi gyorsítótár-at és index-et?</string>
|
||||
<string name="index_update_progress_label">Index frissítés szinkronizálva a %1$s,%2$d,%% mappákhoz</string>
|
||||
<string name="loading_config_starting_syncthing_client">Beállítások betöltése, Syncthing kliens indítása</string>
|
||||
<string name="last_modified_time">Utolsó módosítás: %1$s</string>
|
||||
<string name="remove_device_title">Törlöd a %1$s eszközt?</string>
|
||||
<string name="remove_device_message">Törlöd a %1$s eszközt az ismertek listájáról?</string>
|
||||
<string name="device_import_success">Eszköz sikeresen importálva: %1$s</string>
|
||||
<string name="device_already_known">%1$s eszköz már hozzá van adva</string>
|
||||
<string name="folders_label">Mappák</string>
|
||||
<string name="devices_label">Eszközök</string>
|
||||
<string name="folder_label_format">%1$s (%2$s)</string>
|
||||
<string name="folder_content_info">%1$s, %2$d fájlok, %3$d mappák</string>
|
||||
<string name="file_info">%1$s, utoljára módosítva %2$s</string>
|
||||
<string name="show_device_id">Eszközazonosító megjelenítése</string>
|
||||
<string name="device_id">Eszközazonosító</string>
|
||||
<string name="device_id_copied">Eszközazonosító a vágólapra másolva</string>
|
||||
<string name="share_device_id_chooser">Eszközazonosító megosztása</string>
|
||||
<string name="other_syncthing_instance_title">Egy másik Syncthing folyamat fut</string>
|
||||
<string name="other_syncthing_instance_message">Helyi felfedezés nem fog működni. Állítsd le a másik Syncthing folyamatot a helyi felfedezés bekapcsolásához.</string>
|
||||
<string name="intro_page_one_title">Üdvözöllek a Syncthing Lite-ban</string>
|
||||
<string name="intro_page_one_description">A Syncthing a zárt forrású szinkronizáló és felhő szolgáltatásokat egy nyílt, megbízható és decentralizált szoftverrel váltja fel. A te adatod csak a tiéd és te szabod meg, hogy hol tárolod, kivel osztod meg, és hogyan továbbítod az interneten.</string>
|
||||
<string name="intro_page_two_title">Eszköz hozzáadása</string>
|
||||
<string name="intro_page_three_title">Mappáid megosztása</string>
|
||||
<string name="intro_page_two_description">Adj meg egy Syncthing eszközazonosítót, vagy olvasd be QR kódból</string>
|
||||
<string name="intro_page_three_description">Fogadd el a(z) %1$s azonosítójú eszközt, és ossz meg vele egy mappát. Beletelhet néhány percbe mire az eszközök csatlakoznak.</string>
|
||||
<string name="settings">Beállítások</string>
|
||||
<string name="settings_app_version_title">Verzió</string>
|
||||
<string name="settings_local_device_name">Helyi eszköz neve</string>
|
||||
<string name="settings_local_device_summary">Név amit a többi eszköz fog látni</string>
|
||||
<string name="settings_shutdown_delay_title">Leállítás késleltetés</string>
|
||||
<string name="settings_shutdown_delay_summary">A Syncthing leállítása ennyi idő elteltével a kliens utolsó csatlakozása után</string>
|
||||
<string name="device_id_dialog_title">Eszközazonosító megadása</string>
|
||||
<string name="settings_shutdown_delay_10_seconds">10 másodperc</string>
|
||||
<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>
|
||||
@@ -3,8 +3,8 @@
|
||||
<string name="folder_list_empty_message">Nessuna cartella disponibile</string>
|
||||
<string name="clear_local_cache_index_label">Cancella cache/indice</string>
|
||||
<string name="devices_list_view_empty_message">Nessun dispositivo disponibile</string>
|
||||
<string name="device_id_dialog_title">Inserisci ID Dispositivo</string>
|
||||
<string name="dialog_downloading_file">Download del file %1$s</string>
|
||||
<string name="invalid_device_id">Errore: ID dispositivo non valido</string>
|
||||
<string name="dialog_downloading_file">Scaricamento del file %1$s</string>
|
||||
<string name="toast_file_download_failed">Impossibile scaricare il file</string>
|
||||
<string name="toast_open_file_failed">Nessuna applicazione compatibile trovata</string>
|
||||
<string name="toast_file_upload_failed">Caricamento file fallito</string>
|
||||
@@ -13,8 +13,10 @@
|
||||
<string name="clear_cache_and_index_title">Cancellare la cache locale e l\'indice?</string>
|
||||
<string name="clear_cache_and_index_body">Cancellare tutti i dati della cache locale e i dati dell\'indice?</string>
|
||||
<string name="index_update_progress_label">Aggiornamento dell\'indice per la cartella %1$s, %2$d%% sincronizzato</string>
|
||||
<string name="loading_config_starting_syncthing_client">Caricamento configurazione, avvio del client syncthing...</string>
|
||||
<string name="last_modified_time">Ultima modifica: %1$s</string>
|
||||
<string name="remove_device_title">Rimuovere il dispositivo %1$s\?</string>
|
||||
<string name="remove_device_message">Rimuovere %1$s dalla lista dei dispositivi noti?</string>
|
||||
<string name="device_import_success">Dispositivo %1$s importato con successo</string>
|
||||
<string name="device_already_known">Dispositivo %1$s già presente</string>
|
||||
<string name="folders_label">Cartelle</string>
|
||||
@@ -22,4 +24,27 @@
|
||||
<string name="folder_label_format">%1$s (%2$s)</string>
|
||||
<string name="folder_content_info">%1$s, %2$d file, %3$d cartelle</string>
|
||||
<string name="file_info">%1$s, ultima modifica %2$s</string>
|
||||
</resources>
|
||||
<string name="show_device_id">Mostra l\'ID del dispositivo</string>
|
||||
<string name="device_id">ID dispositivo</string>
|
||||
<string name="device_id_copied">ID dispositivo copiato negli appunti</string>
|
||||
<string name="share_device_id_chooser">Condividi ID dispositivo con</string>
|
||||
<string name="other_syncthing_instance_title">Un\'altra istanza di Syncthing è in esecuzione</string>
|
||||
<string name="other_syncthing_instance_message">L\'individuazione locale non funzionerà. Arresta l\'altra istanza di Syncthing per abilitare l\'individuazione locale.</string>
|
||||
<string name="intro_page_one_title">Benvenuto in Syncthing Lite</string>
|
||||
<string name="intro_page_one_description">Syncthing sostituisce servizi proprietari di sincronizzazione e cloud con qualcosa di aperto, affidabile e decentralizzato. I tuoi dati sono solo tuoi e meriti di scegliere dove vengono immagazzinati, se sono condivisi con terze parti e come sono trasmessi attraverso Internet.</string>
|
||||
<string name="intro_page_two_title">Aggiungi un dispositivo</string>
|
||||
<string name="intro_page_three_title">Condividi le tue cartelle</string>
|
||||
<string name="intro_page_two_description">Immetti un ID dispositivo Syncthing o esegui la scansione di un ID dispositivo da un codice QR</string>
|
||||
<string name="intro_page_three_description">Ora accetta il dispositivo con ID %1$s, e condividi una cartella con esso. Potrebbero essere necessari alcuni minuti prima che i dispositivi si connettano.</string>
|
||||
<string name="settings">Impostazioni</string>
|
||||
<string name="settings_app_version_title">Versione dell\'app</string>
|
||||
<string name="settings_local_device_name">Nome del dispositivo locale</string>
|
||||
<string name="settings_local_device_summary">Il nome che altri dispositivi vedranno per questo dispositivo</string>
|
||||
<string name="settings_shutdown_delay_title">Ritardo di chiusura</string>
|
||||
<string name="settings_shutdown_delay_summary">Tempo prima della chiusura del client Syncthing dopo l\'ultimo utilizzo</string>
|
||||
<string name="device_id_dialog_title">Inserisci ID Dispositivo</string>
|
||||
<string name="settings_shutdown_delay_10_seconds">10 secondi</string>
|
||||
<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>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<string name="folder_list_empty_message">フォルダーがありません</string>
|
||||
<string name="clear_local_cache_index_label">ローカルキャッシュ/索引をクリア</string>
|
||||
<string name="devices_list_view_empty_message">デバイスがありません</string>
|
||||
<string name="device_id_dialog_title">デバイス ID を入力</string>
|
||||
<string name="invalid_device_id">エラー: デバイス ID が無効です</string>
|
||||
<string name="dialog_downloading_file">ファイル %1$s のダウンロード中</string>
|
||||
<string name="toast_file_download_failed">ファイルのダウンロードに失敗しました</string>
|
||||
<string name="toast_open_file_failed">利用できるアプリが見つかりません</string>
|
||||
@@ -13,8 +13,10 @@
|
||||
<string name="clear_cache_and_index_title">ローカルキャッシュと索引をクリアしますか?</string>
|
||||
<string name="clear_cache_and_index_body">すべてのローカルキャッシュデータと索引データをクリアしますか?</string>
|
||||
<string name="index_update_progress_label">フォルダー %1$s の索引を更新しました。 %2$d%% 同期しました</string>
|
||||
<string name="loading_config_starting_syncthing_client">設定の読み込み中、syncthing クライアントの開始中…</string>
|
||||
<string name="last_modified_time">最終更新: %1$s</string>
|
||||
<string name="remove_device_title">デバイス %1$sを削除しますか?</string>
|
||||
<string name="remove_device_message">既存のデバイスのリストから %1$s を削除しますか?</string>
|
||||
<string name="device_import_success">デバイス %1$s のインポートに成功しました</string>
|
||||
<string name="device_already_known">デバイスは既に存在します %1$s</string>
|
||||
<string name="folders_label">フォルダー</string>
|
||||
@@ -22,4 +24,21 @@
|
||||
<string name="folder_label_format">%1$s (%2$s)</string>
|
||||
<string name="folder_content_info">%1$s, %2$d ファイル, %3$d ディレクトリー</string>
|
||||
<string name="file_info">%1$s, 最終更新 %2$s</string>
|
||||
<string name="show_device_id">デバイス ID を表示</string>
|
||||
<string name="device_id">デバイス ID</string>
|
||||
<string name="device_id_copied">デバイス ID をクリップボードにコピーしました</string>
|
||||
<string name="share_device_id_chooser">次とデバイス ID を共有</string>
|
||||
<string name="other_syncthing_instance_title">別の Syncthing インスタンスが実行中です</string>
|
||||
<string name="other_syncthing_instance_message">ローカルの探索は動作しません。他の Syncthing インスタンスを停止して、ローカルの探索を有効にしてください。</string>
|
||||
<string name="intro_page_one_title">Syncthing Lite へようこそ</string>
|
||||
<string name="intro_page_one_description">Syncthing は、プロプライエタリな同期およびクラウドサービスを、オープンで信頼性があり、分散化されたものに置き換えます。 あなたのデータはあなただけのものであり、それが第三者と共有され、インターネット経由で送信される場合、保存される場所を選択する必要があります。</string>
|
||||
<string name="intro_page_two_title">デバイスを追加</string>
|
||||
<string name="intro_page_three_title">フォルダーを共有</string>
|
||||
<string name="intro_page_two_description">Syncthing デバイス ID を入力、または QR コードからデバイス ID をスキャンしてください</string>
|
||||
<string name="intro_page_three_description">ID %1$s のデバイスを承認して、フォルダーを共有しました。デバイスが接続されるまで数分かかることがあります。</string>
|
||||
<string name="settings">設定</string>
|
||||
<string name="settings_app_version_title">アプリバージョン</string>
|
||||
<string name="settings_local_device_name">ローカルのデバイス名</string>
|
||||
<string name="settings_local_device_summary">他のデバイスがこのデバイスを表示する名前</string>
|
||||
<string name="device_id_dialog_title">デバイス ID を入力</string>
|
||||
</resources>
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
<resources>
|
||||
<string name="app_name">Syncthing Lite</string>
|
||||
<string name="folder_list_empty_message">Geen map beschikbaar</string>
|
||||
<string name="clear_local_cache_index_label">Lokaal cachegeheugen/index wissen</string>
|
||||
<string name="devices_list_view_empty_message">Geen apparaten beschikbaar</string>
|
||||
<string name="invalid_device_id">Fout: ongeldigen apparaats-ID</string>
|
||||
<string name="dialog_downloading_file">Bestand %1$s wordt gedownload</string>
|
||||
<string name="toast_file_download_failed">Download van bestand mislukt</string>
|
||||
<string name="toast_open_file_failed">Gene compatibelen app gevonden</string>
|
||||
<string name="toast_file_upload_failed">Uploaden van bestand mislukt</string>
|
||||
<string name="toast_upload_complete">Uploaden van bestand voltooid</string>
|
||||
<string name="dialog_uploading_file">Bestand %1$s wordt geüpload</string>
|
||||
<string name="clear_cache_and_index_title">Lokaal cachegeheugen en index wissen?</string>
|
||||
<string name="clear_cache_and_index_body">Alle lokale cache- en indexgegevens wissen?</string>
|
||||
<string name="index_update_progress_label">Index voor map %1$s wordt bijgewerkt, %2$d%% gesynchroniseerd</string>
|
||||
<string name="loading_config_starting_syncthing_client">Configuratie wordt geladen, Syncthing-cliënt wordt opgestart…</string>
|
||||
<string name="last_modified_time">Laatst gewijzigd: %1$s</string>
|
||||
<string name="remove_device_title">Apparaat %1$s verwijderen?</string>
|
||||
<string name="remove_device_message">%1$s verwijderen uit de lijst van gekende apparaten?</string>
|
||||
<string name="device_import_success">Apparaat %1$s geïmporteerd</string>
|
||||
<string name="device_already_known">Apparaat %1$s reeds aanwezig</string>
|
||||
<string name="folders_label">Mappen</string>
|
||||
<string name="devices_label">Apparaten</string>
|
||||
<string name="folder_label_format">%1$s (%2$s)</string>
|
||||
<string name="folder_content_info">%1$s, %2$d bestanden, %3$d mappen</string>
|
||||
<string name="file_info">%1$s, laatst gewijzigd %2$s</string>
|
||||
<string name="show_device_id">Apparaats-ID tonen</string>
|
||||
<string name="device_id">Apparaats-ID</string>
|
||||
<string name="device_id_copied">Apparaats-ID gekopieerd naar klembord</string>
|
||||
<string name="share_device_id_chooser">Apparaats-ID delen met</string>
|
||||
<string name="other_syncthing_instance_title">Een andere Syncthing-instantie wordt reeds uitgevoerd</string>
|
||||
<string name="other_syncthing_instance_message">Lokale ontdekking gaat niet werken. Stopt de andere Syncthing-instantie voor lokale ontdekking in te schakelen.</string>
|
||||
<string name="intro_page_one_title">Welkom bij Syncthing Lite</string>
|
||||
<string name="intro_page_one_description">Syncthing vervangt niet-vrije synchronisatie- en clouddiensten door iets opens, betrouwbaars en gedecentraliseerds. Uw gegevens behoren enkel u toe en gij bepaalt waar dat ze worden opgeslagen, of dat ze worden gedeeld met een derde partij en hoe dat ze over het internet worden verzonden.</string>
|
||||
<string name="intro_page_two_title">Voegt een apparaat toe</string>
|
||||
<string name="intro_page_three_title">Deelt uw mappen</string>
|
||||
<string name="intro_page_two_description">Voert ne Syncthing-apparaats-ID in, of scant nen apparaats-ID van een QR-code</string>
|
||||
<string name="intro_page_three_description">Aanvaard nu het apparaat met ID %1$s, en deelt er een map mee. Het kan enkele minuten duren vooraleer dat de apparaten verbinding maken.</string>
|
||||
<string name="settings">Instellingen</string>
|
||||
<string name="settings_app_version_title">Appversie</string>
|
||||
<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>
|
||||
</resources>
|
||||
@@ -0,0 +1,2 @@
|
||||
<resources>
|
||||
</resources>
|
||||
@@ -0,0 +1,44 @@
|
||||
<resources>
|
||||
<string name="app_name">Syncthing Lite</string>
|
||||
<string name="folder_list_empty_message">Geen map beschikbaar</string>
|
||||
<string name="clear_local_cache_index_label">Lokale cache/index wissen</string>
|
||||
<string name="devices_list_view_empty_message">Geen apparaten beschikbaar</string>
|
||||
<string name="invalid_device_id">Fout: ongeldige apparaats-ID</string>
|
||||
<string name="dialog_downloading_file">Bestand %1$s wordt gedownload</string>
|
||||
<string name="toast_file_download_failed">Download van bestand mislukt</string>
|
||||
<string name="toast_open_file_failed">Geen compatibele app gevonden</string>
|
||||
<string name="toast_file_upload_failed">Uploaden van bestand mislukt</string>
|
||||
<string name="toast_upload_complete">Uploaden van bestand voltooid</string>
|
||||
<string name="dialog_uploading_file">Bestand %1$s wordt geüpload</string>
|
||||
<string name="clear_cache_and_index_title">Lokale cache en index wissen?</string>
|
||||
<string name="clear_cache_and_index_body">Alle lokale cache- en indexgegevens wissen?</string>
|
||||
<string name="index_update_progress_label">Index voor map %1$s wordt bijgewerkt, %2$d%% gesynchroniseerd</string>
|
||||
<string name="loading_config_starting_syncthing_client">Configuratie wordt geladen, Syncthing-cliënt wordt opgestart…</string>
|
||||
<string name="last_modified_time">Laatst gewijzigd: %1$s</string>
|
||||
<string name="remove_device_title">Apparaat %1$s verwijderen?</string>
|
||||
<string name="remove_device_message">%1$s verwijderen uit de lijst van gekende apparaten?</string>
|
||||
<string name="device_import_success">Apparaat %1$s geïmporteerd</string>
|
||||
<string name="device_already_known">Apparaat %1$s reeds aanwezig</string>
|
||||
<string name="folders_label">Mappen</string>
|
||||
<string name="devices_label">Apparaten</string>
|
||||
<string name="folder_label_format">%1$s (%2$s)</string>
|
||||
<string name="folder_content_info">%1$s, %2$d bestanden, %3$d mappen</string>
|
||||
<string name="file_info">%1$s, laatst gewijzigd %2$s</string>
|
||||
<string name="show_device_id">Apparaats-ID tonen</string>
|
||||
<string name="device_id">Apparaats-ID</string>
|
||||
<string name="device_id_copied">Apparaats-ID gekopieerd naar klembord</string>
|
||||
<string name="share_device_id_chooser">Apparaats-ID delen met</string>
|
||||
<string name="other_syncthing_instance_title">Een andere Syncthing-instantie wordt reeds uitgevoerd</string>
|
||||
<string name="other_syncthing_instance_message">Lokale ontdekking zal niet werken. Stop de andere Syncthing-instantie om lokale ontdekking in te schakelen.</string>
|
||||
<string name="intro_page_one_title">Welkom bij Syncthing Lite</string>
|
||||
<string name="intro_page_one_description">Syncthing vervangt niet-vrije synchronisatie- en clouddiensten door iets opens, betrouwbaars en gedecentraliseerds. Je gegevens behoren enkel jou toe en jij bepaalt waar ze worden opgeslagen, of ze gedeeld worden met een derde partij en hoe ze over het internet verstuurd worden.</string>
|
||||
<string name="intro_page_two_title">Voeg een apparaat toe</string>
|
||||
<string name="intro_page_three_title">Deel je mappen</string>
|
||||
<string name="intro_page_two_description">Voer een Syncthing-apparaats-ID in, of scan een apparaats-ID van een QR-code</string>
|
||||
<string name="intro_page_three_description">Accepteer nu het apparaat met ID %1$s, en deel er een map mee. Het kan enkele minuten duren voordat de apparaten verbinden.</string>
|
||||
<string name="settings">Instellingen</string>
|
||||
<string name="settings_app_version_title">Appversie</string>
|
||||
<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>
|
||||
</resources>
|
||||
@@ -0,0 +1,2 @@
|
||||
<resources>
|
||||
</resources>
|
||||
@@ -0,0 +1,2 @@
|
||||
<resources>
|
||||
</resources>
|
||||
@@ -3,7 +3,7 @@
|
||||
<string name="folder_list_empty_message">Nici un director disponibil</string>
|
||||
<string name="clear_local_cache_index_label">Curăță indexul/memoria locală</string>
|
||||
<string name="devices_list_view_empty_message">Nici un dispozitiv disponibil</string>
|
||||
<string name="device_id_dialog_title">Introduceți ID dispozitiv</string>
|
||||
<string name="invalid_device_id">Eroare: ID dispozitiv invalid</string>
|
||||
<string name="dialog_downloading_file">Se descarcă fișierul %1$s</string>
|
||||
<string name="toast_file_download_failed">Descărcarea fișierului a eșuat</string>
|
||||
<string name="toast_open_file_failed">Nu a fost găsită nici o aplicație compatibilă</string>
|
||||
@@ -12,8 +12,11 @@
|
||||
<string name="dialog_uploading_file">Se încarcă fișierul %1$s</string>
|
||||
<string name="clear_cache_and_index_title">Se curăță memoria locală și indexul?</string>
|
||||
<string name="clear_cache_and_index_body">Se curăță datele din memoria locală și datele indexului?</string>
|
||||
<string name="index_update_progress_label">Actualizarea indexul pentru directorul %1$s, %2$d %% sincronizat</string>
|
||||
<string name="loading_config_starting_syncthing_client">Încărcare setări, pornire client syncthing…</string>
|
||||
<string name="last_modified_time">Modificat ultima dată pe: %1$s</string>
|
||||
<string name="remove_device_title">Ștergere dispozitiv %1$s\?</string>
|
||||
<string name="remove_device_message">Șterge %1$s din lista dispozitivelor cunoscute?</string>
|
||||
<string name="device_import_success">Dispozitiv importat cu succes %1$s</string>
|
||||
<string name="device_already_known">Dispozitiv deja prezent %1$s</string>
|
||||
<string name="folders_label">Directoare</string>
|
||||
@@ -21,4 +24,31 @@
|
||||
<string name="folder_label_format">%1$s (%2$s)</string>
|
||||
<string name="folder_content_info">%1$s, %2$d fișier(e), %3$d director(oare)</string>
|
||||
<string name="file_info">%1$s, modificat ultima dată pe %2$s</string>
|
||||
</resources>
|
||||
<string name="show_device_id">Arată ID dispozitiv</string>
|
||||
<string name="device_id">ID dispozitiv</string>
|
||||
<string name="device_id_copied">ID dispozitiv copiat în memorie</string>
|
||||
<string name="share_device_id_chooser">Partajează ID dispozitiv cu</string>
|
||||
<string name="other_syncthing_instance_title">O altă instanță de Syncthing rulează</string>
|
||||
<string name="other_syncthing_instance_message">Descoperire locală nu va funcționa. Opriți cealaltă instanță de Syncthing pentru a activa descoperirea locală.</string>
|
||||
<string name="intro_page_one_title">Bine ați venit la Syncthing Lite</string>
|
||||
<string name="intro_page_one_description">Syncthing înlocuiește serviciile proprietare de sincronizare și stocare
|
||||
tip cloud cu ceva deschis, de încredere și descentralizat. Datele
|
||||
dumneavoastră vă aparțin în totalitate și meritați să decideți unde vor
|
||||
fi stocate, dacă vor fi partajate cu terțe entități precum și cum vor fi
|
||||
trimise prin Internet.</string>
|
||||
<string name="intro_page_two_title">Adaugă un dispozitiv</string>
|
||||
<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="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="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>
|
||||
</resources>
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
<resources>
|
||||
<string name="app_name">Syncthing Lite</string>
|
||||
<string name="folder_list_empty_message">Ingen mapp tillgänglig</string>
|
||||
<string name="clear_local_cache_index_label">Rensa lokala cache/index</string>
|
||||
<string name="devices_list_view_empty_message">Inga enheter tillgängliga</string>
|
||||
<string name="invalid_device_id">Fel: Ogiltigt enhets-ID</string>
|
||||
<string name="dialog_downloading_file">Hämtar filen %1$s</string>
|
||||
<string name="toast_file_download_failed">Misslyckades med att hämta filen</string>
|
||||
<string name="toast_open_file_failed">Ingen kompatibel app hittades</string>
|
||||
<string name="toast_file_upload_failed">Filöverföring misslyckades</string>
|
||||
<string name="toast_upload_complete">Filöverföring färdig</string>
|
||||
<string name="dialog_uploading_file">Överför filen %1$s</string>
|
||||
<string name="clear_cache_and_index_title">Rensa cache och index lokalt?</string>
|
||||
<string name="clear_cache_and_index_body">Rensa all cachedata och indexdata lokalt?</string>
|
||||
<string name="index_update_progress_label">Index uppdatering för mappen %1$s, %2$d%% synkroniserad</string>
|
||||
<string name="loading_config_starting_syncthing_client">Läser in konfiguration, starta synkroniserande klient...</string>
|
||||
<string name="last_modified_time">Senast ändrad: %1$s</string>
|
||||
<string name="remove_device_title">Ta bort enheten %1$s\?</string>
|
||||
<string name="remove_device_message">Ta bort %1$s från listan över kända enheter?</string>
|
||||
<string name="device_import_success">Importerad enheten %1$s</string>
|
||||
<string name="device_already_known">Enhet som redan finns %1$s</string>
|
||||
<string name="folders_label">Mappar</string>
|
||||
<string name="devices_label">Enheter</string>
|
||||
<string name="folder_label_format">%1$s (%2$s)</string>
|
||||
<string name="folder_content_info">%1$s, %2$d filer, %3$d kataloger</string>
|
||||
<string name="file_info">%1$s, senast ändrad %2$s</string>
|
||||
<string name="show_device_id">Visa enhets-ID</string>
|
||||
<string name="device_id">Enhets-ID</string>
|
||||
<string name="device_id_copied">Enhets-ID kopierad till urklipp</string>
|
||||
<string name="share_device_id_chooser">Dela enhets-ID med</string>
|
||||
<string name="other_syncthing_instance_title">En annan Syncthing-instans körs</string>
|
||||
<string name="other_syncthing_instance_message">Lokal upptäckt kommer inte att fungera. Stoppa den andra Syncthing-instansen för att möjliggöra lokal upptäckt.</string>
|
||||
<string name="intro_page_one_title">Välkommen till Syncthing Lite</string>
|
||||
<string name="intro_page_one_description">Syncthing ersätter proprietära synkroniserings- och molntjänster med något öppet, pålitligt och decentraliserat. Din data är endast din data och du förtjänar att välja var den lagras, om den delas med en tredjepart och hur den överförs över Internet.</string>
|
||||
<string name="intro_page_two_title">Lägg till en enhet</string>
|
||||
<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="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="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>
|
||||
</resources>
|
||||
@@ -0,0 +1,2 @@
|
||||
<resources>
|
||||
</resources>
|
||||
@@ -0,0 +1,49 @@
|
||||
<resources>
|
||||
<string name="app_name">Syncthing Lite</string>
|
||||
<string name="folder_list_empty_message">没有可用的文件夹</string>
|
||||
<string name="clear_local_cache_index_label">清除本地缓存/索引</string>
|
||||
<string name="devices_list_view_empty_message">没有可用的设备</string>
|
||||
<string name="invalid_device_id">错误:无效的设备 ID</string>
|
||||
<string name="dialog_downloading_file">正在下载文件 %1$s</string>
|
||||
<string name="toast_file_download_failed">下载文件失败</string>
|
||||
<string name="toast_open_file_failed">未找到兼容的应用</string>
|
||||
<string name="toast_file_upload_failed">上传文件失败</string>
|
||||
<string name="toast_upload_complete">文件上传完成</string>
|
||||
<string name="dialog_uploading_file">正在上传文件 %1$s</string>
|
||||
<string name="clear_cache_and_index_title">确定清除本地缓存和索引?</string>
|
||||
<string name="clear_cache_and_index_body">确定清除全部本地缓存数据和索引数据?</string>
|
||||
<string name="loading_config_starting_syncthing_client">正在载入配置,启动 syncthing 客户端……</string>
|
||||
<string name="last_modified_time">最后修改:%1$s</string>
|
||||
<string name="remove_device_title">移除设备: %1$s?</string>
|
||||
<string name="remove_device_message">从已知设备列表中移除 %1$s?</string>
|
||||
<string name="device_import_success">已成功导入设备 %1$s</string>
|
||||
<string name="device_already_known">设备已存在 %1$s</string>
|
||||
<string name="folders_label">文件夹</string>
|
||||
<string name="devices_label">设备</string>
|
||||
<string name="folder_label_format">%1$s(%2$s)</string>
|
||||
<string name="folder_content_info">%1$s,%2$d个文件,%3$d个目录</string>
|
||||
<string name="file_info">%1$s,最后修改 %2$s</string>
|
||||
<string name="show_device_id">显示设备 ID</string>
|
||||
<string name="device_id">设备 ID</string>
|
||||
<string name="device_id_copied">设备 ID 已复制到剪贴板</string>
|
||||
<string name="share_device_id_chooser">分享设备 ID 于</string>
|
||||
<string name="other_syncthing_instance_title">另一个 Syncthing 实例正在运行</string>
|
||||
<string name="other_syncthing_instance_message">本地发现将无法工作。停止其他 Syncthing 实例以启用本地发现。</string>
|
||||
<string name="intro_page_one_title">欢迎使用 Syncthing Lite</string>
|
||||
<string name="intro_page_one_description">Syncthing 以开放、可靠并去中心化的软件替换掉封闭的云服务。您的数据仍由您拥有,您可以选择它们的存储位置,如果要共享给第三方您还可以选择如何在互联网上传输它们。</string>
|
||||
<string name="intro_page_two_title">添加设备</string>
|
||||
<string name="intro_page_three_title">共享您的文件夹</string>
|
||||
<string name="intro_page_two_description">输入 Syncthing 设备 ID,或者通过 QR 码扫描设备 ID</string>
|
||||
<string name="intro_page_three_description">已接受 ID 为%1$s 的设备,并与它共享了一个文件夹。设备连接可能需要花上数分钟。</string>
|
||||
<string name="settings">设置</string>
|
||||
<string name="settings_app_version_title">应用版本</string>
|
||||
<string name="settings_local_device_name">本地设备名称</string>
|
||||
<string name="settings_local_device_summary">此设备将被其他设备看到的名称</string>
|
||||
<string name="settings_shutdown_delay_title">关闭延迟</string>
|
||||
<string name="settings_shutdown_delay_summary">关闭 Syncthing 客户端与其最后使用之间的时间</string>
|
||||
<string name="device_id_dialog_title">输入设备 ID</string>
|
||||
<string name="settings_shutdown_delay_10_seconds">10 秒</string>
|
||||
<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>
|
||||
@@ -42,6 +42,8 @@
|
||||
<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="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>
|
||||
@@ -49,8 +51,9 @@
|
||||
<string name="settings_shutdown_delay_5_minutes">5 minutes</string>
|
||||
<string name="dialog_warning_reconnect_problem">
|
||||
Due to the behaviour of this App and the behaviour of the Syncthing Server,
|
||||
you can\'t reconnct for some minutes if the App was killed (due to removing from the recent App list)
|
||||
you can\'t reconnect for some minutes if the App was killed (due to removing from the recent App list)
|
||||
or the connection was interrupted.
|
||||
This does not apply to local discovery connections.
|
||||
</string>
|
||||
<string name="dialog_file_save_as">Save as</string>
|
||||
</resources>
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<paths>
|
||||
<external-cache-path name="files" path="/" />
|
||||
</paths>
|
||||
@@ -24,6 +24,11 @@
|
||||
|
||||
-->
|
||||
|
||||
<CheckBoxPreference
|
||||
android:key="crash_handler"
|
||||
android:title="@string/settings_crash_handler_title"
|
||||
android:summary="@string/settings_crash_handler_summary" />
|
||||
|
||||
<Preference
|
||||
android:key="app_version"
|
||||
android:title="@string/settings_app_version_title"/>
|
||||
|
||||
+4
-3
@@ -1,10 +1,10 @@
|
||||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||
|
||||
buildscript {
|
||||
ext.kotlin_version = '1.2.61'
|
||||
ext.kotlin_version = '1.3.0'
|
||||
ext.support_version = '27.0.2'
|
||||
ext.build_tools_version = '3.2.0'
|
||||
ext.anko_version = '0.10.7'
|
||||
ext.anko_version = '0.10.8'
|
||||
ext.protobuf_lite_version = '3.0.1'
|
||||
repositories {
|
||||
mavenLocal()
|
||||
@@ -17,15 +17,16 @@ buildscript {
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||
classpath 'com.github.ben-manes:gradle-versions-plugin:0.17.0'
|
||||
classpath 'com.google.protobuf:protobuf-gradle-plugin:0.8.6'
|
||||
classpath 'com.github.triplet.gradle:play-publisher:1.2.0'
|
||||
}
|
||||
}
|
||||
|
||||
allprojects {
|
||||
repositories {
|
||||
google()
|
||||
jcenter()
|
||||
maven {
|
||||
url "https://jitpack.io"
|
||||
}
|
||||
google()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
NEW_VERSION_NAME=$1
|
||||
OLD_VERSION_NAME=$(grep "versionName" "app/build.gradle" | awk '{print $2}' | tr -d "\"")
|
||||
if [[ -z ${NEW_VERSION_NAME} ]]
|
||||
then
|
||||
echo "New version name is empty. Please set a new version. Current version: $OLD_VERSION_NAME"
|
||||
exit
|
||||
fi
|
||||
|
||||
echo "
|
||||
|
||||
Updating Translations
|
||||
-----------------------------
|
||||
"
|
||||
tx push -s
|
||||
# Force push/pull to make sure this is executed. Apparently tx only compares timestamps, not file
|
||||
# contents. So if a file was `touch`ed, it won't be updated by default.
|
||||
tx pull -a -f
|
||||
git add -A "app/src/main/res/values-*/strings.xml"
|
||||
if ! git diff --cached --exit-code;
|
||||
then
|
||||
git commit -m "Imported translations"
|
||||
fi
|
||||
|
||||
echo "
|
||||
|
||||
Updating Version
|
||||
-----------------------------
|
||||
"
|
||||
OLD_VERSION_CODE=$(grep "versionCode" "app/build.gradle" -m 1 | awk '{print $2}')
|
||||
NEW_VERSION_CODE=$(($OLD_VERSION_CODE + 1))
|
||||
sed -i "s/versionCode $OLD_VERSION_CODE/versionCode $NEW_VERSION_CODE/" "app/build.gradle"
|
||||
sed -i "s/versionName \"$OLD_VERSION_NAME\"/versionName \"$NEW_VERSION_NAME\"/" "app/build.gradle"
|
||||
|
||||
LIBRARY_NAME="com.github.Nutomic:syncthing-java"
|
||||
sed -i "s/$LIBRARY_NAME:$OLD_VERSION_NAME/$LIBRARY_NAME:$NEW_VERSION_NAME/" "app/build.gradle"
|
||||
|
||||
git add "app/build.gradle"
|
||||
git commit -m "Version $NEW_VERSION_NAME"
|
||||
git tag ${NEW_VERSION_NAME}
|
||||
|
||||
echo "
|
||||
|
||||
Running Lint
|
||||
-----------------------------
|
||||
"
|
||||
./gradlew clean lintVitalRelease
|
||||
|
||||
echo "
|
||||
Update ready.
|
||||
"
|
||||
@@ -1,37 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -e
|
||||
|
||||
version=$(git describe --tags)
|
||||
regex='^[0-9]+\.[0-9]+\.[0-9]+$'
|
||||
if [[ ! ${version} =~ $regex ]]
|
||||
then
|
||||
echo "Current commit is not a release"
|
||||
exit;
|
||||
fi
|
||||
|
||||
echo "
|
||||
|
||||
Pushing to Github
|
||||
-----------------------------
|
||||
"
|
||||
git push
|
||||
git push --tags
|
||||
|
||||
echo "
|
||||
|
||||
Push to Google Play
|
||||
-----------------------------
|
||||
"
|
||||
|
||||
read -s -p "Enter signing password: " password
|
||||
|
||||
SIGNING_PASSWORD=${password} ./gradlew assembleRelease
|
||||
|
||||
# Upload apk and listing to Google Play
|
||||
SIGNING_PASSWORD=${password} ./gradlew publishRelease
|
||||
|
||||
echo "
|
||||
|
||||
Release published!
|
||||
"
|
||||
+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', ':syncthing-http-relay-client'
|
||||
include ':app', ':syncthing-repository-android', ':syncthing-repository-default', ':syncthing-relay-client', ':syncthing-bep', ':syncthing-core', ':syncthing-client', ':syncthing-discovery', ':syncthing-client-cli'
|
||||
|
||||
@@ -6,10 +6,9 @@ dependencies {
|
||||
compile fileTree(dir: 'libs', include: ['*.jar'])
|
||||
compile project(':syncthing-core')
|
||||
compile project(':syncthing-relay-client')
|
||||
compile project(':syncthing-http-relay-client')
|
||||
compile "net.jpountz.lz4:lz4:1.3.0"
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:0.30.2'
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.0.0'
|
||||
implementation "com.google.protobuf:protobuf-lite:$protobuf_lite_version"
|
||||
}
|
||||
|
||||
|
||||
@@ -15,58 +15,46 @@
|
||||
package net.syncthing.java.bep
|
||||
|
||||
import com.google.protobuf.ByteString
|
||||
import kotlinx.coroutines.experimental.*
|
||||
import kotlinx.coroutines.experimental.channels.Channel
|
||||
import net.syncthing.java.bep.BlockExchangeProtos.ErrorCode
|
||||
import net.syncthing.java.bep.BlockExchangeProtos.Request
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import net.syncthing.java.bep.connectionactor.ConnectionActorWrapper
|
||||
import net.syncthing.java.bep.utils.longSumBy
|
||||
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.interfaces.TempRepository
|
||||
import net.syncthing.java.core.utils.NetworkUtils
|
||||
import org.apache.commons.io.FileUtils
|
||||
import org.bouncycastle.util.encoders.Hex
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.io.*
|
||||
import java.lang.Exception
|
||||
import java.security.MessageDigest
|
||||
import java.util.*
|
||||
import kotlin.collections.HashMap
|
||||
|
||||
class BlockPuller internal constructor(private val connectionHandler: ConnectionHandler,
|
||||
private val indexHandler: IndexHandler,
|
||||
private val responseHandler: ResponseHandler,
|
||||
private val tempRepository: TempRepository) {
|
||||
|
||||
object BlockPuller {
|
||||
private val logger = LoggerFactory.getLogger(javaClass)
|
||||
|
||||
fun pullFileSync(
|
||||
suspend fun pullFile(
|
||||
fileInfo: FileInfo,
|
||||
progressListener: (status: BlockPullerStatus) -> Unit = { }
|
||||
progressListener: (status: BlockPullerStatus) -> Unit = { },
|
||||
connections: List<ConnectionActorWrapper>,
|
||||
indexHandler: IndexHandler,
|
||||
tempRepository: TempRepository
|
||||
): InputStream {
|
||||
return runBlocking {
|
||||
pullFileCoroutine(fileInfo, progressListener)
|
||||
val connectionHelper = MultiConnectionHelper(connections) {
|
||||
it.hasFolder(fileInfo.folder)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun pullFileCoroutine(
|
||||
fileInfo: FileInfo,
|
||||
progressListener: (status: BlockPullerStatus) -> Unit = { }
|
||||
): InputStream {
|
||||
val fileBlocks = indexHandler.waitForRemoteIndexAcquired(connectionHandler)
|
||||
.getFileInfoAndBlocksByPath(fileInfo.folder, fileInfo.path)
|
||||
?.value
|
||||
?: throw IOException("file not found in local index for folder = ${fileInfo.folder} path = ${fileInfo.path}")
|
||||
logger.info("pulling file = {}", fileBlocks)
|
||||
NetworkUtils.assertProtocol(connectionHandler.hasFolder(fileBlocks.folder), { "supplied connection handler $connectionHandler will not share folder ${fileBlocks.folder}" })
|
||||
// fail early if there is no matching connection
|
||||
connectionHelper.pickConnection()
|
||||
|
||||
val (newFileInfo, fileBlocks) = indexHandler.getFileInfoAndBlocksByPath(fileInfo.folder, fileInfo.path) ?: throw FileNotFoundException()
|
||||
|
||||
// the file could have changed since the caller read it
|
||||
// this would save the file using a wrong name, so throw here
|
||||
if (fileBlocks.hash != fileInfo.hash) {
|
||||
throw IllegalStateException("the current file entry hash does not match the hash of the provided one")
|
||||
}
|
||||
|
||||
logger.info("pulling file = {}", fileBlocks)
|
||||
|
||||
val blockTempIdByHash = Collections.synchronizedMap(HashMap<String, String>())
|
||||
|
||||
var status = BlockPullerStatus(
|
||||
@@ -75,6 +63,47 @@ class BlockPuller internal constructor(private val connectionHandler: Connection
|
||||
totalFileSize = fileBlocks.size
|
||||
)
|
||||
|
||||
suspend fun pullBlock(fileBlocks: FileBlocks, block: BlockInfo, timeoutInMillis: Long, connectionActorWrapper: ConnectionActorWrapper): ByteArray {
|
||||
logger.debug("sent message for block, hash = {}", block.hash)
|
||||
|
||||
val response =
|
||||
withTimeout(timeoutInMillis) {
|
||||
try {
|
||||
connectionActorWrapper.sendRequest(
|
||||
BlockExchangeProtos.Request.newBuilder()
|
||||
.setFolder(fileBlocks.folder)
|
||||
.setName(fileBlocks.path)
|
||||
.setOffset(block.offset)
|
||||
.setSize(block.size)
|
||||
.setHash(ByteString.copyFrom(Hex.decode(block.hash)))
|
||||
.buildPartial()
|
||||
)
|
||||
} catch (ex: TimeoutCancellationException) {
|
||||
// It seems like the TimeoutCancellationException
|
||||
// is handled differently so that the timeout is ignored.
|
||||
// Due to that, it's converted to an IOException.
|
||||
|
||||
throw IOException("timeout during requesting block")
|
||||
}
|
||||
}
|
||||
|
||||
if (response.code != BlockExchangeProtos.ErrorCode.NO_ERROR) {
|
||||
// the server does not have/ want to provide this file -> don't ask him again
|
||||
connectionHelper.disableConnection(connectionActorWrapper)
|
||||
|
||||
throw IOException("received error response ${response.code}")
|
||||
}
|
||||
|
||||
val data = response.data.toByteArray()
|
||||
val hash = Hex.toHexString(MessageDigest.getInstance("SHA-256").digest(data))
|
||||
|
||||
if (hash != block.hash) {
|
||||
throw IllegalStateException("expected block with hash ${block.hash}, but got block with hash $hash")
|
||||
}
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
try {
|
||||
val reportProgressLock = Object()
|
||||
|
||||
@@ -94,9 +123,31 @@ class BlockPuller internal constructor(private val connectionHandler: Connection
|
||||
repeat(4 /* 4 blocks per time */) { workerNumber ->
|
||||
async {
|
||||
for (block in pipe) {
|
||||
logger.debug("request block with hash = {} from worker {}", block.hash, workerNumber)
|
||||
logger.debug("message block with hash = {} from worker {}", block.hash, workerNumber)
|
||||
|
||||
val blockContent = pullBlock(fileBlocks, block, 1000 * 60 /* 60 seconds timeout per block */)
|
||||
lateinit var blockContent: ByteArray
|
||||
|
||||
val attempts = 0..4
|
||||
|
||||
for (attempt in attempts) {
|
||||
try {
|
||||
blockContent = pullBlock(fileBlocks, block, 1000 * 60 /* 60 seconds timeout per block */, connectionHelper.pickConnection())
|
||||
|
||||
break
|
||||
} catch (ex: IOException) {
|
||||
if (attempt == attempts.last) {
|
||||
throw ex
|
||||
} else {
|
||||
// will retry after a pause
|
||||
// 0: 300 ms after the first attempt
|
||||
// 1: 1200 ms after the second attempt
|
||||
// 2: 2700 ms after the third attempt
|
||||
// 3: 4800 ms after the third attempt
|
||||
// total: 9000 ms
|
||||
delay((attempt + 1) * (attempt + 1) * 300L)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
blockTempIdByHash[block.hash] = tempRepository.pushTempData(blockContent)
|
||||
|
||||
@@ -138,57 +189,6 @@ class BlockPuller internal constructor(private val connectionHandler: Connection
|
||||
throw ex
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun pullBlock(fileBlocks: FileBlocks, block: BlockInfo, timeoutInMillis: Long): ByteArray {
|
||||
logger.debug("sent request for block, hash = {}", block.hash)
|
||||
|
||||
val response =
|
||||
withTimeout(timeoutInMillis) {
|
||||
try {
|
||||
doRequest(
|
||||
Request.newBuilder()
|
||||
.setFolder(fileBlocks.folder)
|
||||
.setName(fileBlocks.path)
|
||||
.setOffset(block.offset)
|
||||
.setSize(block.size)
|
||||
.setHash(ByteString.copyFrom(Hex.decode(block.hash)))
|
||||
)
|
||||
} catch (ex: TimeoutCancellationException) {
|
||||
// It seems like the TimeoutCancellationException
|
||||
// is handled differently so that the timeout is ignored.
|
||||
// Due to that, it's converted to an IOException.
|
||||
|
||||
throw IOException("timeout during requesting block")
|
||||
}
|
||||
}
|
||||
|
||||
NetworkUtils.assertProtocol(response.code == ErrorCode.NO_ERROR) {
|
||||
"received error response, code = ${response.code}"
|
||||
}
|
||||
|
||||
val data = response.data.toByteArray()
|
||||
val hash = Hex.toHexString(MessageDigest.getInstance("SHA-256").digest(data))
|
||||
|
||||
if (hash != block.hash) {
|
||||
throw IllegalStateException("expected block with hash ${block.hash}, but got block with hash $hash")
|
||||
}
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
private suspend fun doRequest(request: Request.Builder): BlockExchangeProtos.Response {
|
||||
return suspendCancellableCoroutine { continuation ->
|
||||
val requestId = responseHandler.registerListener { response ->
|
||||
continuation.resume(response)
|
||||
}
|
||||
|
||||
connectionHandler.sendMessage(
|
||||
request
|
||||
.setId(requestId)
|
||||
.build()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class BlockPullerStatus(
|
||||
|
||||
@@ -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,15 +15,15 @@
|
||||
package net.syncthing.java.bep
|
||||
|
||||
import com.google.protobuf.ByteString
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.async
|
||||
import net.syncthing.java.bep.BlockExchangeProtos.Vector
|
||||
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.utils.BlockUtils
|
||||
import net.syncthing.java.core.utils.NetworkUtils
|
||||
import net.syncthing.java.core.utils.submitLogging
|
||||
import org.apache.commons.io.IOUtils
|
||||
import org.apache.commons.lang3.tuple.Pair
|
||||
import org.bouncycastle.util.encoders.Hex
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.io.Closeable
|
||||
@@ -32,36 +33,35 @@ import java.nio.ByteBuffer
|
||||
import java.security.MessageDigest
|
||||
import java.util.*
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.concurrent.ExecutionException
|
||||
import java.util.concurrent.Executors
|
||||
import java.util.concurrent.Future
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import java.util.concurrent.atomic.AtomicReference
|
||||
|
||||
class BlockPusher internal constructor(private val localDeviceId: DeviceId,
|
||||
private val connectionHandler: ConnectionHandler,
|
||||
private val indexHandler: IndexHandler) {
|
||||
// TODO: refactor this
|
||||
class BlockPusher(private val localDeviceId: DeviceId,
|
||||
private val connectionHandler: ConnectionActorWrapper,
|
||||
private val indexHandler: IndexHandler,
|
||||
private val requestHandlerRegistry: RequestHandlerRegistry) {
|
||||
|
||||
private val logger = LoggerFactory.getLogger(javaClass)
|
||||
|
||||
|
||||
fun pushDelete(folderId: String, targetPath: String): IndexEditObserver {
|
||||
suspend fun pushDelete(folderId: String, targetPath: String): BlockExchangeProtos.IndexUpdate {
|
||||
val fileInfo = indexHandler.waitForRemoteIndexAcquired(connectionHandler).getFileInfoByPath(folderId, targetPath)!!
|
||||
NetworkUtils.assertProtocol(connectionHandler.hasFolder(fileInfo.folder), {"supplied connection handler $connectionHandler will not share folder ${fileInfo.folder}"})
|
||||
return IndexEditObserver(sendIndexUpdate(folderId, BlockExchangeProtos.FileInfo.newBuilder()
|
||||
return sendIndexUpdate(folderId, BlockExchangeProtos.FileInfo.newBuilder()
|
||||
.setName(targetPath)
|
||||
.setType(BlockExchangeProtos.FileInfoType.valueOf(fileInfo.type.name))
|
||||
.setDeleted(true), fileInfo.versionList))
|
||||
.setDeleted(true), fileInfo.versionList)
|
||||
}
|
||||
|
||||
fun pushDir(folder: String, path: String): IndexEditObserver {
|
||||
suspend fun pushDir(folder: String, path: String): BlockExchangeProtos.IndexUpdate {
|
||||
NetworkUtils.assertProtocol(connectionHandler.hasFolder(folder), {"supplied connection handler $connectionHandler will not share folder $folder"})
|
||||
return IndexEditObserver(sendIndexUpdate(folder, BlockExchangeProtos.FileInfo.newBuilder()
|
||||
return sendIndexUpdate(folder, BlockExchangeProtos.FileInfo.newBuilder()
|
||||
.setName(path)
|
||||
.setType(BlockExchangeProtos.FileInfoType.DIRECTORY), null))
|
||||
.setType(BlockExchangeProtos.FileInfoType.DIRECTORY), null)
|
||||
}
|
||||
|
||||
fun pushFile(inputStream: InputStream, folderId: String, targetPath: String): FileUploadObserver {
|
||||
suspend fun pushFile(inputStream: InputStream, folderId: String, targetPath: String): FileUploadObserver {
|
||||
val fileInfo = indexHandler.waitForRemoteIndexAcquired(connectionHandler).getFileInfoByPath(folderId, targetPath)
|
||||
NetworkUtils.assertProtocol(connectionHandler.hasFolder(folderId), {"supplied connection handler $connectionHandler will not share folder $folderId"})
|
||||
assert(fileInfo == null || fileInfo.folder == folderId)
|
||||
@@ -73,38 +73,33 @@ class BlockPusher internal constructor(private val localDeviceId: DeviceId,
|
||||
val uploadError = AtomicReference<Exception>()
|
||||
val isCompleted = AtomicBoolean(false)
|
||||
val updateLock = Object()
|
||||
val listener = {request: BlockExchangeProtos.Request ->
|
||||
if (request.folder == folderId && request.name == targetPath) {
|
||||
val requestFilter = RequestHandlerFilter(
|
||||
deviceId = connectionHandler.deviceId,
|
||||
folderId = folderId,
|
||||
path = targetPath
|
||||
)
|
||||
|
||||
requestHandlerRegistry.registerListener(requestFilter) { request ->
|
||||
GlobalScope.async {
|
||||
val hash = Hex.toHexString(request.hash.toByteArray())
|
||||
logger.debug("handling block request = {}:{}-{} ({})", request.name, request.offset, request.size, hash)
|
||||
val data = dataSource.getBlock(request.offset, request.size, hash)
|
||||
val future = connectionHandler.sendMessage(BlockExchangeProtos.Response.newBuilder()
|
||||
|
||||
sentBlocks.add(hash)
|
||||
synchronized(updateLock) {
|
||||
updateLock.notifyAll()
|
||||
}
|
||||
|
||||
BlockExchangeProtos.Response.newBuilder()
|
||||
.setCode(BlockExchangeProtos.ErrorCode.NO_ERROR)
|
||||
.setData(ByteString.copyFrom(data))
|
||||
.setId(request.id)
|
||||
.build())
|
||||
monitoringProcessExecutorService.submitLogging {
|
||||
try {
|
||||
future.get()
|
||||
sentBlocks.add(hash)
|
||||
synchronized(updateLock) {
|
||||
updateLock.notifyAll()
|
||||
}
|
||||
//TODO retry on error, register error and throw on watcher
|
||||
} catch (ex: InterruptedException) {
|
||||
//return and do nothing
|
||||
} catch (ex: ExecutionException) {
|
||||
uploadError.set(ex)
|
||||
synchronized(updateLock) {
|
||||
updateLock.notifyAll()
|
||||
}
|
||||
}
|
||||
}
|
||||
.build()
|
||||
}
|
||||
}
|
||||
connectionHandler.registerOnRequestMessageReceivedListeners(listener)
|
||||
|
||||
logger.debug("send index update for file = {}", targetPath)
|
||||
val indexListener = { folderInfo: FolderInfo, newRecords: List<FileInfo>, indexInfo: IndexInfo ->
|
||||
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
|
||||
@@ -122,7 +117,7 @@ class BlockPusher internal constructor(private val localDeviceId: DeviceId,
|
||||
.setName(targetPath)
|
||||
.setSize(fileSize)
|
||||
.setType(BlockExchangeProtos.FileInfoType.FILE)
|
||||
.addAllBlocks(dataSource.blocks), fileInfo?.versionList).right
|
||||
.addAllBlocks(dataSource.blocks), fileInfo?.versionList)
|
||||
return object : FileUploadObserver() {
|
||||
|
||||
override fun progressPercentage() = if (isCompleted.get()) 100 else (sentBlocks.size.toFloat() / dataSource.getHashes().size).toInt()
|
||||
@@ -134,7 +129,7 @@ class BlockPusher internal constructor(private val localDeviceId: DeviceId,
|
||||
logger.debug("closing upload process")
|
||||
monitoringProcessExecutorService.shutdown()
|
||||
indexHandler.unregisterOnIndexRecordAcquiredListener(indexListener)
|
||||
connectionHandler.unregisterOnRequestMessageReceivedListeners(listener)
|
||||
requestHandlerRegistry.unregisterListener(requestFilter)
|
||||
val fileInfo1 = indexHandler.pushRecord(indexUpdate.folder, indexUpdate.filesList.single())
|
||||
logger.info("sent file info record = {}", fileInfo1)
|
||||
}
|
||||
@@ -153,8 +148,8 @@ class BlockPusher internal constructor(private val localDeviceId: DeviceId,
|
||||
}
|
||||
}
|
||||
|
||||
private fun sendIndexUpdate(folderId: String, fileInfoBuilder: BlockExchangeProtos.FileInfo.Builder,
|
||||
oldVersions: Iterable<Version>?): Pair<Future<*>, BlockExchangeProtos.IndexUpdate> {
|
||||
private suspend fun sendIndexUpdate(folderId: String, fileInfoBuilder: BlockExchangeProtos.FileInfo.Builder,
|
||||
oldVersions: Iterable<Version>?): BlockExchangeProtos.IndexUpdate {
|
||||
run {
|
||||
val nextSequence = indexHandler.sequencer().nextSequence()
|
||||
val list = oldVersions ?: emptyList()
|
||||
@@ -183,7 +178,10 @@ class BlockPusher internal constructor(private val localDeviceId: DeviceId,
|
||||
.addFiles(fileInfo)
|
||||
.build()
|
||||
logger.debug("index update = {}", fileInfo)
|
||||
return Pair.of(connectionHandler.sendMessage(indexUpdate), indexUpdate)
|
||||
|
||||
connectionHandler.sendIndexUpdate(indexUpdate)
|
||||
|
||||
return indexUpdate
|
||||
}
|
||||
|
||||
abstract inner class FileUploadObserver : Closeable {
|
||||
@@ -204,33 +202,6 @@ class BlockPusher internal constructor(private val localDeviceId: DeviceId,
|
||||
}
|
||||
}
|
||||
|
||||
inner class IndexEditObserver(private val future: Future<*>, private val indexUpdate: BlockExchangeProtos.IndexUpdate) : Closeable {
|
||||
|
||||
//throw exception if job has errors
|
||||
@Throws(InterruptedException::class, ExecutionException::class)
|
||||
fun isCompleted(): Boolean {
|
||||
return if (future.isDone) {
|
||||
future.get()
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
constructor(pair: Pair<Future<*>, BlockExchangeProtos.IndexUpdate>) : this(pair.left, pair.right)
|
||||
|
||||
@Throws(InterruptedException::class, ExecutionException::class)
|
||||
fun waitForComplete() {
|
||||
future.get()
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun close() {
|
||||
indexHandler.pushRecord(indexUpdate.folder, indexUpdate.filesList.single())
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private class DataSource @Throws(IOException::class) constructor(private val inputStream: InputStream) {
|
||||
|
||||
var size: Long = 0
|
||||
|
||||
@@ -1,517 +0,0 @@
|
||||
/*
|
||||
* 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
|
||||
|
||||
import com.google.protobuf.ByteString
|
||||
import com.google.protobuf.MessageLite
|
||||
import net.jpountz.lz4.LZ4Factory
|
||||
import net.syncthing.java.bep.BlockExchangeProtos.*
|
||||
import net.syncthing.java.client.protocol.rp.RelayClient
|
||||
import net.syncthing.java.core.beans.DeviceAddress
|
||||
import net.syncthing.java.core.beans.DeviceId
|
||||
import net.syncthing.java.core.beans.DeviceInfo
|
||||
import net.syncthing.java.core.beans.FolderInfo
|
||||
import net.syncthing.java.core.configuration.Configuration
|
||||
import net.syncthing.java.core.interfaces.TempRepository
|
||||
import net.syncthing.java.core.security.KeystoreHandler
|
||||
import net.syncthing.java.core.utils.NetworkUtils
|
||||
import net.syncthing.java.core.utils.submitLogging
|
||||
import net.syncthing.java.httprelay.HttpRelayClient
|
||||
import org.apache.commons.io.IOUtils
|
||||
import org.apache.commons.lang3.tuple.Pair
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.io.Closeable
|
||||
import java.io.DataInputStream
|
||||
import java.io.DataOutputStream
|
||||
import java.io.IOException
|
||||
import java.lang.reflect.InvocationTargetException
|
||||
import java.nio.ByteBuffer
|
||||
import java.security.cert.CertificateException
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.concurrent.Executors
|
||||
import java.util.concurrent.Future
|
||||
import java.util.concurrent.TimeUnit
|
||||
import javax.net.ssl.SSLSocket
|
||||
|
||||
class ConnectionHandler(private val configuration: Configuration, val address: DeviceAddress,
|
||||
private val indexHandler: IndexHandler,
|
||||
private val tempRepository: TempRepository,
|
||||
private val onNewFolderSharedListener: (ConnectionHandler, FolderInfo) -> Unit,
|
||||
private val onConnectionChangedListener: (ConnectionHandler) -> Unit) : Closeable {
|
||||
|
||||
private val logger = LoggerFactory.getLogger(javaClass)
|
||||
|
||||
private val outExecutorService = Executors.newSingleThreadExecutor()
|
||||
private val inExecutorService = Executors.newSingleThreadExecutor()
|
||||
private val messageProcessingService = Executors.newCachedThreadPool()
|
||||
private val periodicExecutorService = Executors.newSingleThreadScheduledExecutor()
|
||||
private lateinit var socket: SSLSocket
|
||||
private var inputStream: DataInputStream? = null
|
||||
private var outputStream: DataOutputStream? = null
|
||||
private var lastActive = Long.MIN_VALUE
|
||||
internal var clusterConfigInfo: ClusterConfigInfo? = null
|
||||
private set
|
||||
private val clusterConfigWaitingLock = Object()
|
||||
private val responseHandler = ResponseHandler()
|
||||
private val blockPuller = BlockPuller(this, indexHandler, responseHandler, tempRepository)
|
||||
private val blockPusher = BlockPusher(configuration.localDeviceId, this, indexHandler)
|
||||
private val onRequestMessageReceivedListeners = mutableSetOf<(Request) -> Unit>()
|
||||
private var isClosed = false
|
||||
var isConnected = false
|
||||
private set
|
||||
|
||||
fun deviceId(): DeviceId = address.deviceId()
|
||||
|
||||
private fun checkNotClosed() {
|
||||
NetworkUtils.assertProtocol(!isClosed, {"connection $this closed"})
|
||||
}
|
||||
|
||||
internal fun registerOnRequestMessageReceivedListeners(listener: (Request) -> Unit) {
|
||||
onRequestMessageReceivedListeners.add(listener)
|
||||
}
|
||||
|
||||
internal fun unregisterOnRequestMessageReceivedListeners(listener: (Request) -> Unit) {
|
||||
assert(onRequestMessageReceivedListeners.contains(listener))
|
||||
onRequestMessageReceivedListeners.remove(listener)
|
||||
}
|
||||
|
||||
@Throws(IOException::class, KeystoreHandler.CryptoException::class)
|
||||
fun connect(): ConnectionHandler {
|
||||
checkNotClosed()
|
||||
assert(!isConnected, {"already connected!"})
|
||||
logger.info("connecting to {}", address.address)
|
||||
|
||||
val keystoreHandler = KeystoreHandler.Loader().loadKeystore(configuration)
|
||||
|
||||
socket = when (address.getType()) {
|
||||
DeviceAddress.AddressType.TCP -> {
|
||||
logger.debug("opening tcp ssl connection")
|
||||
keystoreHandler.createSocket(address.getSocketAddress(), KeystoreHandler.BEP)
|
||||
}
|
||||
DeviceAddress.AddressType.RELAY -> {
|
||||
logger.debug("opening relay connection")
|
||||
keystoreHandler.wrapSocket(RelayClient(configuration).openRelayConnection(address), KeystoreHandler.BEP)
|
||||
}
|
||||
DeviceAddress.AddressType.HTTP_RELAY, DeviceAddress.AddressType.HTTPS_RELAY -> {
|
||||
logger.debug("opening http relay connection")
|
||||
keystoreHandler.wrapSocket(HttpRelayClient().openRelayConnection(address), KeystoreHandler.BEP)
|
||||
}
|
||||
else -> throw UnsupportedOperationException("unsupported address type = " + address.getType())
|
||||
}
|
||||
inputStream = DataInputStream(socket.inputStream)
|
||||
outputStream = DataOutputStream(socket.outputStream)
|
||||
|
||||
sendHelloMessage(BlockExchangeProtos.Hello.newBuilder()
|
||||
.setClientName(configuration.clientName)
|
||||
.setClientVersion(configuration.clientVersion)
|
||||
.setDeviceName(configuration.localDeviceName)
|
||||
.build().toByteArray())
|
||||
markActivityOnSocket()
|
||||
|
||||
receiveHelloMessage()
|
||||
try {
|
||||
keystoreHandler.checkSocketCertificate(socket, address.deviceId())
|
||||
} catch (e: CertificateException) {
|
||||
throw IOException(e)
|
||||
}
|
||||
|
||||
run {
|
||||
val clusterConfigBuilder = ClusterConfig.newBuilder()
|
||||
for (folder in configuration.folders) {
|
||||
val folderBuilder = Folder.newBuilder()
|
||||
.setId(folder.folderId)
|
||||
.setLabel(folder.label)
|
||||
run {
|
||||
//our device
|
||||
val deviceBuilder = Device.newBuilder()
|
||||
.setId(ByteString.copyFrom(configuration.localDeviceId.toHashData()))
|
||||
.setIndexId(indexHandler.sequencer().indexId())
|
||||
.setMaxSequence(indexHandler.sequencer().currentSequence())
|
||||
folderBuilder.addDevices(deviceBuilder)
|
||||
}
|
||||
run {
|
||||
//other device
|
||||
val deviceBuilder = Device.newBuilder()
|
||||
.setId(ByteString.copyFrom(DeviceId(address.deviceId).toHashData()))
|
||||
val indexSequenceInfo = indexHandler.indexRepository.findIndexInfoByDeviceAndFolder(address.deviceId(), folder.folderId)
|
||||
indexSequenceInfo?.let {
|
||||
deviceBuilder
|
||||
.setIndexId(indexSequenceInfo.indexId)
|
||||
.setMaxSequence(indexSequenceInfo.localSequence)
|
||||
logger.info("send delta index info device = {} index = {} max (local) sequence = {}",
|
||||
indexSequenceInfo.deviceId,
|
||||
indexSequenceInfo.indexId,
|
||||
indexSequenceInfo.localSequence)
|
||||
}
|
||||
folderBuilder.addDevices(deviceBuilder)
|
||||
}
|
||||
clusterConfigBuilder.addFolders(folderBuilder)
|
||||
//TODO other devices??
|
||||
}
|
||||
sendMessage(clusterConfigBuilder.build())
|
||||
}
|
||||
synchronized(clusterConfigWaitingLock) {
|
||||
startMessageListenerService()
|
||||
while (clusterConfigInfo == null && !isClosed) {
|
||||
logger.debug("wait for cluster config")
|
||||
try {
|
||||
clusterConfigWaitingLock.wait()
|
||||
} catch (e: InterruptedException) {
|
||||
throw IOException(e)
|
||||
}
|
||||
}
|
||||
if (clusterConfigInfo == null) {
|
||||
throw IOException("unable to retrieve cluster config from peer!")
|
||||
}
|
||||
}
|
||||
for (folder in configuration.folders) {
|
||||
if (hasFolder(folder.folderId)) {
|
||||
sendIndexMessage(folder.folderId)
|
||||
}
|
||||
}
|
||||
periodicExecutorService.scheduleWithFixedDelay({ this.sendPing() }, 90, 90, TimeUnit.SECONDS)
|
||||
isConnected = true
|
||||
onConnectionChangedListener(this)
|
||||
return this
|
||||
}
|
||||
|
||||
fun getBlockPuller(): BlockPuller {
|
||||
return blockPuller
|
||||
}
|
||||
|
||||
fun getBlockPusher(): BlockPusher {
|
||||
return blockPusher
|
||||
}
|
||||
|
||||
private fun sendIndexMessage(folderId: String) {
|
||||
sendMessage(Index.newBuilder()
|
||||
.setFolder(folderId)
|
||||
.build())
|
||||
}
|
||||
|
||||
fun closeBg() {
|
||||
Thread { close() }.start()
|
||||
}
|
||||
|
||||
/**
|
||||
* Receive hello message and save device name to configuration.
|
||||
*/
|
||||
@Throws(IOException::class)
|
||||
private fun receiveHelloMessage() {
|
||||
val magic = inputStream!!.readInt()
|
||||
NetworkUtils.assertProtocol(magic == MAGIC, {"magic mismatch, expected $MAGIC, got $magic"})
|
||||
val length = inputStream!!.readShort().toInt()
|
||||
NetworkUtils.assertProtocol(length > 0, {"invalid lenght, must be >0, got $length"})
|
||||
val buffer = ByteArray(length)
|
||||
inputStream!!.readFully(buffer)
|
||||
val hello = BlockExchangeProtos.Hello.parseFrom(buffer)
|
||||
logger.info("Received hello message, deviceName=${hello.deviceName}, clientName=${hello.clientName}, clientVersion=${hello.clientVersion}")
|
||||
configuration.peers = configuration.peers.map { peer ->
|
||||
if (peer.deviceId == deviceId()) {
|
||||
DeviceInfo(deviceId(), hello.deviceName)
|
||||
} else {
|
||||
peer
|
||||
}
|
||||
}.toSet()
|
||||
configuration.persistLater()
|
||||
}
|
||||
|
||||
private fun sendHelloMessage(payload: ByteArray): Future<*> {
|
||||
return outExecutorService.submitLogging {
|
||||
try {
|
||||
logger.debug("Sending hello message")
|
||||
val header = ByteBuffer.allocate(6)
|
||||
header.putInt(MAGIC)
|
||||
header.putShort(payload.size.toShort())
|
||||
outputStream!!.write(header.array())
|
||||
outputStream!!.write(payload)
|
||||
outputStream!!.flush()
|
||||
} catch (ex: IOException) {
|
||||
if (outExecutorService.isShutdown) {
|
||||
return@submitLogging
|
||||
}
|
||||
logger.error("error writing to output stream", ex)
|
||||
closeBg()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun sendPing(): Future<*> {
|
||||
return sendMessage(Ping.newBuilder().build())
|
||||
}
|
||||
|
||||
private fun markActivityOnSocket() {
|
||||
lastActive = System.currentTimeMillis()
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
private fun receiveMessage(): Pair<BlockExchangeProtos.MessageType, MessageLite> {
|
||||
var headerLength = inputStream!!.readShort().toInt()
|
||||
while (headerLength == 0) {
|
||||
logger.warn("got headerLength == 0, skipping short")
|
||||
headerLength = inputStream!!.readShort().toInt()
|
||||
}
|
||||
markActivityOnSocket()
|
||||
NetworkUtils.assertProtocol(headerLength > 0, {"invalid lenght, must be >0, got $headerLength"})
|
||||
val headerBuffer = ByteArray(headerLength)
|
||||
inputStream!!.readFully(headerBuffer)
|
||||
val header = BlockExchangeProtos.Header.parseFrom(headerBuffer)
|
||||
var messageLength = 0
|
||||
while (messageLength == 0) {
|
||||
logger.warn("received readInt() == 0, expecting 'bep message header length' (int >0), ignoring (keepalive?)")
|
||||
messageLength = inputStream!!.readInt()
|
||||
}
|
||||
NetworkUtils.assertProtocol(messageLength >= 0, {"invalid lenght, must be >=0, got $messageLength"})
|
||||
var messageBuffer = ByteArray(messageLength)
|
||||
inputStream!!.readFully(messageBuffer)
|
||||
markActivityOnSocket()
|
||||
if (header.compression == BlockExchangeProtos.MessageCompression.LZ4) {
|
||||
val uncompressedLength = ByteBuffer.wrap(messageBuffer).int
|
||||
messageBuffer = LZ4Factory.fastestInstance().fastDecompressor().decompress(messageBuffer, 4, uncompressedLength)
|
||||
}
|
||||
val messageTypeInfo = messageTypesByProtoMessageType[header.type]
|
||||
NetworkUtils.assertProtocol(messageTypeInfo != null, {"unsupported message type = ${header.type}"})
|
||||
try {
|
||||
val message = messageTypeInfo!!.parseFrom(messageBuffer)
|
||||
return Pair.of(header.type, message)
|
||||
} catch (e: Exception) {
|
||||
when (e) {
|
||||
is IllegalAccessException, is IllegalArgumentException, is InvocationTargetException, is NoSuchMethodException, is SecurityException ->
|
||||
throw IOException(e)
|
||||
else -> throw e
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal fun sendMessage(message: MessageLite): Future<*> {
|
||||
checkNotClosed()
|
||||
val messageTypeInfo = messageTypesByJavaClass[message.javaClass]
|
||||
messageTypeInfo!!
|
||||
val header = BlockExchangeProtos.Header.newBuilder()
|
||||
.setCompression(BlockExchangeProtos.MessageCompression.NONE)
|
||||
// invert map
|
||||
.setType(messageTypeInfo.protoMessageType)
|
||||
.build()
|
||||
val headerData = header.toByteArray()
|
||||
val messageData = message.toByteArray() //TODO compression
|
||||
return outExecutorService.submit<Any> {
|
||||
try {
|
||||
logger.debug("sending message type = {} {}", header.type, getIdForMessage(message))
|
||||
markActivityOnSocket()
|
||||
outputStream!!.writeShort(headerData.size)
|
||||
outputStream!!.write(headerData)
|
||||
outputStream!!.writeInt(messageData.size)//with compression, check this
|
||||
outputStream!!.write(messageData)
|
||||
outputStream!!.flush()
|
||||
markActivityOnSocket()
|
||||
} catch (ex: IOException) {
|
||||
if (!outExecutorService.isShutdown) {
|
||||
logger.error("error writing to output stream", ex)
|
||||
closeBg()
|
||||
}
|
||||
throw ex
|
||||
}
|
||||
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
if (!isClosed) {
|
||||
sendMessage(Close.getDefaultInstance())
|
||||
isClosed = true
|
||||
isConnected = false
|
||||
periodicExecutorService.shutdown()
|
||||
outExecutorService.shutdown()
|
||||
inExecutorService.shutdown()
|
||||
messageProcessingService.shutdown()
|
||||
assert(onRequestMessageReceivedListeners.isEmpty())
|
||||
if (outputStream != null) {
|
||||
IOUtils.closeQuietly(outputStream)
|
||||
outputStream = null
|
||||
}
|
||||
if (inputStream != null) {
|
||||
IOUtils.closeQuietly(inputStream)
|
||||
inputStream = null
|
||||
}
|
||||
try {
|
||||
IOUtils.closeQuietly(socket)
|
||||
} catch (ex: Exception) {
|
||||
// ignore this
|
||||
// this can throw an exception if socket was not yet initialized/ set
|
||||
// as Kotlin does an check about this, the closeQuietly does not catch it
|
||||
}
|
||||
logger.info("closed connection {}", address)
|
||||
synchronized(clusterConfigWaitingLock) {
|
||||
clusterConfigWaitingLock.notifyAll()
|
||||
}
|
||||
onConnectionChangedListener(this)
|
||||
try {
|
||||
periodicExecutorService.awaitTermination(2, TimeUnit.SECONDS)
|
||||
outExecutorService.awaitTermination(2, TimeUnit.SECONDS)
|
||||
inExecutorService.awaitTermination(2, TimeUnit.SECONDS)
|
||||
messageProcessingService.awaitTermination(2, TimeUnit.SECONDS)
|
||||
} catch (ex: InterruptedException) {
|
||||
logger.warn("", ex)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* return time elapsed since last activity on socket, inputStream millis
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
fun getLastActive(): Long {
|
||||
return System.currentTimeMillis() - lastActive
|
||||
}
|
||||
|
||||
private fun startMessageListenerService() {
|
||||
inExecutorService.submitLogging {
|
||||
try {
|
||||
while (!Thread.interrupted()) {
|
||||
val message = receiveMessage()
|
||||
messageProcessingService.submitLogging {
|
||||
logger.debug("received message type = {} {}", message.left, getIdForMessage(message.right))
|
||||
when (message.left) {
|
||||
BlockExchangeProtos.MessageType.INDEX -> {
|
||||
val index = message.value as Index
|
||||
indexHandler.handleIndexMessageReceivedEvent(index.folder, index.filesList, this)
|
||||
}
|
||||
BlockExchangeProtos.MessageType.INDEX_UPDATE -> {
|
||||
val update = message.value as IndexUpdate
|
||||
indexHandler.handleIndexMessageReceivedEvent(update.folder, update.filesList, this)
|
||||
}
|
||||
BlockExchangeProtos.MessageType.REQUEST -> {
|
||||
onRequestMessageReceivedListeners.forEach { it(message.value as Request) }
|
||||
}
|
||||
BlockExchangeProtos.MessageType.RESPONSE -> {
|
||||
responseHandler.handleResponse(message.value as Response)
|
||||
}
|
||||
BlockExchangeProtos.MessageType.PING -> logger.debug("ping message received")
|
||||
BlockExchangeProtos.MessageType.CLOSE -> {
|
||||
val close = message.value as BlockExchangeProtos.Close
|
||||
logger.info("received close message, reason=${close.reason}")
|
||||
closeBg()
|
||||
}
|
||||
BlockExchangeProtos.MessageType.CLUSTER_CONFIG -> {
|
||||
NetworkUtils.assertProtocol(clusterConfigInfo == null, {"received cluster config message twice!"})
|
||||
clusterConfigInfo = ClusterConfigInfo()
|
||||
val clusterConfig = message.value as ClusterConfig
|
||||
for (folder in clusterConfig.foldersList ?: emptyList()) {
|
||||
val folderInfo = ClusterConfigFolderInfo(folder.id, folder.label)
|
||||
val devicesById = (folder.devicesList ?: emptyList())
|
||||
.associateBy { input ->
|
||||
DeviceId.fromHashData(input.id!!.toByteArray())
|
||||
}
|
||||
val otherDevice = devicesById[address.deviceId()]
|
||||
val ourDevice = devicesById[configuration.localDeviceId]
|
||||
if (otherDevice != null) {
|
||||
folderInfo.isAnnounced = true
|
||||
}
|
||||
if (ourDevice != null) {
|
||||
folderInfo.isShared = true
|
||||
logger.info("folder shared from device = {} folder = {}", address.deviceId, 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
|
||||
onNewFolderSharedListener(this, fi)
|
||||
logger.info("new folder shared = {}", folderInfo)
|
||||
}
|
||||
} else {
|
||||
logger.info("folder not shared from device = {} folder = {}", address.deviceId, folderInfo)
|
||||
}
|
||||
clusterConfigInfo!!.putFolderInfo(folderInfo)
|
||||
}
|
||||
configuration.persistLater()
|
||||
indexHandler.handleClusterConfigMessageProcessedEvent(clusterConfig)
|
||||
synchronized(clusterConfigWaitingLock) {
|
||||
clusterConfigWaitingLock.notifyAll()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (ex: IOException) {
|
||||
if (inExecutorService.isShutdown) {
|
||||
return@submitLogging
|
||||
}
|
||||
logger.error("error receiving message", ex)
|
||||
closeBg()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return "ConnectionHandler{" + "address=" + address + ", lastActive=" + getLastActive() / 1000.0 + "secs ago}"
|
||||
}
|
||||
|
||||
internal inner class ClusterConfigInfo {
|
||||
|
||||
private val folderInfoById = ConcurrentHashMap<String, ClusterConfigFolderInfo>()
|
||||
|
||||
fun getSharedFolders(): Set<String> = folderInfoById.values.filter { it.isShared }.map { it.folderId }.toSet()
|
||||
|
||||
fun putFolderInfo(folderInfo: ClusterConfigFolderInfo) {
|
||||
folderInfoById[folderInfo.folderId] = folderInfo
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
fun hasFolder(folder: String): Boolean {
|
||||
return clusterConfigInfo!!.getSharedFolders().contains(folder)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private const val MAGIC = 0x2EA7D90B
|
||||
|
||||
private val messageTypes = listOf(
|
||||
MessageTypeInfo(MessageType.CLOSE, Close::class.java) { Close.parseFrom(it) },
|
||||
MessageTypeInfo(MessageType.CLUSTER_CONFIG, ClusterConfig::class.java) { ClusterConfig.parseFrom(it) },
|
||||
MessageTypeInfo(MessageType.DOWNLOAD_PROGRESS, DownloadProgress::class.java) { DownloadProgress.parseFrom(it) },
|
||||
MessageTypeInfo(MessageType.INDEX, Index::class.java) { Index.parseFrom(it) },
|
||||
MessageTypeInfo(MessageType.INDEX_UPDATE, IndexUpdate::class.java) { IndexUpdate.parseFrom(it) },
|
||||
MessageTypeInfo(MessageType.PING, Ping::class.java) { Ping.parseFrom(it) },
|
||||
MessageTypeInfo(MessageType.REQUEST, Request::class.java) { Request.parseFrom(it) },
|
||||
MessageTypeInfo(MessageType.RESPONSE, Response::class.java) { Response.parseFrom(it) }
|
||||
)
|
||||
|
||||
private val messageTypesByProtoMessageType = messageTypes.map { it.protoMessageType to it }.toMap()
|
||||
private val messageTypesByJavaClass = messageTypes.map { it.javaClass to it }.toMap()
|
||||
|
||||
/**
|
||||
* get id for message bean/instance, for log tracking
|
||||
*
|
||||
* @param message
|
||||
* @return id for message bean
|
||||
*/
|
||||
private fun getIdForMessage(message: MessageLite): String {
|
||||
return when (message) {
|
||||
is Request -> Integer.toString(message.id)
|
||||
is Response -> Integer.toString(message.id)
|
||||
else -> Integer.toString(Math.abs(message.hashCode()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class MessageTypeInfo(
|
||||
val protoMessageType: MessageType,
|
||||
val javaClass: Class<out MessageLite>,
|
||||
val parseFrom: (data: ByteArray) -> MessageLite
|
||||
)
|
||||
}
|
||||
@@ -55,7 +55,7 @@ class IndexBrowser internal constructor(private val indexRepository: IndexReposi
|
||||
}
|
||||
}
|
||||
|
||||
internal fun onIndexChangedevent(folder: String, newRecord: FileInfo) {
|
||||
internal fun onIndexChangedevent(folder: String) {
|
||||
if (folder == this.folder) {
|
||||
preloadFileInfoForCurrentPath()
|
||||
}
|
||||
|
||||
@@ -13,18 +13,18 @@
|
||||
*/
|
||||
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.BlockUtils
|
||||
import net.syncthing.java.core.utils.NetworkUtils
|
||||
import net.syncthing.java.core.utils.awaitTerminationSafe
|
||||
import net.syncthing.java.core.utils.submitLogging
|
||||
import net.syncthing.java.core.utils.trySubmitLogging
|
||||
import org.apache.commons.lang3.tuple.Pair
|
||||
import org.apache.http.util.TextUtils
|
||||
import org.bouncycastle.util.encoders.Hex
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.io.Closeable
|
||||
@@ -95,9 +95,9 @@ class IndexHandler(private val configuration: Configuration, val indexRepository
|
||||
}
|
||||
}
|
||||
|
||||
internal fun isRemoteIndexAcquired(clusterConfigInfo: ConnectionHandler.ClusterConfigInfo, peerDeviceId: DeviceId): Boolean {
|
||||
internal fun isRemoteIndexAcquired(clusterConfigInfo: ClusterConfigInfo, peerDeviceId: DeviceId): Boolean {
|
||||
var ready = true
|
||||
for (folder in clusterConfigInfo.getSharedFolders()) {
|
||||
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)
|
||||
@@ -108,12 +108,12 @@ class IndexHandler(private val configuration: Configuration, val indexRepository
|
||||
}
|
||||
|
||||
@Throws(InterruptedException::class)
|
||||
fun waitForRemoteIndexAcquired(connectionHandler: ConnectionHandler, timeoutSecs: Long? = null): IndexHandler {
|
||||
fun waitForRemoteIndexAcquired(connectionHandler: ConnectionActorWrapper, timeoutSecs: Long? = null): IndexHandler {
|
||||
val timeoutMillis = (timeoutSecs ?: DEFAULT_INDEX_TIMEOUT) * 1000
|
||||
synchronized(indexWaitLock) {
|
||||
while (!isRemoteIndexAcquired(connectionHandler.clusterConfigInfo!!, connectionHandler.deviceId())) {
|
||||
while (!isRemoteIndexAcquired(connectionHandler.getClusterConfig(), connectionHandler.deviceId)) {
|
||||
indexWaitLock.wait(timeoutMillis)
|
||||
NetworkUtils.assertProtocol(connectionHandler.getLastActive() < timeoutMillis || lastActive() < timeoutMillis,
|
||||
NetworkUtils.assertProtocol(/* TODO connectionHandler.getLastActive() < timeoutMillis || */ lastActive() < timeoutMillis,
|
||||
{"unable to acquire index from connection $connectionHandler, timeout reached!"})
|
||||
}
|
||||
}
|
||||
@@ -138,8 +138,8 @@ class IndexHandler(private val configuration: Configuration, val indexRepository
|
||||
}
|
||||
}
|
||||
|
||||
fun handleIndexMessageReceivedEvent(folderId: String, filesList: List<BlockExchangeProtos.FileInfo>, connectionHandler: ConnectionHandler) {
|
||||
indexMessageProcessor.handleIndexMessageReceivedEvent(folderId, filesList, connectionHandler)
|
||||
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? {
|
||||
@@ -209,14 +209,14 @@ class IndexHandler(private val configuration: Configuration, val indexRepository
|
||||
private fun addRecord(record: FileInfo, fileBlocks: FileBlocks?): FileInfo? {
|
||||
synchronized(writeAccessLock) {
|
||||
val lastModified = indexRepository.findFileInfoLastModified(record.folder, record.path)
|
||||
return if (lastModified != null && !record.lastModified.after(lastModified)) {
|
||||
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)
|
||||
it.onIndexChangedevent(record.folder)
|
||||
}
|
||||
record
|
||||
}
|
||||
@@ -234,9 +234,9 @@ class IndexHandler(private val configuration: Configuration, val indexRepository
|
||||
} else {
|
||||
assert(fileInfo.isFile())
|
||||
val fileBlocks = indexRepository.findFileBlocks(folder, path)
|
||||
checkNotNull(fileBlocks, {"file blocks not found for file info = $fileInfo"})
|
||||
checkNotNull(fileBlocks) {"file blocks not found for file info = $fileInfo"}
|
||||
|
||||
FileInfo.checkBlocks(fileInfo, fileBlocks!!)
|
||||
FileInfo.checkBlocks(fileInfo, fileBlocks)
|
||||
|
||||
Pair.of(fileInfo, fileBlocks)
|
||||
}
|
||||
@@ -244,7 +244,7 @@ class IndexHandler(private val configuration: Configuration, val indexRepository
|
||||
|
||||
private fun updateFolderInfo(folder: String, label: String?): FolderInfo {
|
||||
var folderInfo: FolderInfo? = folderInfoByFolder[folder]
|
||||
if (folderInfo == null || !TextUtils.isEmpty(label)) {
|
||||
if (folderInfo == null || label.isNullOrEmpty()) {
|
||||
folderInfo = FolderInfo(folder, label)
|
||||
folderInfoByFolder.put(folderInfo.folderId, folderInfo)
|
||||
}
|
||||
@@ -293,11 +293,9 @@ class IndexHandler(private val configuration: Configuration, val indexRepository
|
||||
// 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>, connectionHandler: ConnectionHandler) {
|
||||
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()
|
||||
val clusterConfigInfo = connectionHandler.clusterConfigInfo
|
||||
val peerDeviceId = connectionHandler.deviceId()
|
||||
// List<BlockExchangeProtos.FileInfo> fileList = event.getFilesList();
|
||||
// for (int index = 0; index < fileList.size(); index += MAX_RECORD_PER_PROCESS) {
|
||||
// BlockExchangeProtos.IndexUpdate data = BlockExchangeProtos.IndexUpdate.newBuilder()
|
||||
@@ -321,23 +319,23 @@ class IndexHandler(private val configuration: Configuration, val indexRepository
|
||||
}
|
||||
}
|
||||
|
||||
private fun processBg(data: BlockExchangeProtos.IndexUpdate, clusterConfigInfo: ConnectionHandler.ClusterConfigInfo?, peerDeviceId: DeviceId) {
|
||||
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.submitLogging(object : ProcessingRunnable() {
|
||||
executorService.trySubmitLogging(object : ProcessingRunnable() {
|
||||
override fun runProcess() {
|
||||
doHandleIndexMessageReceivedEvent(data, clusterConfigInfo, peerDeviceId)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun storeAndProcessBg(data: BlockExchangeProtos.IndexUpdate, clusterConfigInfo: ConnectionHandler.ClusterConfigInfo?, peerDeviceId: DeviceId) {
|
||||
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.submitLogging(object : ProcessingRunnable() {
|
||||
executorService.trySubmitLogging(object : ProcessingRunnable() {
|
||||
override fun runProcess() {
|
||||
try {
|
||||
doHandleIndexMessageReceivedEvent(key, clusterConfigInfo, peerDeviceId)
|
||||
@@ -370,7 +368,7 @@ class IndexHandler(private val configuration: Configuration, val indexRepository
|
||||
// return fileSequence < localSequence;
|
||||
// }
|
||||
@Throws(IOException::class)
|
||||
protected fun doHandleIndexMessageReceivedEvent(key: String, clusterConfigInfo: ConnectionHandler.ClusterConfigInfo?, peerDeviceId: DeviceId) {
|
||||
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)
|
||||
@@ -378,7 +376,7 @@ class IndexHandler(private val configuration: Configuration, val indexRepository
|
||||
doHandleIndexMessageReceivedEvent(message, clusterConfigInfo, peerDeviceId)
|
||||
}
|
||||
|
||||
protected fun doHandleIndexMessageReceivedEvent(message: BlockExchangeProtos.IndexUpdate, clusterConfigInfo: ConnectionHandler.ClusterConfigInfo?, peerDeviceId: DeviceId) {
|
||||
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));
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* 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
|
||||
|
||||
import net.syncthing.java.bep.connectionactor.ConnectionActorWrapper
|
||||
import java.io.IOException
|
||||
import java.util.*
|
||||
|
||||
class MultiConnectionHelper (
|
||||
initialConnections: List<ConnectionActorWrapper>,
|
||||
private val connectionFilter: (ConnectionActorWrapper) -> Boolean
|
||||
) {
|
||||
companion object {
|
||||
private val random = Random()
|
||||
}
|
||||
|
||||
private val usableConnections = initialConnections.toMutableList()
|
||||
|
||||
fun pickConnection(): ConnectionActorWrapper {
|
||||
val possibleConnections = synchronized(usableConnections) {
|
||||
usableConnections.filter { it.isConnected and connectionFilter(it) }
|
||||
}
|
||||
|
||||
if (possibleConnections.isEmpty()) {
|
||||
throw IOException("no matching connection is available")
|
||||
} else if (possibleConnections.size == 1) {
|
||||
return possibleConnections.first()
|
||||
} else {
|
||||
return possibleConnections[random.nextInt(possibleConnections.size)]
|
||||
}
|
||||
}
|
||||
|
||||
fun disableConnection(wrapper: ConnectionActorWrapper) {
|
||||
synchronized(usableConnections) {
|
||||
usableConnections.remove(wrapper)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package net.syncthing.java.bep
|
||||
|
||||
import kotlinx.coroutines.Deferred
|
||||
import net.syncthing.java.core.beans.DeviceId
|
||||
import java.io.IOException
|
||||
|
||||
class RequestHandlerRegistry {
|
||||
private val listeners = mutableMapOf<RequestHandlerFilter, (BlockExchangeProtos.Request) -> Deferred<BlockExchangeProtos.Response>>()
|
||||
|
||||
suspend fun handleRequest(source: DeviceId, request: BlockExchangeProtos.Request): BlockExchangeProtos.Response {
|
||||
val rule = RequestHandlerFilter(
|
||||
deviceId = source,
|
||||
folderId = request.folder,
|
||||
path = request.name
|
||||
)
|
||||
|
||||
val matchingListener = synchronized(listeners) {
|
||||
listeners[rule]
|
||||
}
|
||||
|
||||
if (matchingListener != null) {
|
||||
return matchingListener(request).await()
|
||||
} else {
|
||||
return BlockExchangeProtos.Response.newBuilder()
|
||||
.setId(request.id)
|
||||
.setCode(BlockExchangeProtos.ErrorCode.GENERIC)
|
||||
.build()
|
||||
}
|
||||
}
|
||||
|
||||
fun registerListener(filter: RequestHandlerFilter, listener: (BlockExchangeProtos.Request) -> Deferred<BlockExchangeProtos.Response>) {
|
||||
synchronized(listeners) {
|
||||
val oldListener = listeners[filter]
|
||||
|
||||
if (oldListener != null) {
|
||||
throw IOException("there is already an listener for this filter")
|
||||
}
|
||||
|
||||
listeners[filter] = listener
|
||||
}
|
||||
}
|
||||
|
||||
fun unregisterListener(filter: RequestHandlerFilter) {
|
||||
synchronized(listeners) {
|
||||
listeners.remove(filter)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class RequestHandlerFilter(
|
||||
val deviceId: DeviceId,
|
||||
val folderId: String,
|
||||
val path: String
|
||||
)
|
||||
@@ -1,50 +0,0 @@
|
||||
/*
|
||||
* 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
|
||||
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.util.*
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
import kotlin.collections.HashMap
|
||||
|
||||
class ResponseHandler {
|
||||
companion object {
|
||||
private val logger = LoggerFactory.getLogger(ResponseHandler::class.java)
|
||||
}
|
||||
|
||||
private val responseListeners = Collections.synchronizedMap(HashMap<Int, (BlockExchangeProtos.Response) -> Unit>())
|
||||
private val nextRequestId = AtomicInteger(0)
|
||||
|
||||
fun registerListener(listener: (BlockExchangeProtos.Response) -> Unit): Int {
|
||||
val requestId = nextRequestId.getAndIncrement()
|
||||
|
||||
responseListeners[requestId] = listener
|
||||
|
||||
return requestId
|
||||
}
|
||||
|
||||
fun unregisterListener(requestId: Int) {
|
||||
responseListeners.remove(requestId)
|
||||
}
|
||||
|
||||
fun handleResponse(response: BlockExchangeProtos.Response) {
|
||||
val listener = responseListeners.remove(response.id)
|
||||
|
||||
if (listener != null) {
|
||||
listener(response)
|
||||
} else {
|
||||
logger.warn("received response for {} without associated handler", response.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
+139
@@ -0,0 +1,139 @@
|
||||
/*
|
||||
* 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.connectionactor
|
||||
|
||||
import com.google.protobuf.ByteString
|
||||
import net.syncthing.java.bep.BlockExchangeProtos
|
||||
import net.syncthing.java.bep.IndexHandler
|
||||
import net.syncthing.java.core.beans.DeviceId
|
||||
import net.syncthing.java.core.beans.FolderInfo
|
||||
import net.syncthing.java.core.configuration.Configuration
|
||||
import org.slf4j.LoggerFactory
|
||||
|
||||
object ClusterConfigHandler {
|
||||
private val logger = LoggerFactory.getLogger(ClusterConfigHandler::class.java)
|
||||
|
||||
fun buildClusterConfig(
|
||||
configuration: Configuration,
|
||||
indexHandler: IndexHandler,
|
||||
deviceId: DeviceId
|
||||
): BlockExchangeProtos.ClusterConfig {
|
||||
val builder = BlockExchangeProtos.ClusterConfig.newBuilder()
|
||||
|
||||
for (folder in configuration.folders) {
|
||||
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 other device
|
||||
val indexSequenceInfo = indexHandler.indexRepository.findIndexInfoByDeviceAndFolder(deviceId, folder.folderId)
|
||||
|
||||
folderBuilder.addDevices(
|
||||
BlockExchangeProtos.Device.newBuilder()
|
||||
.setId(ByteString.copyFrom(deviceId.toHashData()))
|
||||
.apply {
|
||||
indexSequenceInfo?.let {
|
||||
setIndexId(indexSequenceInfo.indexId)
|
||||
setMaxSequence(indexSequenceInfo.localSequence)
|
||||
|
||||
logger.info("send delta index info device = {} index = {} max (local) sequence = {}",
|
||||
indexSequenceInfo.deviceId,
|
||||
indexSequenceInfo.indexId,
|
||||
indexSequenceInfo.localSequence)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
builder.addFolders(folderBuilder)
|
||||
|
||||
// TODO: add the other devices to the cluster config
|
||||
}
|
||||
|
||||
return builder.build()
|
||||
}
|
||||
|
||||
// TODO: understand this
|
||||
internal fun handleReceivedClusterConfig(
|
||||
clusterConfig: BlockExchangeProtos.ClusterConfig,
|
||||
configuration: Configuration,
|
||||
otherDeviceId: DeviceId,
|
||||
indexHandler: IndexHandler
|
||||
): ClusterConfigInfo {
|
||||
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)
|
||||
}
|
||||
} else {
|
||||
logger.info("folder not shared from device = {} folder = {}", otherDeviceId, folderInfo)
|
||||
}
|
||||
|
||||
folderInfoList.add(folderInfo)
|
||||
}
|
||||
configuration.persistLater()
|
||||
indexHandler.handleClusterConfigMessageProcessedEvent(clusterConfig)
|
||||
|
||||
return ClusterConfigInfo(folderInfoList, newSharedFolders)
|
||||
}
|
||||
}
|
||||
|
||||
class ClusterConfigInfo (val folderInfo: List<ClusterConfigFolderInfo>, val newSharedFolders: List<FolderInfo>) {
|
||||
companion object {
|
||||
val dummy = ClusterConfigInfo(folderInfo = emptyList(), newSharedFolders = emptyList())
|
||||
}
|
||||
|
||||
val folderInfoById = folderInfo.associateBy { it.folderId }
|
||||
val sharedFolderIds: Set<String> by lazy {
|
||||
folderInfo.filter { it.isShared }.map { it.folderId }.toSet()
|
||||
}
|
||||
}
|
||||
|
||||
data class ClusterConfigFolderInfo(
|
||||
val folderId: String,
|
||||
val label: String = folderId,
|
||||
val isAnnounced: Boolean = false,
|
||||
val isShared: Boolean = false
|
||||
) {
|
||||
init {
|
||||
assert(folderId.isNotEmpty())
|
||||
}
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* 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.connectionactor
|
||||
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
import net.syncthing.java.bep.BlockExchangeProtos
|
||||
|
||||
sealed class ConnectionAction
|
||||
object CloseConnectionAction: ConnectionAction()
|
||||
class SendRequestConnectionAction(
|
||||
val request: BlockExchangeProtos.Request,
|
||||
val completableDeferred: CompletableDeferred<BlockExchangeProtos.Response>
|
||||
): ConnectionAction()
|
||||
class ConfirmIsConnectedAction(val completableDeferred: CompletableDeferred<ClusterConfigInfo>): ConnectionAction()
|
||||
class SendIndexUpdateAction(
|
||||
val message: BlockExchangeProtos.IndexUpdate,
|
||||
val completableDeferred: CompletableDeferred<Unit?>
|
||||
): ConnectionAction()
|
||||
+204
@@ -0,0 +1,204 @@
|
||||
/*
|
||||
* 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.connectionactor
|
||||
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.channels.*
|
||||
import net.syncthing.java.bep.BlockExchangeProtos
|
||||
import net.syncthing.java.bep.IndexHandler
|
||||
import net.syncthing.java.core.beans.DeviceAddress
|
||||
import net.syncthing.java.core.configuration.Configuration
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.io.IOException
|
||||
|
||||
object ConnectionActorGenerator {
|
||||
private val closed = Channel<ConnectionAction>().apply { cancel() }
|
||||
private val logger = LoggerFactory.getLogger(ConnectionActorGenerator::class.java)
|
||||
|
||||
private fun deviceAddressesGenerator(deviceAddress: ReceiveChannel<DeviceAddress>) = GlobalScope.produce<List<DeviceAddress>> (capacity = Channel.CONFLATED) {
|
||||
val addresses = mutableMapOf<String, DeviceAddress>()
|
||||
|
||||
deviceAddress.consumeEach { address ->
|
||||
val isNew = addresses[address.address] == null
|
||||
|
||||
addresses[address.address] = address
|
||||
|
||||
if (isNew) {
|
||||
send(
|
||||
addresses.values.sortedBy { it.score }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun <T> waitForFirstValue(source: ReceiveChannel<T>, time: Long) = GlobalScope.produce<T> {
|
||||
source.consume {
|
||||
val firstValue = source.receive()
|
||||
var lastValue = firstValue
|
||||
|
||||
try {
|
||||
withTimeout(time) {
|
||||
while (true) {
|
||||
lastValue = source.receive()
|
||||
}
|
||||
}
|
||||
|
||||
throw IllegalStateException()
|
||||
} catch (ex: TimeoutCancellationException) {
|
||||
// this is expected here
|
||||
}
|
||||
|
||||
send(lastValue)
|
||||
|
||||
// other values without delay
|
||||
for (value in source) {
|
||||
send(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun generateConnectionActors(
|
||||
deviceAddress: ReceiveChannel<DeviceAddress>,
|
||||
configuration: Configuration,
|
||||
indexHandler: IndexHandler,
|
||||
requestHandler: (BlockExchangeProtos.Request) -> Deferred<BlockExchangeProtos.Response>
|
||||
) = generateConnectionActorsFromDeviceAddressList(
|
||||
deviceAddressSource = waitForFirstValue(
|
||||
source = deviceAddressesGenerator(deviceAddress),
|
||||
time = 1000
|
||||
),
|
||||
configuration = configuration,
|
||||
indexHandler = indexHandler,
|
||||
requestHandler = requestHandler
|
||||
)
|
||||
|
||||
fun generateConnectionActorsFromDeviceAddressList(
|
||||
deviceAddressSource: ReceiveChannel<List<DeviceAddress>>,
|
||||
configuration: Configuration,
|
||||
indexHandler: IndexHandler,
|
||||
requestHandler: (BlockExchangeProtos.Request) -> Deferred<BlockExchangeProtos.Response>
|
||||
) = GlobalScope.produce<Pair<SendChannel<ConnectionAction>, ClusterConfigInfo>> {
|
||||
var currentActor: SendChannel<ConnectionAction> = closed
|
||||
var currentDeviceAddress: DeviceAddress? = null
|
||||
|
||||
suspend fun closeCurrent() {
|
||||
if (currentActor != closed) {
|
||||
currentActor.close()
|
||||
currentActor = closed
|
||||
send(currentActor to ClusterConfigInfo.dummy)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun tryConnectingToAddressHandleBaseErrors(deviceAddress: DeviceAddress) = try {
|
||||
val newActor = ConnectionActor.createInstance(deviceAddress, configuration, indexHandler, requestHandler)
|
||||
val clusterConfig = ConnectionActorUtil.waitUntilConnected(newActor)
|
||||
|
||||
newActor to clusterConfig
|
||||
} catch (ex: Exception) {
|
||||
logger.warn("failed to connect to $deviceAddress", ex)
|
||||
|
||||
when (ex) {
|
||||
is IOException -> {/* expected -> ignore */}
|
||||
is InterruptedException -> {/* expected -> ignore */}
|
||||
else -> throw ex
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
logger.debug("connected to $deviceAddress")
|
||||
|
||||
dispatchConnection(connection.first, connection.second, deviceAddress)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
fun isConnected() = !currentActor.isClosedForSend
|
||||
|
||||
invokeOnClose {
|
||||
currentActor.close()
|
||||
}
|
||||
|
||||
val reconnectTicker = ticker(delayMillis = 30 * 1000, initialDelayMillis = 0)
|
||||
|
||||
deviceAddressSource.consume {
|
||||
var lastDeviceAddressList: List<DeviceAddress> = emptyList()
|
||||
|
||||
while (true) {
|
||||
if (isConnected()) {
|
||||
lastDeviceAddressList = deviceAddressSource.poll() ?: lastDeviceAddressList
|
||||
|
||||
if (lastDeviceAddressList.isNotEmpty()) {
|
||||
if (reconnectTicker.poll() != null) {
|
||||
if (currentDeviceAddress != lastDeviceAddressList.first()) {
|
||||
val oldDeviceAddress = currentDeviceAddress!!
|
||||
|
||||
if (!tryConnectingToAddress(lastDeviceAddressList.first())) {
|
||||
tryConnectingToAddress(oldDeviceAddress)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
closeCurrent()
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
// try all addresses
|
||||
for (address in lastDeviceAddressList) {
|
||||
if (tryConnectingToAddress(address)) {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// reset countdown before trying other connection if it would be time now
|
||||
// this does not reset if it has not counted down the whole time yet
|
||||
reconnectTicker.poll()
|
||||
|
||||
// wait for new device address list but not more than 15 seconds before the next iteration
|
||||
lastDeviceAddressList = withTimeoutOrNull(15 * 1000) {
|
||||
deviceAddressSource.receive()
|
||||
} ?: lastDeviceAddressList
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* 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.connectionactor
|
||||
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
import kotlinx.coroutines.channels.SendChannel
|
||||
import net.syncthing.java.bep.BlockExchangeProtos
|
||||
|
||||
object ConnectionActorUtil {
|
||||
suspend fun waitUntilConnected(actor: SendChannel<ConnectionAction>): ClusterConfigInfo {
|
||||
val deferred = CompletableDeferred<ClusterConfigInfo>()
|
||||
|
||||
actor.send(ConfirmIsConnectedAction(deferred))
|
||||
actor.invokeOnClose { deferred.cancel() }
|
||||
|
||||
return deferred.await()
|
||||
}
|
||||
|
||||
suspend fun sendRequest(request: BlockExchangeProtos.Request, actor: SendChannel<ConnectionAction>): BlockExchangeProtos.Response {
|
||||
val deferred = CompletableDeferred<BlockExchangeProtos.Response>()
|
||||
|
||||
actor.send(SendRequestConnectionAction(request, deferred))
|
||||
|
||||
return deferred.await()
|
||||
}
|
||||
|
||||
suspend fun sendIndexUpdate(update: BlockExchangeProtos.IndexUpdate, actor: SendChannel<ConnectionAction>) {
|
||||
val deferred = CompletableDeferred<Unit?>()
|
||||
|
||||
actor.send(SendIndexUpdateAction(update, deferred))
|
||||
|
||||
deferred.await()
|
||||
}
|
||||
}
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
/*
|
||||
* 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.connectionactor
|
||||
|
||||
import kotlinx.coroutines.*
|
||||
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 java.io.IOException
|
||||
|
||||
class ConnectionActorWrapper (
|
||||
private val source: ReceiveChannel<Pair<SendChannel<ConnectionAction>, ClusterConfigInfo>>,
|
||||
val deviceId: DeviceId,
|
||||
val connectivityChangeListener: () -> Unit
|
||||
) {
|
||||
private val job = Job()
|
||||
|
||||
private var currentConnectionActor: SendChannel<ConnectionAction>? = null
|
||||
private var clusterConfigInfo: ClusterConfigInfo? = null
|
||||
|
||||
var isConnected = false
|
||||
get() = currentConnectionActor?.isClosedForSend == false
|
||||
|
||||
init {
|
||||
GlobalScope.launch (job) {
|
||||
source.consumeEach { (connectionActor, clusterConfig) ->
|
||||
currentConnectionActor = connectionActor
|
||||
clusterConfigInfo = clusterConfig
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun sendRequest(request: BlockExchangeProtos.Request) = ConnectionActorUtil.sendRequest(
|
||||
request,
|
||||
currentConnectionActor ?: throw IOException("not connected")
|
||||
)
|
||||
|
||||
suspend fun sendIndexUpdate(update: BlockExchangeProtos.IndexUpdate) = ConnectionActorUtil.sendIndexUpdate(
|
||||
update,
|
||||
currentConnectionActor ?: throw IOException("not connected")
|
||||
)
|
||||
|
||||
fun hasFolder(folderId: String) = clusterConfigInfo?.sharedFolderIds?.contains(folderId) ?: false
|
||||
|
||||
fun getClusterConfig() = clusterConfigInfo ?: throw IOException("not connected")
|
||||
|
||||
fun shutdown() {
|
||||
job.cancel()
|
||||
}
|
||||
|
||||
// this triggers a disconnection
|
||||
// the ConnectionActorGenerator will reconnect soon
|
||||
fun reconnect() {
|
||||
currentConnectionActor?.close()
|
||||
}
|
||||
}
|
||||
+5
-9
@@ -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
|
||||
@@ -11,13 +12,8 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package net.syncthing.java.bep
|
||||
|
||||
internal data class ClusterConfigFolderInfo(val folderId: String, var label: String = folderId,
|
||||
var isAnnounced: Boolean = false, var isShared: Boolean = false) {
|
||||
|
||||
init {
|
||||
assert(folderId.isNotEmpty())
|
||||
}
|
||||
package net.syncthing.java.bep.connectionactor
|
||||
|
||||
object ConnectionConstants {
|
||||
const val MAGIC = 0x2EA7D90B
|
||||
}
|
||||
+95
@@ -0,0 +1,95 @@
|
||||
/*
|
||||
* 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.connectionactor
|
||||
|
||||
import net.syncthing.java.bep.BlockExchangeProtos
|
||||
import net.syncthing.java.core.beans.DeviceId
|
||||
import net.syncthing.java.core.beans.DeviceInfo
|
||||
import net.syncthing.java.core.configuration.Configuration
|
||||
import net.syncthing.java.core.utils.NetworkUtils
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.io.DataInputStream
|
||||
import java.io.DataOutputStream
|
||||
import java.nio.ByteBuffer
|
||||
|
||||
object HelloMessageHandler {
|
||||
private val logger = LoggerFactory.getLogger(HelloMessageHandler::class.java)
|
||||
|
||||
fun sendHelloMessage(configuration: Configuration, outputStream: DataOutputStream) {
|
||||
sendHelloMessage(
|
||||
BlockExchangeProtos.Hello.newBuilder()
|
||||
.setClientName(configuration.clientName)
|
||||
.setClientVersion(configuration.clientVersion)
|
||||
.setDeviceName(configuration.localDeviceName)
|
||||
.build(),
|
||||
outputStream
|
||||
)
|
||||
}
|
||||
|
||||
private fun sendHelloMessage(message: BlockExchangeProtos.Hello, outputStream: DataOutputStream) {
|
||||
sendHelloMessage(message.toByteArray(), outputStream)
|
||||
}
|
||||
|
||||
private fun sendHelloMessage(payload: ByteArray, outputStream: DataOutputStream) {
|
||||
logger.debug("Sending hello message")
|
||||
|
||||
outputStream.apply {
|
||||
write(
|
||||
ByteBuffer.allocate(6).apply {
|
||||
putInt(ConnectionConstants.MAGIC)
|
||||
putShort(payload.size.toShort())
|
||||
}.array()
|
||||
)
|
||||
write(payload)
|
||||
flush()
|
||||
}
|
||||
}
|
||||
|
||||
fun receiveHelloMessage(
|
||||
inputStream: DataInputStream
|
||||
): BlockExchangeProtos.Hello {
|
||||
val magic = inputStream.readInt()
|
||||
NetworkUtils.assertProtocol(magic == ConnectionConstants.MAGIC) {"magic mismatch, got $magic"}
|
||||
|
||||
val length = inputStream.readShort().toInt()
|
||||
NetworkUtils.assertProtocol(length > 0) {"invalid length, must be > 0, got $length"}
|
||||
|
||||
return BlockExchangeProtos.Hello.parseFrom(
|
||||
ByteArray(length).apply {
|
||||
inputStream.readFully(this)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
fun processHelloMessage(
|
||||
hello: BlockExchangeProtos.Hello,
|
||||
configuration: Configuration,
|
||||
deviceId: DeviceId
|
||||
) {
|
||||
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.persistLater()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,225 @@
|
||||
/*
|
||||
* 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.connectionactor
|
||||
|
||||
import com.google.protobuf.MessageLite
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.channels.SendChannel
|
||||
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.core.beans.DeviceAddress
|
||||
import net.syncthing.java.core.configuration.Configuration
|
||||
import net.syncthing.java.core.security.KeystoreHandler
|
||||
import java.io.DataInputStream
|
||||
import java.io.DataOutputStream
|
||||
import java.io.IOException
|
||||
import java.util.*
|
||||
|
||||
object ConnectionActor {
|
||||
fun createInstance(
|
||||
address: DeviceAddress,
|
||||
configuration: Configuration,
|
||||
indexHandler: IndexHandler,
|
||||
requestHandler: (BlockExchangeProtos.Request) -> Deferred<BlockExchangeProtos.Response>
|
||||
): SendChannel<ConnectionAction> {
|
||||
val channel = Channel<ConnectionAction>(Channel.RENDEZVOUS)
|
||||
|
||||
GlobalScope.async (Dispatchers.IO) {
|
||||
OpenConnection.openSocketConnection(address, configuration).use { socket ->
|
||||
val inputStream = DataInputStream(socket.inputStream)
|
||||
val outputStream = DataOutputStream(socket.outputStream)
|
||||
|
||||
val helloMessage = coroutineScope {
|
||||
async { HelloMessageHandler.sendHelloMessage(configuration, outputStream) }
|
||||
async { HelloMessageHandler.receiveHelloMessage(inputStream) }.await()
|
||||
}
|
||||
|
||||
// the hello message exchange should happen before the certificate validation
|
||||
KeystoreHandler.assertSocketCertificateValid(socket, address.deviceId)
|
||||
|
||||
// now (after the validation) use the content of the hello message
|
||||
HelloMessageHandler.processHelloMessage(helloMessage, configuration, address.deviceId)
|
||||
|
||||
// helpers for messages
|
||||
val sendPostAuthMessageLock = Mutex()
|
||||
val receivePostAuthMessageLock = Mutex()
|
||||
|
||||
suspend fun sendPostAuthMessage(message: MessageLite) = sendPostAuthMessageLock.withLock {
|
||||
PostAuthenticationMessageHandler.sendMessage(outputStream, message, markActivityOnSocket = {})
|
||||
}
|
||||
|
||||
suspend fun receivePostAuthMessage() = receivePostAuthMessageLock.withLock {
|
||||
PostAuthenticationMessageHandler.receiveMessage(inputStream, markActivityOnSocket = {})
|
||||
}
|
||||
|
||||
// cluster config exchange
|
||||
val clusterConfig = coroutineScope {
|
||||
launch { sendPostAuthMessage(ClusterConfigHandler.buildClusterConfig(configuration, indexHandler, address.deviceId)) }
|
||||
async { receivePostAuthMessage() }.await()
|
||||
}.second
|
||||
|
||||
if (!(clusterConfig is BlockExchangeProtos.ClusterConfig)) {
|
||||
throw IOException("first message was not a cluster config message")
|
||||
}
|
||||
|
||||
val clusterConfigInfo = ClusterConfigHandler.handleReceivedClusterConfig(
|
||||
clusterConfig = clusterConfig,
|
||||
configuration = configuration,
|
||||
otherDeviceId = address.deviceId,
|
||||
indexHandler = indexHandler
|
||||
)
|
||||
|
||||
fun hasFolder(folder: String) = clusterConfigInfo.sharedFolderIds.contains(folder)
|
||||
|
||||
val messageListeners = Collections.synchronizedMap(mutableMapOf<Int, CompletableDeferred<BlockExchangeProtos.Response>>())
|
||||
|
||||
try {
|
||||
launch {
|
||||
while (isActive) {
|
||||
val message = receivePostAuthMessage().second
|
||||
|
||||
when (message) {
|
||||
is BlockExchangeProtos.Response -> {
|
||||
val listener = messageListeners.remove(message.id)
|
||||
listener
|
||||
?: throw IOException("got response ${message.id} but there is no response listener")
|
||||
listener.complete(message)
|
||||
}
|
||||
is BlockExchangeProtos.Index -> {
|
||||
indexHandler.handleIndexMessageReceivedEvent(
|
||||
folderId = message.folder,
|
||||
filesList = message.filesList,
|
||||
clusterConfigInfo = clusterConfigInfo,
|
||||
peerDeviceId = address.deviceId
|
||||
)
|
||||
}
|
||||
is BlockExchangeProtos.IndexUpdate -> {
|
||||
indexHandler.handleIndexMessageReceivedEvent(
|
||||
folderId = message.folder,
|
||||
filesList = message.filesList,
|
||||
clusterConfigInfo = clusterConfigInfo,
|
||||
peerDeviceId = address.deviceId
|
||||
)
|
||||
}
|
||||
is BlockExchangeProtos.Request -> {
|
||||
launch {
|
||||
val response = requestHandler(message).await()
|
||||
|
||||
try {
|
||||
sendPostAuthMessage(response)
|
||||
} catch (ex: IOException) {
|
||||
// the connection was closed in the time between - ignore it
|
||||
}
|
||||
}
|
||||
}
|
||||
is BlockExchangeProtos.Ping -> { /* nothing to do */
|
||||
}
|
||||
is BlockExchangeProtos.ClusterConfig -> throw IOException("received cluster config twice")
|
||||
is BlockExchangeProtos.Close -> socket.close()
|
||||
else -> throw IOException("unsupported message type ${message.javaClass}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// send index messages - TODO: Why?
|
||||
for (folder in configuration.folders) {
|
||||
if (hasFolder(folder.folderId)) {
|
||||
sendPostAuthMessage(
|
||||
BlockExchangeProtos.Index.newBuilder()
|
||||
.setFolder(folder.folderId)
|
||||
.build()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
launch {
|
||||
// send ping all 90 seconds
|
||||
// TODO: only send when there were no messages for 90 seconds
|
||||
|
||||
while (isActive) {
|
||||
delay(90 * 1000)
|
||||
|
||||
launch { sendPostAuthMessage(BlockExchangeProtos.Ping.getDefaultInstance()) }
|
||||
}
|
||||
}
|
||||
|
||||
var nextRequestId = 0
|
||||
|
||||
channel.consumeEach { action ->
|
||||
when (action) {
|
||||
CloseConnectionAction -> throw InterruptedException()
|
||||
is SendRequestConnectionAction -> {
|
||||
val requestId = nextRequestId++
|
||||
|
||||
messageListeners[requestId] = action.completableDeferred
|
||||
|
||||
// async to allow handling the next action faster
|
||||
async {
|
||||
try {
|
||||
sendPostAuthMessage(
|
||||
action.request.toBuilder()
|
||||
.setId(requestId)
|
||||
.build()
|
||||
)
|
||||
} catch (ex: Exception) {
|
||||
action.completableDeferred.cancel(ex)
|
||||
}
|
||||
}
|
||||
}
|
||||
is ConfirmIsConnectedAction -> {
|
||||
action.completableDeferred.complete(clusterConfigInfo)
|
||||
|
||||
// otherwise, Kotlin would warn that the return
|
||||
// type does not match to the other branches
|
||||
null
|
||||
}
|
||||
is SendIndexUpdateAction -> {
|
||||
async {
|
||||
try {
|
||||
sendPostAuthMessage(action.message)
|
||||
} catch (ex: Exception) {
|
||||
action.completableDeferred.cancel(ex)
|
||||
}
|
||||
}
|
||||
}
|
||||
}.let { /* prevents compiling if one action is not handled */ }
|
||||
}
|
||||
} finally {
|
||||
// send close message
|
||||
withContext(NonCancellable) {
|
||||
if (socket.isConnected) {
|
||||
sendPostAuthMessage(BlockExchangeProtos.Close.getDefaultInstance())
|
||||
}
|
||||
}
|
||||
|
||||
// cancel all pending listeners
|
||||
messageListeners.values.forEach { it.cancel() }
|
||||
}
|
||||
}
|
||||
}.invokeOnCompletion { ex ->
|
||||
if (ex != null) {
|
||||
channel.cancel(ex)
|
||||
} else {
|
||||
channel.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
return channel
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* 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.connectionactor
|
||||
|
||||
import com.google.protobuf.MessageLite
|
||||
import net.syncthing.java.bep.BlockExchangeProtos
|
||||
|
||||
object MessageTypes {
|
||||
val messageTypes = listOf(
|
||||
MessageTypeInfo(BlockExchangeProtos.MessageType.CLOSE, BlockExchangeProtos.Close::class.java) { BlockExchangeProtos.Close.parseFrom(it) },
|
||||
MessageTypeInfo(BlockExchangeProtos.MessageType.CLUSTER_CONFIG, BlockExchangeProtos.ClusterConfig::class.java) { BlockExchangeProtos.ClusterConfig.parseFrom(it) },
|
||||
MessageTypeInfo(BlockExchangeProtos.MessageType.DOWNLOAD_PROGRESS, BlockExchangeProtos.DownloadProgress::class.java) { BlockExchangeProtos.DownloadProgress.parseFrom(it) },
|
||||
MessageTypeInfo(BlockExchangeProtos.MessageType.INDEX, BlockExchangeProtos.Index::class.java) { BlockExchangeProtos.Index.parseFrom(it) },
|
||||
MessageTypeInfo(BlockExchangeProtos.MessageType.INDEX_UPDATE, BlockExchangeProtos.IndexUpdate::class.java) { BlockExchangeProtos.IndexUpdate.parseFrom(it) },
|
||||
MessageTypeInfo(BlockExchangeProtos.MessageType.PING, BlockExchangeProtos.Ping::class.java) { BlockExchangeProtos.Ping.parseFrom(it) },
|
||||
MessageTypeInfo(BlockExchangeProtos.MessageType.REQUEST, BlockExchangeProtos.Request::class.java) { BlockExchangeProtos.Request.parseFrom(it) },
|
||||
MessageTypeInfo(BlockExchangeProtos.MessageType.RESPONSE, BlockExchangeProtos.Response::class.java) { BlockExchangeProtos.Response.parseFrom(it) }
|
||||
)
|
||||
|
||||
val messageTypesByProtoMessageType = messageTypes.map { it.protoMessageType to it }.toMap()
|
||||
val messageTypesByJavaClass = messageTypes.map { it.javaClass to it }.toMap()
|
||||
|
||||
fun getIdForMessage(message: MessageLite) = when (message) {
|
||||
is BlockExchangeProtos.Request -> Integer.toString(message.id)
|
||||
is BlockExchangeProtos.Response -> Integer.toString(message.id)
|
||||
else -> Integer.toString(Math.abs(message.hashCode()))
|
||||
}
|
||||
}
|
||||
|
||||
data class MessageTypeInfo(
|
||||
val protoMessageType: BlockExchangeProtos.MessageType,
|
||||
val javaClass: Class<out MessageLite>,
|
||||
val parseFrom: (data: ByteArray) -> MessageLite
|
||||
)
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* 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.connectionactor
|
||||
|
||||
import net.syncthing.java.client.protocol.rp.RelayClient
|
||||
import net.syncthing.java.core.beans.DeviceAddress
|
||||
import net.syncthing.java.core.configuration.Configuration
|
||||
import net.syncthing.java.core.security.KeystoreHandler
|
||||
import org.slf4j.LoggerFactory
|
||||
import javax.net.ssl.SSLSocket
|
||||
|
||||
object OpenConnection {
|
||||
private val logger = LoggerFactory.getLogger(OpenConnection::class.java)
|
||||
|
||||
fun openSocketConnection(
|
||||
address: DeviceAddress,
|
||||
configuration: Configuration
|
||||
): SSLSocket {
|
||||
val keystoreHandler = KeystoreHandler.Loader().loadKeystore(configuration)
|
||||
|
||||
return when (address.type) {
|
||||
DeviceAddress.AddressType.TCP -> {
|
||||
logger.debug("opening tcp ssl connection")
|
||||
keystoreHandler.createSocket(address.getSocketAddress())
|
||||
}
|
||||
DeviceAddress.AddressType.RELAY -> {
|
||||
logger.debug("opening relay connection")
|
||||
keystoreHandler.wrapSocket(RelayClient(configuration).openRelayConnection(address))
|
||||
}
|
||||
else -> throw UnsupportedOperationException("unsupported address type ${address.type}")
|
||||
}
|
||||
}
|
||||
}
|
||||
+140
@@ -0,0 +1,140 @@
|
||||
/*
|
||||
* 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.connectionactor
|
||||
|
||||
import com.google.protobuf.MessageLite
|
||||
import net.jpountz.lz4.LZ4Factory
|
||||
import net.syncthing.java.bep.BlockExchangeProtos
|
||||
import net.syncthing.java.core.utils.NetworkUtils
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.io.DataInputStream
|
||||
import java.io.DataOutputStream
|
||||
import java.io.IOException
|
||||
import java.lang.reflect.InvocationTargetException
|
||||
import java.nio.ByteBuffer
|
||||
|
||||
object PostAuthenticationMessageHandler {
|
||||
private val logger = LoggerFactory.getLogger(PostAuthenticationMessageHandler::class.java)
|
||||
|
||||
fun sendMessage(
|
||||
outputStream: DataOutputStream,
|
||||
message: MessageLite,
|
||||
markActivityOnSocket: () -> Unit
|
||||
) {
|
||||
val messageTypeInfo = MessageTypes.messageTypesByJavaClass[message.javaClass]!!
|
||||
val header = BlockExchangeProtos.Header.newBuilder()
|
||||
.setCompression(BlockExchangeProtos.MessageCompression.NONE)
|
||||
.setType(messageTypeInfo.protoMessageType)
|
||||
.build()
|
||||
val headerData = header.toByteArray()
|
||||
val messageData = message.toByteArray() //TODO support compression
|
||||
|
||||
logger.debug("sending message type = {} {}", header.type, MessageTypes.getIdForMessage(message))
|
||||
markActivityOnSocket()
|
||||
|
||||
outputStream.apply {
|
||||
writeShort(headerData.size)
|
||||
write(headerData)
|
||||
writeInt(messageData.size)
|
||||
write(messageData)
|
||||
flush()
|
||||
}
|
||||
|
||||
markActivityOnSocket()
|
||||
}
|
||||
|
||||
fun receiveMessage(
|
||||
inputStream: DataInputStream,
|
||||
markActivityOnSocket: () -> Unit
|
||||
): Pair<BlockExchangeProtos.MessageType, MessageLite> {
|
||||
val header = BlockExchangeProtos.Header.parseFrom(readHeader(
|
||||
inputStream = inputStream,
|
||||
retryReadingLength = true,
|
||||
markActivityOnSocket = markActivityOnSocket
|
||||
))
|
||||
|
||||
var messageBuffer = readMessage(
|
||||
inputStream = inputStream,
|
||||
retryReadingLength = true,
|
||||
markActivityOnSocket = markActivityOnSocket
|
||||
)
|
||||
|
||||
if (header.compression == BlockExchangeProtos.MessageCompression.LZ4) {
|
||||
val uncompressedLength = ByteBuffer.wrap(messageBuffer).int
|
||||
messageBuffer = LZ4Factory.fastestInstance().fastDecompressor().decompress(messageBuffer, 4, uncompressedLength)
|
||||
}
|
||||
|
||||
val messageTypeInfo = MessageTypes.messageTypesByProtoMessageType[header.type]
|
||||
NetworkUtils.assertProtocol(messageTypeInfo != null) {"unsupported message type = ${header.type}"}
|
||||
|
||||
try {
|
||||
return header.type to messageTypeInfo!!.parseFrom(messageBuffer)
|
||||
} catch (e: Exception) {
|
||||
when (e) {
|
||||
is IllegalAccessException, is IllegalArgumentException, is InvocationTargetException, is NoSuchMethodException, is SecurityException ->
|
||||
throw IOException(e)
|
||||
else -> throw e
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun readHeader(
|
||||
inputStream: DataInputStream,
|
||||
markActivityOnSocket: () -> Unit,
|
||||
retryReadingLength: Boolean
|
||||
): ByteArray {
|
||||
var headerLength = inputStream.readShort().toInt()
|
||||
|
||||
// TODO: what is this good for?
|
||||
if (retryReadingLength) {
|
||||
while (headerLength == 0) {
|
||||
logger.warn("got headerLength == 0, skipping short")
|
||||
headerLength = inputStream.readShort().toInt()
|
||||
}
|
||||
}
|
||||
|
||||
markActivityOnSocket()
|
||||
|
||||
NetworkUtils.assertProtocol(headerLength > 0) {"invalid length, must be > 0, got $headerLength"}
|
||||
|
||||
return ByteArray(headerLength).apply {
|
||||
inputStream.readFully(this)
|
||||
}
|
||||
}
|
||||
|
||||
private fun readMessage(
|
||||
inputStream: DataInputStream,
|
||||
markActivityOnSocket: () -> Unit,
|
||||
retryReadingLength: Boolean
|
||||
): ByteArray {
|
||||
var messageLength = inputStream.readInt()
|
||||
|
||||
// TODO: what is this good for?
|
||||
if (retryReadingLength) {
|
||||
while (messageLength == 0) {
|
||||
logger.warn("received readInt() == 0, expecting 'bep message header length' (int >0), ignoring (keepalive?)")
|
||||
messageLength = inputStream.readInt()
|
||||
}
|
||||
}
|
||||
|
||||
NetworkUtils.assertProtocol(messageLength >= 0) {"invalid length, must be >= 0, got $messageLength"}
|
||||
|
||||
val messageBuffer = ByteArray(messageLength)
|
||||
inputStream.readFully(messageBuffer)
|
||||
markActivityOnSocket()
|
||||
|
||||
return messageBuffer
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ dependencies {
|
||||
compile project(':syncthing-repository-default')
|
||||
compile "commons-cli:commons-cli:1.4"
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.0.0'
|
||||
}
|
||||
|
||||
run {
|
||||
|
||||
@@ -13,12 +13,13 @@
|
||||
*/
|
||||
package net.syncthing.java.client.cli
|
||||
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import net.syncthing.java.client.SyncthingClient
|
||||
import net.syncthing.java.core.beans.DeviceId
|
||||
import net.syncthing.java.core.beans.DeviceInfo
|
||||
import net.syncthing.java.core.beans.FileInfo
|
||||
import net.syncthing.java.core.configuration.Configuration
|
||||
import net.syncthing.java.repository.repo.SqlRepository
|
||||
import net.syncthing.java.client.SyncthingClient
|
||||
import org.apache.commons.cli.*
|
||||
import org.apache.commons.io.FileUtils
|
||||
import org.slf4j.LoggerFactory
|
||||
@@ -91,28 +92,23 @@ class Main(private val commandLine: CommandLine) {
|
||||
System.out.println("file path = $folderAndPath")
|
||||
val folder = folderAndPath.split(":".toRegex()).dropLastWhile({ it.isEmpty() }).toTypedArray()[0]
|
||||
val path = folderAndPath.split(":".toRegex()).dropLastWhile({ it.isEmpty() }).toTypedArray()[1]
|
||||
val latch = CountDownLatch(1)
|
||||
val fileInfo = FileInfo(folder = folder, path = path, type = FileInfo.FileType.FILE)
|
||||
syncthingClient.getBlockPuller(folder, { blockPuller ->
|
||||
try {
|
||||
val inputStream = blockPuller.pullFileSync(fileInfo)
|
||||
val fileName = syncthingClient.indexHandler.getFileInfoByPath(folder, path)!!.fileName
|
||||
val file =
|
||||
if (commandLine.hasOption("o")) {
|
||||
val param = File(commandLine.getOptionValue("o"))
|
||||
if (param.isDirectory) File(param, fileName) else param
|
||||
} else {
|
||||
File(fileName)
|
||||
}
|
||||
FileUtils.copyInputStreamToFile(inputStream, file)
|
||||
System.out.println("saved file to = $file.absolutePath")
|
||||
} catch (e: InterruptedException) {
|
||||
logger.warn("", e)
|
||||
} catch (e: IOException) {
|
||||
logger.warn("", e)
|
||||
try {
|
||||
val inputStream = syncthingClient.pullFileSync(fileInfo)
|
||||
val fileName = syncthingClient.indexHandler.getFileInfoByPath(folder, path)!!.fileName
|
||||
val file = if (commandLine.hasOption("o")) {
|
||||
val param = File(commandLine.getOptionValue("o"))
|
||||
if (param.isDirectory) File(param, fileName) else param
|
||||
} else {
|
||||
File(fileName)
|
||||
}
|
||||
}, { logger.warn("Failed to pull file") })
|
||||
latch.await()
|
||||
FileUtils.copyInputStreamToFile(inputStream, file)
|
||||
System.out.println("saved file to = $file.absolutePath")
|
||||
} catch (e: InterruptedException) {
|
||||
logger.warn("", e)
|
||||
} catch (e: IOException) {
|
||||
logger.warn("", e)
|
||||
}
|
||||
}
|
||||
"P" -> {
|
||||
var path = option.value
|
||||
@@ -122,20 +118,20 @@ class Main(private val commandLine: CommandLine) {
|
||||
val folder = path.split(":".toRegex()).dropLastWhile({ it.isEmpty() }).toTypedArray()[0]
|
||||
path = path.split(":".toRegex()).dropLastWhile({ it.isEmpty() }).toTypedArray()[1]
|
||||
val latch = CountDownLatch(1)
|
||||
syncthingClient.getBlockPusher(folder, { blockPusher ->
|
||||
val observer = blockPusher.pushFile(FileInputStream(file), folder, path)
|
||||
while (!observer.isCompleted()) {
|
||||
try {
|
||||
observer.waitForProgressUpdate()
|
||||
} catch (e: InterruptedException) {
|
||||
logger.warn("", e)
|
||||
}
|
||||
val blockPusher = syncthingClient.getBlockPusher(folder)
|
||||
|
||||
System.out.println("upload progress ${observer.progressPercentage()}%")
|
||||
val observer = runBlocking {
|
||||
blockPusher.pushFile(FileInputStream(file), folder, path)
|
||||
}
|
||||
while (!observer.isCompleted()) {
|
||||
try {
|
||||
observer.waitForProgressUpdate()
|
||||
} catch (e: InterruptedException) {
|
||||
logger.warn("", e)
|
||||
}
|
||||
latch.countDown()
|
||||
}, { logger.warn("Failed to upload file") })
|
||||
latch.await()
|
||||
|
||||
System.out.println("upload progress ${observer.progressPercentage()}%")
|
||||
}
|
||||
System.out.println("uploaded file to network")
|
||||
}
|
||||
"D" -> {
|
||||
@@ -143,17 +139,16 @@ class Main(private val commandLine: CommandLine) {
|
||||
val folder = path.split(":".toRegex()).dropLastWhile({ it.isEmpty() }).toTypedArray()[0]
|
||||
path = path.split(":".toRegex()).dropLastWhile({ it.isEmpty() }).toTypedArray()[1]
|
||||
System.out.println("delete path = $path")
|
||||
val latch = CountDownLatch(1)
|
||||
syncthingClient.getBlockPusher(folder, { blockPusher ->
|
||||
try {
|
||||
blockPusher.pushDelete(folder, path).waitForComplete()
|
||||
} catch (e: InterruptedException) {
|
||||
logger.warn("", e)
|
||||
}
|
||||
try {
|
||||
val blockPusher = syncthingClient.getBlockPusher(folder)
|
||||
|
||||
latch.countDown()
|
||||
}, { System.out.println("Failed to delete path") })
|
||||
latch.await()
|
||||
runBlocking {
|
||||
blockPusher.pushDelete(folder, path)
|
||||
}
|
||||
} catch (e: InterruptedException) {
|
||||
logger.warn("", e)
|
||||
System.out.println("Failed to delete path")
|
||||
}
|
||||
System.out.println("deleted path")
|
||||
}
|
||||
"M" -> {
|
||||
@@ -161,17 +156,16 @@ class Main(private val commandLine: CommandLine) {
|
||||
val folder = path.split(":".toRegex()).dropLastWhile({ it.isEmpty() }).toTypedArray()[0]
|
||||
path = path.split(":".toRegex()).dropLastWhile({ it.isEmpty() }).toTypedArray()[1]
|
||||
System.out.println("dir path = $path")
|
||||
val latch = CountDownLatch(1)
|
||||
syncthingClient.getBlockPusher(folder, { blockPusher ->
|
||||
try {
|
||||
blockPusher.pushDir(folder, path).waitForComplete()
|
||||
} catch (e: InterruptedException) {
|
||||
logger.warn("", e)
|
||||
}
|
||||
try {
|
||||
val blockPusher = syncthingClient.getBlockPusher(folder)
|
||||
|
||||
latch.countDown()
|
||||
}, { System.out.println("Failed to push directory") })
|
||||
latch.await()
|
||||
runBlocking {
|
||||
blockPusher.pushDir(folder, path)
|
||||
}
|
||||
} catch (e: InterruptedException) {
|
||||
System.out.println("Failed to push directory")
|
||||
logger.warn("", e)
|
||||
}
|
||||
System.out.println("uploaded dir to network")
|
||||
}
|
||||
"L" -> {
|
||||
|
||||
@@ -6,4 +6,5 @@ dependencies {
|
||||
compile project(':syncthing-bep')
|
||||
compile project(':syncthing-discovery')
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.0.0'
|
||||
}
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* 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.client
|
||||
|
||||
import net.syncthing.java.bep.connectionactor.ConnectionActorWrapper
|
||||
import net.syncthing.java.core.beans.DeviceId
|
||||
|
||||
class Connections (val generate: (DeviceId) -> ConnectionActorWrapper) {
|
||||
private val map = mutableMapOf<DeviceId, ConnectionActorWrapper>()
|
||||
|
||||
fun getByDeviceId(deviceId: DeviceId): ConnectionActorWrapper {
|
||||
return synchronized(map) {
|
||||
val oldEntry = map[deviceId]
|
||||
|
||||
if (oldEntry != null) {
|
||||
return oldEntry
|
||||
} else {
|
||||
val newEntry = generate(deviceId)
|
||||
|
||||
map[deviceId] = newEntry
|
||||
|
||||
return newEntry
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun shutdown() {
|
||||
synchronized(map) {
|
||||
map.values.forEach { it.shutdown() }
|
||||
}
|
||||
}
|
||||
|
||||
fun reconnectAllConnections() {
|
||||
synchronized(map) {
|
||||
map.values.forEach { it.reconnect() }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,56 +13,63 @@
|
||||
*/
|
||||
package net.syncthing.java.client
|
||||
|
||||
import net.syncthing.java.bep.BlockPuller
|
||||
import net.syncthing.java.bep.BlockPusher
|
||||
import net.syncthing.java.bep.ConnectionHandler
|
||||
import net.syncthing.java.bep.IndexHandler
|
||||
import net.syncthing.java.core.beans.DeviceAddress
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import net.syncthing.java.bep.*
|
||||
import net.syncthing.java.bep.connectionactor.ConnectionActorGenerator
|
||||
import net.syncthing.java.bep.connectionactor.ConnectionActorWrapper
|
||||
import net.syncthing.java.core.beans.DeviceId
|
||||
import net.syncthing.java.core.beans.DeviceInfo
|
||||
import net.syncthing.java.core.beans.FileInfo
|
||||
import net.syncthing.java.core.configuration.Configuration
|
||||
import net.syncthing.java.core.interfaces.IndexRepository
|
||||
import net.syncthing.java.core.interfaces.TempRepository
|
||||
import net.syncthing.java.core.security.KeystoreHandler
|
||||
import net.syncthing.java.core.utils.awaitTerminationSafe
|
||||
import net.syncthing.java.discovery.DiscoveryHandler
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.io.Closeable
|
||||
import java.io.IOException
|
||||
import java.util.Collections
|
||||
import java.util.TreeSet
|
||||
import java.util.concurrent.Executors
|
||||
import java.util.concurrent.TimeUnit
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import kotlin.collections.ArrayList
|
||||
import java.io.InputStream
|
||||
import java.util.*
|
||||
|
||||
class SyncthingClient(
|
||||
private val configuration: Configuration,
|
||||
private val repository: IndexRepository,
|
||||
private val tempRepository: TempRepository
|
||||
) : Closeable {
|
||||
|
||||
private val logger = LoggerFactory.getLogger(javaClass)
|
||||
val discoveryHandler: DiscoveryHandler
|
||||
val indexHandler: IndexHandler
|
||||
private val connections = Collections.synchronizedSet(createConnectionsSet())
|
||||
private val connectByDeviceIdLocks = Collections.synchronizedMap(HashMap<DeviceId, Object>())
|
||||
val indexHandler = IndexHandler(configuration, repository, tempRepository)
|
||||
val discoveryHandler = DiscoveryHandler(configuration)
|
||||
private val onConnectionChangedListeners = Collections.synchronizedList(mutableListOf<(DeviceId) -> Unit>())
|
||||
private var connectDevicesScheduler = Executors.newSingleThreadScheduledExecutor()
|
||||
|
||||
private fun createConnectionsSet() = TreeSet<ConnectionHandler>(compareBy { it.address.score })
|
||||
|
||||
init {
|
||||
indexHandler = IndexHandler(configuration, repository, tempRepository)
|
||||
discoveryHandler = DiscoveryHandler(configuration)
|
||||
connectDevicesScheduler.scheduleAtFixedRate(this::updateIndexFromPeers, 0, 15, TimeUnit.SECONDS)
|
||||
}
|
||||
private val requestHandlerRegistry = RequestHandlerRegistry()
|
||||
private val connections = Connections(
|
||||
generate = { deviceId ->
|
||||
ConnectionActorWrapper(
|
||||
source = ConnectionActorGenerator.generateConnectionActors(
|
||||
deviceAddress = discoveryHandler.devicesAddressesManager.getDeviceAddressManager(deviceId).streamCurrentDeviceAddresses(),
|
||||
requestHandler = { request ->
|
||||
GlobalScope.async {
|
||||
requestHandlerRegistry.handleRequest(
|
||||
source = deviceId,
|
||||
request = request
|
||||
)
|
||||
}
|
||||
},
|
||||
indexHandler = indexHandler,
|
||||
configuration = configuration
|
||||
),
|
||||
deviceId = deviceId,
|
||||
connectivityChangeListener = {
|
||||
synchronized(onConnectionChangedListeners) {
|
||||
onConnectionChangedListeners.forEach { it(deviceId) }
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
fun clearCacheAndIndex() {
|
||||
indexHandler.clearIndex()
|
||||
configuration.folders = emptySet()
|
||||
configuration.persistLater()
|
||||
updateIndexFromPeers()
|
||||
connections.reconnectAllConnections()
|
||||
}
|
||||
|
||||
fun addOnConnectionChangedListener(listener: (DeviceId) -> Unit) {
|
||||
@@ -74,158 +81,61 @@ class SyncthingClient(
|
||||
onConnectionChangedListeners.remove(listener)
|
||||
}
|
||||
|
||||
@Throws(IOException::class, KeystoreHandler.CryptoException::class)
|
||||
private fun openConnection(deviceAddress: DeviceAddress): ConnectionHandler {
|
||||
logger.debug("Connecting to ${deviceAddress.deviceId}, active connections: ${connections.map { it.deviceId().deviceId }}")
|
||||
val connectionHandler = ConnectionHandler(
|
||||
configuration, deviceAddress, indexHandler, tempRepository, { connectionHandler, _ ->
|
||||
connectionHandler.close()
|
||||
openConnection(deviceAddress)
|
||||
},
|
||||
{connection ->
|
||||
if (!connection.isConnected) {
|
||||
connections.remove(connection)
|
||||
}
|
||||
onConnectionChangedListeners.forEach { it(connection.deviceId()) }
|
||||
})
|
||||
private fun getConnections() = configuration.peerIds.map { connections.getByDeviceId(it) }
|
||||
|
||||
try {
|
||||
connectionHandler.connect()
|
||||
} catch (ex: Exception) {
|
||||
connectionHandler.closeBg()
|
||||
|
||||
throw ex
|
||||
}
|
||||
|
||||
connections.add(connectionHandler)
|
||||
|
||||
return connectionHandler
|
||||
init {
|
||||
discoveryHandler.newDeviceAddressSupplier() // starts the discovery
|
||||
getConnections()
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes discovered addresses from [[DiscoveryHandler]] and connects to devices.
|
||||
*
|
||||
* We need to make sure that we are only connecting once to each device.
|
||||
*/
|
||||
private fun getPeerConnections(listener: (connection: ConnectionHandler) -> Unit, completeListener: () -> Unit) {
|
||||
// create an copy to prevent dispatching an action two times
|
||||
val connectionsWhichWereDispatched = createConnectionsSet()
|
||||
|
||||
synchronized (connections) {
|
||||
connectionsWhichWereDispatched.addAll(connections)
|
||||
}
|
||||
|
||||
connectionsWhichWereDispatched.forEach { listener(it) }
|
||||
|
||||
discoveryHandler.newDeviceAddressSupplier()
|
||||
.takeWhile { it != null }
|
||||
.filterNotNull()
|
||||
.groupBy { it.deviceId() }
|
||||
.filterNot { it.value.isEmpty() }
|
||||
.forEach { (deviceId, addresses) ->
|
||||
// create an lock per device id to prevent multiple connections to one device
|
||||
|
||||
synchronized (connectByDeviceIdLocks) {
|
||||
if (connectByDeviceIdLocks[deviceId] == null) {
|
||||
connectByDeviceIdLocks[deviceId] = Object()
|
||||
}
|
||||
}
|
||||
|
||||
synchronized (connectByDeviceIdLocks[deviceId]!!) {
|
||||
val existingConnection = connections.find { it.deviceId() == deviceId && it.isConnected }
|
||||
|
||||
if (existingConnection != null) {
|
||||
connectionsWhichWereDispatched.add(existingConnection)
|
||||
listener(existingConnection)
|
||||
|
||||
return@synchronized
|
||||
}
|
||||
|
||||
// try to use all addresses
|
||||
for (address in addresses.distinctBy { it.address }) {
|
||||
try {
|
||||
val newConnection = openConnection(address)
|
||||
|
||||
connectionsWhichWereDispatched.add(newConnection)
|
||||
listener(newConnection)
|
||||
|
||||
break // it worked, no need to try more
|
||||
} catch (e: IOException) {
|
||||
logger.warn("error connecting to device = $address", e)
|
||||
} catch (e: KeystoreHandler.CryptoException) {
|
||||
logger.warn("error connecting to device = $address", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// use all connections which were added in the time between and were not added by this function call
|
||||
val newConnectionsBackup = createConnectionsSet()
|
||||
|
||||
synchronized (connections) {
|
||||
newConnectionsBackup.addAll(connections)
|
||||
}
|
||||
|
||||
connectionsWhichWereDispatched.forEach { newConnectionsBackup.remove(it) }
|
||||
|
||||
newConnectionsBackup.forEach { listener(it) }
|
||||
|
||||
completeListener()
|
||||
fun connectToNewlyAddedDevices() {
|
||||
getConnections()
|
||||
}
|
||||
|
||||
private fun updateIndexFromPeers() {
|
||||
getPeerConnections({ connection ->
|
||||
try {
|
||||
indexHandler.waitForRemoteIndexAcquired(connection)
|
||||
} catch (ex: InterruptedException) {
|
||||
logger.warn("exception while waiting for index", ex)
|
||||
}
|
||||
}, {})
|
||||
fun disconnectFromRemovedDevices() {
|
||||
// TODO: implement this
|
||||
}
|
||||
|
||||
private fun getConnectionForFolder(folder: String, listener: (connection: ConnectionHandler) -> Unit,
|
||||
errorListener: () -> Unit) {
|
||||
val isConnected = AtomicBoolean(false)
|
||||
getPeerConnections({ connection ->
|
||||
if (connection.hasFolder(folder) && !isConnected.get()) {
|
||||
listener(connection)
|
||||
isConnected.set(true)
|
||||
}
|
||||
}, {
|
||||
if (!isConnected.get()) {
|
||||
errorListener()
|
||||
}
|
||||
})
|
||||
fun getActiveConnectionsForFolder(folderId: String) = configuration.peerIds
|
||||
.map { connections.getByDeviceId(it) }
|
||||
.filter { it.isConnected && it.hasFolder(folderId) }
|
||||
|
||||
suspend fun pullFile(
|
||||
fileInfo: FileInfo,
|
||||
progressListener: (status: BlockPullerStatus) -> Unit = { }
|
||||
): InputStream = BlockPuller.pullFile(
|
||||
fileInfo = fileInfo,
|
||||
progressListener = progressListener,
|
||||
connections = getConnections(),
|
||||
indexHandler = indexHandler,
|
||||
tempRepository = tempRepository
|
||||
)
|
||||
|
||||
fun pullFileSync(fileInfo: FileInfo) = runBlocking { pullFile(fileInfo) }
|
||||
|
||||
fun getBlockPusher(folderId: String): BlockPusher {
|
||||
val connection = getActiveConnectionsForFolder(folderId).first()
|
||||
|
||||
return BlockPusher(
|
||||
localDeviceId = connection.deviceId,
|
||||
connectionHandler = connection,
|
||||
indexHandler = indexHandler,
|
||||
requestHandlerRegistry = requestHandlerRegistry
|
||||
)
|
||||
}
|
||||
|
||||
fun getBlockPuller(folderId: String, listener: (BlockPuller) -> Unit, errorListener: () -> Unit) {
|
||||
getConnectionForFolder(folderId, { connection ->
|
||||
listener(connection.getBlockPuller())
|
||||
}, errorListener)
|
||||
}
|
||||
|
||||
fun getBlockPusher(folderId: String, listener: (BlockPusher) -> Unit, errorListener: () -> Unit) {
|
||||
getConnectionForFolder(folderId, { connection ->
|
||||
listener(connection.getBlockPusher())
|
||||
}, errorListener)
|
||||
}
|
||||
|
||||
fun getPeerStatus(): List<DeviceInfo> {
|
||||
return configuration.peers.map { device ->
|
||||
val isConnected = connections.find { it.deviceId() == device.deviceId }?.isConnected ?: false
|
||||
device.copy(isConnected = isConnected)
|
||||
}
|
||||
fun getPeerStatus() = configuration.peers.map { device ->
|
||||
device.copy(
|
||||
isConnected = connections.getByDeviceId(device.deviceId).isConnected
|
||||
)
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
connectDevicesScheduler.awaitTerminationSafe()
|
||||
discoveryHandler.close()
|
||||
// Create copy of list, because it will be modified by handleConnectionClosedEvent(), causing ConcurrentModificationException.
|
||||
ArrayList(connections).forEach{it.close()}
|
||||
indexHandler.close()
|
||||
repository.close()
|
||||
tempRepository.close()
|
||||
connections.shutdown()
|
||||
assert(onConnectionChangedListeners.isEmpty())
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ dependencies {
|
||||
compile "org.slf4j:slf4j-api:1.7.25"
|
||||
compile "ch.qos.logback:logback-classic:1.2.3"
|
||||
compile "com.google.code.gson:gson:2.8.2"
|
||||
compile "org.apache.httpcomponents:httpclient:4.5.4"
|
||||
compile "org.bouncycastle:bcmail-jdk15on:1.59"
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/*
|
||||
/*
|
||||
* Copyright (C) 2016 Davide Imbriaco
|
||||
*
|
||||
* This Java file is subject to the terms of the Mozilla Public
|
||||
@@ -13,47 +13,43 @@
|
||||
*/
|
||||
package net.syncthing.java.core.beans
|
||||
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.net.InetAddress
|
||||
import java.net.InetSocketAddress
|
||||
import java.net.UnknownHostException
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
*
|
||||
* TODO: this class cant use [[DeviceId]] because [[GlobalDiscoveryHandler.pickAnnounceServers]] uses that field for discovery server URLs.
|
||||
*/
|
||||
class DeviceAddress private constructor(val deviceId: String, private val instanceId: Long?, val address: String, producer: AddressProducer?, score: Int?, lastModified: Date?) {
|
||||
// TODO: this should use a data class, but the custom equals prevents it
|
||||
class DeviceAddress private constructor(val deviceId: DeviceId, private val instanceId: Long?, val address: String, producer: AddressProducer?, score: Int?, lastModified: Date?) {
|
||||
private val producer = producer ?: AddressProducer.UNKNOWN
|
||||
val score = score ?: Integer.MAX_VALUE
|
||||
private val lastModified = lastModified ?: Date()
|
||||
|
||||
fun deviceId() = DeviceId(deviceId)
|
||||
|
||||
@Throws(UnknownHostException::class)
|
||||
private fun getInetAddress(): InetAddress = InetAddress.getByName(address.replaceFirst("^[^:]+://".toRegex(), "").replaceFirst("(:[0-9]+)?(/.*)?$".toRegex(), ""))
|
||||
|
||||
private fun getPort(): Int = if (address.matches("^[a-z]+://[^:]+:([0-9]+).*".toRegex())) {
|
||||
private val port: Int by lazy {
|
||||
if (address.matches("^[a-z]+://[^:]+:([0-9]+).*".toRegex())) {
|
||||
Integer.parseInt(address.replaceFirst("^[a-z]+://[^:]+:([0-9]+).*".toRegex(), "$1"))
|
||||
} else {
|
||||
DEFAULT_PORT_BY_PROTOCOL[getType()]!!
|
||||
DEFAULT_PORT_BY_PROTOCOL[type]!!
|
||||
}
|
||||
}
|
||||
|
||||
fun getType(): AddressType = when {
|
||||
address.isEmpty() -> AddressType.NULL
|
||||
address.startsWith("tcp://") -> AddressType.TCP
|
||||
address.startsWith("relay://") -> AddressType.RELAY
|
||||
address.startsWith("relay-http://") -> AddressType.HTTP_RELAY
|
||||
address.startsWith("relay-https://") -> AddressType.HTTPS_RELAY
|
||||
else -> AddressType.OTHER
|
||||
val type: AddressType by lazy {
|
||||
when {
|
||||
address.isEmpty() -> AddressType.NULL
|
||||
address.startsWith("tcp://") -> AddressType.TCP
|
||||
address.startsWith("relay://") -> AddressType.RELAY
|
||||
else -> AddressType.OTHER
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(UnknownHostException::class)
|
||||
fun getSocketAddress(): InetSocketAddress = InetSocketAddress(getInetAddress(), getPort())
|
||||
fun getSocketAddress(): InetSocketAddress = InetSocketAddress(getInetAddress(), port)
|
||||
|
||||
fun isWorking(): Boolean = score < Integer.MAX_VALUE
|
||||
|
||||
constructor(deviceId: String, address: String) : this(deviceId, null, address, null, null, null)
|
||||
constructor(deviceId: String, address: String) : this(DeviceId(deviceId), null, address, null, null, null)
|
||||
|
||||
fun containsUriParamValue(key: String): Boolean {
|
||||
return !getUriParam(key).isNullOrEmpty()
|
||||
@@ -77,7 +73,7 @@ class DeviceAddress private constructor(val deviceId: String, private val instan
|
||||
}
|
||||
|
||||
enum class AddressType {
|
||||
TCP, RELAY, OTHER, NULL, HTTP_RELAY, HTTPS_RELAY
|
||||
TCP, RELAY, OTHER, NULL
|
||||
}
|
||||
|
||||
enum class AddressProducer {
|
||||
@@ -95,18 +91,18 @@ class DeviceAddress private constructor(val deviceId: String, private val instan
|
||||
return hash
|
||||
}
|
||||
|
||||
override fun equals(obj: Any?): Boolean {
|
||||
if (this === obj) {
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) {
|
||||
return true
|
||||
}
|
||||
if (obj == null) {
|
||||
if (other == null) {
|
||||
return false
|
||||
}
|
||||
if (javaClass != obj.javaClass) {
|
||||
if (javaClass != other.javaClass) {
|
||||
return false
|
||||
}
|
||||
val other = obj as DeviceAddress?
|
||||
if (this.deviceId != other!!.deviceId) {
|
||||
other as DeviceAddress
|
||||
if (this.deviceId != other.deviceId) {
|
||||
return false
|
||||
}
|
||||
return this.address == other.address
|
||||
@@ -118,7 +114,7 @@ class DeviceAddress private constructor(val deviceId: String, private val instan
|
||||
|
||||
class Builder {
|
||||
|
||||
private var deviceId: String? = null
|
||||
private var deviceId: DeviceId? = null
|
||||
private var instanceId: Long? = null
|
||||
private var address: String? = null
|
||||
private var producer: AddressProducer? = null
|
||||
@@ -127,7 +123,7 @@ class DeviceAddress private constructor(val deviceId: String, private val instan
|
||||
|
||||
constructor()
|
||||
|
||||
internal constructor(deviceId: String, instanceId: Long?, address: String, producer: AddressProducer, score: Int?, lastModified: Date) {
|
||||
internal constructor(deviceId: DeviceId, instanceId: Long?, address: String, producer: AddressProducer, score: Int?, lastModified: Date) {
|
||||
this.deviceId = deviceId
|
||||
this.instanceId = instanceId
|
||||
this.address = address
|
||||
@@ -145,11 +141,11 @@ class DeviceAddress private constructor(val deviceId: String, private val instan
|
||||
return this
|
||||
}
|
||||
|
||||
fun getDeviceId(): String? {
|
||||
fun getDeviceId(): DeviceId? {
|
||||
return deviceId
|
||||
}
|
||||
|
||||
fun setDeviceId(deviceId: String): Builder {
|
||||
fun setDeviceId(deviceId: DeviceId): Builder {
|
||||
this.deviceId = deviceId
|
||||
return this
|
||||
}
|
||||
@@ -198,8 +194,7 @@ class DeviceAddress private constructor(val deviceId: String, private val instan
|
||||
companion object {
|
||||
private val DEFAULT_PORT_BY_PROTOCOL = mapOf(
|
||||
AddressType.TCP to 22000,
|
||||
AddressType.RELAY to 22067,
|
||||
AddressType.HTTP_RELAY to 80,
|
||||
AddressType.HTTPS_RELAY to 443)
|
||||
AddressType.RELAY to 22067
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import com.google.gson.stream.JsonReader
|
||||
import com.google.gson.stream.JsonWriter
|
||||
import net.syncthing.java.core.utils.NetworkUtils
|
||||
import org.apache.commons.codec.binary.Base32
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.io.IOException
|
||||
|
||||
data class DeviceId @Throws(IOException::class) constructor(val deviceId: String) {
|
||||
|
||||
@@ -14,8 +14,7 @@
|
||||
package net.syncthing.java.core.beans
|
||||
|
||||
import org.apache.commons.io.FileUtils
|
||||
|
||||
import java.util.Date
|
||||
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) {
|
||||
|
||||
|
||||
@@ -12,7 +12,8 @@ data class Config(
|
||||
val folders: Set<FolderInfo>,
|
||||
val localDeviceName: String,
|
||||
val localDeviceId: String,
|
||||
val discoveryServers: Set<String>,
|
||||
val customDiscoveryServers: Set<DiscoveryServer>,
|
||||
val useDefaultDiscoveryServers: Boolean,
|
||||
val keystoreAlgorithm: String,
|
||||
val keystoreData: String
|
||||
) {
|
||||
@@ -21,7 +22,8 @@ data class Config(
|
||||
private const val FOLDERS = "folders"
|
||||
private const val LOCAL_DEVICE_NAME = "localDeviceName"
|
||||
private const val LOCAL_DEVICE_ID = "localDeviceId"
|
||||
private const val DISCOVERY_SERVERS = "discoveryServers"
|
||||
private const val USE_DEFAULT_DISCOVERY_SERVERS = "useDefaultDiscoveryServers"
|
||||
private const val CUSTOM_DISCOVERY_SERVERS = "customDiscoveryServers"
|
||||
private const val KEYSTORE_ALGORITHM = "keystoreAlgorithm"
|
||||
private const val KEYSTORE_DATA = "keystoreData"
|
||||
|
||||
@@ -30,7 +32,8 @@ data class Config(
|
||||
var folders: Set<FolderInfo>? = null
|
||||
var localDeviceName: String? = null
|
||||
var localDeviceId: String? = null
|
||||
var discoveryServers: Set<String>? = null
|
||||
var customDiscoveryServers = emptySet<DiscoveryServer>() // this field was added later, so it needs an default value
|
||||
var useDefaultDiscoveryServers = true // this field was added later, so it needs an default value
|
||||
var keystoreAlgorithm: String? = null
|
||||
var keystoreData: String? = null
|
||||
|
||||
@@ -61,17 +64,16 @@ data class Config(
|
||||
}
|
||||
LOCAL_DEVICE_NAME -> localDeviceName = reader.nextString()
|
||||
LOCAL_DEVICE_ID -> localDeviceId = reader.nextString()
|
||||
DISCOVERY_SERVERS -> {
|
||||
val newDiscoveryServers = HashSet<String>()
|
||||
|
||||
reader.beginArray()
|
||||
while (reader.hasNext()) {
|
||||
newDiscoveryServers.add(reader.nextString())
|
||||
CUSTOM_DISCOVERY_SERVERS -> {
|
||||
customDiscoveryServers = mutableSetOf<DiscoveryServer>().apply {
|
||||
reader.beginArray()
|
||||
while (reader.hasNext()) {
|
||||
add(DiscoveryServer.parse(reader))
|
||||
}
|
||||
reader.endArray()
|
||||
}
|
||||
reader.endArray()
|
||||
|
||||
discoveryServers = Collections.unmodifiableSet(newDiscoveryServers)
|
||||
}
|
||||
USE_DEFAULT_DISCOVERY_SERVERS -> useDefaultDiscoveryServers = reader.nextBoolean()
|
||||
KEYSTORE_ALGORITHM -> keystoreAlgorithm = reader.nextString()
|
||||
KEYSTORE_DATA -> keystoreData = reader.nextString()
|
||||
else -> reader.skipValue()
|
||||
@@ -84,7 +86,8 @@ data class Config(
|
||||
folders = folders!!,
|
||||
localDeviceName = localDeviceName!!,
|
||||
localDeviceId = localDeviceId!!,
|
||||
discoveryServers = discoveryServers!!,
|
||||
customDiscoveryServers = customDiscoveryServers,
|
||||
useDefaultDiscoveryServers = useDefaultDiscoveryServers,
|
||||
keystoreAlgorithm = keystoreAlgorithm!!,
|
||||
keystoreData = keystoreData!!
|
||||
)
|
||||
@@ -105,10 +108,12 @@ data class Config(
|
||||
writer.name(LOCAL_DEVICE_NAME).value(localDeviceName)
|
||||
writer.name(LOCAL_DEVICE_ID).value(localDeviceId)
|
||||
|
||||
writer.name(DISCOVERY_SERVERS).beginArray()
|
||||
discoveryServers.forEach { writer.value(it) }
|
||||
writer.name(CUSTOM_DISCOVERY_SERVERS).beginArray()
|
||||
customDiscoveryServers.forEach { it.serialize(writer) }
|
||||
writer.endArray()
|
||||
|
||||
writer.name(USE_DEFAULT_DISCOVERY_SERVERS).value(useDefaultDiscoveryServers)
|
||||
|
||||
writer.name(KEYSTORE_ALGORITHM).value(keystoreAlgorithm)
|
||||
writer.name(KEYSTORE_DATA).value(keystoreData)
|
||||
|
||||
@@ -117,5 +122,6 @@ data class Config(
|
||||
|
||||
// Exclude keystoreData from toString()
|
||||
override fun toString() = "Config(peers=$peers, folders=$folders, localDeviceName=$localDeviceName, " +
|
||||
"localDeviceId=$localDeviceId, discoveryServers=$discoveryServers, keystoreAlgorithm=$keystoreAlgorithm)"
|
||||
"localDeviceId=$localDeviceId, customDiscoveryServers=$customDiscoveryServers, " +
|
||||
"useDefaultDiscoveryServers=$useDefaultDiscoveryServers, keystoreAlgorithm=$keystoreAlgorithm)"
|
||||
}
|
||||
|
||||
+6
-22
@@ -38,26 +38,15 @@ class Configuration(configFolder: File = DefaultConfigFolder) {
|
||||
isSaved = false
|
||||
config = Config(peers = setOf(), folders = setOf(),
|
||||
localDeviceName = localDeviceName,
|
||||
discoveryServers = Companion.DiscoveryServers,
|
||||
localDeviceId = keystoreData.first.deviceId,
|
||||
keystoreData = Base64.toBase64String(keystoreData.second),
|
||||
keystoreAlgorithm = keystoreData.third)
|
||||
keystoreAlgorithm = keystoreData.third,
|
||||
customDiscoveryServers = emptySet(),
|
||||
useDefaultDiscoveryServers = true
|
||||
)
|
||||
persistNow()
|
||||
} else {
|
||||
config = Config.parse(JsonReader(StringReader(configFile.readText())))
|
||||
|
||||
// automatic migration if the old config was used
|
||||
if (config.discoveryServers == OldDiscoveryServers) {
|
||||
config = Config(
|
||||
peers = config.peers,
|
||||
folders = config.folders,
|
||||
localDeviceName = config.localDeviceName,
|
||||
localDeviceId = config.localDeviceId,
|
||||
discoveryServers = Companion.DiscoveryServers,
|
||||
keystoreAlgorithm = config.keystoreAlgorithm,
|
||||
keystoreData = config.keystoreData
|
||||
)
|
||||
}
|
||||
}
|
||||
logger.debug("Loaded config = $config")
|
||||
}
|
||||
@@ -66,11 +55,6 @@ class Configuration(configFolder: File = DefaultConfigFolder) {
|
||||
private val DefaultConfigFolder = File(System.getProperty("user.home"), ".config/syncthing-java/")
|
||||
private const val ConfigFileName = "config.json"
|
||||
private const val DatabaseFolderName = "database"
|
||||
private val DiscoveryServers = setOf(
|
||||
"discovery.syncthing.net", "discovery-v4.syncthing.net", "discovery-v6.syncthing.net")
|
||||
private val OldDiscoveryServers = setOf(
|
||||
"discovery-v4-1.syncthing.net", "discovery-v4-2.syncthing.net", "discovery-v4-3.syncthing.net",
|
||||
"discovery-v6-1.syncthing.net", "discovery-v6-2.syncthing.net", "discovery-v6-3.syncthing.net")
|
||||
}
|
||||
|
||||
val instanceId = Math.abs(Random().nextLong())
|
||||
@@ -78,8 +62,8 @@ class Configuration(configFolder: File = DefaultConfigFolder) {
|
||||
val localDeviceId: DeviceId
|
||||
get() = DeviceId(config.localDeviceId)
|
||||
|
||||
val discoveryServers: Set<String>
|
||||
get() = config.discoveryServers
|
||||
val discoveryServers: Set<DiscoveryServer>
|
||||
get() = config.customDiscoveryServers + (if (config.useDefaultDiscoveryServers) DiscoveryServer.defaultDiscoveryServers else emptySet())
|
||||
|
||||
val keystoreData: ByteArray
|
||||
get() = Base64.decode(config.keystoreData)
|
||||
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
package net.syncthing.java.core.configuration
|
||||
|
||||
import com.google.gson.stream.JsonReader
|
||||
import com.google.gson.stream.JsonWriter
|
||||
import net.syncthing.java.core.beans.DeviceId
|
||||
|
||||
data class DiscoveryServer(
|
||||
val hostname: String,
|
||||
val useForLookup: Boolean,
|
||||
val useForAnnounce: Boolean,
|
||||
val deviceId: DeviceId?
|
||||
) {
|
||||
companion object {
|
||||
private const val JSON_HOSTNAME = "host"
|
||||
private const val JSON_LOOKUP = "lookup"
|
||||
private const val JSON_ANNOUNCE = "announce"
|
||||
private const val JSON_DEVICE_ID = "deviceId"
|
||||
|
||||
// from https://github.com/syncthing/syncthing/blob/add12b43aa0bdf5e67d8f421c57a5ecafb3d25fa/lib/config/config.go#L50-L64
|
||||
// (if you update it, use the most recent commit)
|
||||
private val serverDeviceId = DeviceId("LYXKCHX-VI3NYZR-ALCJBHF-WMZYSPK-QG6QJA3-MPFYMSO-U56GTUK-NA2MIAW")
|
||||
|
||||
private val lookupServer = DiscoveryServer(
|
||||
hostname = "discovery.syncthing.net",
|
||||
useForLookup = true,
|
||||
useForAnnounce = false,
|
||||
deviceId = serverDeviceId
|
||||
)
|
||||
|
||||
private val announceIpV4Server = DiscoveryServer(
|
||||
hostname = "discovery-v4.syncthing.net",
|
||||
useForLookup = false,
|
||||
useForAnnounce = true,
|
||||
deviceId = serverDeviceId
|
||||
)
|
||||
|
||||
private val announceIpV6Server = DiscoveryServer(
|
||||
hostname = "discovery-v6.syncthing.net",
|
||||
useForLookup = false,
|
||||
useForAnnounce = true,
|
||||
deviceId = serverDeviceId
|
||||
)
|
||||
|
||||
val defaultDiscoveryServers = setOf(lookupServer, announceIpV4Server, announceIpV6Server)
|
||||
|
||||
fun parse(reader: JsonReader): DiscoveryServer {
|
||||
var hostname: String? = null
|
||||
var useForLookup: Boolean? = null
|
||||
var useForAnnounce: Boolean? = null
|
||||
var deviceId: DeviceId? = null
|
||||
|
||||
reader.beginObject()
|
||||
while (reader.hasNext()) {
|
||||
when (reader.nextName()) {
|
||||
JSON_HOSTNAME -> hostname = reader.nextString()
|
||||
JSON_LOOKUP -> useForLookup = reader.nextBoolean()
|
||||
JSON_ANNOUNCE -> useForAnnounce = reader.nextBoolean()
|
||||
JSON_DEVICE_ID -> deviceId = DeviceId(reader.nextString())
|
||||
else -> reader.skipValue()
|
||||
}
|
||||
}
|
||||
reader.endObject()
|
||||
|
||||
return DiscoveryServer(
|
||||
hostname = hostname!!,
|
||||
useForLookup = useForLookup!!,
|
||||
useForAnnounce = useForAnnounce!!,
|
||||
deviceId = deviceId
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun serialize(writer: JsonWriter) {
|
||||
writer.beginObject()
|
||||
|
||||
writer.name(JSON_HOSTNAME).value(hostname)
|
||||
writer.name(JSON_LOOKUP).value(useForLookup)
|
||||
writer.name(JSON_ANNOUNCE).value(useForAnnounce)
|
||||
|
||||
if (deviceId != null) {
|
||||
writer.name(JSON_DEVICE_ID).value(deviceId.deviceId)
|
||||
}
|
||||
|
||||
writer.endObject()
|
||||
}
|
||||
}
|
||||
+32
-21
@@ -44,8 +44,6 @@ import javax.security.auth.x500.X500Principal
|
||||
|
||||
class KeystoreHandler private constructor(private val keyStore: KeyStore) {
|
||||
|
||||
private val logger = LoggerFactory.getLogger(javaClass)
|
||||
|
||||
class CryptoException internal constructor(t: Throwable) : GeneralSecurityException(t)
|
||||
|
||||
private val socketFactory: SSLSocketFactory
|
||||
@@ -79,7 +77,7 @@ class KeystoreHandler private constructor(private val keyStore: KeyStore) {
|
||||
}
|
||||
|
||||
@Throws(CryptoException::class, IOException::class)
|
||||
private fun wrapSocket(socket: Socket, isServerSocket: Boolean, protocol: String): SSLSocket {
|
||||
private fun wrapSocket(socket: Socket, isServerSocket: Boolean): SSLSocket {
|
||||
try {
|
||||
logger.debug("wrapping plain socket, server mode = {}", isServerSocket)
|
||||
val sslSocket = socketFactory.createSocket(socket, null, socket.port, true) as SSLSocket
|
||||
@@ -100,7 +98,7 @@ class KeystoreHandler private constructor(private val keyStore: KeyStore) {
|
||||
}
|
||||
|
||||
@Throws(CryptoException::class, IOException::class)
|
||||
fun createSocket(relaySocketAddress: InetSocketAddress, protocol: String): SSLSocket {
|
||||
fun createSocket(relaySocketAddress: InetSocketAddress): SSLSocket {
|
||||
try {
|
||||
val socket = socketFactory.createSocket() as SSLSocket
|
||||
socket.connect(relaySocketAddress, SOCKET_TIMEOUT)
|
||||
@@ -116,24 +114,9 @@ class KeystoreHandler private constructor(private val keyStore: KeyStore) {
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(SSLPeerUnverifiedException::class, CertificateException::class)
|
||||
fun checkSocketCertificate(socket: SSLSocket, deviceId: DeviceId) {
|
||||
val session = socket.session
|
||||
val certs = session.peerCertificates.toList()
|
||||
val certificateFactory = CertificateFactory.getInstance("X.509")
|
||||
val certPath = certificateFactory.generateCertPath(certs)
|
||||
val certificate = certPath.certificates[0]
|
||||
NetworkUtils.assertProtocol(certificate is X509Certificate)
|
||||
val derData = certificate.encoded
|
||||
val deviceIdFromCertificate = derDataToDeviceId(derData)
|
||||
logger.trace("remote pem certificate =\n{}", derToPem(derData))
|
||||
NetworkUtils.assertProtocol(deviceIdFromCertificate == deviceId, {"device id mismatch! expected = $deviceId, got = $deviceIdFromCertificate"})
|
||||
logger.debug("remote ssl certificate match deviceId = {}", deviceId)
|
||||
}
|
||||
|
||||
@Throws(CryptoException::class, IOException::class)
|
||||
fun wrapSocket(relayConnection: RelayConnection, protocol: String): SSLSocket {
|
||||
return wrapSocket(relayConnection.getSocket(), relayConnection.isServerSocket(), protocol)
|
||||
fun wrapSocket(relayConnection: RelayConnection): SSLSocket {
|
||||
return wrapSocket(relayConnection.getSocket(), relayConnection.isServerSocket())
|
||||
}
|
||||
|
||||
class Loader {
|
||||
@@ -269,6 +252,34 @@ class KeystoreHandler private constructor(private val keyStore: KeyStore) {
|
||||
|
||||
const val BEP = "bep/1.0"
|
||||
const val RELAY = "bep-relay"
|
||||
|
||||
private val logger = LoggerFactory.getLogger(KeystoreHandler::class.java)
|
||||
|
||||
@Throws(SSLPeerUnverifiedException::class, CertificateException::class)
|
||||
fun assertSocketCertificateValid(socket: SSLSocket, deviceId: DeviceId) {
|
||||
val session = socket.session
|
||||
val certs = session.peerCertificates.toList()
|
||||
val certificateFactory = CertificateFactory.getInstance("X.509")
|
||||
val certPath = certificateFactory.generateCertPath(certs)
|
||||
val certificate = certPath.certificates[0]
|
||||
|
||||
assertSocketCertificateValid(certificate, deviceId)
|
||||
}
|
||||
|
||||
@Throws(SSLPeerUnverifiedException::class, CertificateException::class)
|
||||
fun assertSocketCertificateValid(certificate: Certificate, deviceId: DeviceId) {
|
||||
NetworkUtils.assertProtocol(certificate is X509Certificate)
|
||||
|
||||
val derData = certificate.encoded
|
||||
val deviceIdFromCertificate = derDataToDeviceId(derData)
|
||||
logger.trace("remote pem certificate =\n{}", derToPem(derData))
|
||||
|
||||
NetworkUtils.assertProtocol(deviceIdFromCertificate == deviceId) {
|
||||
"device id mismatch! expected = $deviceId, got = $deviceIdFromCertificate"
|
||||
}
|
||||
|
||||
logger.debug("remote ssl certificate match deviceId = {}", deviceId)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ 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)
|
||||
@@ -45,3 +46,11 @@ fun <T> ExecutorService.submitLogging(runnable: () -> T): Future<T> {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fun ExecutorService.trySubmitLogging(runnable: Runnable) {
|
||||
try {
|
||||
submitLogging(runnable)
|
||||
} catch (ex: RejectedExecutionException) {
|
||||
logger.warn("could not submit task", ex)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,36 +14,90 @@
|
||||
*/
|
||||
package net.syncthing.java.core.utils
|
||||
|
||||
import org.apache.commons.io.FilenameUtils
|
||||
|
||||
object PathUtils {
|
||||
|
||||
val ROOT_PATH = ""
|
||||
val PATH_SEPARATOR = "/"
|
||||
val PARENT_PATH = ".."
|
||||
|
||||
private fun normalizePath(path: String): String {
|
||||
return FilenameUtils.normalizeNoEndSeparator(path, true).replaceFirst(("^" + PATH_SEPARATOR).toRegex(), "")
|
||||
}
|
||||
const val ROOT_PATH = ""
|
||||
const val PATH_SEPARATOR = "/"
|
||||
const val PATH_SEPARATOR_WIN = "\\"
|
||||
const val PARENT_PATH = ".."
|
||||
const val CURRENT_PATH = "."
|
||||
|
||||
fun isRoot(path: String): Boolean {
|
||||
return path.isEmpty()
|
||||
}
|
||||
|
||||
private fun containsRelativeElements(path: String): Boolean {
|
||||
val pathSegments = path.split(PATH_SEPARATOR)
|
||||
|
||||
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 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")
|
||||
}
|
||||
}
|
||||
|
||||
private fun assertFilenameValid(filename: String) {
|
||||
if (!isFilenameValid(filename)) {
|
||||
throw IllegalArgumentException("provided filename is invalid")
|
||||
}
|
||||
}
|
||||
|
||||
fun isParent(path: String): Boolean {
|
||||
return path == PARENT_PATH
|
||||
}
|
||||
|
||||
fun getParentPath(path: String): String {
|
||||
assert(!isRoot(path), {"cannot get parent of root path"})
|
||||
return normalizePath(path + PATH_SEPARATOR + PARENT_PATH)
|
||||
assertPathValid(path)
|
||||
|
||||
val pathWithoutSuffix = path.removeSuffix(PATH_SEPARATOR)
|
||||
val previousSeparator = pathWithoutSuffix.lastIndexOf(PATH_SEPARATOR)
|
||||
|
||||
return if (previousSeparator == -1) {
|
||||
ROOT_PATH
|
||||
} else {
|
||||
pathWithoutSuffix.substring(0, previousSeparator)
|
||||
}
|
||||
}
|
||||
|
||||
fun getFileName(path: String): String {
|
||||
return FilenameUtils.getName(path)
|
||||
if (path.isEmpty()) {
|
||||
// this is required for IndexHandler.ROOT_FILE_INFO
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
assertPathValid(path)
|
||||
|
||||
val pathWithoutSuffix = path.removeSuffix(PATH_SEPARATOR)
|
||||
val previousSeparator = pathWithoutSuffix.lastIndexOf(PATH_SEPARATOR)
|
||||
|
||||
return if (previousSeparator == -1) {
|
||||
// the file is in the root directory
|
||||
pathWithoutSuffix
|
||||
} else {
|
||||
pathWithoutSuffix.substring(previousSeparator + 1)
|
||||
}
|
||||
}
|
||||
|
||||
fun buildPath(dir: String, file: String): String {
|
||||
return normalizePath(dir + PATH_SEPARATOR + file)
|
||||
assertPathValid(dir)
|
||||
assertFilenameValid(file)
|
||||
|
||||
return dir.removeSuffix(PATH_SEPARATOR) + file
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ dependencies {
|
||||
compile "commons-cli:commons-cli:1.4"
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
||||
implementation "com.google.protobuf:protobuf-lite:$protobuf_lite_version"
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.0.0'
|
||||
}
|
||||
|
||||
run {
|
||||
|
||||
+32
-34
@@ -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,66 +14,63 @@
|
||||
*/
|
||||
package net.syncthing.java.discovery
|
||||
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.selects.select
|
||||
import kotlinx.coroutines.withTimeout
|
||||
import net.syncthing.java.core.beans.DeviceAddress
|
||||
import net.syncthing.java.core.beans.DeviceId
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.util.*
|
||||
import java.io.Closeable
|
||||
|
||||
class DeviceAddressSupplier(private val discoveryHandler: DiscoveryHandler) : Iterable<DeviceAddress?> {
|
||||
class DeviceAddressSupplier(private val peerDevices: Set<DeviceId>, private val devicesAddressesManager: DevicesAddressesManager) : Iterable<DeviceAddress?>, Closeable {
|
||||
private val deviceAddressListStreams = peerDevices.map { deviceId ->
|
||||
devicesAddressesManager.getDeviceAddressManager(deviceId).streamCurrentDeviceAddresses()
|
||||
}
|
||||
|
||||
private val logger = LoggerFactory.getLogger(javaClass)
|
||||
private val deviceAddressQueue = PriorityQueue<DeviceAddress>(11, compareBy { it.score })
|
||||
private val queueLock = Object()
|
||||
|
||||
private fun getDeviceAddress(): DeviceAddress? {
|
||||
synchronized(queueLock) {
|
||||
return deviceAddressQueue.poll()
|
||||
}
|
||||
}
|
||||
|
||||
internal fun onNewDeviceAddressAcquired(address: DeviceAddress) {
|
||||
if (address.isWorking()) {
|
||||
synchronized(queueLock) {
|
||||
deviceAddressQueue.add(address)
|
||||
queueLock.notify()
|
||||
private suspend fun getDeviceAddress(): DeviceAddress? {
|
||||
return select {
|
||||
deviceAddressListStreams.forEach { stream ->
|
||||
stream.onReceive { it }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(InterruptedException::class)
|
||||
fun getDeviceAddressOrWait(): DeviceAddress? = getDeviceAddressOrWait(5000)
|
||||
|
||||
init {
|
||||
synchronized(queueLock) {
|
||||
deviceAddressQueue.addAll(discoveryHandler.getAllWorkingDeviceAddresses())// note: slight risk of duplicate address loading
|
||||
}
|
||||
suspend fun getDeviceAddressOrWait(timeout: Long) = withTimeout(timeout) {
|
||||
getDeviceAddress()
|
||||
}
|
||||
|
||||
@Throws(InterruptedException::class)
|
||||
private fun getDeviceAddressOrWait(timeout: Long): DeviceAddress? {
|
||||
synchronized(queueLock) {
|
||||
if (deviceAddressQueue.isEmpty()) {
|
||||
queueLock.wait(timeout)
|
||||
}
|
||||
return getDeviceAddress()
|
||||
}
|
||||
suspend fun getDeviceAddressOrWait() = getDeviceAddressOrWait(5000L)
|
||||
|
||||
override fun close() {
|
||||
deviceAddressListStreams.forEach { it.cancel() }
|
||||
}
|
||||
|
||||
@Deprecated(message = "iterator is blocking")
|
||||
override fun iterator(): Iterator<DeviceAddress?> {
|
||||
return object : Iterator<DeviceAddress?> {
|
||||
|
||||
private var hasNext: Boolean? = null
|
||||
private var next: DeviceAddress? = null
|
||||
|
||||
override fun hasNext(): Boolean {
|
||||
if (hasNext == null) {
|
||||
try {
|
||||
next = getDeviceAddressOrWait()
|
||||
} catch (ex: InterruptedException) {
|
||||
next = runBlocking {
|
||||
getDeviceAddressOrWait()
|
||||
}
|
||||
} catch (ex: CancellationException) {
|
||||
logger.warn("", ex)
|
||||
}
|
||||
|
||||
hasNext = next != null
|
||||
}
|
||||
|
||||
if (hasNext == false) {
|
||||
close()
|
||||
}
|
||||
|
||||
return hasNext!!
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user