28 Commits

Author SHA1 Message Date
l-jonas 0fb7a9e93d Release 0.3.4 2018-11-13 07:33:03 +01:00
l-jonas 1b4205b04a Update library version to fix proguard warnings 2018-11-13 07:30:36 +01:00
l-jonas 8e00c8b4a0 Release 0.3.3 2018-11-12 17:48:42 +01:00
l-jonas f3ca98be80 Update changelog 2018-11-12 17:48:10 +01:00
l-jonas 96fc8bfc7b Bugfixes (#92)
* Fix loading subdirectories on the main thread (which caused a crash)
* Fix LibraryHandler creation in the background (ContentProvider)
2018-11-12 17:42:19 +01:00
l-jonas 58098aae0f Provide real file names to apps (#79)
* Send correct file names when files are opened from the app UI

This fixes https://github.com/syncthing/syncthing-lite/issues/76
2018-11-12 17:39:38 +01:00
l-jonas c4ad797905 Discovery server validation (#82)
* Add possibility to verify discovery server certificates
* Add new discovery server storage model
* Use new discovery server config
2018-11-12 16:53:32 +01:00
l-jonas a61d8c5c4f Fix file deletion (#87)
Fix applying deleted files
2018-11-12 16:46:49 +01:00
l-jonas af579f8311 Save as dialog (#88)
Add save as support
2018-11-12 16:46:02 +01:00
Jonas L fbdcdbf7ec Update translations 2018-11-11 19:48:48 +01:00
Felix Ableitner e6870a08d6 Update Google Play listing from gradle (fixes #83) (#84) 2018-11-10 14:34:33 +01:00
l-jonas fbee0ca0e8 Remove obsolete link to syncthing-java 2018-11-09 16:42:05 +01:00
l-jonas 65b42475a6 Add note about 0% sync progress 2018-11-09 16:41:33 +01:00
l-jonas af09b763a6 Adaptive icon (#80)
* Move google to the top of allrepositories.google

This should fix build issues according to https://gitlab.com/fdroid/fdroiddata/issues/1423

* Steal adaptive icon from syncthing-android

https://github.com/syncthing/syncthing-android/commit/c43ee663a26ebd3579b2553783ab8ab2108fa350

* Replace background layer
2018-11-08 17:45:49 +01:00
l-jonas 5680c6c554 Use stable coroutines (#72)
* Move google to the top of allrepositories.google

This should fix build issues according to https://gitlab.com/fdroid/fdroiddata/issues/1423

* Update Kotlin and use stable coroutines

* Optimize imports

* Optimize imports again

* Remove some imports manually
2018-11-08 16:27:27 +01:00
l-jonas 2caaebfc33 Document handling of requests for new languages
This closes https://github.com/syncthing/syncthing-lite/issues/77
2018-11-08 13:06:33 +01:00
l-jonas 460d421a79 Release 0.3.2 2018-11-07 07:41:38 +01:00
Jonas L 96edbaa240 Update translations 2018-11-07 07:00:37 +01:00
l-jonas de1566915b Finish spanish translation (#73)
* Move google to the top of allrepositories.google

This should fix build issues according to https://gitlab.com/fdroid/fdroiddata/issues/1423

* Finish spanish translation

This is a import from Transifex because I got a automatic notification that the spanish translation was finished
2018-11-06 21:02:32 +01:00
Felix Ableitner a294d6f06c Add gradle play publisher for automated releases to Google Play 2018-11-06 17:11:07 +01:00
l-jonas 7ce017f48c Update translations (#68)
* Move google to the top of allrepositories.google

This should fix build issues according to https://gitlab.com/fdroid/fdroiddata/issues/1423

* Merge translations

* Import fixed localization of the remove device dialog title

* Add new translations

* Update strings with wrong escaping in Transifex and import them

* Fix escaping at some other strings

* Update translations
2018-11-06 13:46:57 +01:00
l-jonas 55edc592a0 Remove obsolete build instructions from the readme 2018-11-06 12:57:35 +01:00
l-jonas eb5dfcbd46 Refactor discovery code (#64)
* Move discovery server query out of GlobalDiscoveryHandler

* Refactor GlobalDiscoveryHandler

* Run global discovery in bulk (only pick discovery servers once)

* Readd ignoring failed global discovery

* Add new code for local discovery

* Use new code for local discovery

* Fix compiling the discovery CLI

* Fix code style

* Add Copyright headers to all files of syncthing-discovery

* Refactor the AddressRanker

* Fix timeout at the AddressRanker

* Refactor the DiscoveryHandler

* Use HttpUrlConnection in GlobalDiscoveryUtil

* Disable https verification in GlobalDiscoveryUtil

* Get the changes working

* Add newline

* De-hardcode announce server query url generation

* Only ignore specific global discovery exceptions

* Only catch IOException at LocalDiscoveryHandler.startListener()

* Only catch IOException at LocalDiscoveryUtil when processing the package
2018-11-06 12:13:44 +01:00
l-jonas 2b55bd9e76 Fix typo in the App which was at the Readme too 2018-10-29 07:45:14 +01:00
Licaon_Kter eb8360f276 Readme typos (#65) 2018-10-29 07:43:39 +01:00
l-jonas 48f880ee4e Release version 0.3.1 2018-10-28 17:56:02 +01:00
l-jonas 9f5072ed3a Move google() to the top of allprojects.repositories
According to https://forum.f-droid.org/t/syncthing-lite/4394 this is required to make this App build correctly for F-Droid
2018-10-28 17:55:08 +01:00
l-jonas b1743db5af Update wording of the Readme to the one of the dialog in the App 2018-10-27 15:23:02 +02:00
80 changed files with 1649 additions and 566 deletions
+14 -16
View File
@@ -4,19 +4,21 @@
[![MPLv2 License](https://img.shields.io/badge/license-MPLv2-blue.svg?style=flat-square)](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
View File
@@ -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 14
versionName "0.3.4"
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"
+2 -6
View File
@@ -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
@@ -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 -> {
@@ -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))
}
}
+1
View File
@@ -0,0 +1 @@
googleplay@nutomic.com
View File
+1
View File
@@ -0,0 +1 @@
https://syncthing.net
+1
View File
@@ -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
+1
View File
@@ -0,0 +1 @@
Syncthing Lite
+6
View File
@@ -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>
+35
View File
@@ -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

+23 -3
View File
@@ -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>
+52
View File
@@ -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>
+28 -2
View File
@@ -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>
+50
View File
@@ -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>
+28 -3
View File
@@ -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>
+20 -1
View File
@@ -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>
+44
View File
@@ -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>
+32 -2
View File
@@ -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>
+50
View File
@@ -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>
+2
View File
@@ -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>
+2 -1
View File
@@ -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>
-4
View File
@@ -1,4 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<paths>
<external-cache-path name="files" path="/" />
</paths>
+4 -3
View File
@@ -1,10 +1,10 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
ext.kotlin_version = '1.2.61'
ext.kotlin_version = '1.3.0'
ext.support_version = '27.0.2'
ext.build_tools_version = '3.2.0'
ext.anko_version = '0.10.7'
ext.anko_version = '0.10.8'
ext.protobuf_lite_version = '3.0.1'
repositories {
mavenLocal()
@@ -17,15 +17,16 @@ buildscript {
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath 'com.github.ben-manes:gradle-versions-plugin:0.17.0'
classpath 'com.google.protobuf:protobuf-gradle-plugin:0.8.6'
classpath 'com.github.triplet.gradle:play-publisher:1.2.0'
}
}
allprojects {
repositories {
google()
jcenter()
maven {
url "https://jitpack.io"
}
google()
}
}
+1 -1
View File
@@ -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)"
}
@@ -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)
@@ -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()
}
}
@@ -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)
}
}
}
+1
View File
@@ -8,6 +8,7 @@ dependencies {
compile "commons-cli:commons-cli:1.4"
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation "com.google.protobuf:protobuf-lite:$protobuf_lite_version"
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.0.0'
}
run {
@@ -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!!
}
@@ -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) }
}
}
}
@@ -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
}
}
}
@@ -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()
}
}
@@ -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)
}
}
}
@@ -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) }
}
}
@@ -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
@@ -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()
}
}
@@ -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()
}
}
}
}
@@ -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)
}
}
}