Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8e00c8b4a0 | |||
| f3ca98be80 | |||
| 96fc8bfc7b | |||
| 58098aae0f | |||
| c4ad797905 | |||
| a61d8c5c4f | |||
| af579f8311 | |||
| fbdcdbf7ec | |||
| e6870a08d6 | |||
| fbee0ca0e8 | |||
| 65b42475a6 | |||
| af09b763a6 | |||
| 5680c6c554 | |||
| 2caaebfc33 |
@@ -8,11 +8,13 @@ Syncthing devices in the same way a client-server file sharing app accesses its
|
||||
|
||||
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,
|
||||
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 reconnect for some minutes if the App was killed (due to removing from the recent App list) or the connection was interrupted.
|
||||
@@ -24,6 +26,7 @@ This does not apply to local discovery connections.
|
||||
## 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
|
||||
|
||||
|
||||
+10
-3
@@ -19,8 +19,8 @@ android {
|
||||
applicationId "net.syncthing.lite"
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 26
|
||||
versionCode 12
|
||||
versionName "0.3.2"
|
||||
versionCode 13
|
||||
versionName "0.3.3"
|
||||
multiDexEnabled true
|
||||
playAccountConfig = playAccountConfigs.defaultAccountConfig
|
||||
}
|
||||
@@ -60,11 +60,18 @@ android {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
play {
|
||||
jsonFile = file(System.getenv("SYNCTHING_RELEASE_PLAY_ACCOUNT_CONFIG_FILE") ?: 'keys.json')
|
||||
uploadImages = true
|
||||
track = 'production'
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
||||
implementation "org.jetbrains.anko:anko-commons:$anko_version"
|
||||
implementation "org.jetbrains.anko:anko-coroutines:$anko_version"
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:0.30.2'
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.0.0'
|
||||
implementation "com.android.support:design:$support_version"
|
||||
implementation "com.android.support:preference-v14:$support_version"
|
||||
implementation "com.android.support:support-v4:$support_version"
|
||||
|
||||
@@ -22,14 +22,10 @@
|
||||
<activity android:name=".activities.FolderBrowserActivity"
|
||||
android:parentActivityName=".activities.MainActivity"/>
|
||||
<provider
|
||||
android:name="android.support.v4.content.FileProvider"
|
||||
android:name=".library.CacheFileProvider"
|
||||
android:authorities="net.syncthing.lite.fileprovider"
|
||||
android:grantUriPermissions="true"
|
||||
android:exported="false">
|
||||
<meta-data
|
||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/filepaths" />
|
||||
</provider>
|
||||
android:exported="false" />
|
||||
<provider
|
||||
android:name=".library.SyncthingProvider"
|
||||
android:authorities="net.syncthing.lite.documents"
|
||||
|
||||
@@ -5,8 +5,10 @@ import android.content.Intent
|
||||
import android.databinding.DataBindingUtil
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import kotlinx.coroutines.experimental.android.UI
|
||||
import kotlinx.coroutines.experimental.async
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import net.syncthing.java.bep.IndexBrowser
|
||||
import net.syncthing.java.core.beans.FileInfo
|
||||
import net.syncthing.java.core.beans.FolderInfo
|
||||
@@ -16,10 +18,11 @@ import net.syncthing.lite.R
|
||||
import net.syncthing.lite.adapters.FolderContentsAdapter
|
||||
import net.syncthing.lite.adapters.FolderContentsListener
|
||||
import net.syncthing.lite.databinding.ActivityFolderBrowserBinding
|
||||
import net.syncthing.lite.dialogs.FileMenuDialogFragment
|
||||
import net.syncthing.lite.dialogs.FileUploadDialog
|
||||
import net.syncthing.lite.dialogs.ReconnectIssueDialogFragment
|
||||
import net.syncthing.lite.dialogs.downloadfile.DownloadFileDialogFragment
|
||||
import org.jetbrains.anko.custom.async
|
||||
import org.jetbrains.anko.doAsync
|
||||
|
||||
class FolderBrowserActivity : SyncthingActivity() {
|
||||
|
||||
@@ -44,6 +47,16 @@ class FolderBrowserActivity : SyncthingActivity() {
|
||||
override fun onItemClicked(fileInfo: FileInfo) {
|
||||
navigateToFolder(fileInfo)
|
||||
}
|
||||
|
||||
override fun onItemLongClicked(fileInfo: FileInfo): Boolean {
|
||||
return if (fileInfo.type == FileInfo.FileType.FILE) {
|
||||
FileMenuDialogFragment.newInstance(fileInfo).show(supportFragmentManager)
|
||||
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
val folder = intent.getStringExtra(EXTRA_FOLDER_NAME)
|
||||
libraryHandler?.syncthingClient {
|
||||
@@ -70,13 +83,15 @@ class FolderBrowserActivity : SyncthingActivity() {
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, intent: Intent?) {
|
||||
if (requestCode == REQUEST_SELECT_UPLOAD_FILE && resultCode == Activity.RESULT_OK) {
|
||||
libraryHandler?.syncthingClient { syncthingClient ->
|
||||
async (UI) {
|
||||
GlobalScope.launch (Dispatchers.Main) {
|
||||
// FIXME: it would be better if the dialog would use the library handler
|
||||
FileUploadDialog(this@FolderBrowserActivity, syncthingClient, intent!!.data,
|
||||
indexBrowser.folder, indexBrowser.currentPath,
|
||||
{ showFolderListView(indexBrowser.currentPath) }).show()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
super.onActivityResult(requestCode, resultCode, intent)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,7 +106,7 @@ class FolderBrowserActivity : SyncthingActivity() {
|
||||
finish()
|
||||
} else {
|
||||
if (fileInfo.isDirectory()) {
|
||||
async {
|
||||
doAsync {
|
||||
indexBrowser.navigateTo(fileInfo)
|
||||
}
|
||||
|
||||
@@ -108,32 +123,33 @@ class FolderBrowserActivity : SyncthingActivity() {
|
||||
}
|
||||
|
||||
private fun onFolderChanged() {
|
||||
runOnUiThread {
|
||||
binding.isLoading = false
|
||||
GlobalScope.launch {
|
||||
val list = indexBrowser.listFiles()
|
||||
|
||||
async {
|
||||
val list = indexBrowser.listFiles()
|
||||
Log.i("navigateToFolder", "list for path = '" + indexBrowser.currentPath + "' list = " + list.size + " records")
|
||||
Log.d("navigateToFolder", "list for path = '" + indexBrowser.currentPath + "' list = " + list)
|
||||
assert(!list.isEmpty())//list must contain at least the 'parent' path
|
||||
|
||||
async (UI) {
|
||||
Log.i("navigateToFolder", "list for path = '" + indexBrowser.currentPath + "' list = " + list.size + " records")
|
||||
Log.d("navigateToFolder", "list for path = '" + indexBrowser.currentPath + "' list = " + list)
|
||||
assert(!list.isEmpty())//list must contain at least the 'parent' path
|
||||
adapter.data = list
|
||||
binding.listView.scrollToPosition(0)
|
||||
if (indexBrowser.isRoot())
|
||||
libraryHandler?.folderBrowser {
|
||||
val title = it.getFolderInfo(indexBrowser.folder)?.label
|
||||
val title = if (indexBrowser.isRoot()) {
|
||||
val result = CompletableDeferred<String?>()
|
||||
|
||||
async(UI) {
|
||||
supportActionBar?.title = title
|
||||
}
|
||||
}
|
||||
else
|
||||
supportActionBar?.title = indexBrowser.currentPathInfo().fileName
|
||||
libraryHandler.folderBrowser {
|
||||
result.complete(it.getFolderInfo(indexBrowser.folder)?.label)
|
||||
}
|
||||
|
||||
result.await()
|
||||
} else {
|
||||
indexBrowser.currentPathInfo().fileName
|
||||
}
|
||||
|
||||
runOnUiThread {
|
||||
binding.isLoading = false
|
||||
adapter.data = list
|
||||
binding.listView.scrollToPosition(0)
|
||||
supportActionBar?.title = title
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateFolderListView() {
|
||||
showFolderListView(indexBrowser.currentPath)
|
||||
|
||||
@@ -4,8 +4,6 @@ import android.arch.lifecycle.Observer
|
||||
import android.content.Intent
|
||||
import android.databinding.DataBindingUtil
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.support.v4.app.Fragment
|
||||
import android.support.v4.content.ContextCompat
|
||||
import android.text.Html
|
||||
@@ -15,8 +13,9 @@ import android.view.ViewGroup
|
||||
import android.widget.Button
|
||||
import com.github.paolorotolo.appintro.AppIntro
|
||||
import com.google.zxing.integration.android.IntentIntegrator
|
||||
import kotlinx.coroutines.experimental.android.UI
|
||||
import kotlinx.coroutines.experimental.async
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import net.syncthing.java.core.beans.DeviceId
|
||||
import net.syncthing.lite.R
|
||||
import net.syncthing.lite.databinding.FragmentIntroOneBinding
|
||||
@@ -183,7 +182,7 @@ class IntroActivity : AppIntro() {
|
||||
binding = DataBindingUtil.inflate(inflater, R.layout.fragment_intro_three, container, false)
|
||||
|
||||
libraryHandler.library { config, client, _ ->
|
||||
async(UI) {
|
||||
GlobalScope.launch (Dispatchers.Main) {
|
||||
client.addOnConnectionChangedListener(this@IntroFragmentThree::onConnectionChanged)
|
||||
val deviceId = config.localDeviceId.deviceId
|
||||
val desc = activity?.getString(R.string.intro_page_three_description, "<b>$deviceId</b>")
|
||||
@@ -196,7 +195,7 @@ class IntroActivity : AppIntro() {
|
||||
|
||||
private fun onConnectionChanged(deviceId: DeviceId) {
|
||||
libraryHandler.library { config, client, _ ->
|
||||
async(UI) {
|
||||
GlobalScope.launch (Dispatchers.Main) {
|
||||
if (config.folders.isNotEmpty()) {
|
||||
client.removeOnConnectionChangedListener(this@IntroFragmentThree::onConnectionChanged)
|
||||
(activity as IntroActivity?)?.onDonePressed(this@IntroFragmentThree)
|
||||
|
||||
@@ -8,8 +8,9 @@ import android.support.v4.app.Fragment
|
||||
import android.support.v7.app.ActionBarDrawerToggle
|
||||
import android.view.Gravity
|
||||
import android.view.MenuItem
|
||||
import kotlinx.coroutines.experimental.android.UI
|
||||
import kotlinx.coroutines.experimental.async
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import net.syncthing.lite.R
|
||||
import net.syncthing.lite.databinding.ActivityMainBinding
|
||||
import net.syncthing.lite.dialogs.DeviceIdDialogFragment
|
||||
@@ -101,7 +102,7 @@ class MainActivity : SyncthingActivity() {
|
||||
}
|
||||
|
||||
private fun cleanCacheAndIndex() {
|
||||
async(UI) {
|
||||
GlobalScope.launch (Dispatchers.Main) {
|
||||
libraryHandler.syncthingClient { it.clearCacheAndIndex() }
|
||||
recreate()
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package net.syncthing.lite.activities
|
||||
|
||||
import android.app.AlertDialog
|
||||
import android.content.Context
|
||||
import android.databinding.DataBindingUtil
|
||||
import android.os.Bundle
|
||||
import android.support.design.widget.Snackbar
|
||||
|
||||
@@ -50,6 +50,10 @@ class FolderContentsAdapter: RecyclerView.Adapter<FolderContentsViewHolder>() {
|
||||
listener?.onItemClicked(fileInfo)
|
||||
}
|
||||
|
||||
binding.root.setOnLongClickListener {
|
||||
listener?.onItemLongClicked(fileInfo) ?: false
|
||||
}
|
||||
|
||||
binding.executePendingBindings()
|
||||
}
|
||||
|
||||
@@ -59,6 +63,7 @@ class FolderContentsAdapter: RecyclerView.Adapter<FolderContentsViewHolder>() {
|
||||
|
||||
interface FolderContentsListener {
|
||||
fun onItemClicked(fileInfo: FileInfo)
|
||||
fun onItemLongClicked(fileInfo: FileInfo): Boolean
|
||||
}
|
||||
|
||||
class FolderContentsViewHolder(val binding: ListviewFileBinding): RecyclerView.ViewHolder(binding.root)
|
||||
class FolderContentsViewHolder(val binding: ListviewFileBinding): RecyclerView.ViewHolder(binding.root)
|
||||
|
||||
@@ -17,8 +17,9 @@ import android.widget.Toast
|
||||
import com.google.zxing.BarcodeFormat
|
||||
import com.google.zxing.WriterException
|
||||
import com.google.zxing.qrcode.QRCodeWriter
|
||||
import kotlinx.coroutines.experimental.android.UI
|
||||
import kotlinx.coroutines.experimental.async
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import net.syncthing.lite.R
|
||||
import net.syncthing.lite.databinding.DialogDeviceIdBinding
|
||||
import net.syncthing.lite.fragments.SyncthingDialogFragment
|
||||
@@ -62,7 +63,7 @@ class DeviceIdDialogFragment: SyncthingDialogFragment() {
|
||||
))
|
||||
}
|
||||
|
||||
async (UI) {
|
||||
GlobalScope.launch (Dispatchers.Main) {
|
||||
binding.deviceId.text = deviceId.deviceId
|
||||
binding.deviceId.visibility = View.VISIBLE
|
||||
|
||||
@@ -83,7 +84,7 @@ class DeviceIdDialogFragment: SyncthingDialogFragment() {
|
||||
}
|
||||
}
|
||||
|
||||
async(UI) {
|
||||
GlobalScope.launch (Dispatchers.Main) {
|
||||
binding.flipper.displayedChild = 1
|
||||
binding.qrCode.setImageBitmap(bmp)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
package net.syncthing.lite.dialogs
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.support.design.widget.BottomSheetDialogFragment
|
||||
import android.support.v4.app.FragmentManager
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.webkit.MimeTypeMap
|
||||
import net.syncthing.java.core.beans.FileInfo
|
||||
import net.syncthing.lite.databinding.DialogFileBinding
|
||||
import net.syncthing.lite.dialogs.downloadfile.DownloadFileDialogFragment
|
||||
import net.syncthing.lite.dialogs.downloadfile.DownloadFileSpec
|
||||
import org.apache.commons.io.FilenameUtils
|
||||
|
||||
class FileMenuDialogFragment: BottomSheetDialogFragment() {
|
||||
companion object {
|
||||
private const val ARG_FILE_SPEC = "file spec"
|
||||
private const val TAG = "DownloadFileDialog"
|
||||
private const val REQ_SAVE_AS = 1
|
||||
|
||||
fun newInstance(fileInfo: FileInfo) = newInstance(DownloadFileSpec(
|
||||
folder = fileInfo.folder,
|
||||
path = fileInfo.path,
|
||||
fileName = fileInfo.fileName
|
||||
))
|
||||
|
||||
fun newInstance(fileSpec: DownloadFileSpec) = FileMenuDialogFragment().apply {
|
||||
arguments = Bundle().apply {
|
||||
putSerializable(ARG_FILE_SPEC, fileSpec)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val fileSpec: DownloadFileSpec by lazy {
|
||||
arguments!!.getSerializable(ARG_FILE_SPEC) as DownloadFileSpec
|
||||
}
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
val binding = DialogFileBinding.inflate(inflater, container, false)
|
||||
|
||||
binding.filename = fileSpec.fileName
|
||||
|
||||
binding.saveAsButton.setOnClickListener {
|
||||
startActivityForResult(
|
||||
Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
|
||||
addCategory(Intent.CATEGORY_OPENABLE)
|
||||
|
||||
type = MimeTypeMap.getSingleton().getMimeTypeFromExtension(
|
||||
FilenameUtils.getExtension(fileSpec.fileName)
|
||||
)
|
||||
|
||||
putExtra(Intent.EXTRA_TITLE, fileSpec.fileName)
|
||||
},
|
||||
REQ_SAVE_AS
|
||||
)
|
||||
}
|
||||
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
when (requestCode) {
|
||||
REQ_SAVE_AS -> {
|
||||
if (resultCode == Activity.RESULT_OK) {
|
||||
DownloadFileDialogFragment.newInstance(fileSpec, data!!.data!!).show(fragmentManager)
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
else -> super.onActivityResult(requestCode, resultCode, data)
|
||||
}
|
||||
}
|
||||
|
||||
fun show(fragmentManager: FragmentManager?) {
|
||||
show(fragmentManager, TAG)
|
||||
}
|
||||
}
|
||||
@@ -3,8 +3,6 @@ package net.syncthing.lite.dialogs
|
||||
import android.app.ProgressDialog
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import kotlinx.coroutines.experimental.android.UI
|
||||
import kotlinx.coroutines.experimental.async
|
||||
import net.syncthing.java.bep.BlockPusher
|
||||
import net.syncthing.java.client.SyncthingClient
|
||||
import net.syncthing.lite.R
|
||||
|
||||
+42
-18
@@ -7,15 +7,16 @@ import android.arch.lifecycle.ViewModelProviders
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.DialogInterface
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.support.v4.app.DialogFragment
|
||||
import android.support.v4.app.FragmentManager
|
||||
import android.support.v4.content.FileProvider
|
||||
import android.util.Log
|
||||
import android.webkit.MimeTypeMap
|
||||
import net.syncthing.java.core.beans.FileInfo
|
||||
import net.syncthing.lite.BuildConfig
|
||||
import net.syncthing.lite.R
|
||||
import net.syncthing.lite.library.CacheFileProviderUrl
|
||||
import net.syncthing.lite.library.LibraryHandler
|
||||
import org.apache.commons.io.FilenameUtils
|
||||
import org.jetbrains.anko.newTask
|
||||
@@ -24,6 +25,7 @@ import org.jetbrains.anko.toast
|
||||
class DownloadFileDialogFragment : DialogFragment() {
|
||||
companion object {
|
||||
private const val ARG_FILE_SPEC = "file spec"
|
||||
private const val ARG_SAVE_AS_URI = "save as"
|
||||
private const val TAG = "DownloadFileDialog"
|
||||
|
||||
fun newInstance(fileInfo: FileInfo) = newInstance(DownloadFileSpec(
|
||||
@@ -32,9 +34,16 @@ class DownloadFileDialogFragment : DialogFragment() {
|
||||
fileName = fileInfo.fileName
|
||||
))
|
||||
|
||||
fun newInstance(fileSpec: DownloadFileSpec) = DownloadFileDialogFragment().apply {
|
||||
fun newInstance(
|
||||
fileSpec: DownloadFileSpec,
|
||||
outputUri: Uri? = null
|
||||
) = DownloadFileDialogFragment().apply {
|
||||
arguments = Bundle().apply {
|
||||
putSerializable(ARG_FILE_SPEC, fileSpec)
|
||||
|
||||
if (outputUri != null) {
|
||||
putParcelable(ARG_SAVE_AS_URI, outputUri)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -45,11 +54,17 @@ class DownloadFileDialogFragment : DialogFragment() {
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
val fileSpec = arguments!!.getSerializable(ARG_FILE_SPEC) as DownloadFileSpec
|
||||
val outputUri = if (arguments!!.containsKey(ARG_SAVE_AS_URI))
|
||||
arguments!!.getParcelable(ARG_SAVE_AS_URI) as Uri
|
||||
else
|
||||
null
|
||||
|
||||
model.init(
|
||||
libraryHandler = LibraryHandler(context!!),
|
||||
fileSpec = fileSpec,
|
||||
externalCacheDir = context!!.externalCacheDir
|
||||
externalCacheDir = context!!.externalCacheDir,
|
||||
outputUri = outputUri,
|
||||
contentResolver = context!!.contentResolver
|
||||
)
|
||||
|
||||
val progressDialog = ProgressDialog(context).apply {
|
||||
@@ -73,22 +88,31 @@ class DownloadFileDialogFragment : DialogFragment() {
|
||||
is DownloadFileStatusDone -> {
|
||||
dismissAllowingStateLoss()
|
||||
|
||||
try {
|
||||
context!!.startActivity(
|
||||
Intent(Intent.ACTION_VIEW)
|
||||
.setDataAndType(
|
||||
FileProvider.getUriForFile(context!!, "net.syncthing.lite.fileprovider", status.file),
|
||||
MimeTypeMap.getSingleton().getMimeTypeFromExtension(FilenameUtils.getExtension(fileSpec.fileName))
|
||||
)
|
||||
.newTask()
|
||||
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
)
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.w(TAG, "No handler found for file " + status.file.name, e)
|
||||
}
|
||||
if (outputUri == null) {
|
||||
val mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(FilenameUtils.getExtension(fileSpec.fileName))
|
||||
|
||||
context!!.toast(R.string.toast_open_file_failed)
|
||||
try {
|
||||
context!!.startActivity(
|
||||
Intent(Intent.ACTION_VIEW)
|
||||
.setDataAndType(
|
||||
CacheFileProviderUrl.fromFile(
|
||||
filename = fileSpec.fileName,
|
||||
mimeType = mimeType,
|
||||
file = status.file,
|
||||
context = context!!
|
||||
).serialized,
|
||||
mimeType
|
||||
)
|
||||
.newTask()
|
||||
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
)
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.w(TAG, "No handler found for file " + status.file.name, e)
|
||||
}
|
||||
|
||||
context!!.toast(R.string.toast_open_file_failed)
|
||||
}
|
||||
}
|
||||
}
|
||||
is DownloadFileStatusFailed -> {
|
||||
|
||||
+32
-5
@@ -2,12 +2,17 @@ package net.syncthing.lite.dialogs.downloadfile
|
||||
|
||||
import android.arch.lifecycle.LiveData
|
||||
import android.arch.lifecycle.MutableLiveData
|
||||
import android.arch.lifecycle.ViewModel;
|
||||
import android.arch.lifecycle.ViewModel
|
||||
import android.content.ContentResolver
|
||||
import android.net.Uri
|
||||
import android.support.v4.os.CancellationSignal
|
||||
import android.util.Log
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import net.syncthing.lite.BuildConfig
|
||||
import net.syncthing.lite.library.DownloadFileTask
|
||||
import net.syncthing.lite.library.LibraryHandler
|
||||
import org.apache.commons.io.FileUtils
|
||||
import java.io.File
|
||||
|
||||
class DownloadFileDialogViewModel : ViewModel() {
|
||||
@@ -20,7 +25,13 @@ class DownloadFileDialogViewModel : ViewModel() {
|
||||
private val cancellationSignal = CancellationSignal()
|
||||
val status: LiveData<DownloadFileStatus> = statusInternal
|
||||
|
||||
fun init(libraryHandler: LibraryHandler, fileSpec: DownloadFileSpec, externalCacheDir: File) {
|
||||
fun init(
|
||||
libraryHandler: LibraryHandler,
|
||||
fileSpec: DownloadFileSpec,
|
||||
externalCacheDir: File,
|
||||
outputUri: Uri?,
|
||||
contentResolver: ContentResolver
|
||||
) {
|
||||
if (isInitialized) {
|
||||
return
|
||||
}
|
||||
@@ -54,10 +65,26 @@ class DownloadFileDialogViewModel : ViewModel() {
|
||||
statusInternal.value = DownloadFileStatusRunning(newProgress)
|
||||
}
|
||||
},
|
||||
onComplete = {
|
||||
statusInternal.value = DownloadFileStatusDone(it)
|
||||
|
||||
onComplete = { file ->
|
||||
libraryHandler.stop()
|
||||
|
||||
GlobalScope.launch {
|
||||
try {
|
||||
if (outputUri != null) {
|
||||
contentResolver.openOutputStream(outputUri).use { outputStream ->
|
||||
FileUtils.copyFile(file, outputStream)
|
||||
}
|
||||
}
|
||||
|
||||
statusInternal.postValue(DownloadFileStatusDone(file))
|
||||
} catch (ex: Exception) {
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.w(TAG, "downloading file failed", ex)
|
||||
}
|
||||
|
||||
statusInternal.postValue(DownloadFileStatusFailed)
|
||||
}
|
||||
}
|
||||
},
|
||||
onError = {
|
||||
statusInternal.value = DownloadFileStatusFailed
|
||||
|
||||
@@ -10,8 +10,9 @@ import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import com.google.zxing.integration.android.IntentIntegrator
|
||||
import kotlinx.coroutines.experimental.android.UI
|
||||
import kotlinx.coroutines.experimental.async
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import net.syncthing.java.core.beans.DeviceInfo
|
||||
import net.syncthing.lite.R
|
||||
import net.syncthing.lite.adapters.DeviceAdapterListener
|
||||
@@ -76,7 +77,7 @@ class DevicesFragment : SyncthingFragment() {
|
||||
|
||||
private fun updateDeviceList() {
|
||||
libraryHandler.syncthingClient { syncthingClient ->
|
||||
async(UI) {
|
||||
GlobalScope.launch (Dispatchers.Main) {
|
||||
adapter.data = syncthingClient.getPeerStatus()
|
||||
binding.isEmpty = adapter.data.isEmpty()
|
||||
}
|
||||
|
||||
@@ -6,8 +6,9 @@ import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import kotlinx.coroutines.experimental.android.UI
|
||||
import kotlinx.coroutines.experimental.async
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import net.syncthing.java.core.beans.FolderInfo
|
||||
import net.syncthing.java.core.beans.FolderStats
|
||||
import net.syncthing.lite.activities.FolderBrowserActivity
|
||||
@@ -39,7 +40,7 @@ class FoldersFragment : SyncthingFragment() {
|
||||
libraryHandler.folderBrowser { folderBrowser ->
|
||||
val list = folderBrowser.folderInfoAndStatsList()
|
||||
|
||||
async (UI) {
|
||||
GlobalScope.launch (Dispatchers.Main) {
|
||||
Log.i(TAG, "list folders = " + list + " (" + list.size + " records)")
|
||||
val adapter = FoldersListAdapter().apply { data = list }
|
||||
binding.list.adapter = adapter
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
package net.syncthing.lite.library
|
||||
|
||||
import android.content.ContentProvider
|
||||
import android.content.ContentValues
|
||||
import android.content.Context
|
||||
import android.database.Cursor
|
||||
import android.database.MatrixCursor
|
||||
import android.net.Uri
|
||||
import android.os.ParcelFileDescriptor
|
||||
import android.provider.OpenableColumns
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
|
||||
class CacheFileProvider: ContentProvider() {
|
||||
companion object {
|
||||
const val AUTHORITY = "net.syncthing.lite.fileprovider"
|
||||
}
|
||||
|
||||
override fun onCreate() = true
|
||||
|
||||
override fun insert(uri: Uri?, values: ContentValues?): Uri {
|
||||
throw NotImplementedError()
|
||||
}
|
||||
|
||||
override fun update(uri: Uri?, values: ContentValues?, selection: String?, selectionArgs: Array<out String>?): Int {
|
||||
throw NotImplementedError()
|
||||
}
|
||||
|
||||
override fun delete(uri: Uri?, selection: String?, selectionArgs: Array<out String>?): Int {
|
||||
throw NotImplementedError()
|
||||
}
|
||||
|
||||
override fun query(uri: Uri, projection: Array<out String>?, selection: String?, selectionArgs: Array<out String>?, sortOrder: String?): Cursor {
|
||||
val url = CacheFileProviderUrl.fromUri(uri)
|
||||
val file = url.getFile(context)
|
||||
|
||||
val resultProjection = projection ?: arrayOf(OpenableColumns.DISPLAY_NAME, OpenableColumns.SIZE)
|
||||
val resultCursor = MatrixCursor(resultProjection)
|
||||
|
||||
if (file.exists()) {
|
||||
val builder = resultCursor.newRow()
|
||||
|
||||
for (row in resultProjection) {
|
||||
when (row) {
|
||||
OpenableColumns.DISPLAY_NAME -> builder.add(url.filename)
|
||||
OpenableColumns.SIZE -> builder.add(file.length())
|
||||
else -> builder.add(null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return resultCursor
|
||||
}
|
||||
|
||||
override fun getType(uri: Uri): String = CacheFileProviderUrl.fromUri(uri).mimeType
|
||||
|
||||
override fun openFile(uri: Uri, mode: String): ParcelFileDescriptor {
|
||||
if (mode == "r") {
|
||||
val url = CacheFileProviderUrl.fromUri(uri)
|
||||
val file = url.getFile(context)
|
||||
|
||||
return ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY)
|
||||
} else {
|
||||
throw IOException("illegal mode")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class CacheFileProviderUrl(
|
||||
val pathInCacheDirectory: String,
|
||||
val filename: String,
|
||||
val mimeType: String
|
||||
) {
|
||||
companion object {
|
||||
private const val PATH = "path"
|
||||
private const val FILENAME = "filename"
|
||||
private const val MIME_TYPE = "mimeType"
|
||||
|
||||
fun fromUri(uri: Uri) = CacheFileProviderUrl(
|
||||
pathInCacheDirectory = uri.getQueryParameter(PATH),
|
||||
filename = uri.getQueryParameter(FILENAME),
|
||||
mimeType = uri.getQueryParameter(MIME_TYPE)
|
||||
)
|
||||
|
||||
fun fromFile(file: File, filename: String, mimeType: String, context: Context) = CacheFileProviderUrl(
|
||||
filename = filename,
|
||||
mimeType = mimeType,
|
||||
pathInCacheDirectory = file.toRelativeString(context.externalCacheDir)
|
||||
)
|
||||
}
|
||||
|
||||
val serialized: Uri by lazy {
|
||||
Uri.Builder()
|
||||
.scheme("content")
|
||||
.authority(CacheFileProvider.AUTHORITY)
|
||||
.appendQueryParameter(PATH, pathInCacheDirectory)
|
||||
.appendQueryParameter(FILENAME, filename)
|
||||
.appendQueryParameter(MIME_TYPE, mimeType)
|
||||
.build()
|
||||
}
|
||||
|
||||
fun getFile(context: Context): File {
|
||||
return File(context.externalCacheDir, pathInCacheDirectory)
|
||||
}
|
||||
}
|
||||
@@ -4,8 +4,9 @@ import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.support.v4.os.CancellationSignal
|
||||
import android.util.Log
|
||||
import kotlinx.coroutines.experimental.launch
|
||||
import kotlinx.coroutines.experimental.suspendCancellableCoroutine
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import net.syncthing.java.bep.BlockPullerStatus
|
||||
import net.syncthing.java.client.SyncthingClient
|
||||
import net.syncthing.java.core.beans.FileInfo
|
||||
@@ -13,6 +14,8 @@ import net.syncthing.lite.BuildConfig
|
||||
import org.apache.commons.io.FileUtils
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.resumeWithException
|
||||
|
||||
class DownloadFileTask(private val fileStorageDirectory: File,
|
||||
syncthingClient: SyncthingClient,
|
||||
@@ -58,7 +61,7 @@ class DownloadFileTask(private val fileStorageDirectory: File,
|
||||
init {
|
||||
val file = DownloadFilePath(fileStorageDirectory, fileInfo.hash!!)
|
||||
|
||||
launch {
|
||||
GlobalScope.launch {
|
||||
if (file.targetFile.exists()) {
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.d(TAG, "there is already a file")
|
||||
|
||||
@@ -6,8 +6,9 @@ import android.content.Context
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.util.Log
|
||||
import kotlinx.coroutines.experimental.android.UI
|
||||
import kotlinx.coroutines.experimental.async
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import net.syncthing.java.bep.FolderBrowser
|
||||
import net.syncthing.java.client.SyncthingClient
|
||||
import net.syncthing.java.core.beans.DeviceId
|
||||
@@ -35,7 +36,7 @@ class LibraryHandler(context: Context,
|
||||
|
||||
private val libraryManager = DefaultLibraryManager.with(context)
|
||||
private val isStarted = AtomicBoolean(false)
|
||||
private val isListeningPortTakenInternal = MutableLiveData<Boolean>().apply { value = false }
|
||||
private val isListeningPortTakenInternal = MutableLiveData<Boolean>().apply { postValue(false) }
|
||||
|
||||
val isListeningPortTaken: LiveData<Boolean> = isListeningPortTakenInternal
|
||||
|
||||
@@ -88,7 +89,7 @@ class LibraryHandler(context: Context,
|
||||
private fun onIndexRecordAcquired(folderInfo: FolderInfo, newRecords: List<FileInfo>, indexInfo: IndexInfo) {
|
||||
Log.i(TAG, "handleIndexRecordEvent trigger folder list update from index record acquired")
|
||||
|
||||
async(UI) {
|
||||
GlobalScope.launch (Dispatchers.Main) {
|
||||
onIndexUpdateProgressListener(folderInfo, (indexInfo.getCompleted() * 100).toInt())
|
||||
}
|
||||
}
|
||||
@@ -96,7 +97,7 @@ class LibraryHandler(context: Context,
|
||||
private fun onRemoteIndexAcquired(folderInfo: FolderInfo) {
|
||||
Log.i(TAG, "handleIndexAcquiredEvent trigger folder list update from index acquired")
|
||||
|
||||
async(UI) {
|
||||
GlobalScope.launch (Dispatchers.Main) {
|
||||
onIndexUpdateCompleteListener(folderInfo)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,8 +8,8 @@ import android.provider.DocumentsContract.Document
|
||||
import android.provider.DocumentsContract.Root
|
||||
import android.provider.DocumentsProvider
|
||||
import android.util.Log
|
||||
import kotlinx.coroutines.experimental.cancel
|
||||
import kotlinx.coroutines.experimental.runBlocking
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import net.syncthing.java.bep.IndexBrowser
|
||||
import net.syncthing.java.core.beans.FileInfo
|
||||
import net.syncthing.java.core.beans.FolderInfo
|
||||
|
||||
@@ -4,8 +4,9 @@ import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.provider.OpenableColumns
|
||||
import kotlinx.coroutines.experimental.android.UI
|
||||
import kotlinx.coroutines.experimental.async
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import net.syncthing.java.core.beans.DeviceId
|
||||
import net.syncthing.java.core.beans.DeviceInfo
|
||||
import net.syncthing.lite.R
|
||||
@@ -47,12 +48,12 @@ object Util {
|
||||
if (!configuration.peerIds.contains(deviceId2)) {
|
||||
configuration.peers = configuration.peers + DeviceInfo(deviceId2, null)
|
||||
configuration.persistLater()
|
||||
async(UI) {
|
||||
GlobalScope.launch (Dispatchers.Main) {
|
||||
context?.toast(context.getString(R.string.device_import_success, deviceId2.shortId))
|
||||
onComplete()
|
||||
}
|
||||
} else {
|
||||
async(UI) {
|
||||
GlobalScope.launch (Dispatchers.Main) {
|
||||
context?.toast(context.getString(R.string.device_already_known, deviceId2.shortId))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
googleplay@nutomic.com
|
||||
@@ -0,0 +1 @@
|
||||
https://syncthing.net
|
||||
@@ -0,0 +1 @@
|
||||
en-GB
|
||||
@@ -0,0 +1,5 @@
|
||||
This project is an Android app, that works as a client for a Syncthing share (accessing Syncthing devices in the same way a client-server file sharing app access its proprietary server).
|
||||
|
||||
This is a client-oriented implementation, designed to work online by downloading and uploading files from an active device on the network (instead of synchronizing a local copy of the entire repository). This is quite different from the way the syncthing-android works, and its useful from those devices that cannot or wish not to download the entire repository (for example, mobile devices with limited storage available, wishing to access a syncthing share).
|
||||
|
||||
Source code: https://github.com/syncthing/syncthing-lite
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 73 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 40 KiB |
@@ -0,0 +1 @@
|
||||
A browser app for Syncthing-compatible shares
|
||||
@@ -0,0 +1 @@
|
||||
Syncthing Lite
|
||||
@@ -0,0 +1,6 @@
|
||||
- add option to export files
|
||||
- send correct file names to apps by which files are opened
|
||||
- adaptive icon
|
||||
- updated translations
|
||||
- validate discovery servers
|
||||
- bugfixes
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24.0"
|
||||
android:viewportHeight="24.0">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M17,3L5,3c-1.11,0 -2,0.9 -2,2v14c0,1.1 0.89,2 2,2h14c1.1,0 2,-0.9 2,-2L21,7l-4,-4zM12,19c-1.66,0 -3,-1.34 -3,-3s1.34,-3 3,-3 3,1.34 3,3 -1.34,3 -3,3zM15,9L5,9L5,5h10v4z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,35 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<data>
|
||||
<variable
|
||||
name="filename"
|
||||
type="String" />
|
||||
</data>
|
||||
|
||||
<LinearLayout
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<TextView
|
||||
android:padding="8dp"
|
||||
android:textAppearance="?android:textAppearanceMedium"
|
||||
android:text="@{filename}"
|
||||
tools:text="Filename.type"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
<Button
|
||||
android:drawableStart="@drawable/ic_save_black_24dp"
|
||||
android:id="@+id/save_as_button"
|
||||
android:padding="8dp"
|
||||
android:gravity="start|center_vertical"
|
||||
android:text="@string/dialog_file_save_as"
|
||||
android:background="?selectableItemBackground"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
</LinearLayout>
|
||||
</layout>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@mipmap/ic_background"/>
|
||||
<foreground android:drawable="@mipmap/ic_foreground"/>
|
||||
</adaptive-icon>
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 116 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 7.6 KiB |
@@ -19,7 +19,7 @@
|
||||
<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$simportado con éxito</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>
|
||||
|
||||
@@ -40,5 +40,11 @@
|
||||
<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>
|
||||
</resources>
|
||||
<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>
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
<resources>
|
||||
</resources>
|
||||
@@ -3,33 +3,47 @@
|
||||
<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="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_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="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="remove_device_title">移除设备: %1$s</string>
|
||||
<string name="device_import_success">成功导入的设备: %1$s</string>
|
||||
<string name="device_already_known">已经存在的设备: %1$s</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_two_title">添加一个设备</string>
|
||||
<string name="intro_page_three_title">分享您的文件夹</string>
|
||||
<string name="intro_page_two_description">输入一个 Syncthing 设备 ID,或者通过 QR 码扫描一个设备 ID</string>
|
||||
<string name="settings">设定</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_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>
|
||||
</resources>
|
||||
<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>
|
||||
|
||||
@@ -53,4 +53,5 @@
|
||||
or the connection was interrupted.
|
||||
This does not apply to local discovery connections.
|
||||
</string>
|
||||
<string name="dialog_file_save_as">Save as</string>
|
||||
</resources>
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<paths>
|
||||
<external-cache-path name="files" path="/" />
|
||||
</paths>
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||
|
||||
buildscript {
|
||||
ext.kotlin_version = '1.2.61'
|
||||
ext.kotlin_version = '1.3.0'
|
||||
ext.support_version = '27.0.2'
|
||||
ext.build_tools_version = '3.2.0'
|
||||
ext.anko_version = '0.10.7'
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -4,7 +4,6 @@ import com.google.gson.stream.JsonReader
|
||||
import com.google.gson.stream.JsonWriter
|
||||
import net.syncthing.java.core.utils.NetworkUtils
|
||||
import org.apache.commons.codec.binary.Base32
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.io.IOException
|
||||
|
||||
data class DeviceId @Throws(IOException::class) constructor(val deviceId: String) {
|
||||
|
||||
@@ -14,8 +14,7 @@
|
||||
package net.syncthing.java.core.beans
|
||||
|
||||
import org.apache.commons.io.FileUtils
|
||||
|
||||
import java.util.Date
|
||||
import java.util.*
|
||||
|
||||
class FolderStats private constructor(val fileCount: Long, val dirCount: Long, val size: Long, val lastUpdate: Date, folder: String, label: String?) : FolderInfo(folder, label) {
|
||||
|
||||
|
||||
@@ -12,7 +12,8 @@ data class Config(
|
||||
val folders: Set<FolderInfo>,
|
||||
val localDeviceName: String,
|
||||
val localDeviceId: String,
|
||||
val discoveryServers: Set<String>,
|
||||
val customDiscoveryServers: Set<DiscoveryServer>,
|
||||
val useDefaultDiscoveryServers: Boolean,
|
||||
val keystoreAlgorithm: String,
|
||||
val keystoreData: String
|
||||
) {
|
||||
@@ -21,7 +22,8 @@ data class Config(
|
||||
private const val FOLDERS = "folders"
|
||||
private const val LOCAL_DEVICE_NAME = "localDeviceName"
|
||||
private const val LOCAL_DEVICE_ID = "localDeviceId"
|
||||
private const val DISCOVERY_SERVERS = "discoveryServers"
|
||||
private const val USE_DEFAULT_DISCOVERY_SERVERS = "useDefaultDiscoveryServers"
|
||||
private const val CUSTOM_DISCOVERY_SERVERS = "customDiscoveryServers"
|
||||
private const val KEYSTORE_ALGORITHM = "keystoreAlgorithm"
|
||||
private const val KEYSTORE_DATA = "keystoreData"
|
||||
|
||||
@@ -30,7 +32,8 @@ data class Config(
|
||||
var folders: Set<FolderInfo>? = null
|
||||
var localDeviceName: String? = null
|
||||
var localDeviceId: String? = null
|
||||
var discoveryServers: Set<String>? = null
|
||||
var customDiscoveryServers = emptySet<DiscoveryServer>() // this field was added later, so it needs an default value
|
||||
var useDefaultDiscoveryServers = true // this field was added later, so it needs an default value
|
||||
var keystoreAlgorithm: String? = null
|
||||
var keystoreData: String? = null
|
||||
|
||||
@@ -61,17 +64,16 @@ data class Config(
|
||||
}
|
||||
LOCAL_DEVICE_NAME -> localDeviceName = reader.nextString()
|
||||
LOCAL_DEVICE_ID -> localDeviceId = reader.nextString()
|
||||
DISCOVERY_SERVERS -> {
|
||||
val newDiscoveryServers = HashSet<String>()
|
||||
|
||||
reader.beginArray()
|
||||
while (reader.hasNext()) {
|
||||
newDiscoveryServers.add(reader.nextString())
|
||||
CUSTOM_DISCOVERY_SERVERS -> {
|
||||
customDiscoveryServers = mutableSetOf<DiscoveryServer>().apply {
|
||||
reader.beginArray()
|
||||
while (reader.hasNext()) {
|
||||
add(DiscoveryServer.parse(reader))
|
||||
}
|
||||
reader.endArray()
|
||||
}
|
||||
reader.endArray()
|
||||
|
||||
discoveryServers = Collections.unmodifiableSet(newDiscoveryServers)
|
||||
}
|
||||
USE_DEFAULT_DISCOVERY_SERVERS -> useDefaultDiscoveryServers = reader.nextBoolean()
|
||||
KEYSTORE_ALGORITHM -> keystoreAlgorithm = reader.nextString()
|
||||
KEYSTORE_DATA -> keystoreData = reader.nextString()
|
||||
else -> reader.skipValue()
|
||||
@@ -84,7 +86,8 @@ data class Config(
|
||||
folders = folders!!,
|
||||
localDeviceName = localDeviceName!!,
|
||||
localDeviceId = localDeviceId!!,
|
||||
discoveryServers = discoveryServers!!,
|
||||
customDiscoveryServers = customDiscoveryServers,
|
||||
useDefaultDiscoveryServers = useDefaultDiscoveryServers,
|
||||
keystoreAlgorithm = keystoreAlgorithm!!,
|
||||
keystoreData = keystoreData!!
|
||||
)
|
||||
@@ -105,10 +108,12 @@ data class Config(
|
||||
writer.name(LOCAL_DEVICE_NAME).value(localDeviceName)
|
||||
writer.name(LOCAL_DEVICE_ID).value(localDeviceId)
|
||||
|
||||
writer.name(DISCOVERY_SERVERS).beginArray()
|
||||
discoveryServers.forEach { writer.value(it) }
|
||||
writer.name(CUSTOM_DISCOVERY_SERVERS).beginArray()
|
||||
customDiscoveryServers.forEach { it.serialize(writer) }
|
||||
writer.endArray()
|
||||
|
||||
writer.name(USE_DEFAULT_DISCOVERY_SERVERS).value(useDefaultDiscoveryServers)
|
||||
|
||||
writer.name(KEYSTORE_ALGORITHM).value(keystoreAlgorithm)
|
||||
writer.name(KEYSTORE_DATA).value(keystoreData)
|
||||
|
||||
@@ -117,5 +122,6 @@ data class Config(
|
||||
|
||||
// Exclude keystoreData from toString()
|
||||
override fun toString() = "Config(peers=$peers, folders=$folders, localDeviceName=$localDeviceName, " +
|
||||
"localDeviceId=$localDeviceId, discoveryServers=$discoveryServers, keystoreAlgorithm=$keystoreAlgorithm)"
|
||||
"localDeviceId=$localDeviceId, customDiscoveryServers=$customDiscoveryServers, " +
|
||||
"useDefaultDiscoveryServers=$useDefaultDiscoveryServers, keystoreAlgorithm=$keystoreAlgorithm)"
|
||||
}
|
||||
|
||||
+6
-22
@@ -38,26 +38,15 @@ class Configuration(configFolder: File = DefaultConfigFolder) {
|
||||
isSaved = false
|
||||
config = Config(peers = setOf(), folders = setOf(),
|
||||
localDeviceName = localDeviceName,
|
||||
discoveryServers = Companion.DiscoveryServers,
|
||||
localDeviceId = keystoreData.first.deviceId,
|
||||
keystoreData = Base64.toBase64String(keystoreData.second),
|
||||
keystoreAlgorithm = keystoreData.third)
|
||||
keystoreAlgorithm = keystoreData.third,
|
||||
customDiscoveryServers = emptySet(),
|
||||
useDefaultDiscoveryServers = true
|
||||
)
|
||||
persistNow()
|
||||
} else {
|
||||
config = Config.parse(JsonReader(StringReader(configFile.readText())))
|
||||
|
||||
// automatic migration if the old config was used
|
||||
if (config.discoveryServers == OldDiscoveryServers) {
|
||||
config = Config(
|
||||
peers = config.peers,
|
||||
folders = config.folders,
|
||||
localDeviceName = config.localDeviceName,
|
||||
localDeviceId = config.localDeviceId,
|
||||
discoveryServers = Companion.DiscoveryServers,
|
||||
keystoreAlgorithm = config.keystoreAlgorithm,
|
||||
keystoreData = config.keystoreData
|
||||
)
|
||||
}
|
||||
}
|
||||
logger.debug("Loaded config = $config")
|
||||
}
|
||||
@@ -66,11 +55,6 @@ class Configuration(configFolder: File = DefaultConfigFolder) {
|
||||
private val DefaultConfigFolder = File(System.getProperty("user.home"), ".config/syncthing-java/")
|
||||
private const val ConfigFileName = "config.json"
|
||||
private const val DatabaseFolderName = "database"
|
||||
private val DiscoveryServers = setOf(
|
||||
"discovery.syncthing.net", "discovery-v4.syncthing.net", "discovery-v6.syncthing.net")
|
||||
private val OldDiscoveryServers = setOf(
|
||||
"discovery-v4-1.syncthing.net", "discovery-v4-2.syncthing.net", "discovery-v4-3.syncthing.net",
|
||||
"discovery-v6-1.syncthing.net", "discovery-v6-2.syncthing.net", "discovery-v6-3.syncthing.net")
|
||||
}
|
||||
|
||||
val instanceId = Math.abs(Random().nextLong())
|
||||
@@ -78,8 +62,8 @@ class Configuration(configFolder: File = DefaultConfigFolder) {
|
||||
val localDeviceId: DeviceId
|
||||
get() = DeviceId(config.localDeviceId)
|
||||
|
||||
val discoveryServers: Set<String>
|
||||
get() = config.discoveryServers
|
||||
val discoveryServers: Set<DiscoveryServer>
|
||||
get() = config.customDiscoveryServers + (if (config.useDefaultDiscoveryServers) DiscoveryServer.defaultDiscoveryServers else emptySet())
|
||||
|
||||
val keystoreData: ByteArray
|
||||
get() = Base64.decode(config.keystoreData)
|
||||
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
package net.syncthing.java.core.configuration
|
||||
|
||||
import com.google.gson.stream.JsonReader
|
||||
import com.google.gson.stream.JsonWriter
|
||||
import net.syncthing.java.core.beans.DeviceId
|
||||
|
||||
data class DiscoveryServer(
|
||||
val hostname: String,
|
||||
val useForLookup: Boolean,
|
||||
val useForAnnounce: Boolean,
|
||||
val deviceId: DeviceId?
|
||||
) {
|
||||
companion object {
|
||||
private const val JSON_HOSTNAME = "host"
|
||||
private const val JSON_LOOKUP = "lookup"
|
||||
private const val JSON_ANNOUNCE = "announce"
|
||||
private const val JSON_DEVICE_ID = "deviceId"
|
||||
|
||||
// from https://github.com/syncthing/syncthing/blob/add12b43aa0bdf5e67d8f421c57a5ecafb3d25fa/lib/config/config.go#L50-L64
|
||||
// (if you update it, use the most recent commit)
|
||||
private val serverDeviceId = DeviceId("LYXKCHX-VI3NYZR-ALCJBHF-WMZYSPK-QG6QJA3-MPFYMSO-U56GTUK-NA2MIAW")
|
||||
|
||||
private val lookupServer = DiscoveryServer(
|
||||
hostname = "discovery.syncthing.net",
|
||||
useForLookup = true,
|
||||
useForAnnounce = false,
|
||||
deviceId = serverDeviceId
|
||||
)
|
||||
|
||||
private val announceIpV4Server = DiscoveryServer(
|
||||
hostname = "discovery-v4.syncthing.net",
|
||||
useForLookup = false,
|
||||
useForAnnounce = true,
|
||||
deviceId = serverDeviceId
|
||||
)
|
||||
|
||||
private val announceIpV6Server = DiscoveryServer(
|
||||
hostname = "discovery-v6.syncthing.net",
|
||||
useForLookup = false,
|
||||
useForAnnounce = true,
|
||||
deviceId = serverDeviceId
|
||||
)
|
||||
|
||||
val defaultDiscoveryServers = setOf(lookupServer, announceIpV4Server, announceIpV6Server)
|
||||
|
||||
fun parse(reader: JsonReader): DiscoveryServer {
|
||||
var hostname: String? = null
|
||||
var useForLookup: Boolean? = null
|
||||
var useForAnnounce: Boolean? = null
|
||||
var deviceId: DeviceId? = null
|
||||
|
||||
reader.beginObject()
|
||||
while (reader.hasNext()) {
|
||||
when (reader.nextName()) {
|
||||
JSON_HOSTNAME -> hostname = reader.nextString()
|
||||
JSON_LOOKUP -> useForLookup = reader.nextBoolean()
|
||||
JSON_ANNOUNCE -> useForAnnounce = reader.nextBoolean()
|
||||
JSON_DEVICE_ID -> deviceId = DeviceId(reader.nextString())
|
||||
else -> reader.skipValue()
|
||||
}
|
||||
}
|
||||
reader.endObject()
|
||||
|
||||
return DiscoveryServer(
|
||||
hostname = hostname!!,
|
||||
useForLookup = useForLookup!!,
|
||||
useForAnnounce = useForAnnounce!!,
|
||||
deviceId = deviceId
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun serialize(writer: JsonWriter) {
|
||||
writer.beginObject()
|
||||
|
||||
writer.name(JSON_HOSTNAME).value(hostname)
|
||||
writer.name(JSON_LOOKUP).value(useForLookup)
|
||||
writer.name(JSON_ANNOUNCE).value(useForAnnounce)
|
||||
|
||||
if (deviceId != null) {
|
||||
writer.name(JSON_DEVICE_ID).value(deviceId.deviceId)
|
||||
}
|
||||
|
||||
writer.endObject()
|
||||
}
|
||||
}
|
||||
+23
-17
@@ -44,8 +44,6 @@ import javax.security.auth.x500.X500Principal
|
||||
|
||||
class KeystoreHandler private constructor(private val keyStore: KeyStore) {
|
||||
|
||||
private val logger = LoggerFactory.getLogger(javaClass)
|
||||
|
||||
class CryptoException internal constructor(t: Throwable) : GeneralSecurityException(t)
|
||||
|
||||
private val socketFactory: SSLSocketFactory
|
||||
@@ -116,21 +114,6 @@ class KeystoreHandler private constructor(private val keyStore: KeyStore) {
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(SSLPeerUnverifiedException::class, CertificateException::class)
|
||||
fun checkSocketCertificate(socket: SSLSocket, deviceId: DeviceId) {
|
||||
val session = socket.session
|
||||
val certs = session.peerCertificates.toList()
|
||||
val certificateFactory = CertificateFactory.getInstance("X.509")
|
||||
val certPath = certificateFactory.generateCertPath(certs)
|
||||
val certificate = certPath.certificates[0]
|
||||
NetworkUtils.assertProtocol(certificate is X509Certificate)
|
||||
val derData = certificate.encoded
|
||||
val deviceIdFromCertificate = derDataToDeviceId(derData)
|
||||
logger.trace("remote pem certificate =\n{}", derToPem(derData))
|
||||
NetworkUtils.assertProtocol(deviceIdFromCertificate == deviceId, {"device id mismatch! expected = $deviceId, got = $deviceIdFromCertificate"})
|
||||
logger.debug("remote ssl certificate match deviceId = {}", deviceId)
|
||||
}
|
||||
|
||||
@Throws(CryptoException::class, IOException::class)
|
||||
fun wrapSocket(relayConnection: RelayConnection, protocol: String): SSLSocket {
|
||||
return wrapSocket(relayConnection.getSocket(), relayConnection.isServerSocket(), protocol)
|
||||
@@ -269,6 +252,29 @@ class KeystoreHandler private constructor(private val keyStore: KeyStore) {
|
||||
|
||||
const val BEP = "bep/1.0"
|
||||
const val RELAY = "bep-relay"
|
||||
|
||||
private val logger = LoggerFactory.getLogger(KeystoreHandler::class.java)
|
||||
|
||||
@Throws(SSLPeerUnverifiedException::class, CertificateException::class)
|
||||
fun assertSocketCertificateValid(socket: SSLSocket, deviceId: DeviceId) {
|
||||
val session = socket.session
|
||||
val certs = session.peerCertificates.toList()
|
||||
val certificateFactory = CertificateFactory.getInstance("X.509")
|
||||
val certPath = certificateFactory.generateCertPath(certs)
|
||||
val certificate = certPath.certificates[0]
|
||||
|
||||
assertSocketCertificateValid(certificate, deviceId)
|
||||
}
|
||||
|
||||
@Throws(SSLPeerUnverifiedException::class, CertificateException::class)
|
||||
fun assertSocketCertificateValid(certificate: Certificate, deviceId: DeviceId) {
|
||||
NetworkUtils.assertProtocol(certificate is X509Certificate)
|
||||
val derData = certificate.encoded
|
||||
val deviceIdFromCertificate = derDataToDeviceId(derData)
|
||||
logger.trace("remote pem certificate =\n{}", derToPem(derData))
|
||||
NetworkUtils.assertProtocol(deviceIdFromCertificate == deviceId, {"device id mismatch! expected = $deviceId, got = $deviceIdFromCertificate"})
|
||||
logger.debug("remote ssl certificate match deviceId = {}", deviceId)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -8,7 +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:0.30.2'
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.0.0'
|
||||
}
|
||||
|
||||
run {
|
||||
|
||||
+4
-4
@@ -14,10 +14,10 @@
|
||||
*/
|
||||
package net.syncthing.java.discovery
|
||||
|
||||
import kotlinx.coroutines.experimental.CancellationException
|
||||
import kotlinx.coroutines.experimental.runBlocking
|
||||
import kotlinx.coroutines.experimental.selects.select
|
||||
import kotlinx.coroutines.experimental.withTimeout
|
||||
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
|
||||
|
||||
+2
-2
@@ -13,8 +13,8 @@
|
||||
*/
|
||||
package net.syncthing.java.discovery
|
||||
|
||||
import kotlinx.coroutines.experimental.channels.Channel
|
||||
import kotlinx.coroutines.experimental.channels.ReceiveChannel
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.channels.ReceiveChannel
|
||||
import net.syncthing.java.core.beans.DeviceAddress
|
||||
import net.syncthing.java.core.beans.DeviceId
|
||||
|
||||
|
||||
+2
-2
@@ -14,8 +14,8 @@
|
||||
*/
|
||||
package net.syncthing.java.discovery
|
||||
|
||||
import kotlinx.coroutines.experimental.GlobalScope
|
||||
import kotlinx.coroutines.experimental.launch
|
||||
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
|
||||
|
||||
+15
-13
@@ -14,14 +14,14 @@
|
||||
*/
|
||||
package net.syncthing.java.discovery.protocol
|
||||
|
||||
import kotlinx.coroutines.experimental.GlobalScope
|
||||
import kotlinx.coroutines.experimental.async
|
||||
import kotlinx.coroutines.experimental.coroutineScope
|
||||
import kotlinx.coroutines.experimental.launch
|
||||
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 net.syncthing.java.core.configuration.DiscoveryServer
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.io.IOException
|
||||
|
||||
@@ -38,7 +38,7 @@ internal class GlobalDiscoveryHandler(private val configuration: Configuration)
|
||||
}
|
||||
|
||||
suspend fun query(deviceIds: Collection<DeviceId>): List<DeviceAddress> {
|
||||
val discoveryServers = pickAnnounceServers()
|
||||
val discoveryServers = getLookupServers()
|
||||
|
||||
return coroutineScope {
|
||||
deviceIds
|
||||
@@ -57,15 +57,13 @@ internal class GlobalDiscoveryHandler(private val configuration: Configuration)
|
||||
}
|
||||
|
||||
suspend fun query(deviceId: DeviceId) = queryAnnounceServers(
|
||||
servers = pickAnnounceServers(),
|
||||
servers = getLookupServers(),
|
||||
deviceId = deviceId
|
||||
)
|
||||
|
||||
suspend fun pickAnnounceServers() = AddressRanker
|
||||
.pingAddresses(configuration.discoveryServers.map { DeviceAddress(it, "tcp://$it:443") })
|
||||
.map { it.deviceId }
|
||||
fun getLookupServers() = configuration.discoveryServers.filter { it.useForLookup }
|
||||
|
||||
suspend fun queryAnnounceServers(servers: List<String>, deviceId: DeviceId) = coroutineScope {
|
||||
suspend fun queryAnnounceServers(servers: List<DiscoveryServer>, deviceId: DeviceId) = coroutineScope {
|
||||
servers
|
||||
.map { server ->
|
||||
async {
|
||||
@@ -91,9 +89,13 @@ internal class GlobalDiscoveryHandler(private val configuration: Configuration)
|
||||
}
|
||||
|
||||
companion object {
|
||||
suspend fun queryAnnounceServer(server: String, deviceId: DeviceId) =
|
||||
suspend fun queryAnnounceServer(server: DiscoveryServer, deviceId: DeviceId) =
|
||||
GlobalDiscoveryUtil
|
||||
.queryAnnounceServer(server, deviceId)
|
||||
.queryAnnounceServer(
|
||||
server = server.hostname,
|
||||
requestedDeviceId = deviceId,
|
||||
serverDeviceId = server.deviceId
|
||||
)
|
||||
.addresses.map { DeviceAddress(deviceId.deviceId, it) }
|
||||
}
|
||||
}
|
||||
|
||||
+29
-5
@@ -15,15 +15,17 @@
|
||||
package net.syncthing.java.discovery.protocol
|
||||
|
||||
import com.google.gson.stream.JsonReader
|
||||
import kotlinx.coroutines.experimental.Dispatchers
|
||||
import kotlinx.coroutines.experimental.withContext
|
||||
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
|
||||
@@ -34,11 +36,33 @@ object GlobalDiscoveryUtil {
|
||||
private fun queryAnnounceServerUrl(server: String, deviceId: DeviceId) =
|
||||
"https://$server/v2/?device=${deviceId.deviceId}"
|
||||
|
||||
suspend fun queryAnnounceServer(server: String, deviceId: DeviceId): AnnouncementMessage {
|
||||
suspend fun queryAnnounceServer(
|
||||
server: String,
|
||||
requestedDeviceId: DeviceId,
|
||||
serverDeviceId: DeviceId?
|
||||
): AnnouncementMessage {
|
||||
return withContext(Dispatchers.IO) {
|
||||
val url = URL(queryAnnounceServerUrl(server, deviceId))
|
||||
val url = URL(queryAnnounceServerUrl(server, requestedDeviceId))
|
||||
val connection = (url.openConnection() as HttpsURLConnection).apply {
|
||||
hostnameVerifier = HostnameVerifier { _, _ -> true }
|
||||
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?) {
|
||||
|
||||
+4
-4
@@ -14,10 +14,10 @@
|
||||
*/
|
||||
package net.syncthing.java.discovery.protocol
|
||||
|
||||
import kotlinx.coroutines.experimental.GlobalScope
|
||||
import kotlinx.coroutines.experimental.Job
|
||||
import kotlinx.coroutines.experimental.channels.consumeEach
|
||||
import kotlinx.coroutines.experimental.launch
|
||||
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 org.slf4j.LoggerFactory
|
||||
|
||||
+5
-5
@@ -15,11 +15,11 @@
|
||||
package net.syncthing.java.discovery.protocol
|
||||
|
||||
import com.google.protobuf.ByteString
|
||||
import kotlinx.coroutines.experimental.Dispatchers
|
||||
import kotlinx.coroutines.experimental.GlobalScope
|
||||
import kotlinx.coroutines.experimental.channels.ReceiveChannel
|
||||
import kotlinx.coroutines.experimental.channels.produce
|
||||
import kotlinx.coroutines.experimental.withContext
|
||||
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
|
||||
|
||||
+1
-1
@@ -14,7 +14,7 @@
|
||||
*/
|
||||
package net.syncthing.java.discovery.utils
|
||||
|
||||
import kotlinx.coroutines.experimental.*
|
||||
import kotlinx.coroutines.*
|
||||
import net.syncthing.java.core.beans.DeviceAddress
|
||||
import net.syncthing.java.core.beans.DeviceAddress.AddressType
|
||||
import org.slf4j.LoggerFactory
|
||||
|
||||
Reference in New Issue
Block a user