Compare commits
26 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 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
|
||||
|
||||
+19
-3
@@ -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 13
|
||||
versionName "0.3.3"
|
||||
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"
|
||||
|
||||
@@ -22,14 +22,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,6 +47,16 @@ 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 {
|
||||
@@ -70,13 +83,15 @@ 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) {
|
||||
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
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
@@ -76,7 +77,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
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -47,12 +48,12 @@ object Util {
|
||||
if (!configuration.peerIds.contains(deviceId2)) {
|
||||
configuration.peers = configuration.peers + DeviceInfo(deviceId2, null)
|
||||
configuration.persistLater()
|
||||
async(UI) {
|
||||
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,6 @@
|
||||
- add option to export files
|
||||
- send correct file names to apps by which files are opened
|
||||
- adaptive icon
|
||||
- updated translations
|
||||
- validate discovery servers
|
||||
- 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>
|
||||
@@ -49,8 +49,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>
|
||||
+3
-2
@@ -1,7 +1,7 @@
|
||||
// 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'
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ dependencies {
|
||||
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,8 +15,8 @@
|
||||
package net.syncthing.java.bep
|
||||
|
||||
import com.google.protobuf.ByteString
|
||||
import kotlinx.coroutines.experimental.*
|
||||
import kotlinx.coroutines.experimental.channels.Channel
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import net.syncthing.java.bep.BlockExchangeProtos.ErrorCode
|
||||
import net.syncthing.java.bep.BlockExchangeProtos.Request
|
||||
import net.syncthing.java.bep.utils.longSumBy
|
||||
@@ -25,14 +25,16 @@ 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.io.ByteArrayInputStream
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.io.SequenceInputStream
|
||||
import java.security.MessageDigest
|
||||
import java.util.*
|
||||
import kotlin.collections.HashMap
|
||||
import kotlin.coroutines.resume
|
||||
|
||||
class BlockPuller internal constructor(private val connectionHandler: ConnectionHandler,
|
||||
private val indexHandler: IndexHandler,
|
||||
|
||||
@@ -17,7 +17,6 @@ import com.google.protobuf.ByteString
|
||||
import net.syncthing.java.bep.BlockExchangeProtos.Vector
|
||||
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
|
||||
|
||||
@@ -122,7 +122,7 @@ class ConnectionHandler(private val configuration: Configuration, val address: D
|
||||
|
||||
receiveHelloMessage()
|
||||
try {
|
||||
keystoreHandler.checkSocketCertificate(socket, address.deviceId())
|
||||
KeystoreHandler.assertSocketCertificateValid(socket, address.deviceId())
|
||||
} catch (e: CertificateException) {
|
||||
throw IOException(e)
|
||||
}
|
||||
|
||||
@@ -19,7 +19,6 @@ 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
|
||||
@@ -209,7 +208,7 @@ 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 {
|
||||
|
||||
@@ -13,12 +13,12 @@
|
||||
*/
|
||||
package net.syncthing.java.client.cli
|
||||
|
||||
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
|
||||
|
||||
@@ -35,6 +35,7 @@ import java.util.concurrent.Executors
|
||||
import java.util.concurrent.TimeUnit
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import kotlin.collections.ArrayList
|
||||
import kotlin.collections.HashMap
|
||||
|
||||
class SyncthingClient(
|
||||
private val configuration: Configuration,
|
||||
|
||||
@@ -13,7 +13,6 @@
|
||||
*/
|
||||
package net.syncthing.java.core.beans
|
||||
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.net.InetAddress
|
||||
import java.net.InetSocketAddress
|
||||
import java.net.UnknownHostException
|
||||
@@ -28,8 +27,11 @@ class DeviceAddress private constructor(val deviceId: String, private val instan
|
||||
val score = score ?: Integer.MAX_VALUE
|
||||
private val lastModified = lastModified ?: Date()
|
||||
|
||||
@Deprecated(message = "should use deviceIdObject instead")
|
||||
fun deviceId() = DeviceId(deviceId)
|
||||
|
||||
val deviceIdObject: DeviceId by lazy { DeviceId(deviceId) }
|
||||
|
||||
@Throws(UnknownHostException::class)
|
||||
private fun getInetAddress(): InetAddress = InetAddress.getByName(address.replaceFirst("^[^:]+://".toRegex(), "").replaceFirst("(:[0-9]+)?(/.*)?$".toRegex(), ""))
|
||||
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
+23
-17
@@ -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
|
||||
@@ -116,21 +114,6 @@ 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)
|
||||
@@ -269,6 +252,29 @@ 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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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!!
|
||||
}
|
||||
|
||||
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
/*
|
||||
* 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.discovery
|
||||
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.channels.ReceiveChannel
|
||||
import net.syncthing.java.core.beans.DeviceAddress
|
||||
import net.syncthing.java.core.beans.DeviceId
|
||||
|
||||
class DeviceAddressesManager (val deviceId: DeviceId) {
|
||||
private val lock = Object()
|
||||
private val deviceAddressesCache = mutableListOf<DeviceAddress>()
|
||||
private val listeners = mutableListOf<(DeviceAddress) -> Unit>()
|
||||
|
||||
fun putAddress(address: DeviceAddress) {
|
||||
if (address.deviceIdObject != deviceId) {
|
||||
throw IllegalArgumentException()
|
||||
}
|
||||
|
||||
synchronized(lock) {
|
||||
deviceAddressesCache.add(address)
|
||||
listeners.forEach { it(address) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun addListener(listener: (DeviceAddress) -> Unit) {
|
||||
synchronized(lock) {
|
||||
listeners.add(listener)
|
||||
}
|
||||
}
|
||||
|
||||
private fun removeListener(listener: (DeviceAddress) -> Unit) {
|
||||
synchronized(lock) {
|
||||
listeners.remove(listener)
|
||||
}
|
||||
}
|
||||
|
||||
// this creates a copy of the set
|
||||
fun getCurrentDeviceAddresses() = synchronized(lock) {
|
||||
deviceAddressesCache.toList()
|
||||
}
|
||||
|
||||
fun streamCurrentDeviceAddresses(): ReceiveChannel<DeviceAddress> = Channel<DeviceAddress>(capacity = Channel.UNLIMITED).apply {
|
||||
val listener: (DeviceAddress) -> Unit = {
|
||||
offer(it)
|
||||
}
|
||||
|
||||
invokeOnClose {
|
||||
removeListener(listener)
|
||||
}
|
||||
|
||||
synchronized(lock) {
|
||||
addListener(listener)
|
||||
deviceAddressesCache.forEach { listener(it) }
|
||||
}
|
||||
}
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* 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.discovery
|
||||
|
||||
import net.syncthing.java.core.beans.DeviceId
|
||||
|
||||
class DevicesAddressesManager {
|
||||
private val data = mutableMapOf<DeviceId, DeviceAddressesManager>()
|
||||
|
||||
fun getDeviceAddressManager(deviceId: DeviceId) = synchronized(data) {
|
||||
val item = data[deviceId]
|
||||
|
||||
if (item != null) {
|
||||
item
|
||||
} else {
|
||||
val newItem = DeviceAddressesManager(deviceId)
|
||||
|
||||
data[deviceId] = newItem
|
||||
|
||||
newItem
|
||||
}
|
||||
}
|
||||
}
|
||||
+44
-37
@@ -1,4 +1,4 @@
|
||||
/*
|
||||
/*
|
||||
* Copyright (C) 2016 Davide Imbriaco
|
||||
* Copyright (C) 2018 Jonas Lochmann
|
||||
*
|
||||
@@ -14,91 +14,98 @@
|
||||
*/
|
||||
package net.syncthing.java.discovery
|
||||
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import net.syncthing.java.core.beans.DeviceAddress
|
||||
import net.syncthing.java.core.beans.DeviceId
|
||||
import net.syncthing.java.core.configuration.Configuration
|
||||
import net.syncthing.java.core.utils.awaitTerminationSafe
|
||||
import net.syncthing.java.core.utils.submitLogging
|
||||
import net.syncthing.java.discovery.protocol.GlobalDiscoveryHandler
|
||||
import net.syncthing.java.discovery.protocol.LocalDiscoveryHandler
|
||||
import net.syncthing.java.discovery.utils.AddressRanker
|
||||
import org.apache.commons.lang3.tuple.Pair
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.io.Closeable
|
||||
import java.util.*
|
||||
import java.util.concurrent.Executors
|
||||
|
||||
class DiscoveryHandler(private val configuration: Configuration) : Closeable {
|
||||
|
||||
private val logger = LoggerFactory.getLogger(javaClass)
|
||||
private val globalDiscoveryHandler = GlobalDiscoveryHandler(configuration)
|
||||
private val localDiscoveryHandler = LocalDiscoveryHandler(configuration, { _, deviceAddresses ->
|
||||
private val localDiscoveryHandler = LocalDiscoveryHandler(configuration, { message ->
|
||||
logger.info("received device address list from local discovery")
|
||||
processDeviceAddressBg(deviceAddresses)
|
||||
|
||||
GlobalScope.launch {
|
||||
processDeviceAddressBg(message.addresses)
|
||||
}
|
||||
}, { deviceId ->
|
||||
onMessageFromUnknownDeviceListeners.forEach { listener -> listener(deviceId) }
|
||||
})
|
||||
private val executorService = Executors.newCachedThreadPool()
|
||||
private val deviceAddressMap = Collections.synchronizedMap(hashMapOf<Pair<DeviceId, String>, DeviceAddress>())
|
||||
private val deviceAddressSupplier = DeviceAddressSupplier(this)
|
||||
private val devicesAddressesManager = DevicesAddressesManager()
|
||||
private var isClosed = false
|
||||
private val onMessageFromUnknownDeviceListeners = Collections.synchronizedSet(HashSet<(DeviceId) -> Unit>())
|
||||
|
||||
private var shouldLoadFromGlobal = true
|
||||
private var shouldStartLocalDiscovery = true
|
||||
|
||||
fun getAllWorkingDeviceAddresses() = deviceAddressMap.values.filter { it.isWorking() }
|
||||
private fun doGlobalDiscoveryIfNotYetDone() {
|
||||
// TODO: timeout for reload
|
||||
// TODO: retry if connectivity changed
|
||||
|
||||
private fun updateAddressesBg() {
|
||||
if (shouldLoadFromGlobal) {
|
||||
shouldLoadFromGlobal = false
|
||||
GlobalScope.launch {
|
||||
processDeviceAddressBg(globalDiscoveryHandler.query(configuration.peerIds))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun initLocalDiscoveryIfNotYetDone() {
|
||||
if (shouldStartLocalDiscovery) {
|
||||
shouldStartLocalDiscovery = false
|
||||
localDiscoveryHandler.startListener()
|
||||
localDiscoveryHandler.sendAnnounceMessage()
|
||||
}
|
||||
if (shouldLoadFromGlobal) {
|
||||
shouldLoadFromGlobal = false //TODO timeout for reload
|
||||
executorService.submitLogging {
|
||||
for (deviceId in configuration.peerIds) {
|
||||
globalDiscoveryHandler.query(deviceId, this::processDeviceAddressBg)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun processDeviceAddressBg(deviceAddresses: Iterable<DeviceAddress>) {
|
||||
private suspend fun processDeviceAddressBg(deviceAddresses: Iterable<DeviceAddress>) {
|
||||
if (isClosed) {
|
||||
logger.debug("discarding device addresses, discovery handler already closed")
|
||||
} else {
|
||||
executorService.submitLogging {
|
||||
val list = deviceAddresses.toList()
|
||||
val peers = configuration.peerIds
|
||||
//do not process address already processed
|
||||
list.filter { deviceAddress ->
|
||||
!peers.contains(deviceAddress.deviceId()) || deviceAddressMap.containsKey(Pair.of(DeviceId(deviceAddress.deviceId), deviceAddress.address))
|
||||
}
|
||||
AddressRanker.pingAddresses(list)
|
||||
.forEach { putDeviceAddress(it) }
|
||||
val list = deviceAddresses.toList()
|
||||
val peers = configuration.peerIds
|
||||
//do not process address already processed
|
||||
list.filter { deviceAddress ->
|
||||
!peers.contains(deviceAddress.deviceIdObject)
|
||||
}
|
||||
|
||||
AddressRanker.pingAddresses(list)
|
||||
.forEach { putDeviceAddress(it) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun putDeviceAddress(deviceAddress: DeviceAddress) {
|
||||
deviceAddressMap[Pair.of(DeviceId(deviceAddress.deviceId), deviceAddress.address)] = deviceAddress
|
||||
deviceAddressSupplier.onNewDeviceAddressAcquired(deviceAddress)
|
||||
devicesAddressesManager.getDeviceAddressManager(
|
||||
deviceId = deviceAddress.deviceIdObject
|
||||
).putAddress(deviceAddress)
|
||||
}
|
||||
|
||||
fun newDeviceAddressSupplier(): DeviceAddressSupplier {
|
||||
updateAddressesBg()
|
||||
return deviceAddressSupplier
|
||||
if (isClosed) {
|
||||
throw IllegalStateException()
|
||||
}
|
||||
|
||||
doGlobalDiscoveryIfNotYetDone()
|
||||
initLocalDiscoveryIfNotYetDone()
|
||||
|
||||
return DeviceAddressSupplier(
|
||||
peerDevices = configuration.peerIds,
|
||||
devicesAddressesManager = devicesAddressesManager
|
||||
)
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
if (!isClosed) {
|
||||
isClosed = true
|
||||
localDiscoveryHandler.close()
|
||||
globalDiscoveryHandler.close()
|
||||
executorService.shutdown()
|
||||
executorService.awaitTerminationSafe()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,14 +16,12 @@ package net.syncthing.java.discovery
|
||||
import net.syncthing.java.core.beans.DeviceAddress
|
||||
import net.syncthing.java.core.beans.DeviceId
|
||||
import net.syncthing.java.core.configuration.Configuration
|
||||
import net.syncthing.java.core.security.KeystoreHandler
|
||||
import net.syncthing.java.discovery.protocol.GlobalDiscoveryHandler
|
||||
import net.syncthing.java.discovery.protocol.LocalDiscoveryHandler
|
||||
import org.apache.commons.cli.DefaultParser
|
||||
import org.apache.commons.cli.HelpFormatter
|
||||
import org.apache.commons.cli.Option
|
||||
import org.apache.commons.cli.Options
|
||||
import org.apache.commons.io.FileUtils
|
||||
import java.io.File
|
||||
import java.util.concurrent.CountDownLatch
|
||||
|
||||
@@ -85,10 +83,10 @@ class Main {
|
||||
private fun queryLocalDiscovery(configuration: Configuration, deviceId: DeviceId): Collection<DeviceAddress> {
|
||||
val lock = Object()
|
||||
val discoveredAddresses = mutableListOf<DeviceAddress>()
|
||||
val handler = LocalDiscoveryHandler(configuration, { discoveredDeviceId, deviceAddresses ->
|
||||
val handler = LocalDiscoveryHandler(configuration, { message ->
|
||||
synchronized(lock) {
|
||||
if (discoveredDeviceId == deviceId) {
|
||||
discoveredAddresses.addAll(deviceAddresses)
|
||||
if (message.deviceId == deviceId) {
|
||||
discoveredAddresses.addAll(message.addresses)
|
||||
lock.notify()
|
||||
}
|
||||
}
|
||||
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* 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.discovery.protocol
|
||||
|
||||
import com.google.gson.stream.JsonReader
|
||||
import com.google.gson.stream.JsonToken
|
||||
import java.util.*
|
||||
|
||||
data class AnnouncementMessage(val addresses: List<String>) {
|
||||
companion object {
|
||||
private const val ADDRESSES = "addresses"
|
||||
|
||||
fun parse(reader: JsonReader): AnnouncementMessage {
|
||||
var addresses = listOf<String>()
|
||||
|
||||
reader.beginObject()
|
||||
while (reader.hasNext()) {
|
||||
when (reader.nextName()) {
|
||||
ADDRESSES -> {
|
||||
val newAddresses = ArrayList<String>()
|
||||
|
||||
if (reader.peek() == JsonToken.NULL) {
|
||||
reader.skipValue()
|
||||
} else {
|
||||
reader.beginArray()
|
||||
while (reader.hasNext()) {
|
||||
newAddresses.add(reader.nextString())
|
||||
}
|
||||
reader.endArray()
|
||||
}
|
||||
|
||||
addresses = Collections.unmodifiableList(newAddresses)
|
||||
}
|
||||
else -> reader.skipValue()
|
||||
}
|
||||
}
|
||||
reader.endObject()
|
||||
|
||||
return AnnouncementMessage(addresses)
|
||||
}
|
||||
}
|
||||
}
|
||||
+62
-99
@@ -1,4 +1,4 @@
|
||||
/*
|
||||
/*
|
||||
* Copyright (C) 2016 Davide Imbriaco
|
||||
* Copyright (C) 2018 Jonas Lochmann
|
||||
*
|
||||
@@ -14,125 +14,88 @@
|
||||
*/
|
||||
package net.syncthing.java.discovery.protocol
|
||||
|
||||
import com.google.gson.stream.JsonReader
|
||||
import com.google.gson.stream.JsonToken
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import net.syncthing.java.core.beans.DeviceAddress
|
||||
import net.syncthing.java.core.beans.DeviceId
|
||||
import net.syncthing.java.core.configuration.Configuration
|
||||
import net.syncthing.java.discovery.utils.AddressRanker
|
||||
import org.apache.http.HttpStatus
|
||||
import org.apache.http.client.methods.HttpGet
|
||||
import org.apache.http.conn.ssl.SSLConnectionSocketFactory
|
||||
import org.apache.http.conn.ssl.SSLContextBuilder
|
||||
import org.apache.http.conn.ssl.TrustSelfSignedStrategy
|
||||
import org.apache.http.impl.client.HttpClients
|
||||
import org.apache.http.util.EntityUtils
|
||||
import net.syncthing.java.core.configuration.DiscoveryServer
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.io.Closeable
|
||||
import java.io.IOException
|
||||
import java.io.StringReader
|
||||
import java.security.KeyManagementException
|
||||
import java.security.KeyStoreException
|
||||
import java.security.NoSuchAlgorithmException
|
||||
import java.util.*
|
||||
|
||||
internal class GlobalDiscoveryHandler(private val configuration: Configuration) : Closeable {
|
||||
|
||||
internal class GlobalDiscoveryHandler(private val configuration: Configuration) {
|
||||
private val logger = LoggerFactory.getLogger(javaClass)
|
||||
|
||||
fun query(deviceId: DeviceId, callback: (List<DeviceAddress>) -> Unit) {
|
||||
val addresses = pickAnnounceServers()
|
||||
.map {
|
||||
try {
|
||||
queryAnnounceServer(it, deviceId)
|
||||
} catch (e: IOException) {
|
||||
logger.warn("Failed to query $it", e)
|
||||
listOf<DeviceAddress>()
|
||||
}
|
||||
}
|
||||
.flatten()
|
||||
callback(addresses)
|
||||
}
|
||||
|
||||
private fun pickAnnounceServers(): List<String> {
|
||||
val list = AddressRanker
|
||||
.pingAddresses(configuration.discoveryServers.map { DeviceAddress(it, "tcp://$it:443") })
|
||||
return list.map { it.deviceId }
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
private fun queryAnnounceServer(server: String, deviceId: DeviceId): List<DeviceAddress> {
|
||||
@Deprecated(message = "coroutine version should be used instead of callback")
|
||||
fun query(deviceId: DeviceId, callback: (List<DeviceAddress>) -> Unit) = GlobalScope.launch {
|
||||
try {
|
||||
logger.debug("querying server {} for device id {}", server, deviceId)
|
||||
val httpClient = HttpClients.custom()
|
||||
.setSSLSocketFactory(SSLConnectionSocketFactory(SSLContextBuilder().loadTrustMaterial(null, TrustSelfSignedStrategy()).build(), SSLConnectionSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER))
|
||||
.build()
|
||||
val httpGet = HttpGet("https://$server/v2/?device=${deviceId.deviceId}")
|
||||
return httpClient.execute<List<DeviceAddress>>(httpGet) { response ->
|
||||
when (response.statusLine.statusCode) {
|
||||
HttpStatus.SC_NOT_FOUND -> {
|
||||
logger.debug("device not found: {}", deviceId)
|
||||
return@execute emptyList()
|
||||
}
|
||||
HttpStatus.SC_OK -> {
|
||||
val announcementMessage = AnnouncementMessage.parse(
|
||||
JsonReader(
|
||||
StringReader(
|
||||
EntityUtils.toString(response.entity)
|
||||
)
|
||||
)
|
||||
)
|
||||
return@execute (announcementMessage.addresses)
|
||||
.map { DeviceAddress(deviceId.deviceId, it) }
|
||||
}
|
||||
else -> {
|
||||
throw IOException("http error ${response.statusLine}, response ${EntityUtils.toString(response.entity)}")
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
when (e) {
|
||||
is IOException, is NoSuchAlgorithmException, is KeyStoreException, is KeyManagementException ->
|
||||
throw IOException(e)
|
||||
else -> throw e
|
||||
}
|
||||
callback(query(deviceId))
|
||||
} catch (ex: Exception) {
|
||||
callback(emptyList())
|
||||
}
|
||||
}
|
||||
|
||||
override fun close() {}
|
||||
suspend fun query(deviceIds: Collection<DeviceId>): List<DeviceAddress> {
|
||||
val discoveryServers = getLookupServers()
|
||||
|
||||
private data class AnnouncementMessage(val addresses: List<String>) {
|
||||
companion object {
|
||||
private const val ADDRESSES = "addresses"
|
||||
return coroutineScope {
|
||||
deviceIds
|
||||
.distinct()
|
||||
.map { deviceId ->
|
||||
async {
|
||||
queryAnnounceServers(
|
||||
servers = discoveryServers,
|
||||
deviceId = deviceId
|
||||
)
|
||||
}
|
||||
}
|
||||
.map { it.await() }
|
||||
.flatten()
|
||||
}
|
||||
}
|
||||
|
||||
fun parse(reader: JsonReader): AnnouncementMessage {
|
||||
var addresses = listOf<String>()
|
||||
suspend fun query(deviceId: DeviceId) = queryAnnounceServers(
|
||||
servers = getLookupServers(),
|
||||
deviceId = deviceId
|
||||
)
|
||||
|
||||
reader.beginObject()
|
||||
while (reader.hasNext()) {
|
||||
when (reader.nextName()) {
|
||||
ADDRESSES -> {
|
||||
val newAddresses = ArrayList<String>()
|
||||
fun getLookupServers() = configuration.discoveryServers.filter { it.useForLookup }
|
||||
|
||||
if (reader.peek() == JsonToken.NULL) {
|
||||
reader.skipValue()
|
||||
} else {
|
||||
reader.beginArray()
|
||||
while (reader.hasNext()) {
|
||||
newAddresses.add(reader.nextString())
|
||||
}
|
||||
reader.endArray()
|
||||
suspend fun queryAnnounceServers(servers: List<DiscoveryServer>, deviceId: DeviceId) = coroutineScope {
|
||||
servers
|
||||
.map { server ->
|
||||
async {
|
||||
try {
|
||||
queryAnnounceServer(server, deviceId)
|
||||
} catch (ex: Exception) {
|
||||
logger.warn("Failed to query $server for $deviceId", ex)
|
||||
|
||||
when (ex) {
|
||||
is IOException -> { /* ignore */ }
|
||||
is DeviceNotFoundException -> { /* ignore */ }
|
||||
is TooManyRequestsException -> { /* ignore */ }
|
||||
else -> throw ex
|
||||
}
|
||||
|
||||
addresses = Collections.unmodifiableList(newAddresses)
|
||||
emptyList<DeviceAddress>()
|
||||
}
|
||||
else -> reader.skipValue()
|
||||
}
|
||||
}
|
||||
reader.endObject()
|
||||
.map { it.await() }
|
||||
.flatten()
|
||||
// .distinct() is not required because the device addresses contain the used discovery server
|
||||
}
|
||||
|
||||
return AnnouncementMessage(addresses)
|
||||
}
|
||||
}
|
||||
companion object {
|
||||
suspend fun queryAnnounceServer(server: DiscoveryServer, deviceId: DeviceId) =
|
||||
GlobalDiscoveryUtil
|
||||
.queryAnnounceServer(
|
||||
server = server.hostname,
|
||||
requestedDeviceId = deviceId,
|
||||
serverDeviceId = server.deviceId
|
||||
)
|
||||
.addresses.map { DeviceAddress(deviceId.deviceId, it) }
|
||||
}
|
||||
}
|
||||
|
||||
+103
@@ -0,0 +1,103 @@
|
||||
/*
|
||||
* 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.discovery.protocol
|
||||
|
||||
import com.google.gson.stream.JsonReader
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import net.syncthing.java.core.beans.DeviceId
|
||||
import net.syncthing.java.core.security.KeystoreHandler
|
||||
import java.io.BufferedInputStream
|
||||
import java.io.IOException
|
||||
import java.io.InputStreamReader
|
||||
import java.net.HttpURLConnection
|
||||
import java.net.URL
|
||||
import java.security.SecureRandom
|
||||
import java.security.cert.CertificateException
|
||||
import java.security.cert.X509Certificate
|
||||
import javax.net.ssl.HostnameVerifier
|
||||
import javax.net.ssl.HttpsURLConnection
|
||||
import javax.net.ssl.SSLContext
|
||||
import javax.net.ssl.X509TrustManager
|
||||
|
||||
object GlobalDiscoveryUtil {
|
||||
private fun queryAnnounceServerUrl(server: String, deviceId: DeviceId) =
|
||||
"https://$server/v2/?device=${deviceId.deviceId}"
|
||||
|
||||
suspend fun queryAnnounceServer(
|
||||
server: String,
|
||||
requestedDeviceId: DeviceId,
|
||||
serverDeviceId: DeviceId?
|
||||
): AnnouncementMessage {
|
||||
return withContext(Dispatchers.IO) {
|
||||
val url = URL(queryAnnounceServerUrl(server, requestedDeviceId))
|
||||
val connection = (url.openConnection() as HttpsURLConnection).apply {
|
||||
hostnameVerifier = HostnameVerifier { _, session ->
|
||||
try {
|
||||
if (serverDeviceId != null) {
|
||||
if (session.peerCertificates.isEmpty()) {
|
||||
throw IOException("no certificate found")
|
||||
}
|
||||
|
||||
KeystoreHandler.assertSocketCertificateValid(session.peerCertificates.first(), serverDeviceId)
|
||||
}
|
||||
|
||||
true
|
||||
} catch (ex: Exception) {
|
||||
when (ex) {
|
||||
is IOException -> false
|
||||
is CertificateException -> false
|
||||
else -> throw ex
|
||||
}
|
||||
}
|
||||
}
|
||||
sslSocketFactory = SSLContext.getInstance("SSL").apply {
|
||||
init(null, arrayOf(object: X509TrustManager {
|
||||
override fun checkClientTrusted(p0: Array<out X509Certificate>?, p1: String?) {
|
||||
// check nothing
|
||||
}
|
||||
|
||||
override fun checkServerTrusted(p0: Array<out X509Certificate>?, p1: String?) {
|
||||
// check nothing
|
||||
}
|
||||
|
||||
override fun getAcceptedIssuers(): Array<X509Certificate> = emptyArray()
|
||||
}), SecureRandom())
|
||||
}.socketFactory
|
||||
}
|
||||
|
||||
try {
|
||||
connection.connect()
|
||||
|
||||
when (connection.responseCode) {
|
||||
HttpURLConnection.HTTP_NOT_FOUND -> throw DeviceNotFoundException()
|
||||
429 -> throw TooManyRequestsException()
|
||||
HttpURLConnection.HTTP_OK -> {
|
||||
JsonReader(InputStreamReader(BufferedInputStream(connection.inputStream))).use { reader ->
|
||||
AnnouncementMessage.parse(reader)
|
||||
}
|
||||
}
|
||||
else -> throw IOException("http error ${connection.responseCode}: ${connection.responseMessage}")
|
||||
}
|
||||
} finally {
|
||||
connection.disconnect()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class DeviceNotFoundException: RuntimeException()
|
||||
class TooManyRequestsException: RuntimeException()
|
||||
// TODO: handle too many requests -> stop sending requests for some time
|
||||
+25
-134
@@ -1,4 +1,4 @@
|
||||
/*
|
||||
/*
|
||||
* Copyright (C) 2016 Davide Imbriaco
|
||||
* Copyright (C) 2018 Jonas Lochmann
|
||||
*
|
||||
@@ -14,164 +14,55 @@
|
||||
*/
|
||||
package net.syncthing.java.discovery.protocol
|
||||
|
||||
import com.google.protobuf.ByteString
|
||||
import com.google.protobuf.InvalidProtocolBufferException
|
||||
import net.syncthing.java.core.beans.DeviceAddress
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.channels.consumeEach
|
||||
import kotlinx.coroutines.launch
|
||||
import net.syncthing.java.core.beans.DeviceId
|
||||
import net.syncthing.java.core.configuration.Configuration
|
||||
import net.syncthing.java.core.events.DeviceAddressReceivedEvent
|
||||
import net.syncthing.java.core.utils.NetworkUtils
|
||||
import net.syncthing.java.core.utils.submitLogging
|
||||
import net.syncthing.java.discovery.protocol.LocalDiscoveryProtos.Announce
|
||||
import org.apache.commons.io.IOUtils
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.Closeable
|
||||
import java.io.DataOutputStream
|
||||
import java.io.IOException
|
||||
import java.net.DatagramPacket
|
||||
import java.net.DatagramSocket
|
||||
import java.net.InetAddress
|
||||
import java.net.NetworkInterface
|
||||
import java.nio.ByteBuffer
|
||||
import java.util.concurrent.Executors
|
||||
|
||||
internal class LocalDiscoveryHandler(private val configuration: Configuration,
|
||||
private val onMessageReceivedListener: (DeviceId, List<DeviceAddress>) -> Unit,
|
||||
private val onMessageReceivedListener: (LocalDiscoveryMessage) -> Unit,
|
||||
private val onMessageFromUnknownDeviceListener: (DeviceId) -> Unit = {}) : Closeable {
|
||||
|
||||
companion object {
|
||||
private const val MAGIC = 0x2EA7D90B
|
||||
private const val LISTENING_PORT = 21027
|
||||
private const val INCOMING_BUFFER_SIZE = 1024
|
||||
}
|
||||
|
||||
private val logger = LoggerFactory.getLogger(javaClass)
|
||||
private val listeningExecutor = Executors.newSingleThreadScheduledExecutor()
|
||||
private val processingExecutor = Executors.newCachedThreadPool()
|
||||
|
||||
private var datagramSocket: DatagramSocket? = null
|
||||
private val job = Job()
|
||||
|
||||
fun sendAnnounceMessage() {
|
||||
processingExecutor.submitLogging {
|
||||
try {
|
||||
val out = ByteArrayOutputStream()
|
||||
DataOutputStream(out).writeInt(MAGIC)
|
||||
Announce.newBuilder()
|
||||
.setId(ByteString.copyFrom(configuration.localDeviceId.toHashData()))
|
||||
.setInstanceId(configuration.instanceId)
|
||||
.build().writeTo(out)
|
||||
val data = out.toByteArray()
|
||||
val networkInterfaces = NetworkInterface.getNetworkInterfaces()
|
||||
while (networkInterfaces.hasMoreElements()) {
|
||||
val networkInterface = networkInterfaces.nextElement()
|
||||
for (interfaceAddress in networkInterface.interfaceAddresses) {
|
||||
val broadcastAddress = interfaceAddress.broadcast
|
||||
logger.trace("interface = {} address = {} broadcast = {}", networkInterface, interfaceAddress, broadcastAddress)
|
||||
if (broadcastAddress != null) {
|
||||
logger.debug("sending broadcast announce on {}", broadcastAddress)
|
||||
DatagramSocket().use { broadcastSocket ->
|
||||
broadcastSocket.broadcast = true
|
||||
val datagramPacket = DatagramPacket(
|
||||
data, data.size, broadcastAddress, LISTENING_PORT)
|
||||
broadcastSocket.send(datagramPacket)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
logger.warn("Failed to send local announce message", e)
|
||||
}
|
||||
GlobalScope.launch {
|
||||
LocalDiscoveryUtil.sendAnnounceMessage(
|
||||
ownDeviceId = configuration.localDeviceId,
|
||||
instanceId = configuration.instanceId
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun startListener() {
|
||||
if (datagramSocket == null || datagramSocket!!.isClosed) {
|
||||
GlobalScope.launch (job) {
|
||||
try {
|
||||
datagramSocket = DatagramSocket(LISTENING_PORT, InetAddress.getByName("0.0.0.0"))
|
||||
logger.info("Opened udp socket {}", datagramSocket!!.localSocketAddress)
|
||||
} catch (e: IOException) {
|
||||
logger.warn("Failed to open listening socket on port $LISTENING_PORT, ${e.message}")
|
||||
return
|
||||
}
|
||||
LocalDiscoveryUtil.listenForAnnounceMessages().consumeEach { message ->
|
||||
if (message.deviceId == configuration.localDeviceId) {
|
||||
// ignore announcement received from ourselves.
|
||||
} else if (!configuration.peerIds.contains(message.deviceId)) {
|
||||
logger.trace("Received local announce from ${message.deviceId} which is not a peer, ignoring")
|
||||
|
||||
}
|
||||
onMessageFromUnknownDeviceListener(message.deviceId)
|
||||
} else {
|
||||
logger.debug("received local announce from device id = {}", message.deviceId)
|
||||
|
||||
listeningExecutor.submitLogging(object : Runnable {
|
||||
override fun run() {
|
||||
try {
|
||||
val datagramPacket = DatagramPacket(ByteArray(INCOMING_BUFFER_SIZE), INCOMING_BUFFER_SIZE)
|
||||
logger.trace("waiting for message on socket addr = {}", datagramSocket!!.localSocketAddress)
|
||||
datagramSocket!!.receive(datagramPacket)
|
||||
processingExecutor.submitLogging { handleReceivedDatagram(datagramPacket) }
|
||||
listeningExecutor.submitLogging(this)
|
||||
} catch (e: IOException) {
|
||||
if (e.message == "Socket closed") {
|
||||
// Ignore exception on socket close.
|
||||
return
|
||||
onMessageReceivedListener(message)
|
||||
}
|
||||
logger.warn("Error receiving datagram", e)
|
||||
close()
|
||||
}
|
||||
|
||||
} catch (ex: IOException) {
|
||||
logger.warn("Failed to listen for announcement messages", ex)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun handleReceivedDatagram(datagramPacket: DatagramPacket) {
|
||||
try {
|
||||
val sourceAddress = datagramPacket.address.hostAddress
|
||||
val byteBuffer = ByteBuffer.wrap(
|
||||
datagramPacket.data, datagramPacket.offset, datagramPacket.length)
|
||||
val magic = byteBuffer.int
|
||||
NetworkUtils.assertProtocol(magic == MAGIC, {"magic mismatch, expected $MAGIC, got $magic"})
|
||||
val announce = Announce.parseFrom(ByteString.copyFrom(byteBuffer))
|
||||
val deviceId = DeviceId.fromHashData(announce.id.toByteArray())
|
||||
|
||||
// Ignore announcement received from ourselves.
|
||||
if (deviceId == configuration.localDeviceId)
|
||||
return
|
||||
|
||||
if (!configuration.peerIds.contains(deviceId)) {
|
||||
logger.trace("Received local announce from $deviceId which is not a peer, ignoring")
|
||||
|
||||
onMessageFromUnknownDeviceListener(deviceId)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
logger.debug("received local announce from device id = {}", deviceId)
|
||||
val addressesList = announce.addressesList ?: listOf<String>()
|
||||
val deviceAddresses = addressesList.map { address ->
|
||||
// When interpreting addresses with an unspecified address, e.g.,
|
||||
// tcp://0.0.0.0:22000 or tcp://:42424, the source address of the
|
||||
// discovery announcement is to be used.
|
||||
DeviceAddress.Builder()
|
||||
.setAddress(address.replaceFirst("tcp://(0.0.0.0|):".toRegex(), "tcp://$sourceAddress:"))
|
||||
.setDeviceId(deviceId.deviceId)
|
||||
.setInstanceId(announce.instanceId)
|
||||
.setProducer(DeviceAddress.AddressProducer.LOCAL_DISCOVERY)
|
||||
.build()
|
||||
}
|
||||
onMessageReceivedListener(deviceId, deviceAddresses)
|
||||
} catch (ex: InvalidProtocolBufferException) {
|
||||
logger.warn("error processing datagram", ex)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
processingExecutor.shutdown()
|
||||
listeningExecutor.shutdown()
|
||||
if (datagramSocket != null) {
|
||||
IOUtils.closeQuietly(datagramSocket)
|
||||
}
|
||||
}
|
||||
|
||||
abstract inner class MessageReceivedEvent : DeviceAddressReceivedEvent {
|
||||
|
||||
abstract fun deviceId(): DeviceId
|
||||
|
||||
abstract override fun getDeviceAddresses(): List<DeviceAddress>
|
||||
job.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
+143
@@ -0,0 +1,143 @@
|
||||
/*
|
||||
* 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.discovery.protocol
|
||||
|
||||
import com.google.protobuf.ByteString
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.channels.ReceiveChannel
|
||||
import kotlinx.coroutines.channels.produce
|
||||
import kotlinx.coroutines.withContext
|
||||
import net.syncthing.java.core.beans.DeviceAddress
|
||||
import net.syncthing.java.core.beans.DeviceId
|
||||
import net.syncthing.java.core.utils.NetworkUtils
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.DataOutputStream
|
||||
import java.io.IOException
|
||||
import java.net.*
|
||||
import java.nio.ByteBuffer
|
||||
|
||||
object LocalDiscoveryUtil {
|
||||
private const val LISTENING_PORT = 21027
|
||||
private const val MAGIC = 0x2EA7D90B
|
||||
private const val INCOMING_BUFFER_SIZE = 1024
|
||||
|
||||
private val logger = LoggerFactory.getLogger(javaClass)
|
||||
|
||||
suspend fun listenForAnnounceMessages(): ReceiveChannel<LocalDiscoveryMessage> = GlobalScope.produce {
|
||||
DatagramSocket(LISTENING_PORT, InetAddress.getByName("0.0.0.0")).use { datagramSocket ->
|
||||
invokeOnClose {
|
||||
datagramSocket.close()
|
||||
}
|
||||
|
||||
withContext(Dispatchers.IO) {
|
||||
val datagramPacket = DatagramPacket(ByteArray(INCOMING_BUFFER_SIZE), INCOMING_BUFFER_SIZE)
|
||||
|
||||
while (!isClosedForSend) {
|
||||
try {
|
||||
datagramSocket.receive(datagramPacket)
|
||||
} catch (ex: SocketException) {
|
||||
if (datagramSocket.isClosed) {
|
||||
// if the socket was closed by the invokeOnClose, then ignore it
|
||||
|
||||
return@withContext
|
||||
} else {
|
||||
// otherwise it's more serious and it is rethrown
|
||||
|
||||
throw ex
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
val sourceAddress = datagramPacket.address.hostAddress
|
||||
val byteBuffer = ByteBuffer.wrap(datagramPacket.data, datagramPacket.offset, datagramPacket.length)
|
||||
val magic = byteBuffer.int
|
||||
NetworkUtils.assertProtocol(magic == MAGIC) {
|
||||
"magic mismatch, expected ${MAGIC}, got $magic"
|
||||
}
|
||||
val announce = LocalDiscoveryProtos.Announce.parseFrom(ByteString.copyFrom(byteBuffer))
|
||||
val deviceId = DeviceId.fromHashData(announce.id.toByteArray())
|
||||
|
||||
val deviceAddresses = (announce.addressesList ?: emptyList()).map { address ->
|
||||
// When interpreting addresses with an unspecified address, e.g.,
|
||||
// tcp://0.0.0.0:22000 or tcp://:42424, the source address of the
|
||||
// discovery announcement is to be used.
|
||||
DeviceAddress.Builder()
|
||||
.setAddress(address.replaceFirst("tcp://(0.0.0.0|):".toRegex(), "tcp://$sourceAddress:"))
|
||||
.setDeviceId(deviceId.deviceId)
|
||||
.setInstanceId(announce.instanceId)
|
||||
.setProducer(DeviceAddress.AddressProducer.LOCAL_DISCOVERY)
|
||||
.build()
|
||||
}
|
||||
|
||||
val message = LocalDiscoveryMessage(
|
||||
deviceId = deviceId,
|
||||
addresses = deviceAddresses
|
||||
)
|
||||
|
||||
send(message)
|
||||
} catch (ex: IOException) {
|
||||
logger.warn("error during handling received package", ex)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun sendAnnounceMessage(ownDeviceId: DeviceId, instanceId: Long) {
|
||||
val discoveryMessage = ByteArrayOutputStream().apply {
|
||||
DataOutputStream(this).writeInt(MAGIC)
|
||||
|
||||
LocalDiscoveryProtos.Announce.newBuilder()
|
||||
.setId(ByteString.copyFrom(ownDeviceId.toHashData()))
|
||||
.setInstanceId(instanceId)
|
||||
.build()
|
||||
.writeTo(this)
|
||||
}.toByteArray()
|
||||
|
||||
for (networkInterface in NetworkInterface.getNetworkInterfaces()) {
|
||||
for (interfaceAddress in networkInterface.interfaceAddresses) {
|
||||
val broadcastAddress = interfaceAddress.broadcast
|
||||
|
||||
logger.trace("interface = {} address = {} broadcast = {}", networkInterface, interfaceAddress, broadcastAddress)
|
||||
|
||||
if (broadcastAddress != null) {
|
||||
logger.debug("sending broadcast announce on {}", broadcastAddress)
|
||||
|
||||
DatagramSocket().use { broadcastSocket ->
|
||||
broadcastSocket.broadcast = true
|
||||
|
||||
broadcastSocket.send(DatagramPacket(
|
||||
discoveryMessage,
|
||||
discoveryMessage.size,
|
||||
broadcastAddress,
|
||||
LISTENING_PORT))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class LocalDiscoveryMessage(val deviceId: DeviceId, val addresses: List<DeviceAddress>) {
|
||||
init {
|
||||
addresses.forEach { address ->
|
||||
if (address.deviceIdObject != deviceId) {
|
||||
throw IllegalArgumentException()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+47
-58
@@ -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,69 +14,65 @@
|
||||
*/
|
||||
package net.syncthing.java.discovery.utils
|
||||
|
||||
import kotlinx.coroutines.*
|
||||
import net.syncthing.java.core.beans.DeviceAddress
|
||||
import net.syncthing.java.core.beans.DeviceAddress.AddressType
|
||||
import net.syncthing.java.core.utils.submitLogging
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.io.Closeable
|
||||
import java.io.IOException
|
||||
import java.net.Socket
|
||||
import java.util.concurrent.*
|
||||
|
||||
internal class AddressRanker private constructor(private val sourceAddresses: List<DeviceAddress>) : Closeable {
|
||||
|
||||
companion object {
|
||||
|
||||
private const val TCP_CONNECTION_TIMEOUT = 5000
|
||||
private val BASE_SCORE_MAP = mapOf(
|
||||
AddressType.TCP to 0,
|
||||
AddressType.RELAY to 2000,
|
||||
AddressType.HTTP_RELAY to 1000 * 2000,
|
||||
AddressType.HTTPS_RELAY to 1000 * 2000)
|
||||
private val ACCEPTED_ADDRESS_TYPES = BASE_SCORE_MAP.keys
|
||||
|
||||
fun pingAddresses(list: List<DeviceAddress>): List<DeviceAddress> {
|
||||
AddressRanker(list).use { addressRanker ->
|
||||
return addressRanker.testAndRankAndWait()
|
||||
}
|
||||
}
|
||||
}
|
||||
object AddressRanker {
|
||||
|
||||
private const val TCP_CONNECTION_TIMEOUT = 5000
|
||||
private val BASE_SCORE_MAP = mapOf(
|
||||
AddressType.TCP to 0,
|
||||
AddressType.RELAY to 2000,
|
||||
AddressType.HTTP_RELAY to 1000 * 2000,
|
||||
AddressType.HTTPS_RELAY to 1000 * 2000
|
||||
)
|
||||
private val ACCEPTED_ADDRESS_TYPES = BASE_SCORE_MAP.keys
|
||||
private val logger = LoggerFactory.getLogger(javaClass)
|
||||
private val executorService = Executors.newCachedThreadPool()
|
||||
|
||||
private fun addHttpRelays(list: List<DeviceAddress>): List<DeviceAddress> {
|
||||
val httpRelays = list
|
||||
.filter { address ->
|
||||
address.getType() == AddressType.RELAY && address.containsUriParamValue("httpUrl")
|
||||
}
|
||||
.map { address ->
|
||||
val httpUrl = address.getUriParam("httpUrl")
|
||||
address.copyBuilder().setAddress("relay-" + httpUrl!!).build()
|
||||
}
|
||||
return httpRelays + list
|
||||
}
|
||||
|
||||
private fun testAndRankAndWait(): List<DeviceAddress> {
|
||||
return addHttpRelays(sourceAddresses)
|
||||
suspend fun pingAddresses(sourceAddresses: List<DeviceAddress>) = coroutineScope {
|
||||
addHttpRelays(sourceAddresses)
|
||||
.filter { ACCEPTED_ADDRESS_TYPES.contains(it.getType()) }
|
||||
.map { executorService.submitLogging<DeviceAddress?> { pingAddresses(it) } }
|
||||
.mapNotNull { future ->
|
||||
try {
|
||||
future.get((TCP_CONNECTION_TIMEOUT * 2).toLong(), TimeUnit.MILLISECONDS)
|
||||
} catch (e: ExecutionException) {
|
||||
logger.warn("Failed to ping device", e)
|
||||
null
|
||||
} catch (e: InterruptedException) {
|
||||
logger.warn("Failed to ping device", e)
|
||||
null
|
||||
.toList() // the following should happen parallel
|
||||
.map {
|
||||
async {
|
||||
try {
|
||||
withTimeout(TCP_CONNECTION_TIMEOUT * 2L) {
|
||||
// this nested async ensures that cancelling/ the timeout has got an effect without delay
|
||||
GlobalScope.async (Dispatchers.IO) {
|
||||
pingAddressSync(it)
|
||||
}.await()
|
||||
}
|
||||
} catch (ex: Exception) {
|
||||
logger.warn("Failed to ping device", ex)
|
||||
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
.map { it.await() }
|
||||
.filterNotNull()
|
||||
.sortedBy { it.score }
|
||||
}
|
||||
|
||||
private fun pingAddresses(deviceAddress: DeviceAddress): DeviceAddress? {
|
||||
private fun getHttpRelays(list: List<DeviceAddress>) = list
|
||||
.asSequence()
|
||||
.filter { address ->
|
||||
address.getType() == AddressType.RELAY && address.containsUriParamValue("httpUrl")
|
||||
}
|
||||
.map { address ->
|
||||
val httpUrl = address.getUriParam("httpUrl")
|
||||
address.copyBuilder().setAddress("relay-" + httpUrl!!).build()
|
||||
}
|
||||
|
||||
private fun addHttpRelays(list: List<DeviceAddress>) = getHttpRelays(list) + list
|
||||
|
||||
private fun pingAddressSync(deviceAddress: DeviceAddress): DeviceAddress? {
|
||||
val startTime = System.currentTimeMillis()
|
||||
|
||||
try {
|
||||
Socket().use { socket ->
|
||||
socket.soTimeout = TCP_CONNECTION_TIMEOUT
|
||||
@@ -85,18 +82,10 @@ internal class AddressRanker private constructor(private val sourceAddresses: Li
|
||||
logger.debug("address unreacheable = $deviceAddress, ${ex.message}")
|
||||
return null
|
||||
}
|
||||
val ping = (System.currentTimeMillis() - startTime).toInt()
|
||||
|
||||
val ping = (System.currentTimeMillis() - startTime).toInt()
|
||||
val baseScore = BASE_SCORE_MAP[deviceAddress.getType()] ?: 0
|
||||
|
||||
return deviceAddress.copyBuilder().setScore(ping + baseScore).build()
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
executorService.shutdown()
|
||||
try {
|
||||
executorService.awaitTermination(2, TimeUnit.SECONDS)
|
||||
} catch (ex: InterruptedException) {
|
||||
logger.warn("", ex)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user