Compare commits
28 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f9b91f6ef8 | |||
| 01fb92e2c9 | |||
| 4b519e84e3 | |||
| f3d51f0cb9 | |||
| fa30beb9d5 | |||
| 919fdc31bd | |||
| b3f2af0ee7 | |||
| f33364939b | |||
| 1a773daf24 | |||
| b115a99907 | |||
| 5d09d011b1 | |||
| 71a433edf6 | |||
| 974817b7a3 | |||
| f00760bddd | |||
| 5f539c4149 | |||
| 1869a49c2c | |||
| 91289b05ce | |||
| 98bc67939f | |||
| fcb31ae9fa | |||
| 147ad6abcc | |||
| 4c13af3662 | |||
| 17f9ad336c | |||
| 852fc0d230 | |||
| 0032726e3e | |||
| c70211bc24 | |||
| a7f80fa45c | |||
| 461d64950b | |||
| c37832d084 |
+4
-3
@@ -1,8 +1,9 @@
|
||||
# Releasing
|
||||
|
||||
- do tests
|
||||
- update translations using ``tx pull -a -af`` (as extra merge request or branch for the case it does not build correctly)
|
||||
- update the version name and version code of the app
|
||||
- update translations using ``tx pull -af`` (as extra merge request or branch for the case it does not build correctly)
|
||||
- update the version name and version code of the app [here](https://github.com/syncthing/syncthing-lite/blob/master/app/build.gradle)
|
||||
- update the changelog at [app/src/main/play/en-GB/whatsnew](https://github.com/syncthing/syncthing-lite/blob/master/app/src/main/play/en-GB/whatsnew)
|
||||
- create a tag/ release in GitHub with an changelog; The tag name should be the version number
|
||||
- F-Droid picks up the release by the tag; additonally, the tag triggers a CI build which uploads the generated APK to Google Play
|
||||
- trigger a release at <https://build.syncthing.net/> to publish the release to google play
|
||||
- F-Droid picks up the release by the tag
|
||||
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
# Roadmap
|
||||
|
||||
## What should happen
|
||||
|
||||
- fixing bugs and crashs
|
||||
- just create a issue WITH a detailed crash report (not: it does not work)
|
||||
- search if there is an other issue for it before creating a new one
|
||||
- add option to manually select the IP address of an device (<https://github.com/syncthing/syncthing-lite/issues/25>)
|
||||
- allow custom discovery servers or disabling device discovery (<https://github.com/syncthing/syncthing-lite/issues/105>)
|
||||
- downloading all files of an folder (<https://github.com/syncthing/syncthing-lite/issues/34>)
|
||||
- better server offline handling (<https://github.com/syncthing/syncthing-lite/issues/63>)
|
||||
- file uploading support (it currently does not work) <https://github.com/syncthing/syncthing-lite/issues/70>
|
||||
|
||||
## What could happen
|
||||
|
||||
- thumbnails (<https://github.com/syncthing/syncthing-lite/issues/37>)
|
||||
|
||||
## What will not happen
|
||||
|
||||
- additional encryption within the App (see <https://github.com/syncthing/syncthing-lite/issues/85>)
|
||||
+3
-13
@@ -6,7 +6,6 @@ apply plugin: 'com.github.triplet.play'
|
||||
|
||||
android {
|
||||
compileSdkVersion 27
|
||||
buildToolsVersion "28.0.2"
|
||||
dataBinding.enabled = true
|
||||
|
||||
playAccountConfigs {
|
||||
@@ -19,8 +18,8 @@ android {
|
||||
applicationId "net.syncthing.lite"
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 26
|
||||
versionCode 16
|
||||
versionName "0.3.6"
|
||||
versionCode 19
|
||||
versionName "0.3.9"
|
||||
multiDexEnabled true
|
||||
playAccountConfig = playAccountConfigs.defaultAccountConfig
|
||||
}
|
||||
@@ -77,16 +76,6 @@ dependencies {
|
||||
implementation "com.android.support:support-v4:$support_version"
|
||||
implementation 'android.arch.lifecycle:extensions:1.1.1'
|
||||
|
||||
/**
|
||||
* syncthing-java depends on the Apache HTTP Client
|
||||
* https://github.com/syncthing/syncthing-java/blob/dd020737ba5fc6a7c681a1d258025b8ddb2e8f67/core/build.gradle#L9
|
||||
*
|
||||
* Android itself contains an older version of this HTTP Client. Due to that, there is an
|
||||
* extra version of it which does not cause conflicts with the builtin client of Android.
|
||||
*
|
||||
* This extra implementation is included below. As this other version is used,
|
||||
* it's ignored as dependency of syncthing-java.
|
||||
*/
|
||||
implementation(project(':syncthing-client')) {
|
||||
exclude group: 'commons-logging', module: 'commons-logging'
|
||||
exclude group: 'org.slf4j'
|
||||
@@ -99,4 +88,5 @@ dependencies {
|
||||
implementation 'com.github.apl-devs:appintro:v4.2.3'
|
||||
|
||||
implementation project(':syncthing-repository-android')
|
||||
implementation project(':syncthing-temp-repository-encryption')
|
||||
}
|
||||
|
||||
Vendored
+3
@@ -28,6 +28,9 @@
|
||||
volatile <fields>;
|
||||
}
|
||||
|
||||
# fix detecting the main dispatcher
|
||||
-keepnames class kotlinx.coroutines.internal.MainDispatcherFactory {}
|
||||
|
||||
# disable warnings
|
||||
-dontwarn com.google.protobuf.UnsafeUtil
|
||||
-dontwarn com.google.protobuf.UnsafeUtil$1
|
||||
|
||||
@@ -17,6 +17,7 @@ import net.syncthing.lite.R
|
||||
import net.syncthing.lite.adapters.FolderContentsAdapter
|
||||
import net.syncthing.lite.adapters.FolderContentsListener
|
||||
import net.syncthing.lite.databinding.ActivityFolderBrowserBinding
|
||||
import net.syncthing.lite.dialogs.EnableFolderSyncForNewDeviceDialog
|
||||
import net.syncthing.lite.dialogs.FileMenuDialogFragment
|
||||
import net.syncthing.lite.dialogs.FileUploadDialog
|
||||
import net.syncthing.lite.dialogs.ReconnectIssueDialogFragment
|
||||
@@ -109,6 +110,29 @@ class FolderBrowserActivity : SyncthingActivity() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (savedInstanceState == null) {
|
||||
launch {
|
||||
val devicesToAskFor = libraryHandler.libraryManager.withLibrary {
|
||||
val folderInfo = it.configuration.folders.find { it.folderId == folder }
|
||||
val notIgnoredBlacklistEntries = folderInfo?.notIgnoredBlacklistEntries ?: emptySet()
|
||||
|
||||
notIgnoredBlacklistEntries.mapNotNull { deviceId ->
|
||||
it.configuration.peers.find { peer -> peer.deviceId == deviceId }
|
||||
}
|
||||
}
|
||||
|
||||
if (devicesToAskFor.isNotEmpty()) {
|
||||
EnableFolderSyncForNewDeviceDialog.newInstance(
|
||||
folderId = folder,
|
||||
devices = devicesToAskFor,
|
||||
folderName = libraryHandler.libraryManager.withLibrary {
|
||||
it.configuration.folders.find { it.folderId == folder }?.label ?: folder
|
||||
}
|
||||
).show(supportFragmentManager)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
|
||||
@@ -12,9 +12,9 @@ import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Button
|
||||
import com.github.paolorotolo.appintro.AppIntro
|
||||
import com.github.paolorotolo.appintro.ISlidePolicy
|
||||
import com.google.zxing.integration.android.IntentIntegrator
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.channels.consumeEach
|
||||
import kotlinx.coroutines.launch
|
||||
import net.syncthing.java.core.beans.DeviceId
|
||||
import net.syncthing.lite.R
|
||||
@@ -40,14 +40,6 @@ class IntroActivity : AppIntro() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
// Disable continue button on second slide until a valid device ID is entered.
|
||||
nextButton.setOnClickListener {
|
||||
val fragment = fragments[pager.currentItem]
|
||||
if (fragment !is IntroFragmentTwo || fragment.isDeviceIdValid()) {
|
||||
pager.goToNextSlide()
|
||||
}
|
||||
}
|
||||
|
||||
addSlide(IntroFragmentOne())
|
||||
addSlide(IntroFragmentTwo())
|
||||
addSlide(IntroFragmentThree())
|
||||
@@ -72,6 +64,19 @@ class IntroActivity : AppIntro() {
|
||||
* Display some simple welcome text.
|
||||
*/
|
||||
class IntroFragmentOne : SyncthingFragment() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
launch {
|
||||
libraryHandler.libraryManager.withLibrary { library ->
|
||||
library.configuration.update { oldConfig ->
|
||||
oldConfig.copy(localDeviceName = Util.getDeviceName())
|
||||
}
|
||||
|
||||
library.configuration.persistLater()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
val binding = FragmentIntroOneBinding.inflate(inflater, container, false)
|
||||
@@ -80,21 +85,12 @@ class IntroActivity : AppIntro() {
|
||||
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onLibraryLoaded() {
|
||||
super.onLibraryLoaded()
|
||||
|
||||
libraryHandler.configuration { config ->
|
||||
config.localDeviceName = Util.getDeviceName()
|
||||
config.persistLater()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Display device ID entry field and QR scanner option.
|
||||
*/
|
||||
class IntroFragmentTwo : SyncthingFragment() {
|
||||
class IntroFragmentTwo : SyncthingFragment(), ISlidePolicy {
|
||||
|
||||
private lateinit var binding: FragmentIntroTwoBinding
|
||||
|
||||
@@ -122,7 +118,7 @@ class IntroActivity : AppIntro() {
|
||||
fun isDeviceIdValid(): Boolean {
|
||||
return try {
|
||||
val deviceId = binding.enterDeviceId.deviceId.text.toString()
|
||||
Util.importDeviceId(libraryHandler, context, deviceId, { })
|
||||
Util.importDeviceId(libraryHandler.libraryManager, context!!, deviceId, { })
|
||||
true
|
||||
} catch (e: IOException) {
|
||||
binding.enterDeviceId.deviceId.error = getString(R.string.invalid_device_id)
|
||||
@@ -130,6 +126,12 @@ class IntroActivity : AppIntro() {
|
||||
}
|
||||
}
|
||||
|
||||
override fun isPolicyRespected() = isDeviceIdValid()
|
||||
|
||||
override fun onUserIllegallyRequestedNextPage() {
|
||||
// nothing to do, but some user feedback would be nice
|
||||
}
|
||||
|
||||
private val addedDeviceIds = HashSet<DeviceId>()
|
||||
|
||||
override fun onResume() {
|
||||
@@ -176,32 +178,31 @@ class IntroActivity : AppIntro() {
|
||||
*/
|
||||
class IntroFragmentThree : SyncthingFragment() {
|
||||
|
||||
private lateinit var binding: FragmentIntroThreeBinding
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
binding = DataBindingUtil.inflate(inflater, R.layout.fragment_intro_three, container, false)
|
||||
val binding = FragmentIntroThreeBinding.inflate(inflater, container, false)
|
||||
|
||||
libraryHandler.library { config, client, _ ->
|
||||
GlobalScope.launch (Dispatchers.Main) {
|
||||
client.addOnConnectionChangedListener(this@IntroFragmentThree::onConnectionChanged)
|
||||
val deviceId = config.localDeviceId.deviceId
|
||||
val desc = activity?.getString(R.string.intro_page_three_description, "<b>$deviceId</b>")
|
||||
binding.description.text = Html.fromHtml(desc)
|
||||
launch {
|
||||
val ownDeviceId = libraryHandler.libraryManager.withLibrary { it.configuration.localDeviceId }
|
||||
|
||||
libraryHandler.subscribeToConnectionStatus().consumeEach {
|
||||
if (it.values.find { it.addresses.isNotEmpty() } != null) {
|
||||
val desc = activity?.getString(R.string.intro_page_three_description, "<b>$ownDeviceId</b>")
|
||||
binding.description.text = Html.fromHtml(desc)
|
||||
} else {
|
||||
binding.description.text = getString(R.string.intro_page_three_searching_device)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
launch {
|
||||
libraryHandler.subscribeToFolderStatusList().consumeEach {
|
||||
if (it.isNotEmpty()) {
|
||||
(activity as IntroActivity?)?.onDonePressed(this@IntroFragmentThree)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return binding.root
|
||||
}
|
||||
|
||||
private fun onConnectionChanged(deviceId: DeviceId) {
|
||||
libraryHandler.library { config, client, _ ->
|
||||
GlobalScope.launch (Dispatchers.Main) {
|
||||
if (config.folders.isNotEmpty()) {
|
||||
client.removeOnConnectionChangedListener(this@IntroFragmentThree::onConnectionChanged)
|
||||
(activity as IntroActivity?)?.onDonePressed(this@IntroFragmentThree)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,9 +102,10 @@ class MainActivity : SyncthingActivity() {
|
||||
}
|
||||
|
||||
private fun cleanCacheAndIndex() {
|
||||
GlobalScope.launch (Dispatchers.Main) {
|
||||
libraryHandler.syncthingClient { it.clearCacheAndIndex() }
|
||||
recreate()
|
||||
launch {
|
||||
libraryHandler.libraryManager.withLibrary {
|
||||
it.syncthingClient.clearCacheAndIndex()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,12 +3,15 @@ package net.syncthing.lite.adapters
|
||||
import android.support.v7.widget.RecyclerView
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import net.syncthing.java.bep.connectionactor.ConnectionInfo
|
||||
import net.syncthing.java.bep.connectionactor.ConnectionStatus
|
||||
import net.syncthing.java.core.beans.DeviceInfo
|
||||
import net.syncthing.lite.R
|
||||
import net.syncthing.lite.databinding.ListviewDeviceBinding
|
||||
import kotlin.properties.Delegates
|
||||
|
||||
class DevicesAdapter: RecyclerView.Adapter<DeviceViewHolder>() {
|
||||
var data: List<DeviceInfo> by Delegates.observable(listOf()) {
|
||||
var data: List<Pair<DeviceInfo, ConnectionInfo>> by Delegates.observable(listOf()) {
|
||||
_, _, _ -> notifyDataSetChanged()
|
||||
}
|
||||
|
||||
@@ -19,7 +22,7 @@ class DevicesAdapter: RecyclerView.Adapter<DeviceViewHolder>() {
|
||||
}
|
||||
|
||||
override fun getItemCount() = data.size
|
||||
override fun getItemId(position: Int) = data[position].deviceId.deviceId.hashCode().toLong()
|
||||
override fun getItemId(position: Int) = data[position].first.deviceId.deviceId.hashCode().toLong()
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = DeviceViewHolder(
|
||||
ListviewDeviceBinding.inflate(
|
||||
@@ -28,13 +31,23 @@ class DevicesAdapter: RecyclerView.Adapter<DeviceViewHolder>() {
|
||||
)
|
||||
|
||||
override fun onBindViewHolder(holder: DeviceViewHolder, position: Int) {
|
||||
val deviceStats = data[position]
|
||||
val binding = holder.binding
|
||||
val context = binding.root.context
|
||||
val (deviceInfo, connectionInfo) = data[position]
|
||||
|
||||
binding.name = deviceStats.name
|
||||
binding.isConnected = deviceStats.isConnected
|
||||
binding.name = deviceInfo.name
|
||||
binding.isConnected = connectionInfo.status == ConnectionStatus.Connected
|
||||
|
||||
binding.root.setOnLongClickListener { listener?.onDeviceLongClicked(deviceStats) ?: false }
|
||||
binding.status = when (connectionInfo.status) {
|
||||
ConnectionStatus.Connected -> context.getString(R.string.device_status_connected, connectionInfo.currentAddress?.address)
|
||||
ConnectionStatus.Connecting -> context.getString(R.string.device_status_connecting, connectionInfo.currentAddress?.address)
|
||||
ConnectionStatus.Disconnected -> if (connectionInfo.addresses.isEmpty())
|
||||
context.getString(R.string.device_status_no_address)
|
||||
else
|
||||
context.getString(R.string.device_status_disconnected, connectionInfo.addresses.size)
|
||||
}
|
||||
|
||||
binding.root.setOnLongClickListener { listener?.onDeviceLongClicked(deviceInfo) ?: false }
|
||||
|
||||
binding.executePendingBindings()
|
||||
}
|
||||
@@ -44,4 +57,4 @@ interface DeviceAdapterListener {
|
||||
fun onDeviceLongClicked(deviceInfo: DeviceInfo): Boolean
|
||||
}
|
||||
|
||||
class DeviceViewHolder(val binding: ListviewDeviceBinding): RecyclerView.ViewHolder(binding.root)
|
||||
class DeviceViewHolder(val binding: ListviewDeviceBinding): RecyclerView.ViewHolder(binding.root)
|
||||
|
||||
@@ -55,6 +55,10 @@ class FoldersListAdapter: RecyclerView.Adapter<FolderListViewHolder>() {
|
||||
binding.root.setOnClickListener {
|
||||
listener?.onFolderClicked(folderInfo, folderStats)
|
||||
}
|
||||
|
||||
binding.root.setOnLongClickListener {
|
||||
listener?.onFolderLongClicked(folderInfo) ?: false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,4 +66,5 @@ class FolderListViewHolder(val binding: ListviewFolderBinding): RecyclerView.Vie
|
||||
|
||||
interface FolderListAdapterListener {
|
||||
fun onFolderClicked(folderInfo: FolderInfo, folderStats: FolderStats)
|
||||
fun onFolderLongClicked(folderInfo: FolderInfo): Boolean
|
||||
}
|
||||
|
||||
@@ -1,28 +1,20 @@
|
||||
package net.syncthing.lite.android
|
||||
|
||||
import android.app.Application
|
||||
import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.util.Log
|
||||
import net.syncthing.lite.BuildConfig
|
||||
import org.jetbrains.anko.defaultSharedPreferences
|
||||
import java.io.PrintWriter
|
||||
import java.io.StringWriter
|
||||
import net.syncthing.lite.error.ErrorStorage
|
||||
|
||||
class Application: Application() {
|
||||
companion object {
|
||||
private const val LOG_TAG = "Application"
|
||||
private const val PREF_ENABLE_CRASH_HANDLER = "crash_handler"
|
||||
private val handler = Handler(Looper.getMainLooper())
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
|
||||
val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||
val defaultHandler = Thread.getDefaultUncaughtExceptionHandler()
|
||||
val mainThread = Thread.currentThread()
|
||||
|
||||
@@ -33,18 +25,10 @@ class Application: Application() {
|
||||
fun handleCrash(ex: Throwable) {
|
||||
Log.w(LOG_TAG, "app crashed", ex)
|
||||
|
||||
val enableCustomCrashHandling = defaultSharedPreferences.getBoolean(PREF_ENABLE_CRASH_HANDLER, false)
|
||||
|
||||
if (enableCustomCrashHandling) {
|
||||
clipboard.primaryClip = ClipData.newPlainText(
|
||||
"stacktrace",
|
||||
StringWriter().apply {
|
||||
append("Version: ").append(BuildConfig.VERSION_NAME).append('\n')
|
||||
append(Log.getStackTraceString(ex)).append('\n')
|
||||
ex.printStackTrace(PrintWriter(this))
|
||||
}.buffer.toString()
|
||||
)
|
||||
}
|
||||
ErrorStorage.reportError(
|
||||
this,
|
||||
Log.getStackTraceString(ex)
|
||||
)
|
||||
|
||||
if (defaultHandler != null) {
|
||||
defaultHandler.uncaughtException(mainThread, ex)
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
package net.syncthing.lite.async
|
||||
|
||||
import android.support.v4.app.DialogFragment
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
|
||||
abstract class CoroutineDialogFragment: DialogFragment(), CoroutineScope {
|
||||
val job = Job()
|
||||
|
||||
override val coroutineContext: CoroutineContext
|
||||
get() = job + Dispatchers.Main
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
job.cancel()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
package net.syncthing.lite.dialogs
|
||||
|
||||
import android.app.AlertDialog
|
||||
import android.app.Dialog
|
||||
import android.os.Bundle
|
||||
import android.support.v4.app.FragmentManager
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import net.syncthing.java.core.beans.DeviceInfo
|
||||
import net.syncthing.lite.R
|
||||
import net.syncthing.lite.fragments.SyncthingDialogFragment
|
||||
|
||||
class EnableFolderSyncForNewDeviceDialog: SyncthingDialogFragment() {
|
||||
companion object {
|
||||
private const val FOLDER_ID = "folderId"
|
||||
private const val FOLDER_NAME = "folderName"
|
||||
private const val DEVICES = "devices"
|
||||
private const val STATUS_CURRENT_DEVICE_ID = "currentDeviceId"
|
||||
|
||||
private const val TAG = "EnableFolderSyncForNewDeviceDialog"
|
||||
|
||||
fun newInstance(folderId: String, folderName: String, devices: List<DeviceInfo>) = EnableFolderSyncForNewDeviceDialog().apply {
|
||||
arguments = Bundle().apply {
|
||||
putString(FOLDER_ID, folderId)
|
||||
putString(FOLDER_NAME, folderName)
|
||||
putSerializable(DEVICES, ArrayList<DeviceInfo>(devices))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var currentDeviceId = 0
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
val folderId = arguments!!.getString(FOLDER_ID)
|
||||
val folderName = arguments!!.getString(FOLDER_NAME)
|
||||
val devices = arguments!!.getSerializable(DEVICES) as ArrayList<DeviceInfo>
|
||||
|
||||
if (savedInstanceState != null) {
|
||||
currentDeviceId = savedInstanceState.getInt(STATUS_CURRENT_DEVICE_ID)
|
||||
}
|
||||
|
||||
val dialog = AlertDialog.Builder(context!!)
|
||||
.setTitle(R.string.dialog_enable_folder_sync_for_new_device_title)
|
||||
.setMessage(R.string.dialog_enable_folder_sync_for_new_device_text)
|
||||
.setPositiveButton(R.string.dialog_enable_folder_sync_for_new_device_positive, null)
|
||||
.setNegativeButton(R.string.dialog_enable_folder_sync_for_new_device_negative, null)
|
||||
.create()
|
||||
|
||||
fun bindDeviceId() {
|
||||
if (currentDeviceId >= devices.size) {
|
||||
dismissAllowingStateLoss()
|
||||
} else {
|
||||
val device = devices[currentDeviceId]
|
||||
|
||||
dialog.setMessage(getString(
|
||||
R.string.dialog_enable_folder_sync_for_new_device_text,
|
||||
folderName,
|
||||
device.name,
|
||||
device.deviceId.deviceId
|
||||
))
|
||||
|
||||
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener {
|
||||
GlobalScope.launch {
|
||||
libraryHandler.libraryManager.withLibrary {
|
||||
val oldFolderEntry = it.configuration.folders.find { it.folderId == folderId }!!
|
||||
|
||||
it.configuration.update { oldConfig ->
|
||||
oldConfig.copy(
|
||||
folders = oldConfig.folders.filter { it != oldFolderEntry }.toSet() + setOf(
|
||||
oldFolderEntry.copy(
|
||||
deviceIdWhitelist = oldFolderEntry.deviceIdWhitelist + setOf(device.deviceId),
|
||||
deviceIdBlacklist = oldFolderEntry.deviceIdBlacklist - setOf(device.deviceId)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
it.syncthingClient.reconnect(device.deviceId)
|
||||
it.configuration.persistLater()
|
||||
}
|
||||
}
|
||||
|
||||
currentDeviceId++
|
||||
bindDeviceId()
|
||||
}
|
||||
|
||||
dialog.getButton(AlertDialog.BUTTON_NEGATIVE).setOnClickListener {
|
||||
GlobalScope.launch {
|
||||
libraryHandler.libraryManager.withLibrary {
|
||||
val oldFolderEntry = it.configuration.folders.find { it.folderId == folderId }!!
|
||||
|
||||
it.configuration.update { oldConfig ->
|
||||
oldConfig.copy(
|
||||
folders = oldConfig.folders.filter { it != oldFolderEntry }.toSet() + setOf(
|
||||
oldFolderEntry.copy(
|
||||
ignoredDeviceIdList = oldFolderEntry.deviceIdWhitelist + setOf(device.deviceId)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
it.syncthingClient.reconnect(device.deviceId)
|
||||
it.configuration.persistLater()
|
||||
}
|
||||
}
|
||||
|
||||
currentDeviceId++
|
||||
bindDeviceId()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dialog.setOnShowListener { bindDeviceId() }
|
||||
|
||||
return dialog
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
super.onSaveInstanceState(outState)
|
||||
|
||||
outState.putInt(STATUS_CURRENT_DEVICE_ID, currentDeviceId)
|
||||
}
|
||||
|
||||
fun show(fragmentManager: FragmentManager) = show(fragmentManager, TAG)
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package net.syncthing.lite.dialogs
|
||||
|
||||
import android.app.Dialog
|
||||
import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.support.v4.app.DialogFragment
|
||||
import android.support.v4.app.FragmentManager
|
||||
import android.support.v7.app.AlertDialog
|
||||
import android.widget.Toast
|
||||
import net.syncthing.lite.R
|
||||
|
||||
class ErrorReportDialog: DialogFragment() {
|
||||
companion object {
|
||||
private const val REPORT = "report"
|
||||
private const val TAG = "ErrorReportDialog"
|
||||
|
||||
fun newInstance(report: String) = ErrorReportDialog().apply {
|
||||
arguments = Bundle().apply {
|
||||
putString(REPORT, report)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
val report = arguments!!.getString(REPORT)
|
||||
val clipboard = context!!.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||
|
||||
return AlertDialog.Builder(context!!)
|
||||
.setTitle(R.string.settings_last_error_title)
|
||||
.setMessage(report)
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.setNeutralButton(R.string.copy_to_clipboard, null)
|
||||
.create()
|
||||
.apply {
|
||||
setOnShowListener {
|
||||
getButton(AlertDialog.BUTTON_NEUTRAL).setOnClickListener {
|
||||
clipboard.primaryClip = ClipData.newPlainText(
|
||||
context!!.getString(R.string.settings_last_error_title),
|
||||
report
|
||||
)
|
||||
|
||||
Toast.makeText(context, context!!.getString(R.string.copied_to_clipboard), Toast.LENGTH_SHORT)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun show(fragmentManager: FragmentManager) = show(fragmentManager, TAG)
|
||||
}
|
||||
@@ -8,12 +8,11 @@ import android.support.v4.app.FragmentManager
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.webkit.MimeTypeMap
|
||||
import net.syncthing.java.core.beans.FileInfo
|
||||
import net.syncthing.lite.databinding.DialogFileBinding
|
||||
import net.syncthing.lite.dialogs.downloadfile.DownloadFileDialogFragment
|
||||
import net.syncthing.lite.dialogs.downloadfile.DownloadFileSpec
|
||||
import org.apache.commons.io.FilenameUtils
|
||||
import net.syncthing.lite.utils.MimeType
|
||||
|
||||
class FileMenuDialogFragment: BottomSheetDialogFragment() {
|
||||
companion object {
|
||||
@@ -48,9 +47,7 @@ class FileMenuDialogFragment: BottomSheetDialogFragment() {
|
||||
Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
|
||||
addCategory(Intent.CATEGORY_OPENABLE)
|
||||
|
||||
type = MimeTypeMap.getSingleton().getMimeTypeFromExtension(
|
||||
FilenameUtils.getExtension(fileSpec.fileName)
|
||||
)
|
||||
type = MimeType.getFromUrl(fileSpec.fileName)
|
||||
|
||||
putExtra(Intent.EXTRA_TITLE, fileSpec.fileName)
|
||||
},
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
package net.syncthing.lite.dialogs
|
||||
|
||||
import android.app.Dialog
|
||||
import android.os.Bundle
|
||||
import android.support.v4.app.FragmentManager
|
||||
import android.support.v7.app.AlertDialog
|
||||
import android.support.v7.widget.AppCompatCheckBox
|
||||
import android.view.LayoutInflater
|
||||
import kotlinx.coroutines.launch
|
||||
import net.syncthing.lite.R
|
||||
import net.syncthing.lite.databinding.DialogFolderInfoBinding
|
||||
import net.syncthing.lite.fragments.SyncthingDialogFragment
|
||||
|
||||
class FolderInfoDialog: SyncthingDialogFragment() {
|
||||
companion object {
|
||||
fun newInstance(folderId: String) = FolderInfoDialog().apply {
|
||||
arguments = Bundle().apply {
|
||||
putString(FOLDER_ID, folderId)
|
||||
}
|
||||
}
|
||||
|
||||
private const val FOLDER_ID = "folderId"
|
||||
private const val TAG = "FolderInfoDialog"
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
val folderId = arguments!!.getString(FOLDER_ID)
|
||||
val binding = DialogFolderInfoBinding.inflate(LayoutInflater.from(context))
|
||||
|
||||
val dialog = AlertDialog.Builder(context!!)
|
||||
.setTitle(folderId)
|
||||
.setView(binding.root)
|
||||
.create()
|
||||
|
||||
launch {
|
||||
val configuration = libraryHandler.libraryManager.withLibrary { it.configuration }
|
||||
|
||||
val folderInfo = configuration.folders.find { it.folderId == folderId }
|
||||
|
||||
if (folderInfo == null) {
|
||||
dismissAllowingStateLoss()
|
||||
return@launch
|
||||
}
|
||||
|
||||
dialog.setTitle(folderInfo.label)
|
||||
|
||||
binding.deviceCheckboxesContainer.removeAllViews()
|
||||
|
||||
val allRelatedDevices = (folderInfo.deviceIdWhitelist + folderInfo.deviceIdBlacklist).toSet()
|
||||
|
||||
allRelatedDevices.forEach { deviceId ->
|
||||
val deviceInfo = configuration.peers.find { it.deviceId == deviceId }
|
||||
|
||||
val deviceLabel = if (deviceInfo == null)
|
||||
deviceId.deviceId
|
||||
else
|
||||
context!!.getString(R.string.dialog_folder_info_device_list_item, deviceInfo.name, deviceId.deviceId)
|
||||
|
||||
binding.deviceCheckboxesContainer.addView(
|
||||
AppCompatCheckBox(context!!).apply {
|
||||
text = deviceLabel
|
||||
isChecked = folderInfo.deviceIdWhitelist.contains(deviceId)
|
||||
|
||||
setOnCheckedChangeListener { _, isShared ->
|
||||
this@FolderInfoDialog.launch {
|
||||
libraryHandler.libraryManager.withLibrary { library ->
|
||||
// update the config
|
||||
library.configuration.update { oldConfig ->
|
||||
val oldFolders = oldConfig.folders
|
||||
var folderToChange = oldFolders.find { it.folderId == folderId }!!
|
||||
val foldersNotToChange = oldFolders.filterNot { it.folderId == folderId }.toSet()
|
||||
|
||||
if (isShared) {
|
||||
folderToChange = folderToChange.copy(
|
||||
ignoredDeviceIdList = folderToChange.ignoredDeviceIdList.filterNot { it == deviceId }.toSet(),
|
||||
deviceIdBlacklist = folderToChange.deviceIdBlacklist.filterNot { it == deviceId }.toSet(),
|
||||
deviceIdWhitelist = folderToChange.deviceIdWhitelist + setOf(deviceId)
|
||||
)
|
||||
} else {
|
||||
folderToChange = folderToChange.copy(
|
||||
deviceIdWhitelist = folderToChange.deviceIdWhitelist.filterNot { it == deviceId }.toSet(),
|
||||
deviceIdBlacklist = folderToChange.deviceIdBlacklist + setOf(deviceId),
|
||||
ignoredDeviceIdList = folderToChange.ignoredDeviceIdList + setOf(deviceId)
|
||||
)
|
||||
}
|
||||
|
||||
oldConfig.copy(folders = foldersNotToChange + folderToChange)
|
||||
}
|
||||
library.configuration.persistLater()
|
||||
|
||||
// apply the change
|
||||
library.syncthingClient.reconnect(deviceId)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return dialog
|
||||
}
|
||||
|
||||
fun show(fragmentManager: FragmentManager) = show(fragmentManager, TAG)
|
||||
}
|
||||
+2
-3
@@ -12,13 +12,12 @@ import android.os.Bundle
|
||||
import android.support.v4.app.DialogFragment
|
||||
import android.support.v4.app.FragmentManager
|
||||
import android.util.Log
|
||||
import android.webkit.MimeTypeMap
|
||||
import net.syncthing.java.core.beans.FileInfo
|
||||
import net.syncthing.lite.BuildConfig
|
||||
import net.syncthing.lite.R
|
||||
import net.syncthing.lite.library.CacheFileProviderUrl
|
||||
import net.syncthing.lite.library.LibraryHandler
|
||||
import org.apache.commons.io.FilenameUtils
|
||||
import net.syncthing.lite.utils.MimeType
|
||||
import org.jetbrains.anko.newTask
|
||||
import org.jetbrains.anko.toast
|
||||
|
||||
@@ -89,7 +88,7 @@ class DownloadFileDialogFragment : DialogFragment() {
|
||||
dismissAllowingStateLoss()
|
||||
|
||||
if (outputUri == null) {
|
||||
val mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(FilenameUtils.getExtension(fileSpec.fileName))
|
||||
val mimeType = MimeType.getFromUrl(fileSpec.fileName)
|
||||
|
||||
try {
|
||||
context!!.startActivity(
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
package net.syncthing.lite.error
|
||||
|
||||
import android.content.Context
|
||||
import org.jetbrains.anko.defaultSharedPreferences
|
||||
|
||||
object ErrorStorage {
|
||||
private const val PREF_KEY = "last_error"
|
||||
|
||||
fun reportError(context: Context, error: String) {
|
||||
// this uses commit because the App could be quit directly after that
|
||||
context.defaultSharedPreferences.edit()
|
||||
.putString(PREF_KEY, error)
|
||||
.commit()
|
||||
}
|
||||
|
||||
fun getLastErrorReport(context: Context) = context.defaultSharedPreferences.getString(PREF_KEY, "there is no saved report")
|
||||
}
|
||||
@@ -10,9 +10,9 @@ import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import com.google.zxing.integration.android.IntentIntegrator
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.channels.consumeEach
|
||||
import kotlinx.coroutines.launch
|
||||
import net.syncthing.java.bep.connectionactor.ConnectionInfo
|
||||
import net.syncthing.java.core.beans.DeviceInfo
|
||||
import net.syncthing.lite.R
|
||||
import net.syncthing.lite.adapters.DeviceAdapterListener
|
||||
@@ -34,25 +34,7 @@ class DevicesFragment : SyncthingFragment() {
|
||||
savedInstanceState: Bundle?): View? {
|
||||
binding = DataBindingUtil.inflate(layoutInflater, R.layout.fragment_devices, container, false)
|
||||
binding.addDevice.setOnClickListener { showDialog() }
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
libraryHandler.syncthingClient { it.addOnConnectionChangedListener { _ -> updateDeviceList() } }
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
libraryHandler.syncthingClient { it.removeOnConnectionChangedListener{ _ -> updateDeviceList() } }
|
||||
}
|
||||
|
||||
override fun onLibraryLoaded() {
|
||||
initDeviceList()
|
||||
updateDeviceList()
|
||||
}
|
||||
|
||||
private fun initDeviceList() {
|
||||
binding.list.adapter = adapter
|
||||
|
||||
adapter.listener = object: DeviceAdapterListener {
|
||||
@@ -61,12 +43,21 @@ class DevicesFragment : SyncthingFragment() {
|
||||
.setTitle(getString(R.string.remove_device_title, deviceInfo.name))
|
||||
.setMessage(getString(R.string.remove_device_message, deviceInfo.deviceId.deviceId.substring(0, 7)))
|
||||
.setPositiveButton(android.R.string.yes) { _, _ ->
|
||||
libraryHandler.library { config, syncthingClient, _ ->
|
||||
config.peers = config.peers.filterNot { it.deviceId == deviceInfo.deviceId }.toSet()
|
||||
config.persistLater()
|
||||
updateDeviceList()
|
||||
launch {
|
||||
libraryHandler.libraryManager.withLibrary { library ->
|
||||
library.configuration.update { oldConfig ->
|
||||
oldConfig.copy(
|
||||
peers = oldConfig.peers
|
||||
.filterNot { it.deviceId == deviceInfo.deviceId }
|
||||
.toSet()
|
||||
)
|
||||
}
|
||||
|
||||
syncthingClient.disconnectFromRemovedDevices()
|
||||
library.configuration.persistLater()
|
||||
|
||||
// TODO: update the device list (should become a side effect of the call below)
|
||||
library.syncthingClient.disconnectFromRemovedDevices()
|
||||
}
|
||||
}
|
||||
}
|
||||
.setNegativeButton(android.R.string.no, null)
|
||||
@@ -75,15 +66,17 @@ class DevicesFragment : SyncthingFragment() {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateDeviceList() {
|
||||
libraryHandler.syncthingClient { syncthingClient ->
|
||||
GlobalScope.launch (Dispatchers.Main) {
|
||||
adapter.data = syncthingClient.getPeerStatus()
|
||||
binding.isEmpty = adapter.data.isEmpty()
|
||||
launch {
|
||||
libraryHandler.subscribeToConnectionStatus().consumeEach { connectionInfo ->
|
||||
val devices = libraryHandler.libraryManager.withLibrary { it.configuration.peers }
|
||||
|
||||
adapter.data = devices.map { device -> device to (connectionInfo[device.deviceId] ?: ConnectionInfo.empty) }
|
||||
binding.isEmpty = devices.isEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, intent: Intent?) {
|
||||
@@ -94,35 +87,38 @@ class DevicesFragment : SyncthingFragment() {
|
||||
}
|
||||
|
||||
private fun showDialog() {
|
||||
addDeviceDialogBinding = DataBindingUtil.inflate(LayoutInflater.from(context), R.layout.view_enter_device_id, null, false)
|
||||
addDeviceDialogBinding?.let { binding ->
|
||||
binding.scanQrCode.setOnClickListener {
|
||||
FragmentIntentIntegrator(this@DevicesFragment).initiateScan()
|
||||
}
|
||||
binding.deviceId.post {
|
||||
val imm = context!!.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
|
||||
imm.showSoftInput(binding.deviceId, InputMethodManager.SHOW_IMPLICIT)
|
||||
}
|
||||
val binding = ViewEnterDeviceIdBinding.inflate(LayoutInflater.from(context), null, false)
|
||||
addDeviceDialogBinding = binding
|
||||
|
||||
addDeviceDialog = AlertDialog.Builder(context)
|
||||
.setTitle(R.string.device_id_dialog_title)
|
||||
.setView(binding.root)
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.show()
|
||||
|
||||
// Use different listener to keep dialog open after button click.
|
||||
// https://stackoverflow.com/a/15619098
|
||||
addDeviceDialog?.getButton(AlertDialog.BUTTON_POSITIVE)
|
||||
?.setOnClickListener {
|
||||
try {
|
||||
val deviceId = binding.deviceId.text.toString()
|
||||
Util.importDeviceId(libraryHandler, context, deviceId, { updateDeviceList() })
|
||||
addDeviceDialog?.dismiss()
|
||||
} catch (e: IOException) {
|
||||
binding.deviceId.error = getString(R.string.invalid_device_id)
|
||||
}
|
||||
}
|
||||
binding.scanQrCode.setOnClickListener {
|
||||
FragmentIntentIntegrator(this@DevicesFragment).initiateScan()
|
||||
}
|
||||
binding.deviceId.post {
|
||||
val imm = context!!.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
|
||||
imm.showSoftInput(binding.deviceId, InputMethodManager.SHOW_IMPLICIT)
|
||||
}
|
||||
|
||||
val dialog = AlertDialog.Builder(context)
|
||||
.setTitle(R.string.device_id_dialog_title)
|
||||
.setView(binding.root)
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.show()
|
||||
|
||||
addDeviceDialog = dialog
|
||||
|
||||
fun handleAddClick() {
|
||||
try {
|
||||
val deviceId = binding.deviceId.text.toString()
|
||||
Util.importDeviceId(libraryHandler.libraryManager, context!!, deviceId, { /* TODO: Is updateDeviceList() still required? */ })
|
||||
dialog.dismiss()
|
||||
} catch (e: IOException) {
|
||||
binding.deviceId.error = getString(R.string.invalid_device_id)
|
||||
}
|
||||
}
|
||||
|
||||
// Use different listener to keep dialog open after button click.
|
||||
// https://stackoverflow.com/a/15619098
|
||||
dialog.getButton(AlertDialog.BUTTON_POSITIVE)!!.setOnClickListener { handleAddClick() }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import net.syncthing.lite.activities.FolderBrowserActivity
|
||||
import net.syncthing.lite.adapters.FolderListAdapterListener
|
||||
import net.syncthing.lite.adapters.FoldersListAdapter
|
||||
import net.syncthing.lite.databinding.FragmentFoldersBinding
|
||||
import net.syncthing.lite.dialogs.FolderInfoDialog
|
||||
import org.jetbrains.anko.intentFor
|
||||
|
||||
class FoldersFragment : SyncthingFragment() {
|
||||
@@ -27,6 +28,14 @@ class FoldersFragment : SyncthingFragment() {
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
override fun onFolderLongClicked(folderInfo: FolderInfo): Boolean {
|
||||
FolderInfoDialog
|
||||
.newInstance(folderId = folderInfo.folderId)
|
||||
.show(fragmentManager!!)
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
val binding = FragmentFoldersBinding.inflate(layoutInflater, container, false)
|
||||
|
||||
@@ -1,10 +1,17 @@
|
||||
package net.syncthing.lite.fragments
|
||||
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.support.v7.preference.EditTextPreference
|
||||
import android.support.v7.preference.PreferenceFragmentCompat
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import net.syncthing.lite.R
|
||||
import net.syncthing.lite.activities.SyncthingActivity
|
||||
import net.syncthing.lite.dialogs.ErrorReportDialog
|
||||
import net.syncthing.lite.error.ErrorStorage
|
||||
import net.syncthing.lite.library.DefaultLibraryManager
|
||||
|
||||
class SettingsFragment : PreferenceFragmentCompat() {
|
||||
|
||||
@@ -14,25 +21,47 @@ class SettingsFragment : PreferenceFragmentCompat() {
|
||||
val localDeviceName = findPreference("local_device_name") as EditTextPreference
|
||||
val appVersion = findPreference("app_version")
|
||||
val forceStop = findPreference("force_stop")
|
||||
val lastCrash = findPreference("last_crash")
|
||||
val reportBug = findPreference("report_bug")
|
||||
val libraryManager = DefaultLibraryManager.with(context!!)
|
||||
|
||||
(activity as SyncthingActivity?)?.let { activity ->
|
||||
val versionName = activity.packageManager.getPackageInfo(activity.packageName, 0)?.versionName
|
||||
appVersion.summary = versionName
|
||||
|
||||
activity.libraryHandler.configuration { localDeviceName.text = it.localDeviceName }
|
||||
localDeviceName.setOnPreferenceChangeListener { _, _ ->
|
||||
activity.libraryHandler.configuration { conf ->
|
||||
conf.localDeviceName = localDeviceName.text
|
||||
conf.persistLater()
|
||||
}
|
||||
true
|
||||
GlobalScope.launch (Dispatchers.Main) {
|
||||
libraryManager.withLibrary { library ->
|
||||
localDeviceName.text = library.configuration.localDeviceName
|
||||
}
|
||||
}
|
||||
|
||||
appVersion.summary = context!!.packageManager.getPackageInfo(context!!.packageName, 0)?.versionName
|
||||
|
||||
localDeviceName.setOnPreferenceChangeListener { _, _ ->
|
||||
val newDeviceName = localDeviceName.text
|
||||
|
||||
GlobalScope.launch {
|
||||
libraryManager.withLibrary { library ->
|
||||
library.configuration.update { it.copy(localDeviceName = newDeviceName) }
|
||||
library.configuration.persistLater()
|
||||
}
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
forceStop.setOnPreferenceClickListener {
|
||||
System.exit(0)
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
lastCrash.setOnPreferenceClickListener {
|
||||
ErrorReportDialog.newInstance(ErrorStorage.getLastErrorReport(context!!)).show(fragmentManager!!)
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
reportBug.setOnPreferenceClickListener {
|
||||
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse("https://github.com/syncthing/syncthing-lite/issues")))
|
||||
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
package net.syncthing.lite.fragments
|
||||
|
||||
import android.support.v4.app.DialogFragment
|
||||
import net.syncthing.lite.async.CoroutineDialogFragment
|
||||
import net.syncthing.lite.library.LibraryHandler
|
||||
|
||||
abstract class SyncthingDialogFragment : DialogFragment() {
|
||||
abstract class SyncthingDialogFragment : CoroutineDialogFragment() {
|
||||
val libraryHandler: LibraryHandler by lazy { LibraryHandler(
|
||||
context = context!!
|
||||
)}
|
||||
|
||||
@@ -4,8 +4,10 @@ import android.content.Context
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import net.syncthing.lite.BuildConfig
|
||||
import net.syncthing.lite.R
|
||||
import net.syncthing.lite.error.ErrorStorage
|
||||
import org.jetbrains.anko.defaultSharedPreferences
|
||||
|
||||
object DefaultLibraryManager {
|
||||
@@ -39,7 +41,16 @@ object DefaultLibraryManager {
|
||||
}
|
||||
|
||||
instance = LibraryManager(
|
||||
synchronousInstanceCreator = { LibraryInstance(context) },
|
||||
synchronousInstanceCreator = {
|
||||
LibraryInstance(context) { ex ->
|
||||
// this delay ensures that the toast is shown even if the UI thread is busy
|
||||
handler.postDelayed({
|
||||
Toast.makeText(context, R.string.toast_error, Toast.LENGTH_LONG).show()
|
||||
}, 100L)
|
||||
|
||||
ErrorStorage.reportError(context, "${ex.component}\n${ex.detailsReadableString}\n${Log.getStackTraceString(ex.exception)}")
|
||||
}
|
||||
},
|
||||
userCounterListener = {
|
||||
newUserCounter ->
|
||||
|
||||
|
||||
@@ -9,8 +9,10 @@ import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.channels.BroadcastChannel
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.channels.ConflatedBroadcastChannel
|
||||
import kotlinx.coroutines.channels.consumeEach
|
||||
import kotlinx.coroutines.launch
|
||||
import net.syncthing.java.bep.connectionactor.ConnectionInfo
|
||||
import net.syncthing.java.bep.folder.FolderBrowser
|
||||
import net.syncthing.java.bep.folder.FolderStatus
|
||||
import net.syncthing.java.client.SyncthingClient
|
||||
@@ -37,6 +39,7 @@ class LibraryHandler(context: Context) {
|
||||
private val isListeningPortTakenInternal = MutableLiveData<Boolean>().apply { postValue(false) }
|
||||
private val indexUpdateCompleteMessages = BroadcastChannel<String>(capacity = 16)
|
||||
private val folderStatusList = BroadcastChannel<List<FolderStatus>>(capacity = Channel.CONFLATED)
|
||||
private val connectionStatus = ConflatedBroadcastChannel<Map<DeviceId, ConnectionInfo>>()
|
||||
private var job: Job = Job()
|
||||
|
||||
val isListeningPortTaken: LiveData<Boolean> = isListeningPortTakenInternal
|
||||
@@ -78,6 +81,12 @@ class LibraryHandler(context: Context) {
|
||||
folderStatusList.send(it)
|
||||
}
|
||||
}
|
||||
|
||||
GlobalScope.launch (job) {
|
||||
libraryInstance.syncthingClient.subscribeToConnectionStatus().consumeEach {
|
||||
connectionStatus.send(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -139,4 +148,5 @@ class LibraryHandler(context: Context) {
|
||||
|
||||
fun subscribeToOnFullIndexAcquiredEvents() = indexUpdateCompleteMessages.openSubscription()
|
||||
fun subscribeToFolderStatusList() = folderStatusList.openSubscription()
|
||||
fun subscribeToConnectionStatus() = connectionStatus.openSubscription()
|
||||
}
|
||||
|
||||
@@ -2,12 +2,16 @@ package net.syncthing.lite.library
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import net.syncthing.java.client.SyncthingClient
|
||||
import net.syncthing.java.core.configuration.Configuration
|
||||
import net.syncthing.java.core.exception.ExceptionReport
|
||||
import net.syncthing.java.repository.EncryptedTempRepository
|
||||
import net.syncthing.repository.android.SqliteIndexRepository
|
||||
import net.syncthing.repository.android.TempDirectoryLocalRepository
|
||||
import net.syncthing.repository.android.database.RepositoryDatabase
|
||||
import org.jetbrains.anko.defaultSharedPreferences
|
||||
import java.io.File
|
||||
import java.net.DatagramSocket
|
||||
import java.net.InetAddress
|
||||
@@ -22,7 +26,10 @@ import java.net.SocketException
|
||||
*
|
||||
* The creation and the shutdown are synchronous, so keep them out of the UI Thread
|
||||
*/
|
||||
class LibraryInstance (context: Context) {
|
||||
class LibraryInstance (
|
||||
context: Context,
|
||||
private val exceptionReportHandler: (ExceptionReport) -> Unit
|
||||
) {
|
||||
companion object {
|
||||
private const val LOG_TAG = "LibraryInstance"
|
||||
|
||||
@@ -43,7 +50,11 @@ class LibraryInstance (context: Context) {
|
||||
}
|
||||
}
|
||||
|
||||
private val tempRepository = TempDirectoryLocalRepository(File(context.filesDir, "temp_repository"))
|
||||
private val tempRepository = EncryptedTempRepository(
|
||||
TempDirectoryLocalRepository(
|
||||
File(context.filesDir, "temp_repository")
|
||||
)
|
||||
)
|
||||
|
||||
val isListeningPortTaken = checkIsListeningPortTaken() // this must come first to work correctly
|
||||
val configuration = Configuration(configFolder = context.filesDir)
|
||||
@@ -52,15 +63,21 @@ class LibraryInstance (context: Context) {
|
||||
repository = SqliteIndexRepository(
|
||||
database = RepositoryDatabase.with(context),
|
||||
closeDatabaseOnClose = false,
|
||||
clearTempStorageHook = { tempRepository.deleteAllData() }
|
||||
clearTempStorageHook = { tempRepository.deleteAllTempData() }
|
||||
),
|
||||
tempRepository = tempRepository,
|
||||
enableDetailedException = context.defaultSharedPreferences.getBoolean("detailed_exception", false)
|
||||
exceptionReportHandler = { ex ->
|
||||
Log.w(LOG_TAG, "${ex.component}\n${ex.detailsReadableString}\n${Log.getStackTraceString(ex.exception)}")
|
||||
|
||||
GlobalScope.launch (Dispatchers.Main) {
|
||||
exceptionReportHandler(ex)
|
||||
}
|
||||
}
|
||||
)
|
||||
val folderBrowser = syncthingClient.indexHandler.folderBrowser
|
||||
val indexBrowser = syncthingClient.indexHandler.indexBrowser
|
||||
|
||||
fun shutdown() {
|
||||
suspend fun shutdown() {
|
||||
syncthingClient.close()
|
||||
configuration.persistNow()
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.channels.ConflatedBroadcastChannel
|
||||
import kotlinx.coroutines.channels.consumeEach
|
||||
import kotlinx.coroutines.channels.produce
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import java.util.concurrent.Executors
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
@@ -93,7 +94,7 @@ class LibraryManager (
|
||||
fun shutdownIfThereAreZeroUsers(listener: (wasShutdownPerformed: Boolean) -> Unit = {}) {
|
||||
startStopExecutor.submit {
|
||||
if (userCounter == 0) {
|
||||
instanceStream.value?.shutdown()
|
||||
runBlocking { instanceStream.value?.shutdown() }
|
||||
instanceStream.offer(null)
|
||||
|
||||
handler.post { isRunningListener(false) }
|
||||
|
||||
@@ -15,8 +15,8 @@ import net.syncthing.java.bep.index.browser.DirectoryNotFoundListing
|
||||
import net.syncthing.java.core.beans.FileInfo
|
||||
import net.syncthing.java.core.beans.FolderInfo
|
||||
import net.syncthing.lite.R
|
||||
import net.syncthing.lite.utils.MimeType
|
||||
import java.io.FileNotFoundException
|
||||
import java.net.URLConnection
|
||||
|
||||
class SyncthingProvider : DocumentsProvider() {
|
||||
|
||||
@@ -159,7 +159,7 @@ class SyncthingProvider : DocumentsProvider() {
|
||||
if (fileInfo.isDirectory())
|
||||
Document.MIME_TYPE_DIR
|
||||
else
|
||||
URLConnection.guessContentTypeFromName(fileInfo.fileName)
|
||||
MimeType.getFromUrl(fileInfo.fileName)
|
||||
)
|
||||
add(Document.COLUMN_LAST_MODIFIED, fileInfo.lastModified)
|
||||
add(Document.COLUMN_FLAGS, 0)
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
package net.syncthing.lite.utils
|
||||
|
||||
import android.webkit.MimeTypeMap
|
||||
|
||||
object MimeType {
|
||||
private const val DEFAULT_MIME_TYPE = "application/octet-stream"
|
||||
|
||||
private fun getFromExtension(extension: String): String {
|
||||
val mimeType: String? = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension)
|
||||
|
||||
return mimeType ?: DEFAULT_MIME_TYPE
|
||||
}
|
||||
|
||||
fun getFromUrl(url: String) = getFromExtension(
|
||||
MimeTypeMap.getFileExtensionFromUrl(url).toLowerCase()
|
||||
)
|
||||
}
|
||||
@@ -10,7 +10,7 @@ import kotlinx.coroutines.launch
|
||||
import net.syncthing.java.core.beans.DeviceId
|
||||
import net.syncthing.java.core.beans.DeviceInfo
|
||||
import net.syncthing.lite.R
|
||||
import net.syncthing.lite.library.LibraryHandler
|
||||
import net.syncthing.lite.library.LibraryManager
|
||||
import org.apache.commons.lang3.StringUtils.capitalize
|
||||
import org.jetbrains.anko.toast
|
||||
import java.io.IOException
|
||||
@@ -23,11 +23,11 @@ object Util {
|
||||
val manufacturer = Build.MANUFACTURER ?: ""
|
||||
val model = Build.MODEL ?: ""
|
||||
val deviceName =
|
||||
if (model.startsWith(manufacturer)) {
|
||||
capitalize(model)
|
||||
} else {
|
||||
capitalize(manufacturer) + " " + model
|
||||
}
|
||||
if (model.startsWith(manufacturer)) {
|
||||
capitalize(model)
|
||||
} else {
|
||||
capitalize(manufacturer) + " " + model
|
||||
}
|
||||
return deviceName ?: "android"
|
||||
}
|
||||
|
||||
@@ -41,22 +41,34 @@ object Util {
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
fun importDeviceId(libraryHandler: LibraryHandler?, context: Context?, deviceId: String,
|
||||
onComplete: () -> Unit) {
|
||||
val deviceId2 = DeviceId(deviceId.toUpperCase(Locale.US))
|
||||
libraryHandler?.library { configuration, syncthingClient, _ ->
|
||||
if (!configuration.peerIds.contains(deviceId2)) {
|
||||
configuration.peers = configuration.peers + DeviceInfo(deviceId2, null)
|
||||
configuration.persistLater()
|
||||
syncthingClient.connectToNewlyAddedDevices()
|
||||
GlobalScope.launch (Dispatchers.Main) {
|
||||
context?.toast(context.getString(R.string.device_import_success, deviceId2.shortId))
|
||||
fun importDeviceId(libraryManager: LibraryManager, context: Context, deviceId: String, onComplete: () -> Unit) {
|
||||
val newDeviceId = DeviceId(deviceId.toUpperCase(Locale.US))
|
||||
|
||||
GlobalScope.launch (Dispatchers.Main) {
|
||||
libraryManager.withLibrary { library ->
|
||||
val didAddDevice = library.configuration.update { oldConfig ->
|
||||
if (oldConfig.peers.find { it.deviceId == newDeviceId } != null) {
|
||||
// already known
|
||||
|
||||
oldConfig
|
||||
} else {
|
||||
oldConfig.copy(
|
||||
peers = oldConfig.peers + DeviceInfo(newDeviceId, newDeviceId.shortId)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (didAddDevice) {
|
||||
library.configuration.persistLater()
|
||||
library.syncthingClient.connectToNewlyAddedDevices()
|
||||
|
||||
context.toast(context.getString(R.string.device_import_success, newDeviceId.shortId))
|
||||
onComplete()
|
||||
} else {
|
||||
context.toast(context.getString(R.string.device_already_known, newDeviceId.shortId))
|
||||
}
|
||||
} else {
|
||||
GlobalScope.launch (Dispatchers.Main) {
|
||||
context?.toast(context.getString(R.string.device_already_known, deviceId2.shortId))
|
||||
}
|
||||
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1 +1,7 @@
|
||||
- new (faster) index handling
|
||||
- update translations
|
||||
- encrypt some temporarily data which is stored on disk
|
||||
- update path validation
|
||||
- fix crash when cleaning the cache
|
||||
- fix crash of the index handler after cleaning the cache
|
||||
- convert file extensions to lower case for detecting Apps to open it
|
||||
- fix crash when accessing (e.g. trying to close) closed connections
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layout xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<LinearLayout
|
||||
android:padding="8dp"
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<TextView
|
||||
android:textAppearance="?android:textAppearanceMedium"
|
||||
android:text="@string/dialog_folder_info_device_list"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/device_checkboxes_container"
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<!--
|
||||
<CheckBox
|
||||
android:text="Test device 1 (the very very very very very very very very very very very long id)"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
<CheckBox
|
||||
android:text="Test device 2 (the very very very very very very very very very very very long id)"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
-->
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
</layout>
|
||||
@@ -7,46 +7,59 @@
|
||||
name="name"
|
||||
type="String" />
|
||||
|
||||
<variable
|
||||
name="status"
|
||||
type="String" />
|
||||
|
||||
<variable
|
||||
name="isConnected"
|
||||
type="Boolean" />
|
||||
</data>
|
||||
|
||||
<RelativeLayout
|
||||
<LinearLayout
|
||||
android:orientation="horizontal"
|
||||
android:background="?selectableItemBackground"
|
||||
android:padding="8dp"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingBottom="12dp"
|
||||
android:paddingLeft="24dp"
|
||||
android:paddingRight="24dp"
|
||||
android:paddingTop="12dp">
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<ImageView
|
||||
android:layout_gravity="center_vertical"
|
||||
android:id="@+id/device_icon"
|
||||
android:layout_width="32dp"
|
||||
android:layout_height="32dp"
|
||||
tools:src="@drawable/ic_laptop_green_24dp"
|
||||
android:src="@{safeUnbox(isConnected) ? @drawable/ic_laptop_green_24dp : @drawable/ic_laptop_red_24dp}"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_alignParentTop="true"/>
|
||||
android:src="@{safeUnbox(isConnected) ? @drawable/ic_laptop_green_24dp : @drawable/ic_laptop_red_24dp}" />
|
||||
|
||||
<TextView
|
||||
tools:text="Computer"
|
||||
android:text="@{name}"
|
||||
android:id="@+id/device_name"
|
||||
<View
|
||||
android:layout_width="16dp"
|
||||
android:layout_height="0dp" />
|
||||
|
||||
<LinearLayout
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:layout_alignParentTop="true"
|
||||
android:gravity="top"
|
||||
android:textAlignment="gravity"
|
||||
android:paddingStart="40dp"
|
||||
android:paddingEnd="40dp"
|
||||
android:textSize="18sp"
|
||||
android:textStyle="bold"/>
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
</RelativeLayout>
|
||||
<TextView
|
||||
tools:text="Computer"
|
||||
android:text="@{name}"
|
||||
android:id="@+id/device_name"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:textAlignment="gravity"
|
||||
android:textSize="18sp"
|
||||
android:textStyle="bold"/>
|
||||
|
||||
<TextView
|
||||
android:text="@{status}"
|
||||
tools:text="Trying to connect to 127.0.0.1 ..."
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</layout>
|
||||
|
||||
@@ -36,9 +36,33 @@
|
||||
<string name="intro_page_three_title">Ordner teilen</string>
|
||||
<string name="intro_page_two_description">Eine Syncthing Geräte ID eingeben oder QR Code einer Geräte ID scannen.</string>
|
||||
<string name="intro_page_three_description">Akzeptieren Sie nun das Gerät mit der ID %1$s und geben Sie einen Ordner mit ihm frei. Es kann einige Minuten dauern, bis sich die Geräte verbinden.</string>
|
||||
<string name="intro_page_three_searching_device">Versuche das andere Gerät zu finden. Dies kann einen Moment dauern.</string>
|
||||
<string name="settings">Einstellungen</string>
|
||||
<string name="settings_app_version_title">App Version</string>
|
||||
<string name="settings_local_device_name">Lokaler Geräte Namen</string>
|
||||
<string name="settings_local_device_summary">Name, den das andere Gerät für dieses Gerät sehen wird</string>
|
||||
<string name="settings_force_stop">Beenden dieser App erzwingen</string>
|
||||
<string name="settings_last_error_title">Letzter Fehler</string>
|
||||
<string name="settings_last_error_summary">Details des letzten Fehlers anzeigen</string>
|
||||
<string name="settings_report_bug_title">Einen Fehler melden</string>
|
||||
<string name="settings_report_bug_summary">Den Bugtracker bei GitHub für diese App öffnen</string>
|
||||
<string name="copy_to_clipboard">In die Zwischenablage kopieren</string>
|
||||
<string name="copied_to_clipboard">In die Zwischenablage kopiert</string>
|
||||
<string name="device_id_dialog_title">Geräte ID eingeben</string>
|
||||
</resources>
|
||||
<string name="dialog_warning_reconnect_problem">
|
||||
Aufgrund des Verhaltens dieser App und des Verhaltens des Syncthing-Servers können Sie sich für einige Minuten nicht verbinden, wenn die App erzwungen beendet wurde (durch das Entfernen aus der Liste der aktiven Apps) oder die Verbindung unterbrochen wurde.
|
||||
Dies gilt nicht für Verbindungen, die per lokaler Gerätesuche hergestellt wurden.</string>
|
||||
<string name="dialog_file_save_as">Speichern unter</string>
|
||||
<string name="pending_index_updates">%d Index-Updates verbleibend</string>
|
||||
<string name="device_status_connecting">Verbinden mit %s</string>
|
||||
<string name="device_status_connected">Mit %s verbunden</string>
|
||||
<string name="device_status_disconnected">Verbinden wird bald erneut versucht - es sind %d Adressen bekannt</string>
|
||||
<string name="device_status_no_address">Keine bekannte Adresse für dieses Gerät</string>
|
||||
<string name="dialog_enable_folder_sync_for_new_device_title">Ordnersynchronisation für neues Gerät aktivieren</string>
|
||||
<string name="dialog_enable_folder_sync_for_new_device_text">Möchten Sie den Ordner %1$s mit %2$s (%3$s) synchronisieren?</string>
|
||||
<string name="dialog_enable_folder_sync_for_new_device_positive">synchronisieren</string>
|
||||
<string name="dialog_enable_folder_sync_for_new_device_negative">nicht synchronisieren</string>
|
||||
<string name="dialog_folder_info_device_list">Ordner teilen mit:</string>
|
||||
<string name="dialog_folder_info_device_list_item">%1$s (%2$s)</string>
|
||||
<string name="toast_error">Etwas in Syncthing Lite hat nicht funktioniert. Sie können die Details in den Einstellungen von Syncthing Lite anzeigen.</string>
|
||||
</resources>
|
||||
|
||||
@@ -49,4 +49,5 @@
|
||||
<string name="settings_shutdown_delay_30_seconds">30 segundos</string>
|
||||
<string name="settings_shutdown_delay_1_minute">1 minuto</string>
|
||||
<string name="settings_shutdown_delay_5_minutes">5 minutos</string>
|
||||
</resources>
|
||||
<string name="dialog_folder_info_device_list_item">%1$s (%2$s)</string>
|
||||
</resources>
|
||||
|
||||
@@ -47,4 +47,5 @@
|
||||
<string name="settings_shutdown_delay_30_seconds">30 secondes</string>
|
||||
<string name="settings_shutdown_delay_1_minute">1 minute</string>
|
||||
<string name="settings_shutdown_delay_5_minutes">5 minutes</string>
|
||||
</resources>
|
||||
<string name="dialog_folder_info_device_list_item">%1$s (%2$s)</string>
|
||||
</resources>
|
||||
|
||||
@@ -47,4 +47,5 @@
|
||||
<string name="settings_shutdown_delay_30_seconds">30 másodperc</string>
|
||||
<string name="settings_shutdown_delay_1_minute">1 perc</string>
|
||||
<string name="settings_shutdown_delay_5_minutes">5 perc</string>
|
||||
</resources>
|
||||
<string name="dialog_folder_info_device_list_item">%1$s (%2$s)</string>
|
||||
</resources>
|
||||
|
||||
@@ -47,4 +47,5 @@
|
||||
<string name="settings_shutdown_delay_30_seconds">30 secondi</string>
|
||||
<string name="settings_shutdown_delay_1_minute">1 minuto</string>
|
||||
<string name="settings_shutdown_delay_5_minutes">5 minuti</string>
|
||||
</resources>
|
||||
<string name="dialog_folder_info_device_list_item">%1$s (%2$s)</string>
|
||||
</resources>
|
||||
|
||||
@@ -41,4 +41,5 @@
|
||||
<string name="settings_local_device_name">ローカルのデバイス名</string>
|
||||
<string name="settings_local_device_summary">他のデバイスがこのデバイスを表示する名前</string>
|
||||
<string name="device_id_dialog_title">デバイス ID を入力</string>
|
||||
<string name="dialog_folder_info_device_list_item">%1$s (%2$s)</string>
|
||||
</resources>
|
||||
|
||||
@@ -41,4 +41,5 @@
|
||||
<string name="settings_local_device_name">Naam van lokaal apparaat</string>
|
||||
<string name="settings_local_device_summary">De naam die dat andere apparaten voor dit apparaat gaan zien</string>
|
||||
<string name="device_id_dialog_title">Voert nen apparaats-ID in</string>
|
||||
<string name="dialog_folder_info_device_list_item">%1$s (%2$s)</string>
|
||||
</resources>
|
||||
|
||||
@@ -41,4 +41,5 @@
|
||||
<string name="settings_local_device_name">Naam van lokaal apparaat</string>
|
||||
<string name="settings_local_device_summary">De naam die andere apparaten voor dit apparaat zullen zien</string>
|
||||
<string name="device_id_dialog_title">Voer een apparaats-ID in</string>
|
||||
<string name="dialog_folder_info_device_list_item">%1$s (%2$s)</string>
|
||||
</resources>
|
||||
|
||||
@@ -40,15 +40,36 @@ fi stocate, dacă vor fi partajate cu terțe entități precum și cum vor fi
|
||||
<string name="intro_page_three_title">Partajați-vă directoarele</string>
|
||||
<string name="intro_page_two_description">Introduceți ID-ul Syncthing al unui dispozitiv sau scanați ID-ul unui dispozitiv dintr-un cod QR</string>
|
||||
<string name="intro_page_three_description">Acceptați acum dispozitivul cu ID-ul %1$s, și partajați un director cu el. S-ar putea să dureze câteva minute până când dispozitivele se vor conecta.</string>
|
||||
<string name="intro_page_three_searching_device">Se încearcă găsirea celuilalt dispozitiv. Această operație poate dura un moment.</string>
|
||||
<string name="settings">Setări</string>
|
||||
<string name="settings_app_version_title">Versiune aplicație</string>
|
||||
<string name="settings_local_device_name">Nume local dispozitiv</string>
|
||||
<string name="settings_local_device_summary">Numele pe care celălalt dispozitiv îl va vedea pentru acest dispozitiv</string>
|
||||
<string name="settings_shutdown_delay_title">Temporizare oprire</string>
|
||||
<string name="settings_shutdown_delay_summary">După cât timp se va închide clientul Syncthing în funcție de ultima utilizare</string>
|
||||
<string name="settings_force_stop">Forțează oprirea acestei aplicații</string>
|
||||
<string name="settings_last_error_title">Ultima eroare</string>
|
||||
<string name="settings_last_error_summary">Arată detaliile ultimei erori</string>
|
||||
<string name="settings_report_bug_title">Raportează o eroare</string>
|
||||
<string name="settings_report_bug_summary">Deschideți un raport de eroare pentru această aplicație pe GitHub</string>
|
||||
<string name="copy_to_clipboard">Copiază în memorie</string>
|
||||
<string name="copied_to_clipboard">Copiat în memorie</string>
|
||||
<string name="device_id_dialog_title">Introduceți ID dispozitiv</string>
|
||||
<string name="settings_shutdown_delay_10_seconds">10 secunde</string>
|
||||
<string name="settings_shutdown_delay_30_seconds">30 secunde</string>
|
||||
<string name="settings_shutdown_delay_1_minute">1 minut</string>
|
||||
<string name="settings_shutdown_delay_5_minutes">5 minute</string>
|
||||
<string name="dialog_file_save_as">Salvează ca</string>
|
||||
<string name="pending_index_updates">%d actualizări de index în așteptare</string>
|
||||
<string name="device_status_connecting">Conectare la %s</string>
|
||||
<string name="device_status_connected">Conectat la %s</string>
|
||||
<string name="device_status_disconnected">Se va încerca conectarea în curând - există %d adrese cunoscute</string>
|
||||
<string name="device_status_no_address">Nici o adresă cunoscută pentru acest dispozitiv</string>
|
||||
<string name="dialog_enable_folder_sync_for_new_device_title">Activați sincronizarea directorului pentru un dispozitiv nou</string>
|
||||
<string name="dialog_enable_folder_sync_for_new_device_text">Doriți să sincronizați %1$s cu %2$s (%3$s)?</string>
|
||||
<string name="dialog_enable_folder_sync_for_new_device_positive">Se sincronizează</string>
|
||||
<string name="dialog_enable_folder_sync_for_new_device_negative">Nu se sincronizează</string>
|
||||
<string name="dialog_folder_info_device_list">Partajează directorul cu:</string>
|
||||
<string name="dialog_folder_info_device_list_item">%1$s (%2$s)</string>
|
||||
<string name="toast_error">O eroare s-a produs in Syncthing Lite. Puteți vedea detalii în setările Syncthing Lite.</string>
|
||||
</resources>
|
||||
|
||||
@@ -36,15 +36,42 @@
|
||||
<string name="intro_page_three_title">Dela dina mappar</string>
|
||||
<string name="intro_page_two_description">Ange ett Syncthing enhets-ID, eller skanna ett enhets-ID-nummer från en QR-kod</string>
|
||||
<string name="intro_page_three_description">Acceptera nu enheten med ID %1$s och dela en mapp med den. Det kan ta några minuter tills enheterna ansluter.</string>
|
||||
<string name="intro_page_three_searching_device">Försöker hitta den andra enheten. Det kan ta ett ögonblick.</string>
|
||||
<string name="settings">Inställningar</string>
|
||||
<string name="settings_app_version_title">Appversion</string>
|
||||
<string name="settings_local_device_name">Lokala enhetens namn</string>
|
||||
<string name="settings_local_device_summary">Namnet som andra enheter kommer att se för den här enheten</string>
|
||||
<string name="settings_shutdown_delay_title">Avstängningsfördröjning</string>
|
||||
<string name="settings_shutdown_delay_summary">Tid innan du stänger av Syncthing-klienten efter den senaste användningen</string>
|
||||
<string name="settings_force_stop">Tvinga stoppa denna App</string>
|
||||
<string name="settings_last_error_title">Senaste felet</string>
|
||||
<string name="settings_last_error_summary">Visa detaljerna för det senaste felet</string>
|
||||
<string name="settings_report_bug_title">Rapportera ett fel</string>
|
||||
<string name="settings_report_bug_summary">Öppna problemen för den här appen på GitHub</string>
|
||||
<string name="copy_to_clipboard">Kopiera till urklipp</string>
|
||||
<string name="copied_to_clipboard">Kopieras till urklippet</string>
|
||||
<string name="device_id_dialog_title">Ange enhets-ID</string>
|
||||
<string name="settings_shutdown_delay_10_seconds">10 sekunder</string>
|
||||
<string name="settings_shutdown_delay_30_seconds">30 sekunder</string>
|
||||
<string name="settings_shutdown_delay_1_minute">1 minut</string>
|
||||
<string name="settings_shutdown_delay_5_minutes">5 minuter</string>
|
||||
<string name="dialog_warning_reconnect_problem">
|
||||
På grund av beteendet hos denna App och beteendet hos Syncthing-servern,
|
||||
du kan inte återansluta i några minuter om appen dödades (på grund av att du tog bort från den senaste applistan)
|
||||
eller anslutningen avbröts.
|
||||
Detta gäller inte lokala upptäcktsanslutningar.
|
||||
</string>
|
||||
<string name="dialog_file_save_as">Spara som</string>
|
||||
<string name="pending_index_updates">%d indexuppdateringar som väntar</string>
|
||||
<string name="device_status_connecting">Ansluter till %s</string>
|
||||
<string name="device_status_connected">Ansluten till %s</string>
|
||||
<string name="device_status_disconnected">Kommer att försöka ansluta snart - det finns%d kända adresser</string>
|
||||
<string name="device_status_no_address">Ingen känd adress för enheten</string>
|
||||
<string name="dialog_enable_folder_sync_for_new_device_title">Aktivera mappsynkronisering för ny enhet</string>
|
||||
<string name="dialog_enable_folder_sync_for_new_device_text">Vill du synkronisera %1$s med %2$s (%3$s)?</string>
|
||||
<string name="dialog_enable_folder_sync_for_new_device_positive">Synkronisera</string>
|
||||
<string name="dialog_enable_folder_sync_for_new_device_negative">Synkronisera inte</string>
|
||||
<string name="dialog_folder_info_device_list">Dela mapp med:</string>
|
||||
<string name="dialog_folder_info_device_list_item">%1$s (%2$s)</string>
|
||||
<string name="toast_error">Något gick fel i Syncthing Lite. Du kan visa detaljerna från inställningarna för Syncthing Lite.</string>
|
||||
</resources>
|
||||
|
||||
@@ -46,4 +46,5 @@
|
||||
<string name="settings_shutdown_delay_30_seconds">30 秒</string>
|
||||
<string name="settings_shutdown_delay_1_minute">1 分钟</string>
|
||||
<string name="settings_shutdown_delay_5_minutes">5 分钟</string>
|
||||
</resources>
|
||||
<string name="dialog_folder_info_device_list_item">%1$s(%2$s)</string>
|
||||
</resources>
|
||||
|
||||
@@ -36,21 +36,20 @@
|
||||
<string name="intro_page_three_title">Share your folders</string>
|
||||
<string name="intro_page_two_description">Enter a Syncthing device ID, or scan a device ID from a QR code</string>
|
||||
<string name="intro_page_three_description">Now accept the device with ID %1$s, and share a folder with it. It may take a few minutes until the devices connect.</string>
|
||||
<string name="intro_page_three_searching_device">Trying to find the other device. This may take a moment.</string>
|
||||
<string name="settings">Settings</string>
|
||||
<string name="settings_app_version_title">App version</string>
|
||||
<string name="settings_local_device_name">Local device name</string>
|
||||
<string name="settings_local_device_summary">The name that other devices will see for this device</string>
|
||||
<string name="settings_shutdown_delay_title">Shutdown delay</string>
|
||||
<string name="settings_shutdown_delay_summary">Time before shuting down the Syncthing client after its last usage</string>
|
||||
<string name="settings_crash_handler_title">Custom Crash-Handler</string>
|
||||
<string name="settings_crash_handler_summary">Copy the error message to the clipboard when the App crashes</string>
|
||||
<string name="settings_detailed_exception_title">Enable more detailed crash reports</string>
|
||||
<string name="settings_detailed_exception_summary">
|
||||
This could leak private data.
|
||||
You should only use it with the custom crash handler and should not send a crash report without review.
|
||||
Changes of this need an App restart (use force stop to be safe).
|
||||
</string>
|
||||
<string name="settings_force_stop">Force stop this App</string>
|
||||
<string name="settings_last_error_title">Last error</string>
|
||||
<string name="settings_last_error_summary">View the details of the last error</string>
|
||||
<string name="settings_report_bug_title">Report a bug</string>
|
||||
<string name="settings_report_bug_summary">Open the issues for this App at GitHub</string>
|
||||
<string name="copy_to_clipboard">Copy to clipboard</string>
|
||||
<string name="copied_to_clipboard">Copied to the clipboard</string>
|
||||
<string name="device_id_dialog_title">Enter Device ID</string>
|
||||
<string name="settings_shutdown_delay_10_seconds">10 seconds</string>
|
||||
<string name="settings_shutdown_delay_30_seconds">30 seconds</string>
|
||||
@@ -64,4 +63,15 @@
|
||||
</string>
|
||||
<string name="dialog_file_save_as">Save as</string>
|
||||
<string name="pending_index_updates">%d index updates pending</string>
|
||||
<string name="device_status_connecting">Connecting to %s</string>
|
||||
<string name="device_status_connected">Connected to %s</string>
|
||||
<string name="device_status_disconnected">Will retry connecting soon - there are %d known addresses</string>
|
||||
<string name="device_status_no_address">No known address for the device</string>
|
||||
<string name="dialog_enable_folder_sync_for_new_device_title">Enable folder sync for new device</string>
|
||||
<string name="dialog_enable_folder_sync_for_new_device_text">Do you want to sync %1$s with %2$s (%3$s)?</string>
|
||||
<string name="dialog_enable_folder_sync_for_new_device_positive">Sync</string>
|
||||
<string name="dialog_enable_folder_sync_for_new_device_negative">Do not sync</string>
|
||||
<string name="dialog_folder_info_device_list">Share folder with:</string>
|
||||
<string name="dialog_folder_info_device_list_item">%1$s (%2$s)</string>
|
||||
<string name="toast_error">Something went wrong in Syncthing Lite. You can view the details from the settings of Syncthing Lite.</string>
|
||||
</resources>
|
||||
|
||||
@@ -24,20 +24,20 @@
|
||||
|
||||
-->
|
||||
|
||||
<CheckBoxPreference
|
||||
android:key="crash_handler"
|
||||
android:title="@string/settings_crash_handler_title"
|
||||
android:summary="@string/settings_crash_handler_summary" />
|
||||
|
||||
<CheckBoxPreference
|
||||
android:key="detailed_exception"
|
||||
android:title="@string/settings_detailed_exception_title"
|
||||
android:summary="@string/settings_detailed_exception_summary" />
|
||||
<Preference
|
||||
android:key="last_crash"
|
||||
android:title="@string/settings_last_error_title"
|
||||
android:summary="@string/settings_last_error_summary" />
|
||||
|
||||
<Preference
|
||||
android:key="app_version"
|
||||
android:title="@string/settings_app_version_title"/>
|
||||
|
||||
<Preference
|
||||
android:key="report_bug"
|
||||
android:title="@string/settings_report_bug_title"
|
||||
android:summary="@string/settings_report_bug_summary" />
|
||||
|
||||
<Preference
|
||||
android:key="force_stop"
|
||||
android:title="@string/settings_force_stop" />
|
||||
|
||||
+2
-2
@@ -2,8 +2,8 @@
|
||||
|
||||
buildscript {
|
||||
ext.kotlin_version = '1.3.0'
|
||||
ext.support_version = '27.0.2'
|
||||
ext.build_tools_version = '3.2.0'
|
||||
ext.support_version = '27.1.1'
|
||||
ext.build_tools_version = '3.2.1'
|
||||
ext.anko_version = '0.10.8'
|
||||
ext.protobuf_lite_version = '3.0.1'
|
||||
repositories {
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
include ':app', ':syncthing-repository-android', ':syncthing-repository-default', ':syncthing-relay-client', ':syncthing-bep', ':syncthing-core', ':syncthing-client', ':syncthing-discovery', ':syncthing-client-cli'
|
||||
include ':app', ':syncthing-repository-android', ':syncthing-repository-default', ':syncthing-relay-client', ':syncthing-bep', ':syncthing-core', ':syncthing-client', ':syncthing-discovery', ':syncthing-client-cli', ':syncthing-temp-repository-encryption'
|
||||
|
||||
@@ -22,10 +22,7 @@ import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import net.syncthing.java.bep.BlockExchangeProtos.Vector
|
||||
import net.syncthing.java.bep.connectionactor.ConnectionActorWrapper
|
||||
import net.syncthing.java.bep.index.FolderStatsUpdateCollector
|
||||
import net.syncthing.java.bep.index.IndexElementProcessor
|
||||
import net.syncthing.java.bep.index.IndexHandler
|
||||
import net.syncthing.java.bep.index.IndexMessageProcessor
|
||||
import net.syncthing.java.bep.index.*
|
||||
import net.syncthing.java.core.beans.BlockInfo
|
||||
import net.syncthing.java.core.beans.DeviceId
|
||||
import net.syncthing.java.core.beans.FileInfo.Version
|
||||
@@ -108,16 +105,20 @@ class BlockPusher(private val localDeviceId: DeviceId,
|
||||
}
|
||||
|
||||
logger.debug("send index update for file = {}", targetPath)
|
||||
val indexListenerStream = indexHandler.subscribeToOnIndexRecordAcquiredEvents()
|
||||
val indexListenerStream = indexHandler.subscribeToOnIndexUpdateEvents()
|
||||
GlobalScope.launch {
|
||||
indexListenerStream.consumeEach { (indexFolderId, newRecords, _) ->
|
||||
if (indexFolderId == folderId) {
|
||||
for (fileInfo2 in newRecords) {
|
||||
if (fileInfo2.path == targetPath && fileInfo2.hash == dataSource.getHash()) { //TODO check not invalid
|
||||
// sentBlocks.addAll(dataSource.getHashes());
|
||||
isCompleted.set(true)
|
||||
synchronized(updateLock) {
|
||||
updateLock.notifyAll()
|
||||
indexListenerStream.consumeEach { event ->
|
||||
if (event is IndexRecordAcquiredEvent) {
|
||||
val (indexFolderId, newRecords, _) = event
|
||||
|
||||
if (indexFolderId == folderId) {
|
||||
for (fileInfo2 in newRecords) {
|
||||
if (fileInfo2.path == targetPath && fileInfo2.hash == dataSource.getHash()) { //TODO check not invalid
|
||||
// sentBlocks.addAll(dataSource.getHashes());
|
||||
isCompleted.set(true)
|
||||
synchronized(updateLock) {
|
||||
updateLock.notifyAll()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+93
-61
@@ -33,42 +33,44 @@ object ClusterConfigHandler {
|
||||
val builder = BlockExchangeProtos.ClusterConfig.newBuilder()
|
||||
|
||||
indexHandler.indexRepository.runInTransaction { indexTransaction ->
|
||||
for (folder in configuration.folders) {
|
||||
val folderBuilder = BlockExchangeProtos.Folder.newBuilder()
|
||||
.setId(folder.folderId)
|
||||
.setLabel(folder.label)
|
||||
configuration.folders
|
||||
.filter { it.deviceIdWhitelist.contains(deviceId) }
|
||||
.forEach { folder ->
|
||||
val folderBuilder = BlockExchangeProtos.Folder.newBuilder()
|
||||
.setId(folder.folderId)
|
||||
.setLabel(folder.label)
|
||||
|
||||
// add this device
|
||||
folderBuilder.addDevices(
|
||||
BlockExchangeProtos.Device.newBuilder()
|
||||
.setId(ByteString.copyFrom(configuration.localDeviceId.toHashData()))
|
||||
.setIndexId(indexTransaction.getSequencer().indexId())
|
||||
.setMaxSequence(indexTransaction.getSequencer().currentSequence())
|
||||
)
|
||||
// add this device
|
||||
folderBuilder.addDevices(
|
||||
BlockExchangeProtos.Device.newBuilder()
|
||||
.setId(ByteString.copyFrom(configuration.localDeviceId.toHashData()))
|
||||
.setIndexId(indexTransaction.getSequencer().indexId())
|
||||
.setMaxSequence(indexTransaction.getSequencer().currentSequence())
|
||||
)
|
||||
|
||||
// add other device
|
||||
val indexSequenceInfo = indexTransaction.findIndexInfoByDeviceAndFolder(deviceId, folder.folderId)
|
||||
// add other device
|
||||
val indexSequenceInfo = indexTransaction.findIndexInfoByDeviceAndFolder(deviceId, folder.folderId)
|
||||
|
||||
folderBuilder.addDevices(
|
||||
BlockExchangeProtos.Device.newBuilder()
|
||||
.setId(ByteString.copyFrom(deviceId.toHashData()))
|
||||
.apply {
|
||||
indexSequenceInfo?.let {
|
||||
setIndexId(indexSequenceInfo.indexId)
|
||||
setMaxSequence(indexSequenceInfo.localSequence)
|
||||
folderBuilder.addDevices(
|
||||
BlockExchangeProtos.Device.newBuilder()
|
||||
.setId(ByteString.copyFrom(deviceId.toHashData()))
|
||||
.apply {
|
||||
indexSequenceInfo?.let {
|
||||
indexId = indexSequenceInfo.indexId
|
||||
maxSequence = indexSequenceInfo.localSequence
|
||||
|
||||
logger.info("send delta index info device = {} index = {} max (local) sequence = {}",
|
||||
indexSequenceInfo.deviceId,
|
||||
indexSequenceInfo.indexId,
|
||||
indexSequenceInfo.localSequence)
|
||||
}
|
||||
}
|
||||
)
|
||||
logger.info("send delta index info device = {} index = {} max (local) sequence = {}",
|
||||
indexSequenceInfo.deviceId,
|
||||
indexSequenceInfo.indexId,
|
||||
indexSequenceInfo.localSequence)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
builder.addFolders(folderBuilder)
|
||||
builder.addFolders(folderBuilder)
|
||||
|
||||
// TODO: add the other devices to the cluster config
|
||||
}
|
||||
// TODO: add the other devices to the cluster config
|
||||
}
|
||||
}
|
||||
|
||||
return builder.build()
|
||||
@@ -84,40 +86,69 @@ object ClusterConfigHandler {
|
||||
val folderInfoList = mutableListOf<ClusterConfigFolderInfo>()
|
||||
val newSharedFolders = mutableListOf<FolderInfo>()
|
||||
|
||||
for (folder in clusterConfig.foldersList ?: emptyList()) {
|
||||
var folderInfo = ClusterConfigFolderInfo(folder.id, folder.label)
|
||||
val devicesById = (folder.devicesList ?: emptyList())
|
||||
.associateBy { input ->
|
||||
DeviceId.fromHashData(input.id!!.toByteArray())
|
||||
}
|
||||
val otherDevice = devicesById[otherDeviceId]
|
||||
val ourDevice = devicesById[configuration.localDeviceId]
|
||||
if (otherDevice != null) {
|
||||
folderInfo = folderInfo.copy(isAnnounced = true)
|
||||
}
|
||||
if (ourDevice != null) {
|
||||
folderInfo = folderInfo.copy(isShared = true)
|
||||
logger.info("folder shared from device = {} folder = {}", otherDeviceId, folderInfo)
|
||||
configuration.update { oldConfig ->
|
||||
val configFolders = oldConfig.folders.toMutableSet()
|
||||
|
||||
val newFolderInfo = FolderInfo(folderInfo.folderId, folderInfo.label)
|
||||
|
||||
val oldFolderEntry = configuration.folders.find { it.folderId == folderInfo.folderId }
|
||||
|
||||
if (oldFolderEntry == null) {
|
||||
configuration.folders = configuration.folders + newFolderInfo
|
||||
newSharedFolders.add(newFolderInfo)
|
||||
logger.info("new folder shared = {}", folderInfo)
|
||||
} else {
|
||||
if (oldFolderEntry != newFolderInfo) {
|
||||
configuration.folders = configuration.folders.filter { it != oldFolderEntry }.toSet() + setOf(newFolderInfo)
|
||||
}
|
||||
for (folder in clusterConfig.foldersList ?: emptyList()) {
|
||||
var folderInfo = ClusterConfigFolderInfo(folder.id, folder.label, isDeviceInSharedFolderWhitelist = false)
|
||||
val devicesById = (folder.devicesList ?: emptyList())
|
||||
.associateBy { input ->
|
||||
DeviceId.fromHashData(input.id!!.toByteArray())
|
||||
}
|
||||
val otherDevice = devicesById[otherDeviceId]
|
||||
val ourDevice = devicesById[configuration.localDeviceId]
|
||||
if (otherDevice != null) {
|
||||
folderInfo = folderInfo.copy(isAnnounced = true)
|
||||
}
|
||||
} else {
|
||||
logger.info("folder not shared from device = {} folder = {}", otherDeviceId, folderInfo)
|
||||
if (ourDevice != null) {
|
||||
folderInfo = folderInfo.copy(isShared = true)
|
||||
logger.info("folder shared from device = {} folder = {}", otherDeviceId, folderInfo)
|
||||
|
||||
val oldFolderEntry = configFolders.find { it.folderId == folderInfo.folderId }
|
||||
|
||||
if (oldFolderEntry == null) {
|
||||
folderInfo = folderInfo.copy(isDeviceInSharedFolderWhitelist = true)
|
||||
|
||||
val newFolderInfo = FolderInfo(
|
||||
folderId = folderInfo.folderId,
|
||||
label = folderInfo.label,
|
||||
deviceIdWhitelist = setOf(otherDeviceId),
|
||||
deviceIdBlacklist = emptySet(),
|
||||
ignoredDeviceIdList = emptySet()
|
||||
)
|
||||
|
||||
configFolders.add(newFolderInfo)
|
||||
newSharedFolders.add(newFolderInfo)
|
||||
logger.info("new folder shared = {}", folderInfo)
|
||||
} else {
|
||||
if (oldFolderEntry.deviceIdWhitelist.contains(otherDeviceId)) {
|
||||
folderInfo = folderInfo.copy(isDeviceInSharedFolderWhitelist = true)
|
||||
|
||||
if (oldFolderEntry.label != folderInfo.label) {
|
||||
configFolders.remove(oldFolderEntry)
|
||||
configFolders.add(oldFolderEntry.copy(label = folderInfo.label))
|
||||
}
|
||||
} else {
|
||||
if (!oldFolderEntry.deviceIdBlacklist.contains(otherDeviceId)) {
|
||||
configFolders.remove(oldFolderEntry)
|
||||
configFolders.add(
|
||||
oldFolderEntry.copy(
|
||||
deviceIdBlacklist = oldFolderEntry.deviceIdBlacklist + setOf(otherDeviceId)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
logger.info("folder not shared from device = {} folder = {}", otherDeviceId, folderInfo)
|
||||
}
|
||||
|
||||
folderInfoList.add(folderInfo)
|
||||
}
|
||||
|
||||
folderInfoList.add(folderInfo)
|
||||
oldConfig.copy(folders = configFolders)
|
||||
}
|
||||
|
||||
configuration.persistLater()
|
||||
indexHandler.handleClusterConfigMessageProcessedEvent(clusterConfig)
|
||||
|
||||
@@ -132,7 +163,7 @@ class ClusterConfigInfo (val folderInfo: List<ClusterConfigFolderInfo>, val newS
|
||||
|
||||
val folderInfoById = folderInfo.associateBy { it.folderId }
|
||||
val sharedFolderIds: Set<String> by lazy {
|
||||
folderInfo.filter { it.isShared }.map { it.folderId }.toSet()
|
||||
folderInfo.filter { it.isShared && it.isDeviceInSharedFolderWhitelist }.map { it.folderId }.toSet()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -140,7 +171,8 @@ data class ClusterConfigFolderInfo(
|
||||
val folderId: String,
|
||||
val label: String = folderId,
|
||||
val isAnnounced: Boolean = false,
|
||||
val isShared: Boolean = false
|
||||
val isShared: Boolean = false,
|
||||
val isDeviceInSharedFolderWhitelist: Boolean
|
||||
) {
|
||||
init {
|
||||
assert(folderId.isNotEmpty())
|
||||
|
||||
+79
-27
@@ -23,6 +23,11 @@ import net.syncthing.java.core.configuration.Configuration
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.io.IOException
|
||||
|
||||
data class Connection (
|
||||
val actor: SendChannel<ConnectionAction>,
|
||||
val clusterConfigInfo: ClusterConfigInfo
|
||||
)
|
||||
|
||||
object ConnectionActorGenerator {
|
||||
private val closed = Channel<ConnectionAction>().apply { cancel() }
|
||||
private val logger = LoggerFactory.getLogger(ConnectionActorGenerator::class.java)
|
||||
@@ -89,18 +94,42 @@ object ConnectionActorGenerator {
|
||||
configuration: Configuration,
|
||||
indexHandler: IndexHandler,
|
||||
requestHandler: (BlockExchangeProtos.Request) -> Deferred<BlockExchangeProtos.Response>
|
||||
) = GlobalScope.produce<Pair<SendChannel<ConnectionAction>, ClusterConfigInfo>> {
|
||||
) = GlobalScope.produce<Pair<Connection, ConnectionInfo>> {
|
||||
var currentActor: SendChannel<ConnectionAction> = closed
|
||||
var currentClusterConfig = ClusterConfigInfo.dummy
|
||||
var currentDeviceAddress: DeviceAddress? = null
|
||||
var currentStatus = ConnectionInfo.empty
|
||||
|
||||
suspend fun dispatchStatus() {
|
||||
send(Connection(currentActor, currentClusterConfig) to currentStatus)
|
||||
}
|
||||
|
||||
suspend fun closeCurrent() {
|
||||
if (currentActor != closed) {
|
||||
currentActor.close()
|
||||
currentActor = closed
|
||||
send(currentActor to ClusterConfigInfo.dummy)
|
||||
currentClusterConfig = ClusterConfigInfo.dummy
|
||||
|
||||
if (currentStatus.status != ConnectionStatus.Disconnected) {
|
||||
currentStatus = currentStatus.copy(status = ConnectionStatus.Disconnected)
|
||||
}
|
||||
|
||||
dispatchStatus()
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun dispatchConnection(
|
||||
connection: SendChannel<ConnectionAction>,
|
||||
clusterConfig: ClusterConfigInfo,
|
||||
deviceAddress: DeviceAddress
|
||||
) {
|
||||
currentActor = connection
|
||||
currentDeviceAddress = deviceAddress
|
||||
currentClusterConfig = clusterConfig
|
||||
|
||||
dispatchStatus()
|
||||
}
|
||||
|
||||
suspend fun tryConnectingToAddressHandleBaseErrors(deviceAddress: DeviceAddress) = try {
|
||||
val newActor = ConnectionActor.createInstance(deviceAddress, configuration, indexHandler, requestHandler)
|
||||
val clusterConfig = ConnectionActorUtil.waitUntilConnected(newActor)
|
||||
@@ -118,31 +147,37 @@ object ConnectionActorGenerator {
|
||||
null
|
||||
}
|
||||
|
||||
suspend fun dispatchConnection(
|
||||
connection: SendChannel<ConnectionAction>,
|
||||
clusterConfig: ClusterConfigInfo,
|
||||
deviceAddress: DeviceAddress
|
||||
) {
|
||||
currentActor = connection
|
||||
currentDeviceAddress = deviceAddress
|
||||
|
||||
send(connection to clusterConfig)
|
||||
}
|
||||
|
||||
suspend fun tryConnectingToAddress(deviceAddress: DeviceAddress): Boolean {
|
||||
closeCurrent()
|
||||
|
||||
var connection = tryConnectingToAddressHandleBaseErrors(deviceAddress) ?: return false
|
||||
suspend fun handleCancel() {
|
||||
currentStatus = currentStatus.copy(
|
||||
status = ConnectionStatus.Disconnected
|
||||
)
|
||||
dispatchStatus()
|
||||
}
|
||||
|
||||
currentStatus = currentStatus.copy(
|
||||
status = ConnectionStatus.Connecting,
|
||||
currentAddress = deviceAddress
|
||||
)
|
||||
dispatchStatus()
|
||||
|
||||
var connection = tryConnectingToAddressHandleBaseErrors(deviceAddress) ?: return run {handleCancel(); false}
|
||||
|
||||
if (connection.second.newSharedFolders.isNotEmpty()) {
|
||||
logger.debug("connected to $deviceAddress with new folders -> reconnect")
|
||||
// reconnect to send new cluster config
|
||||
connection.first.close()
|
||||
connection = tryConnectingToAddressHandleBaseErrors(deviceAddress) ?: return false
|
||||
connection = tryConnectingToAddressHandleBaseErrors(deviceAddress) ?: return run {handleCancel(); false}
|
||||
}
|
||||
|
||||
logger.debug("connected to $deviceAddress")
|
||||
|
||||
currentStatus = currentStatus.copy(
|
||||
status = ConnectionStatus.Connected,
|
||||
currentAddress = deviceAddress
|
||||
)
|
||||
dispatchConnection(connection.first, connection.second, deviceAddress)
|
||||
|
||||
return true
|
||||
@@ -157,18 +192,26 @@ object ConnectionActorGenerator {
|
||||
val reconnectTicker = ticker(delayMillis = 30 * 1000, initialDelayMillis = 0)
|
||||
|
||||
deviceAddressSource.consume {
|
||||
var lastDeviceAddressList: List<DeviceAddress> = emptyList()
|
||||
|
||||
while (true) {
|
||||
if (isConnected()) {
|
||||
lastDeviceAddressList = deviceAddressSource.poll() ?: lastDeviceAddressList
|
||||
run {
|
||||
// get the new list version if there is any
|
||||
val newDeviceAddressList = deviceAddressSource.poll()
|
||||
|
||||
if (lastDeviceAddressList.isNotEmpty()) {
|
||||
if (newDeviceAddressList != null) {
|
||||
currentStatus = currentStatus.copy(addresses = newDeviceAddressList)
|
||||
dispatchStatus()
|
||||
}
|
||||
}
|
||||
|
||||
if (isConnected()) {
|
||||
val deviceAddressList = currentStatus.addresses
|
||||
|
||||
if (deviceAddressList.isNotEmpty()) {
|
||||
if (reconnectTicker.poll() != null) {
|
||||
if (currentDeviceAddress != lastDeviceAddressList.first()) {
|
||||
if (currentDeviceAddress != deviceAddressList.first()) {
|
||||
val oldDeviceAddress = currentDeviceAddress!!
|
||||
|
||||
if (!tryConnectingToAddress(lastDeviceAddressList.first())) {
|
||||
if (!tryConnectingToAddress(deviceAddressList.first())) {
|
||||
tryConnectingToAddress(oldDeviceAddress)
|
||||
}
|
||||
}
|
||||
@@ -179,11 +222,15 @@ object ConnectionActorGenerator {
|
||||
|
||||
delay(500) // don't take too much CPU
|
||||
} else /* is not connected */ {
|
||||
// get the new list version if there is any
|
||||
lastDeviceAddressList = deviceAddressSource.poll() ?: lastDeviceAddressList
|
||||
if (currentStatus.status == ConnectionStatus.Connected) {
|
||||
currentStatus = currentStatus.copy(status = ConnectionStatus.Disconnected)
|
||||
dispatchStatus()
|
||||
}
|
||||
|
||||
val deviceAddressList = currentStatus.addresses
|
||||
|
||||
// try all addresses
|
||||
for (address in lastDeviceAddressList) {
|
||||
for (address in deviceAddressList) {
|
||||
if (tryConnectingToAddress(address)) {
|
||||
break
|
||||
}
|
||||
@@ -194,9 +241,14 @@ object ConnectionActorGenerator {
|
||||
reconnectTicker.poll()
|
||||
|
||||
// wait for new device address list but not more than 15 seconds before the next iteration
|
||||
lastDeviceAddressList = withTimeoutOrNull(15 * 1000) {
|
||||
val newDeviceAddressList = withTimeoutOrNull(15 * 1000) {
|
||||
deviceAddressSource.receive()
|
||||
} ?: lastDeviceAddressList
|
||||
}
|
||||
|
||||
if (newDeviceAddressList != null) {
|
||||
currentStatus = currentStatus.copy(addresses = newDeviceAddressList)
|
||||
dispatchStatus()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+20
-7
@@ -16,6 +16,7 @@ package net.syncthing.java.bep.connectionactor
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
import kotlinx.coroutines.channels.SendChannel
|
||||
import net.syncthing.java.bep.BlockExchangeProtos
|
||||
import java.io.IOException
|
||||
|
||||
object ConnectionActorUtil {
|
||||
suspend fun waitUntilConnected(actor: SendChannel<ConnectionAction>): ClusterConfigInfo {
|
||||
@@ -28,22 +29,34 @@ object ConnectionActorUtil {
|
||||
}
|
||||
|
||||
suspend fun sendRequest(request: BlockExchangeProtos.Request, actor: SendChannel<ConnectionAction>): BlockExchangeProtos.Response {
|
||||
val deferred = CompletableDeferred<BlockExchangeProtos.Response>()
|
||||
try {
|
||||
val deferred = CompletableDeferred<BlockExchangeProtos.Response>()
|
||||
|
||||
actor.send(SendRequestConnectionAction(request, deferred))
|
||||
actor.send(SendRequestConnectionAction(request, deferred))
|
||||
|
||||
return deferred.await()
|
||||
return deferred.await()
|
||||
} catch (ex: Exception) {
|
||||
throw IOException("not connected", ex)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun sendIndexUpdate(update: BlockExchangeProtos.IndexUpdate, actor: SendChannel<ConnectionAction>) {
|
||||
val deferred = CompletableDeferred<Unit?>()
|
||||
try {
|
||||
val deferred = CompletableDeferred<Unit?>()
|
||||
|
||||
actor.send(SendIndexUpdateAction(update, deferred))
|
||||
actor.send(SendIndexUpdateAction(update, deferred))
|
||||
|
||||
deferred.await()
|
||||
deferred.await()
|
||||
} catch (ex: Exception) {
|
||||
throw IOException("not connected", ex)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun disconnect(actor: SendChannel<ConnectionAction>) {
|
||||
actor.send(CloseConnectionAction)
|
||||
try {
|
||||
actor.send(CloseConnectionAction)
|
||||
} catch (ex: Exception) {
|
||||
// ignore if the channel is closed already
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+22
-34
@@ -14,74 +14,60 @@
|
||||
package net.syncthing.java.bep.connectionactor
|
||||
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.channels.ConflatedBroadcastChannel
|
||||
import kotlinx.coroutines.channels.ReceiveChannel
|
||||
import kotlinx.coroutines.channels.SendChannel
|
||||
import kotlinx.coroutines.channels.consumeEach
|
||||
import net.syncthing.java.bep.BlockExchangeProtos
|
||||
import net.syncthing.java.core.beans.DeviceId
|
||||
import net.syncthing.java.core.exception.ExceptionReport
|
||||
import net.syncthing.java.core.exception.reportExceptions
|
||||
import java.io.IOException
|
||||
|
||||
class ConnectionActorWrapper (
|
||||
private val source: ReceiveChannel<Pair<SendChannel<ConnectionAction>, ClusterConfigInfo>>,
|
||||
private val source: ReceiveChannel<Pair<Connection, ConnectionInfo>>,
|
||||
val deviceId: DeviceId,
|
||||
val connectivityChangeListener: () -> Unit
|
||||
private val exceptionReportHandler: (ExceptionReport) -> Unit
|
||||
) {
|
||||
private val job = Job()
|
||||
|
||||
private var currentConnectionActor: SendChannel<ConnectionAction>? = null
|
||||
private var clusterConfigInfo: ClusterConfigInfo? = null
|
||||
private var connection: Connection? = null
|
||||
private val connectionInfo = ConflatedBroadcastChannel<ConnectionInfo>(ConnectionInfo.empty)
|
||||
|
||||
var isConnected = false
|
||||
get() = currentConnectionActor?.isClosedForSend == false
|
||||
val isConnected
|
||||
get() = connectionInfo.valueOrNull?.status == ConnectionStatus.Connected
|
||||
|
||||
init {
|
||||
GlobalScope.launch (job) {
|
||||
source.consumeEach { (connectionActor, clusterConfig) ->
|
||||
currentConnectionActor = connectionActor
|
||||
clusterConfigInfo = clusterConfig
|
||||
GlobalScope.async (job) {
|
||||
source.consumeEach { (connection, connectionInfo) ->
|
||||
this@ConnectionActorWrapper.connection = connection
|
||||
this@ConnectionActorWrapper.connectionInfo.send(connectionInfo)
|
||||
}
|
||||
}
|
||||
|
||||
// this is a very simple solution but it does its job
|
||||
GlobalScope.launch (job) {
|
||||
var previousConnected = false
|
||||
|
||||
while (isActive) {
|
||||
val nowConnected = isConnected
|
||||
|
||||
if (previousConnected != nowConnected) {
|
||||
previousConnected = nowConnected
|
||||
|
||||
connectivityChangeListener()
|
||||
}
|
||||
|
||||
delay(200)
|
||||
}
|
||||
}
|
||||
}.reportExceptions("ConnectionActorWrapper(${deviceId.deviceId})", exceptionReportHandler)
|
||||
}
|
||||
|
||||
suspend fun sendRequest(request: BlockExchangeProtos.Request) = ConnectionActorUtil.sendRequest(
|
||||
request,
|
||||
currentConnectionActor ?: throw IOException("not connected")
|
||||
connection?.actor ?: throw IOException("not connected")
|
||||
)
|
||||
|
||||
suspend fun sendIndexUpdate(update: BlockExchangeProtos.IndexUpdate) = ConnectionActorUtil.sendIndexUpdate(
|
||||
update,
|
||||
currentConnectionActor ?: throw IOException("not connected")
|
||||
connection?.actor ?: throw IOException("not connected")
|
||||
)
|
||||
|
||||
fun hasFolder(folderId: String) = clusterConfigInfo?.sharedFolderIds?.contains(folderId) ?: false
|
||||
fun hasFolder(folderId: String) = connection?.clusterConfigInfo?.sharedFolderIds?.contains(folderId) ?: false
|
||||
|
||||
fun getClusterConfig() = clusterConfigInfo ?: throw IOException("not connected")
|
||||
fun getClusterConfig() = connection?.clusterConfigInfo ?: throw IOException("not connected")
|
||||
|
||||
fun shutdown() {
|
||||
job.cancel()
|
||||
connectionInfo.close()
|
||||
}
|
||||
|
||||
// this triggers a disconnection
|
||||
// the ConnectionActorGenerator will reconnect soon
|
||||
fun reconnect() {
|
||||
val actor = currentConnectionActor
|
||||
val actor = connection?.actor
|
||||
|
||||
GlobalScope.launch {
|
||||
if (actor != null) {
|
||||
@@ -89,4 +75,6 @@ class ConnectionActorWrapper (
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun subscribeToConnectionInfo() = connectionInfo.openSubscription()
|
||||
}
|
||||
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
package net.syncthing.java.bep.connectionactor
|
||||
|
||||
import net.syncthing.java.core.beans.DeviceAddress
|
||||
|
||||
data class ConnectionInfo (
|
||||
val addresses: List<DeviceAddress>,
|
||||
val currentAddress: DeviceAddress?,
|
||||
val status: ConnectionStatus
|
||||
) {
|
||||
companion object {
|
||||
val empty = ConnectionInfo(
|
||||
addresses = emptyList(),
|
||||
currentAddress = null,
|
||||
status = ConnectionStatus.Disconnected
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
enum class ConnectionStatus {
|
||||
Disconnected, Connecting, Connected
|
||||
}
|
||||
+12
-9
@@ -73,7 +73,7 @@ object HelloMessageHandler {
|
||||
)
|
||||
}
|
||||
|
||||
fun processHelloMessage(
|
||||
suspend fun processHelloMessage(
|
||||
hello: BlockExchangeProtos.Hello,
|
||||
configuration: Configuration,
|
||||
deviceId: DeviceId
|
||||
@@ -81,14 +81,17 @@ object HelloMessageHandler {
|
||||
logger.info("Received hello message, deviceName=${hello.deviceName}, clientName=${hello.clientName}, clientVersion=${hello.clientVersion}")
|
||||
|
||||
// update the local device name
|
||||
// TODO: this could need some locking
|
||||
configuration.peers = configuration.peers.map { peer ->
|
||||
if (peer.deviceId == deviceId) {
|
||||
DeviceInfo(deviceId, hello.deviceName)
|
||||
} else {
|
||||
peer
|
||||
}
|
||||
}.toSet()
|
||||
configuration.update { oldConfig ->
|
||||
oldConfig.copy(
|
||||
peers = oldConfig.peers.map { peer ->
|
||||
if (peer.deviceId == deviceId) {
|
||||
DeviceInfo(deviceId, hello.deviceName)
|
||||
} else {
|
||||
peer
|
||||
}
|
||||
}.toSet()
|
||||
)
|
||||
}
|
||||
|
||||
configuration.persistLater()
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ import kotlinx.coroutines.channels.first
|
||||
import kotlinx.coroutines.channels.produce
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import net.syncthing.java.bep.index.IndexHandler
|
||||
import net.syncthing.java.bep.index.*
|
||||
import net.syncthing.java.core.beans.FolderStats
|
||||
import net.syncthing.java.core.configuration.Configuration
|
||||
import java.io.Closeable
|
||||
@@ -35,7 +35,7 @@ class FolderBrowser internal constructor(private val indexHandler: IndexHandler,
|
||||
// get initial status
|
||||
val currentFolderStats = mutableMapOf<String, FolderStats>()
|
||||
|
||||
var currentIndexInfo = withContext(Dispatchers.IO) {
|
||||
val currentIndexInfo = withContext(Dispatchers.IO) {
|
||||
indexHandler.indexRepository.runInTransaction { indexTransaction ->
|
||||
configuration.folders.map { it.folderId }.forEach { folderId ->
|
||||
currentFolderStats[folderId] = indexTransaction.findFolderStats(folderId) ?: FolderStats.createDummy(folderId)
|
||||
@@ -64,9 +64,12 @@ class FolderBrowser internal constructor(private val indexHandler: IndexHandler,
|
||||
val updateLock = Mutex()
|
||||
|
||||
async {
|
||||
indexHandler.subscribeFolderStatsUpdatedEvents().consumeEach { folderStats ->
|
||||
indexHandler.subscribeFolderStatsUpdatedEvents().consumeEach { event ->
|
||||
updateLock.withLock {
|
||||
currentFolderStats[folderStats.folderId] = folderStats
|
||||
when (event) {
|
||||
is FolderStatsUpdatedEvent -> currentFolderStats[event.folderStats.folderId] = event.folderStats
|
||||
FolderStatsResetEvent -> currentFolderStats.clear()
|
||||
}.let { /* require that all cases are handled */ }
|
||||
|
||||
dispatch()
|
||||
}
|
||||
@@ -74,16 +77,28 @@ class FolderBrowser internal constructor(private val indexHandler: IndexHandler,
|
||||
}
|
||||
|
||||
async {
|
||||
indexHandler.subscribeToOnIndexRecordAcquiredEvents().consumeEach { event ->
|
||||
indexHandler.subscribeToOnIndexUpdateEvents().consumeEach { event ->
|
||||
updateLock.withLock {
|
||||
val oldList = currentIndexInfo[event.folderId] ?: emptyList()
|
||||
val newList = oldList.filter { it.deviceId != event.indexInfo.deviceId } + event.indexInfo
|
||||
currentIndexInfo[event.folderId] = newList
|
||||
when (event) {
|
||||
is IndexRecordAcquiredEvent -> {
|
||||
val oldList = currentIndexInfo[event.folderId] ?: emptyList()
|
||||
val newList = oldList.filter { it.deviceId != event.indexInfo.deviceId } + event.indexInfo
|
||||
|
||||
currentIndexInfo[event.folderId] = newList
|
||||
}
|
||||
IndexInfoClearedEvent -> currentIndexInfo.clear()
|
||||
}.let { /* require that all cases are handled */ }
|
||||
|
||||
dispatch()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async {
|
||||
configuration.subscribe().consumeEach {
|
||||
dispatch()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,13 @@ data class FolderStatus(
|
||||
) {
|
||||
companion object {
|
||||
fun createDummy(folder: String) = FolderStatus(
|
||||
info = FolderInfo(folder, folder),
|
||||
info = FolderInfo(
|
||||
folder,
|
||||
folder,
|
||||
deviceIdBlacklist = emptySet(),
|
||||
deviceIdWhitelist = emptySet(),
|
||||
ignoredDeviceIdList = emptySet()
|
||||
),
|
||||
stats = FolderStats.createDummy(folder),
|
||||
indexInfo = emptyList()
|
||||
)
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
package net.syncthing.java.bep.index
|
||||
|
||||
import net.syncthing.java.core.beans.FolderStats
|
||||
|
||||
sealed class FolderStatsChangedEvent
|
||||
data class FolderStatsUpdatedEvent(val folderStats: FolderStats): FolderStatsChangedEvent()
|
||||
object FolderStatsResetEvent: FolderStatsChangedEvent()
|
||||
+4
-13
@@ -4,6 +4,8 @@ import net.syncthing.java.bep.BlockExchangeProtos
|
||||
import net.syncthing.java.core.beans.BlockInfo
|
||||
import net.syncthing.java.core.beans.FileBlocks
|
||||
import net.syncthing.java.core.beans.FileInfo
|
||||
import net.syncthing.java.core.exception.ExceptionDetailException
|
||||
import net.syncthing.java.core.exception.ExceptionDetails
|
||||
import net.syncthing.java.core.interfaces.IndexTransaction
|
||||
import org.bouncycastle.util.encoders.Hex
|
||||
import org.slf4j.LoggerFactory
|
||||
@@ -18,8 +20,7 @@ object IndexElementProcessor {
|
||||
folder: String,
|
||||
updates: List<BlockExchangeProtos.FileInfo>,
|
||||
oldRecords: Map<String, FileInfo>,
|
||||
folderStatsUpdateCollector: FolderStatsUpdateCollector,
|
||||
enableDetailedException: Boolean
|
||||
folderStatsUpdateCollector: FolderStatsUpdateCollector
|
||||
): List<FileInfo> {
|
||||
// this always keeps the last version per path
|
||||
val filesToProcess = updates
|
||||
@@ -28,17 +29,7 @@ object IndexElementProcessor {
|
||||
.distinctBy { it.name /* this is the whole path */ }
|
||||
.reversed()
|
||||
|
||||
val preparedUpdates = filesToProcess.mapNotNull {
|
||||
try {
|
||||
prepareUpdate(folder, it)
|
||||
} catch (ex: Exception) {
|
||||
if (enableDetailedException) {
|
||||
throw IOException("error processing index update: ${it.name}", ex)
|
||||
} else {
|
||||
throw ex
|
||||
}
|
||||
}
|
||||
}
|
||||
val preparedUpdates = filesToProcess.mapNotNull { prepareUpdate(folder, it) }
|
||||
|
||||
val updatesToApply = preparedUpdates.filter { shouldUpdateRecord(oldRecords[it.first.path], it.first) }
|
||||
|
||||
|
||||
@@ -14,8 +14,10 @@
|
||||
*/
|
||||
package net.syncthing.java.bep.index
|
||||
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.channels.BroadcastChannel
|
||||
import kotlinx.coroutines.channels.consume
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.coroutines.withTimeoutOrNull
|
||||
import net.syncthing.java.bep.BlockExchangeProtos
|
||||
import net.syncthing.java.bep.connectionactor.ClusterConfigInfo
|
||||
@@ -24,6 +26,7 @@ import net.syncthing.java.bep.folder.FolderBrowser
|
||||
import net.syncthing.java.bep.index.browser.IndexBrowser
|
||||
import net.syncthing.java.core.beans.*
|
||||
import net.syncthing.java.core.configuration.Configuration
|
||||
import net.syncthing.java.core.exception.ExceptionReport
|
||||
import net.syncthing.java.core.interfaces.IndexRepository
|
||||
import net.syncthing.java.core.interfaces.IndexTransaction
|
||||
import net.syncthing.java.core.interfaces.TempRepository
|
||||
@@ -32,37 +35,40 @@ import org.slf4j.LoggerFactory
|
||||
import java.io.Closeable
|
||||
import java.io.IOException
|
||||
|
||||
data class IndexRecordAcquiredEvent(val folderId: String, val files: List<FileInfo>, val indexInfo: IndexInfo)
|
||||
|
||||
class IndexHandler(
|
||||
configuration: Configuration,
|
||||
val indexRepository: IndexRepository,
|
||||
tempRepository: TempRepository,
|
||||
enableDetailedException: Boolean
|
||||
exceptionReportHandler: (ExceptionReport) -> Unit
|
||||
) : Closeable {
|
||||
private val logger = LoggerFactory.getLogger(javaClass)
|
||||
private val onIndexRecordAcquiredEvents = BroadcastChannel<IndexRecordAcquiredEvent>(capacity = 16)
|
||||
private val indexInfoUpdateEvents = BroadcastChannel<IndexInfoUpdateEvent>(capacity = 16)
|
||||
private val onFullIndexAcquiredEvents = BroadcastChannel<String>(capacity = 16)
|
||||
private val onFolderStatsUpdatedEvents = BroadcastChannel<FolderStats>(capacity = 16)
|
||||
private val onFolderStatsUpdatedEvents = BroadcastChannel<FolderStatsChangedEvent>(capacity = 16)
|
||||
|
||||
private val indexMessageProcessor = IndexMessageQueueProcessor(
|
||||
indexRepository = indexRepository,
|
||||
tempRepository = tempRepository,
|
||||
isRemoteIndexAcquired = ::isRemoteIndexAcquired,
|
||||
onIndexRecordAcquiredEvents = onIndexRecordAcquiredEvents,
|
||||
onIndexRecordAcquiredEvents = indexInfoUpdateEvents,
|
||||
onFullIndexAcquiredEvents = onFullIndexAcquiredEvents,
|
||||
onFolderStatsUpdatedEvents = onFolderStatsUpdatedEvents,
|
||||
enableDetailedException = enableDetailedException
|
||||
exceptionReportHandler = exceptionReportHandler
|
||||
)
|
||||
|
||||
fun subscribeToOnFullIndexAcquiredEvents() = onFullIndexAcquiredEvents.openSubscription()
|
||||
fun subscribeToOnIndexRecordAcquiredEvents() = onIndexRecordAcquiredEvents.openSubscription()
|
||||
fun subscribeToOnIndexUpdateEvents() = indexInfoUpdateEvents.openSubscription()
|
||||
fun subscribeFolderStatsUpdatedEvents() = onFolderStatsUpdatedEvents.openSubscription()
|
||||
|
||||
fun getNextSequenceNumber() = indexRepository.runInTransaction { it.getSequencer().nextSequence() }
|
||||
|
||||
fun clearIndex() {
|
||||
indexRepository.runInTransaction { it.clearIndex() }
|
||||
suspend fun clearIndex() {
|
||||
withContext(Dispatchers.IO) {
|
||||
indexRepository.runInTransaction { it.clearIndex() }
|
||||
}
|
||||
|
||||
onFolderStatsUpdatedEvents.send(FolderStatsResetEvent)
|
||||
indexInfoUpdateEvents.send(IndexInfoClearedEvent)
|
||||
}
|
||||
|
||||
private fun isRemoteIndexAcquiredWithoutTransaction(clusterConfigInfo: ClusterConfigInfo, peerDeviceId: DeviceId): Boolean {
|
||||
@@ -123,7 +129,7 @@ class IndexHandler(
|
||||
for (deviceRecord in folderRecord.devicesList) {
|
||||
val deviceId = DeviceId.fromHashData(deviceRecord.id.toByteArray())
|
||||
if (deviceRecord.hasIndexId() && deviceRecord.hasMaxSequence()) {
|
||||
val folderIndexInfo = UpdateIndexInfo.updateIndexInfo(transaction, folder, deviceId, deviceRecord.indexId, deviceRecord.maxSequence, null)
|
||||
val folderIndexInfo = UpdateIndexInfo.updateIndexInfoFromClusterConfig(transaction, folder, deviceId, deviceRecord.indexId, deviceRecord.maxSequence)
|
||||
logger.debug("acquired folder index info from cluster config = {}", folderIndexInfo)
|
||||
updatedIndexInfos.add(folderIndexInfo)
|
||||
}
|
||||
@@ -134,7 +140,7 @@ class IndexHandler(
|
||||
}
|
||||
|
||||
updatedIndexInfos.forEach {
|
||||
onIndexRecordAcquiredEvents.send(
|
||||
indexInfoUpdateEvents.send(
|
||||
IndexRecordAcquiredEvent(
|
||||
folderId = it.folderId,
|
||||
indexInfo = it,
|
||||
@@ -175,11 +181,11 @@ class IndexHandler(
|
||||
val indexBrowser = IndexBrowser(indexRepository, this)
|
||||
|
||||
suspend fun sendFolderStatsUpdate(event: FolderStats) {
|
||||
onFolderStatsUpdatedEvents.send(event)
|
||||
onFolderStatsUpdatedEvents.send(FolderStatsUpdatedEvent(event))
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
onIndexRecordAcquiredEvents.close()
|
||||
indexInfoUpdateEvents.close()
|
||||
onFullIndexAcquiredEvents.close()
|
||||
indexMessageProcessor.stop()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
package net.syncthing.java.bep.index
|
||||
|
||||
import net.syncthing.java.core.beans.FileInfo
|
||||
import net.syncthing.java.core.beans.IndexInfo
|
||||
|
||||
sealed class IndexInfoUpdateEvent
|
||||
data class IndexRecordAcquiredEvent(val folderId: String, val files: List<FileInfo>, val indexInfo: IndexInfo): IndexInfoUpdateEvent()
|
||||
object IndexInfoClearedEvent: IndexInfoUpdateEvent()
|
||||
+17
-11
@@ -7,6 +7,7 @@ import net.syncthing.java.core.beans.FolderStats
|
||||
import net.syncthing.java.core.beans.IndexInfo
|
||||
import net.syncthing.java.core.interfaces.IndexTransaction
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.lang.RuntimeException
|
||||
|
||||
object IndexMessageProcessor {
|
||||
private val logger = LoggerFactory.getLogger(IndexMessageProcessor::class.java)
|
||||
@@ -14,10 +15,11 @@ object IndexMessageProcessor {
|
||||
fun doHandleIndexMessageReceivedEvent(
|
||||
message: BlockExchangeProtos.IndexUpdate,
|
||||
peerDeviceId: DeviceId,
|
||||
transaction: IndexTransaction,
|
||||
enableDetailedException: Boolean
|
||||
transaction: IndexTransaction
|
||||
): Result {
|
||||
val folderId = message.folder
|
||||
val oldIndexInfo = transaction.findIndexInfoByDeviceAndFolder(peerDeviceId, folderId)
|
||||
?: throw IndexInfoNotFoundException()
|
||||
|
||||
logger.debug("processing {} index records for folder {}", message.filesList.size, folderId)
|
||||
|
||||
@@ -29,20 +31,23 @@ object IndexMessageProcessor {
|
||||
oldRecords = oldRecords,
|
||||
folder = folderId,
|
||||
folderStatsUpdateCollector = folderStatsUpdateCollector,
|
||||
updates = message.filesList,
|
||||
enableDetailedException = enableDetailedException
|
||||
updates = message.filesList
|
||||
)
|
||||
|
||||
var sequence: Long = -1
|
||||
val newIndexInfo = if (message.filesList.isEmpty()) {
|
||||
oldIndexInfo
|
||||
} else {
|
||||
var sequence: Long = -1
|
||||
|
||||
for (newRecord in message.filesList) {
|
||||
sequence = Math.max(newRecord.sequence, sequence)
|
||||
for (newRecord in message.filesList) {
|
||||
sequence = Math.max(newRecord.sequence, sequence)
|
||||
}
|
||||
|
||||
handleFolderStatsUpdate(transaction, folderStatsUpdateCollector)
|
||||
|
||||
UpdateIndexInfo.updateIndexInfoFromIndexElementProcessor(transaction, oldIndexInfo, sequence)
|
||||
}
|
||||
|
||||
handleFolderStatsUpdate(transaction, folderStatsUpdateCollector)
|
||||
|
||||
val newIndexInfo = UpdateIndexInfo.updateIndexInfo(transaction, folderId, peerDeviceId, null, null, sequence)
|
||||
|
||||
return Result(newIndexInfo, newRecords.toList(), transaction.findFolderStats(folderId) ?: FolderStats.createDummy(folderId))
|
||||
}
|
||||
|
||||
@@ -61,4 +66,5 @@ object IndexMessageProcessor {
|
||||
}
|
||||
|
||||
data class Result(val newIndexInfo: IndexInfo, val updatedFiles: List<FileInfo>, val newFolderStats: FolderStats)
|
||||
class IndexInfoNotFoundException: RuntimeException()
|
||||
}
|
||||
|
||||
+28
-17
@@ -14,19 +14,17 @@
|
||||
*/
|
||||
package net.syncthing.java.bep.index
|
||||
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.channels.BroadcastChannel
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.channels.consumeEach
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import net.syncthing.java.bep.BlockExchangeProtos
|
||||
import net.syncthing.java.bep.connectionactor.ClusterConfigInfo
|
||||
import net.syncthing.java.core.beans.DeviceId
|
||||
import net.syncthing.java.core.beans.FolderStats
|
||||
import net.syncthing.java.core.exception.ExceptionReport
|
||||
import net.syncthing.java.core.exception.reportExceptions
|
||||
import net.syncthing.java.core.interfaces.IndexRepository
|
||||
import net.syncthing.java.core.interfaces.IndexTransaction
|
||||
import net.syncthing.java.core.interfaces.TempRepository
|
||||
@@ -35,11 +33,11 @@ import org.slf4j.LoggerFactory
|
||||
class IndexMessageQueueProcessor (
|
||||
private val indexRepository: IndexRepository,
|
||||
private val tempRepository: TempRepository,
|
||||
private val onIndexRecordAcquiredEvents: BroadcastChannel<IndexRecordAcquiredEvent>,
|
||||
private val onIndexRecordAcquiredEvents: BroadcastChannel<IndexInfoUpdateEvent>,
|
||||
private val onFullIndexAcquiredEvents: BroadcastChannel<String>,
|
||||
private val onFolderStatsUpdatedEvents: BroadcastChannel<FolderStats>,
|
||||
private val onFolderStatsUpdatedEvents: BroadcastChannel<FolderStatsChangedEvent>,
|
||||
private val isRemoteIndexAcquired: (ClusterConfigInfo, DeviceId, IndexTransaction) -> Boolean,
|
||||
private val enableDetailedException: Boolean
|
||||
exceptionReportHandler: (ExceptionReport) -> Unit
|
||||
) {
|
||||
private data class IndexUpdateAction(val update: BlockExchangeProtos.IndexUpdate, val clusterConfigInfo: ClusterConfigInfo, val peerDeviceId: DeviceId)
|
||||
private data class StoredIndexUpdateAction(val updateId: String, val clusterConfigInfo: ClusterConfigInfo, val peerDeviceId: DeviceId)
|
||||
@@ -81,13 +79,20 @@ class IndexMessageQueueProcessor (
|
||||
}
|
||||
|
||||
init {
|
||||
GlobalScope.launch(Dispatchers.IO + job) {
|
||||
GlobalScope.async(Dispatchers.IO + job) {
|
||||
indexUpdateProcessingQueue.consumeEach {
|
||||
doHandleIndexMessageReceivedEvent(it)
|
||||
}
|
||||
}
|
||||
try {
|
||||
doHandleIndexMessageReceivedEvent(it)
|
||||
} catch (ex: IndexMessageProcessor.IndexInfoNotFoundException) {
|
||||
// ignored
|
||||
// this is expected when the data is deleted but some index updates are still in the queue
|
||||
|
||||
GlobalScope.launch(Dispatchers.IO + job) {
|
||||
logger.warn("could not find index info for index update")
|
||||
}
|
||||
}
|
||||
}.reportExceptions("IndexMessageQueueProcessor.indexUpdateProcessingQueue", exceptionReportHandler)
|
||||
|
||||
GlobalScope.async(Dispatchers.IO + job) {
|
||||
indexUpdateProcessStoredQueue.consumeEach { action ->
|
||||
logger.debug("processing index message event from temp record {}", action.updateId)
|
||||
|
||||
@@ -100,12 +105,19 @@ class IndexMessageQueueProcessor (
|
||||
action.peerDeviceId
|
||||
))
|
||||
}
|
||||
}
|
||||
}.reportExceptions("IndexMessageQueueProcessor.indexUpdateProcessStoredQueue", exceptionReportHandler)
|
||||
}
|
||||
|
||||
private suspend fun doHandleIndexMessageReceivedEvent(action: IndexUpdateAction) {
|
||||
val (message, clusterConfigInfo, peerDeviceId) = action
|
||||
|
||||
val folderInfo = clusterConfigInfo.folderInfoById[message.folder]
|
||||
?: throw IllegalStateException("got folder info for folder without known folder info")
|
||||
|
||||
if (!folderInfo.isDeviceInSharedFolderWhitelist) {
|
||||
throw IllegalStateException("received index update for folder which is not shared")
|
||||
}
|
||||
|
||||
logger.info("processing index message with {} records", message.filesCount)
|
||||
|
||||
val (indexResult, wasIndexAcquired) = indexRepository.runInTransaction { indexTransaction ->
|
||||
@@ -116,8 +128,7 @@ class IndexMessageQueueProcessor (
|
||||
val indexResult = IndexMessageProcessor.doHandleIndexMessageReceivedEvent(
|
||||
message = message,
|
||||
peerDeviceId = peerDeviceId,
|
||||
transaction = indexTransaction,
|
||||
enableDetailedException = enableDetailedException
|
||||
transaction = indexTransaction
|
||||
)
|
||||
|
||||
val endTime = System.currentTimeMillis()
|
||||
@@ -133,7 +144,7 @@ class IndexMessageQueueProcessor (
|
||||
onIndexRecordAcquiredEvents.send(IndexRecordAcquiredEvent(message.folder, indexResult.updatedFiles, indexResult.newIndexInfo))
|
||||
}
|
||||
|
||||
onFolderStatsUpdatedEvents.send(indexResult.newFolderStats)
|
||||
onFolderStatsUpdatedEvents.send(FolderStatsUpdatedEvent(indexResult.newFolderStats))
|
||||
|
||||
if (wasIndexAcquired) {
|
||||
logger.debug("index acquired")
|
||||
|
||||
@@ -5,46 +5,53 @@ import net.syncthing.java.core.beans.IndexInfo
|
||||
import net.syncthing.java.core.interfaces.IndexTransaction
|
||||
|
||||
object UpdateIndexInfo {
|
||||
fun updateIndexInfo(
|
||||
fun updateIndexInfoFromClusterConfig(
|
||||
transaction: IndexTransaction,
|
||||
folder: String,
|
||||
deviceId: DeviceId,
|
||||
indexId: Long?,
|
||||
maxSequence: Long?,
|
||||
localSequence: Long?
|
||||
indexId: Long,
|
||||
maxSequence: Long
|
||||
): IndexInfo {
|
||||
val oldIndexSequenceInfo = transaction.findIndexInfoByDeviceAndFolder(deviceId, folder)
|
||||
|
||||
var newIndexSequenceInfo = oldIndexSequenceInfo ?: kotlin.run {
|
||||
assert(indexId != null) {
|
||||
"index sequence info not found, and supplied null index id (folder = $folder, device = $deviceId)"
|
||||
}
|
||||
var newIndexSequenceInfo = oldIndexSequenceInfo ?: IndexInfo(
|
||||
folderId = folder,
|
||||
deviceId = deviceId.deviceId,
|
||||
indexId = indexId,
|
||||
localSequence = 0,
|
||||
maxSequence = -1
|
||||
)
|
||||
|
||||
IndexInfo(
|
||||
folderId = folder,
|
||||
deviceId = deviceId.deviceId,
|
||||
indexId = indexId!!,
|
||||
localSequence = 0,
|
||||
maxSequence = -1
|
||||
)
|
||||
}
|
||||
|
||||
if (indexId != null && indexId != newIndexSequenceInfo.indexId) {
|
||||
if (indexId != newIndexSequenceInfo.indexId) {
|
||||
newIndexSequenceInfo = newIndexSequenceInfo.copy(indexId = indexId)
|
||||
}
|
||||
|
||||
if (maxSequence != null && maxSequence > newIndexSequenceInfo.maxSequence) {
|
||||
if (maxSequence > newIndexSequenceInfo.maxSequence) {
|
||||
newIndexSequenceInfo = newIndexSequenceInfo.copy(maxSequence = maxSequence)
|
||||
}
|
||||
|
||||
if (localSequence != null && localSequence > newIndexSequenceInfo.localSequence) {
|
||||
newIndexSequenceInfo = newIndexSequenceInfo.copy(localSequence = localSequence)
|
||||
}
|
||||
|
||||
if (oldIndexSequenceInfo != newIndexSequenceInfo) {
|
||||
transaction.updateIndexInfo(newIndexSequenceInfo)
|
||||
}
|
||||
|
||||
return newIndexSequenceInfo
|
||||
}
|
||||
|
||||
fun updateIndexInfoFromIndexElementProcessor(
|
||||
transaction: IndexTransaction,
|
||||
oldIndexInfo: IndexInfo,
|
||||
localSequence: Long?
|
||||
): IndexInfo {
|
||||
var newIndexSequenceInfo = oldIndexInfo
|
||||
|
||||
if (localSequence != null && localSequence > newIndexSequenceInfo.localSequence) {
|
||||
newIndexSequenceInfo = newIndexSequenceInfo.copy(localSequence = localSequence)
|
||||
}
|
||||
|
||||
if (oldIndexInfo != newIndexSequenceInfo) {
|
||||
transaction.updateIndexInfo(newIndexSequenceInfo)
|
||||
}
|
||||
|
||||
return newIndexSequenceInfo
|
||||
}
|
||||
}
|
||||
|
||||
+30
-25
@@ -19,7 +19,10 @@ import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.channels.consume
|
||||
import kotlinx.coroutines.channels.produce
|
||||
import kotlinx.coroutines.withContext
|
||||
import net.syncthing.java.bep.index.FolderStatsUpdatedEvent
|
||||
import net.syncthing.java.bep.index.IndexHandler
|
||||
import net.syncthing.java.bep.index.IndexInfoUpdateEvent
|
||||
import net.syncthing.java.bep.index.IndexRecordAcquiredEvent
|
||||
import net.syncthing.java.core.beans.FileInfo
|
||||
import net.syncthing.java.core.interfaces.IndexRepository
|
||||
import net.syncthing.java.core.interfaces.IndexTransaction
|
||||
@@ -64,7 +67,7 @@ class IndexBrowser internal constructor(
|
||||
}
|
||||
|
||||
fun streamDirectoryListing(folder: String, path: String) = GlobalScope.produce {
|
||||
indexHandler.subscribeToOnIndexRecordAcquiredEvents().consume {
|
||||
indexHandler.subscribeToOnIndexUpdateEvents().consume {
|
||||
val directoryName = PathUtils.getFileName(path)
|
||||
val parentPath = if (PathUtils.isRoot(path)) null else PathUtils.getParentPath(path)
|
||||
val parentDirectoryName = if (parentPath != null) PathUtils.getFileName(parentPath) else null
|
||||
@@ -107,37 +110,39 @@ class IndexBrowser internal constructor(
|
||||
|
||||
// handle updates
|
||||
for (event in this) {
|
||||
var hadChanges = false
|
||||
if (event is IndexRecordAcquiredEvent) {
|
||||
var hadChanges = false
|
||||
|
||||
if (event.folderId == folder) {
|
||||
event.files.forEach { fileUpdate ->
|
||||
// entry change
|
||||
if (fileUpdate.parent == path) {
|
||||
hadChanges = true
|
||||
if (event.folderId == folder) {
|
||||
event.files.forEach { fileUpdate ->
|
||||
// entry change
|
||||
if (fileUpdate.parent == path) {
|
||||
hadChanges = true
|
||||
|
||||
entries = entries.filter { it.fileName != fileUpdate.fileName }
|
||||
entries = entries.filter { it.fileName != fileUpdate.fileName }
|
||||
|
||||
if (!fileUpdate.isDeleted) {
|
||||
entries += listOf(fileUpdate)
|
||||
if (!fileUpdate.isDeleted) {
|
||||
entries += listOf(fileUpdate)
|
||||
}
|
||||
}
|
||||
|
||||
// handle directory info changes
|
||||
if (fileUpdate.parent == parentPath && fileUpdate.fileName == directoryName) {
|
||||
directoryInfo = if (fileUpdate.isDeleted) null else fileUpdate
|
||||
hadChanges = true
|
||||
}
|
||||
|
||||
// handle parent directory info changes
|
||||
if (fileUpdate.parent == parentParentPath && fileUpdate.fileName == parentDirectoryName) {
|
||||
parentEntry = if (fileUpdate.isDeleted) null else fileUpdate
|
||||
hadChanges = true
|
||||
}
|
||||
}
|
||||
|
||||
// handle directory info changes
|
||||
if (fileUpdate.parent == parentPath && fileUpdate.fileName == directoryName) {
|
||||
directoryInfo = if (fileUpdate.isDeleted) null else fileUpdate
|
||||
hadChanges = true
|
||||
}
|
||||
|
||||
// handle parent directory info changes
|
||||
if (fileUpdate.parent == parentParentPath && fileUpdate.fileName == parentDirectoryName) {
|
||||
parentEntry = if (fileUpdate.isDeleted) null else fileUpdate
|
||||
hadChanges = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (hadChanges) {
|
||||
dispatch()
|
||||
if (hadChanges) {
|
||||
dispatch()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,7 +48,13 @@ class Main(private val commandLine: CommandLine) {
|
||||
|
||||
val repository = SqlRepository(configuration.databaseFolder)
|
||||
|
||||
SyncthingClient(configuration, repository, repository).use { syncthingClient ->
|
||||
SyncthingClient(
|
||||
configuration,
|
||||
repository,
|
||||
repository
|
||||
) { ex ->
|
||||
throw ex.exception
|
||||
}.use { syncthingClient ->
|
||||
val main = Main(cmd)
|
||||
cmd.options.forEach { main.handleOption(it, configuration, syncthingClient) }
|
||||
}
|
||||
@@ -86,8 +92,16 @@ class Main(private val commandLine: CommandLine) {
|
||||
.map { DeviceId(it.trim()) }
|
||||
.toList()
|
||||
System.out.println("set peers = $peers")
|
||||
configuration.peers = peers.map { DeviceInfo(it, null) }.toSet()
|
||||
configuration.persistNow()
|
||||
|
||||
runBlocking {
|
||||
configuration.update { oldConfig ->
|
||||
oldConfig.copy(
|
||||
peers = peers.map { DeviceInfo(it, it.shortId) }.toSet()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
runBlocking { configuration.persistNow() }
|
||||
}
|
||||
"p" -> {
|
||||
val folderAndPath = option.value
|
||||
|
||||
@@ -13,14 +13,25 @@
|
||||
*/
|
||||
package net.syncthing.java.client
|
||||
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.channels.ConflatedBroadcastChannel
|
||||
import kotlinx.coroutines.channels.consumeEach
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import net.syncthing.java.bep.connectionactor.ConnectionActorWrapper
|
||||
import net.syncthing.java.bep.connectionactor.ConnectionInfo
|
||||
import net.syncthing.java.core.beans.DeviceId
|
||||
|
||||
class Connections (val generate: (DeviceId) -> ConnectionActorWrapper) {
|
||||
private val map = mutableMapOf<DeviceId, ConnectionActorWrapper>()
|
||||
private val connectionStatus = ConflatedBroadcastChannel<Map<DeviceId, ConnectionInfo>>(emptyMap())
|
||||
private val connectionStatusLock = Mutex()
|
||||
private val job = Job()
|
||||
|
||||
fun getByDeviceId(deviceId: DeviceId): ConnectionActorWrapper {
|
||||
return synchronized(map) {
|
||||
synchronized(map) {
|
||||
val oldEntry = map[deviceId]
|
||||
|
||||
if (oldEntry != null) {
|
||||
@@ -30,6 +41,17 @@ class Connections (val generate: (DeviceId) -> ConnectionActorWrapper) {
|
||||
|
||||
map[deviceId] = newEntry
|
||||
|
||||
GlobalScope.launch (job) {
|
||||
newEntry.subscribeToConnectionInfo().consumeEach { status ->
|
||||
connectionStatusLock.withLock {
|
||||
connectionStatus.send(
|
||||
connectionStatus.value +
|
||||
mapOf(deviceId to status)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return newEntry
|
||||
}
|
||||
}
|
||||
@@ -39,6 +61,8 @@ class Connections (val generate: (DeviceId) -> ConnectionActorWrapper) {
|
||||
synchronized(map) {
|
||||
map.values.forEach { it.shutdown() }
|
||||
}
|
||||
|
||||
job.cancel()
|
||||
}
|
||||
|
||||
fun reconnectAllConnections() {
|
||||
@@ -46,4 +70,12 @@ class Connections (val generate: (DeviceId) -> ConnectionActorWrapper) {
|
||||
map.values.forEach { it.reconnect() }
|
||||
}
|
||||
}
|
||||
|
||||
fun reconnect(deviceId: DeviceId) {
|
||||
synchronized(map) {
|
||||
map[deviceId]?.reconnect()
|
||||
}
|
||||
}
|
||||
|
||||
fun subscribeToConnectionStatusMap() = connectionStatus.openSubscription()
|
||||
}
|
||||
|
||||
@@ -26,22 +26,21 @@ import net.syncthing.java.bep.index.IndexHandler
|
||||
import net.syncthing.java.core.beans.DeviceId
|
||||
import net.syncthing.java.core.beans.FileInfo
|
||||
import net.syncthing.java.core.configuration.Configuration
|
||||
import net.syncthing.java.core.exception.ExceptionReport
|
||||
import net.syncthing.java.core.interfaces.IndexRepository
|
||||
import net.syncthing.java.core.interfaces.TempRepository
|
||||
import net.syncthing.java.discovery.DiscoveryHandler
|
||||
import java.io.Closeable
|
||||
import java.io.InputStream
|
||||
import java.util.*
|
||||
|
||||
class SyncthingClient(
|
||||
private val configuration: Configuration,
|
||||
private val repository: IndexRepository,
|
||||
private val tempRepository: TempRepository,
|
||||
enableDetailedException: Boolean = false
|
||||
exceptionReportHandler: (ExceptionReport) -> Unit
|
||||
) : Closeable {
|
||||
val indexHandler = IndexHandler(configuration, repository, tempRepository, enableDetailedException)
|
||||
val discoveryHandler = DiscoveryHandler(configuration)
|
||||
private val onConnectionChangedListeners = Collections.synchronizedList(mutableListOf<(DeviceId) -> Unit>())
|
||||
val indexHandler = IndexHandler(configuration, repository, tempRepository, exceptionReportHandler)
|
||||
val discoveryHandler = DiscoveryHandler(configuration, exceptionReportHandler)
|
||||
|
||||
private val requestHandlerRegistry = RequestHandlerRegistry()
|
||||
private val connections = Connections(
|
||||
@@ -61,31 +60,20 @@ class SyncthingClient(
|
||||
configuration = configuration
|
||||
),
|
||||
deviceId = deviceId,
|
||||
connectivityChangeListener = {
|
||||
synchronized(onConnectionChangedListeners) {
|
||||
onConnectionChangedListeners.forEach { it(deviceId) }
|
||||
}
|
||||
}
|
||||
exceptionReportHandler = exceptionReportHandler
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
fun clearCacheAndIndex() {
|
||||
suspend fun clearCacheAndIndex() {
|
||||
indexHandler.clearIndex()
|
||||
configuration.folders = emptySet()
|
||||
configuration.update {
|
||||
it.copy(folders = emptySet())
|
||||
}
|
||||
configuration.persistLater()
|
||||
connections.reconnectAllConnections()
|
||||
}
|
||||
|
||||
fun addOnConnectionChangedListener(listener: (DeviceId) -> Unit) {
|
||||
onConnectionChangedListeners.add(listener)
|
||||
}
|
||||
|
||||
fun removeOnConnectionChangedListener(listener: (DeviceId) -> Unit) {
|
||||
assert(onConnectionChangedListeners.contains(listener))
|
||||
onConnectionChangedListeners.remove(listener)
|
||||
}
|
||||
|
||||
private fun getConnections() = configuration.peerIds.map { connections.getByDeviceId(it) }
|
||||
|
||||
init {
|
||||
@@ -93,6 +81,10 @@ class SyncthingClient(
|
||||
getConnections()
|
||||
}
|
||||
|
||||
fun reconnect(deviceId: DeviceId) {
|
||||
connections.reconnect(deviceId)
|
||||
}
|
||||
|
||||
fun connectToNewlyAddedDevices() {
|
||||
getConnections()
|
||||
}
|
||||
@@ -129,11 +121,7 @@ class SyncthingClient(
|
||||
)
|
||||
}
|
||||
|
||||
fun getPeerStatus() = configuration.peers.map { device ->
|
||||
device.copy(
|
||||
isConnected = connections.getByDeviceId(device.deviceId).isConnected
|
||||
)
|
||||
}
|
||||
fun subscribeToConnectionStatus() = connections.subscribeToConnectionStatusMap()
|
||||
|
||||
override fun close() {
|
||||
discoveryHandler.close()
|
||||
@@ -141,6 +129,5 @@ class SyncthingClient(
|
||||
repository.close()
|
||||
tempRepository.close()
|
||||
connections.shutdown()
|
||||
assert(onConnectionChangedListeners.isEmpty())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,4 +11,5 @@ dependencies {
|
||||
compile "com.google.code.gson:gson:2.8.2"
|
||||
compile "org.bouncycastle:bcmail-jdk15on:1.59"
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.0.0'
|
||||
}
|
||||
|
||||
@@ -5,8 +5,9 @@ import com.google.gson.stream.JsonWriter
|
||||
import net.syncthing.java.core.utils.NetworkUtils
|
||||
import org.apache.commons.codec.binary.Base32
|
||||
import java.io.IOException
|
||||
import java.io.Serializable
|
||||
|
||||
data class DeviceId @Throws(IOException::class) constructor(val deviceId: String) {
|
||||
data class DeviceId @Throws(IOException::class) constructor(val deviceId: String): Serializable {
|
||||
|
||||
init {
|
||||
val withoutDashes = this.deviceId.replace("-", "")
|
||||
|
||||
@@ -16,8 +16,9 @@ package net.syncthing.java.core.beans
|
||||
|
||||
import com.google.gson.stream.JsonReader
|
||||
import com.google.gson.stream.JsonWriter
|
||||
import java.io.Serializable
|
||||
|
||||
data class DeviceInfo(val deviceId: DeviceId, val name: String, val isConnected: Boolean? = null) {
|
||||
data class DeviceInfo(val deviceId: DeviceId, val name: String): Serializable {
|
||||
|
||||
companion object {
|
||||
private const val DEVICE_ID = "deviceId"
|
||||
@@ -44,9 +45,6 @@ data class DeviceInfo(val deviceId: DeviceId, val name: String, val isConnected:
|
||||
}
|
||||
}
|
||||
|
||||
constructor(deviceId: DeviceId, name: String?) :
|
||||
this(deviceId, if (name != null && !name.isBlank()) name else deviceId.shortId, null)
|
||||
|
||||
fun serialize(writer: JsonWriter) {
|
||||
writer.beginObject()
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/*
|
||||
/*
|
||||
* Copyright (C) 2016 Davide Imbriaco
|
||||
* Copyright (C) 2018 Jonas Lochmann
|
||||
*
|
||||
@@ -17,20 +17,72 @@ package net.syncthing.java.core.beans
|
||||
import com.google.gson.stream.JsonReader
|
||||
import com.google.gson.stream.JsonWriter
|
||||
|
||||
data class FolderInfo(val folderId: String, val label: String) {
|
||||
// the whitelist are device ids with which the folder should be shared
|
||||
|
||||
// the blacklist are device ids with which the folder should not be shared
|
||||
|
||||
// the ignored device ids are devices for which the user confirmed the blacklist entry so that
|
||||
// there should not be any question
|
||||
data class FolderInfo(
|
||||
val folderId: String,
|
||||
val label: String,
|
||||
val deviceIdWhitelist: Set<DeviceId>,
|
||||
val deviceIdBlacklist: Set<DeviceId>,
|
||||
val ignoredDeviceIdList: Set<DeviceId>
|
||||
) {
|
||||
companion object {
|
||||
private const val FOLDER_ID = "folderId"
|
||||
private const val LABEL = "label"
|
||||
private const val DEVICE_ID_WHITELIST = "deviceWhitelist"
|
||||
private const val DEVICE_ID_BLACKLIST = "deviceBlacklist"
|
||||
private const val IGNORED_DEVICE_ID_LIST = "ignoredDeviceIdList"
|
||||
|
||||
fun parse(reader: JsonReader): FolderInfo {
|
||||
var folderId: String? = null
|
||||
var label: String? = null
|
||||
// the following fields were added later and thus have got a default value
|
||||
var deviceIdWhitelist = emptySet<DeviceId>()
|
||||
var deviceIdBlacklist = emptySet<DeviceId>()
|
||||
var ignoredDeviceIdList = emptySet<DeviceId>()
|
||||
|
||||
reader.beginObject()
|
||||
while (reader.hasNext()) {
|
||||
when (reader.nextName()) {
|
||||
FOLDER_ID -> folderId = reader.nextString()
|
||||
LABEL -> label = reader.nextString()
|
||||
DEVICE_ID_WHITELIST -> {
|
||||
reader.beginArray()
|
||||
|
||||
deviceIdWhitelist = mutableSetOf<DeviceId>().apply {
|
||||
while (reader.hasNext()) {
|
||||
add(DeviceId(reader.nextString()))
|
||||
}
|
||||
}
|
||||
|
||||
reader.endArray()
|
||||
}
|
||||
DEVICE_ID_BLACKLIST -> {
|
||||
reader.beginArray()
|
||||
|
||||
deviceIdBlacklist = mutableSetOf<DeviceId>().apply {
|
||||
while (reader.hasNext()) {
|
||||
add(DeviceId(reader.nextString()))
|
||||
}
|
||||
}
|
||||
|
||||
reader.endArray()
|
||||
}
|
||||
IGNORED_DEVICE_ID_LIST -> {
|
||||
reader.beginArray()
|
||||
|
||||
ignoredDeviceIdList = mutableSetOf<DeviceId>().apply {
|
||||
while (reader.hasNext()) {
|
||||
add(DeviceId(reader.nextString()))
|
||||
}
|
||||
}
|
||||
|
||||
reader.endArray()
|
||||
}
|
||||
else -> reader.skipValue()
|
||||
}
|
||||
}
|
||||
@@ -38,13 +90,23 @@ data class FolderInfo(val folderId: String, val label: String) {
|
||||
|
||||
return FolderInfo(
|
||||
folderId = folderId!!,
|
||||
label = label!!
|
||||
label = label!!,
|
||||
deviceIdBlacklist = deviceIdBlacklist,
|
||||
deviceIdWhitelist = deviceIdWhitelist,
|
||||
ignoredDeviceIdList = ignoredDeviceIdList
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
assert(!folderId.isEmpty())
|
||||
assert(deviceIdWhitelist.find { deviceIdBlacklist.contains(it) } == null)
|
||||
}
|
||||
|
||||
val notIgnoredBlacklistEntries: Set<DeviceId> by lazy {
|
||||
deviceIdBlacklist
|
||||
.filterNot { ignoredDeviceIdList.contains(it) }
|
||||
.toSet()
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
@@ -57,7 +119,18 @@ data class FolderInfo(val folderId: String, val label: String) {
|
||||
writer.name(FOLDER_ID).value(folderId)
|
||||
writer.name(LABEL).value(label)
|
||||
|
||||
writer.name(DEVICE_ID_WHITELIST).beginArray()
|
||||
deviceIdWhitelist.forEach { writer.value(it.deviceId) }
|
||||
writer.endArray()
|
||||
|
||||
writer.name(DEVICE_ID_BLACKLIST).beginArray()
|
||||
deviceIdBlacklist.forEach { writer.value(it.deviceId) }
|
||||
writer.endArray()
|
||||
|
||||
writer.name(IGNORED_DEVICE_ID_LIST).beginArray()
|
||||
ignoredDeviceIdList.forEach { writer.value(it.deviceId) }
|
||||
writer.endArray()
|
||||
|
||||
writer.endObject()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+74
-43
@@ -2,6 +2,14 @@ package net.syncthing.java.core.configuration
|
||||
|
||||
import com.google.gson.stream.JsonReader
|
||||
import com.google.gson.stream.JsonWriter
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.channels.ConflatedBroadcastChannel
|
||||
import kotlinx.coroutines.channels.sendBlocking
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import net.syncthing.java.core.beans.DeviceId
|
||||
import net.syncthing.java.core.beans.DeviceInfo
|
||||
import net.syncthing.java.core.beans.FolderInfo
|
||||
@@ -17,12 +25,14 @@ import java.util.*
|
||||
class Configuration(configFolder: File = DefaultConfigFolder) {
|
||||
|
||||
private val logger = LoggerFactory.getLogger(javaClass)
|
||||
private val modifyLock = Mutex()
|
||||
private val saveLock = Mutex()
|
||||
private val configChannel = ConflatedBroadcastChannel<Config>()
|
||||
|
||||
private val configFile = File(configFolder, ConfigFileName)
|
||||
val databaseFolder = File(configFolder, DatabaseFolderName)
|
||||
|
||||
private var isSaved = true
|
||||
private var config: Config
|
||||
|
||||
init {
|
||||
configFolder.mkdirs()
|
||||
@@ -36,19 +46,23 @@ class Configuration(configFolder: File = DefaultConfigFolder) {
|
||||
}
|
||||
val keystoreData = KeystoreHandler.Loader().generateKeystore()
|
||||
isSaved = false
|
||||
config = Config(peers = setOf(), folders = setOf(),
|
||||
localDeviceName = localDeviceName,
|
||||
localDeviceId = keystoreData.first.deviceId,
|
||||
keystoreData = Base64.toBase64String(keystoreData.second),
|
||||
keystoreAlgorithm = keystoreData.third,
|
||||
customDiscoveryServers = emptySet(),
|
||||
useDefaultDiscoveryServers = true
|
||||
configChannel.sendBlocking(
|
||||
Config(peers = setOf(), folders = setOf(),
|
||||
localDeviceName = localDeviceName,
|
||||
localDeviceId = keystoreData.first.deviceId,
|
||||
keystoreData = Base64.toBase64String(keystoreData.second),
|
||||
keystoreAlgorithm = keystoreData.third,
|
||||
customDiscoveryServers = emptySet(),
|
||||
useDefaultDiscoveryServers = true
|
||||
)
|
||||
)
|
||||
persistNow()
|
||||
runBlocking { persistNow() }
|
||||
} else {
|
||||
config = Config.parse(JsonReader(StringReader(configFile.readText())))
|
||||
configChannel.sendBlocking(
|
||||
Config.parse(JsonReader(StringReader(configFile.readText())))
|
||||
)
|
||||
}
|
||||
logger.debug("Loaded config = $config")
|
||||
logger.debug("Loaded config = ${configChannel.value}")
|
||||
}
|
||||
|
||||
companion object {
|
||||
@@ -60,72 +74,89 @@ class Configuration(configFolder: File = DefaultConfigFolder) {
|
||||
val instanceId = Math.abs(Random().nextLong())
|
||||
|
||||
val localDeviceId: DeviceId
|
||||
get() = DeviceId(config.localDeviceId)
|
||||
get() = DeviceId(configChannel.value.localDeviceId)
|
||||
|
||||
val discoveryServers: Set<DiscoveryServer>
|
||||
get() = config.customDiscoveryServers + (if (config.useDefaultDiscoveryServers) DiscoveryServer.defaultDiscoveryServers else emptySet())
|
||||
get() = configChannel.value.let { config ->
|
||||
config.customDiscoveryServers + (if (config.useDefaultDiscoveryServers) DiscoveryServer.defaultDiscoveryServers else emptySet())
|
||||
}
|
||||
|
||||
val keystoreData: ByteArray
|
||||
get() = Base64.decode(config.keystoreData)
|
||||
get() = Base64.decode(configChannel.value.keystoreData)
|
||||
|
||||
val keystoreAlgorithm: String
|
||||
get() = config.keystoreAlgorithm
|
||||
get() = configChannel.value.keystoreAlgorithm
|
||||
|
||||
val clientName = "syncthing-java"
|
||||
|
||||
val clientVersion = javaClass.`package`.implementationVersion ?: "0.0.0"
|
||||
|
||||
val peerIds: Set<DeviceId>
|
||||
get() = config.peers.map { it.deviceId }.toSet()
|
||||
get() = configChannel.value.peers.map { it.deviceId }.toSet()
|
||||
|
||||
var localDeviceName: String
|
||||
get() = config.localDeviceName
|
||||
set(localDeviceName) {
|
||||
config = config.copy(localDeviceName = localDeviceName)
|
||||
isSaved = false
|
||||
val localDeviceName: String
|
||||
get() = configChannel.value.localDeviceName
|
||||
|
||||
val folders: Set<FolderInfo>
|
||||
get() = configChannel.value.folders
|
||||
|
||||
val peers: Set<DeviceInfo>
|
||||
get() = configChannel.value.peers
|
||||
|
||||
suspend fun update(operation: suspend (Config) -> Config): Boolean {
|
||||
modifyLock.withLock {
|
||||
val oldConfig = configChannel.value
|
||||
val newConfig = operation(oldConfig)
|
||||
|
||||
if (oldConfig != newConfig) {
|
||||
configChannel.send(newConfig)
|
||||
isSaved = false
|
||||
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var folders: Set<FolderInfo>
|
||||
get() = config.folders
|
||||
set(folders) {
|
||||
config = config.copy(folders = folders)
|
||||
isSaved = false
|
||||
}
|
||||
|
||||
var peers: Set<DeviceInfo>
|
||||
get() = config.peers
|
||||
set(peers) {
|
||||
config = config.copy(peers = peers)
|
||||
isSaved = false
|
||||
}
|
||||
|
||||
fun persistNow() {
|
||||
suspend fun persistNow() {
|
||||
persist()
|
||||
}
|
||||
|
||||
fun persistLater() {
|
||||
Thread { persist() }.start()
|
||||
GlobalScope.launch (Dispatchers.IO) { persist() }
|
||||
}
|
||||
|
||||
private fun persist() {
|
||||
if (isSaved)
|
||||
return
|
||||
private suspend fun persist() {
|
||||
saveLock.withLock {
|
||||
val (config1, isConfig1Saved) = modifyLock.withLock { configChannel.value to isSaved }
|
||||
|
||||
if (isConfig1Saved) {
|
||||
return
|
||||
}
|
||||
|
||||
config.let {
|
||||
System.out.println("writing config to $configFile")
|
||||
|
||||
configFile.writeText(
|
||||
StringWriter().apply {
|
||||
JsonWriter(this).apply {
|
||||
setIndent(" ")
|
||||
|
||||
config.serialize(this)
|
||||
config1.serialize(this)
|
||||
}
|
||||
}.toString()
|
||||
)
|
||||
isSaved = true
|
||||
|
||||
modifyLock.withLock {
|
||||
if (config1 === configChannel.value) {
|
||||
isSaved = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun subscribe() = configChannel.openSubscription()
|
||||
|
||||
override fun toString() = "Configuration(peers=$peers, folders=$folders, localDeviceName=$localDeviceName, " +
|
||||
"localDeviceId=${localDeviceId.deviceId}, discoveryServers=$discoveryServers, instanceId=$instanceId, " +
|
||||
"configFile=$configFile, databaseFolder=$databaseFolder)"
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
/*
|
||||
* Copyright 2018 Jonas Lochmann
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package net.syncthing.java.core.exception
|
||||
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlin.Exception
|
||||
|
||||
data class ExceptionReport(
|
||||
val component: String,
|
||||
val exception: Exception,
|
||||
val details: List<ExceptionDetails>
|
||||
) {
|
||||
companion object {
|
||||
fun fromException(exception: Exception, component: String) = ExceptionReport(
|
||||
component,
|
||||
exception,
|
||||
ExceptionDetailException.getExceptionReportDetails(exception)
|
||||
)
|
||||
}
|
||||
|
||||
val detailsReadableString: String by lazy {
|
||||
details.map { it.readableString }.joinToString("\n")
|
||||
}
|
||||
}
|
||||
|
||||
data class ExceptionDetails(
|
||||
val component: String,
|
||||
val details: String
|
||||
) {
|
||||
val readableString: String by lazy { component + "\n" + details + "\n" }
|
||||
}
|
||||
|
||||
class ExceptionDetailException(
|
||||
cause: Throwable,
|
||||
val details: ExceptionDetails
|
||||
): Exception(cause) {
|
||||
companion object {
|
||||
fun getExceptionReportDetails(exception: Exception): List<ExceptionDetails> {
|
||||
val result = mutableListOf<ExceptionDetails>()
|
||||
|
||||
var ex: Throwable? = exception
|
||||
|
||||
while (ex != null) {
|
||||
if (ex is ExceptionDetailException) {
|
||||
result.add(ex.details)
|
||||
}
|
||||
|
||||
ex = ex.cause
|
||||
}
|
||||
|
||||
return result.reversed()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun Job.reportExceptions(component: String, exceptionReportHandler: (ExceptionReport) -> Unit) {
|
||||
invokeOnCompletion {
|
||||
if (it != null) {
|
||||
if (it is Exception) {
|
||||
if (it is CancellationException) {
|
||||
// ignore
|
||||
} else {
|
||||
exceptionReportHandler(ExceptionReport.fromException(it, component))
|
||||
}
|
||||
} else {
|
||||
throw it
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
/*
|
||||
* Copyright (C) 2016 Davide Imbriaco
|
||||
* Copyright (C) 2018 Jonas Lochmann
|
||||
*
|
||||
* This Java file is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
@@ -22,4 +23,6 @@ interface TempRepository: Closeable {
|
||||
fun popTempData(key: String): ByteArray
|
||||
|
||||
fun deleteTempData(keys: List<String>)
|
||||
|
||||
fun deleteAllTempData()
|
||||
}
|
||||
|
||||
@@ -14,6 +14,9 @@
|
||||
*/
|
||||
package net.syncthing.java.core.utils
|
||||
|
||||
import net.syncthing.java.core.exception.ExceptionDetailException
|
||||
import net.syncthing.java.core.exception.ExceptionDetails
|
||||
|
||||
object PathUtils {
|
||||
const val ROOT_PATH = ""
|
||||
const val PATH_SEPARATOR = "/"
|
||||
@@ -31,29 +34,56 @@ object PathUtils {
|
||||
return pathSegments.contains(PARENT_PATH) or pathSegments.contains(CURRENT_PATH)
|
||||
}
|
||||
|
||||
private fun isTrimmed(value: String) = value.trim() == value
|
||||
private fun containsWindowsPathSeparator(path: String) = path.contains(PATH_SEPARATOR_WIN)
|
||||
private fun startsWithPathSeperator(path: String) = path.startsWith(PATH_SEPARATOR)
|
||||
private fun isValidPath(path: String) = (!containsRelativeElements(path)) and
|
||||
(!containsWindowsPathSeparator(path)) and
|
||||
path.isNotEmpty() and
|
||||
(!startsWithPathSeperator(path)) and
|
||||
isTrimmed(path)
|
||||
private fun startsWithPathSeparator(path: String) = path.startsWith(PATH_SEPARATOR)
|
||||
|
||||
private fun containsPathSeparator(file: String) = file.contains(PATH_SEPARATOR) or file.contains(PATH_SEPARATOR_WIN)
|
||||
private fun isFilenameValid(file: String) = file.isNotBlank() and
|
||||
(!containsPathSeparator(file)) and
|
||||
isTrimmed(file)
|
||||
|
||||
private fun assertPathValid(path: String) {
|
||||
if (!isValidPath(path)) {
|
||||
throw IllegalArgumentException("provided path is invalid")
|
||||
fun throwException(reason: String) {
|
||||
throw ExceptionDetailException(
|
||||
IllegalArgumentException("provided path is invalid because it $reason"),
|
||||
ExceptionDetails(
|
||||
component = "PathUtils",
|
||||
details = "processed path: \"$path\""
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
if (containsRelativeElements(path)) {
|
||||
throwException("contains relative path elements")
|
||||
}
|
||||
|
||||
if (containsWindowsPathSeparator(path)) {
|
||||
throwException("contains windows path separators")
|
||||
}
|
||||
|
||||
if (path.isEmpty()) {
|
||||
throwException("is empty")
|
||||
}
|
||||
|
||||
if (startsWithPathSeparator(path)) {
|
||||
throwException("starts with a path separator")
|
||||
}
|
||||
}
|
||||
|
||||
private fun assertFilenameValid(filename: String) {
|
||||
if (!isFilenameValid(filename)) {
|
||||
throw IllegalArgumentException("provided filename is invalid")
|
||||
fun throwException(reason: String) {
|
||||
throw ExceptionDetailException(
|
||||
IllegalArgumentException("provided filename is invalid because the filename $reason"),
|
||||
ExceptionDetails(
|
||||
component = "PathUtils",
|
||||
details = "processed filename: \"$filename\""
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
if (filename.isBlank()) {
|
||||
throwException("is blank")
|
||||
}
|
||||
|
||||
if (containsPathSeparator(filename)) {
|
||||
throwException("contains a path separator")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+18
-9
@@ -20,6 +20,7 @@ import kotlinx.coroutines.launch
|
||||
import net.syncthing.java.core.beans.DeviceAddress
|
||||
import net.syncthing.java.core.beans.DeviceId
|
||||
import net.syncthing.java.core.configuration.Configuration
|
||||
import net.syncthing.java.core.exception.ExceptionReport
|
||||
import net.syncthing.java.discovery.protocol.GlobalDiscoveryHandler
|
||||
import net.syncthing.java.discovery.protocol.LocalDiscoveryHandler
|
||||
import net.syncthing.java.discovery.utils.AddressRanker
|
||||
@@ -27,19 +28,27 @@ import org.slf4j.LoggerFactory
|
||||
import java.io.Closeable
|
||||
import java.util.*
|
||||
|
||||
class DiscoveryHandler(private val configuration: Configuration) : Closeable {
|
||||
class DiscoveryHandler(
|
||||
private val configuration: Configuration,
|
||||
exceptionReportHandler: (ExceptionReport) -> Unit
|
||||
) : Closeable {
|
||||
|
||||
private val logger = LoggerFactory.getLogger(javaClass)
|
||||
private val globalDiscoveryHandler = GlobalDiscoveryHandler(configuration)
|
||||
private val localDiscoveryHandler = LocalDiscoveryHandler(configuration, { message ->
|
||||
logger.info("received device address list from local discovery")
|
||||
private val localDiscoveryHandler = LocalDiscoveryHandler(
|
||||
configuration,
|
||||
exceptionReportHandler,
|
||||
{ message ->
|
||||
logger.info("received device address list from local discovery")
|
||||
|
||||
GlobalScope.launch {
|
||||
processDeviceAddressBg(message.addresses)
|
||||
}
|
||||
}, { deviceId ->
|
||||
onMessageFromUnknownDeviceListeners.forEach { listener -> listener(deviceId) }
|
||||
})
|
||||
GlobalScope.launch {
|
||||
processDeviceAddressBg(message.addresses)
|
||||
}
|
||||
},
|
||||
{ deviceId ->
|
||||
onMessageFromUnknownDeviceListeners.forEach { listener -> listener(deviceId) }
|
||||
}
|
||||
)
|
||||
val devicesAddressesManager = DevicesAddressesManager()
|
||||
private var isClosed = false
|
||||
private val onMessageFromUnknownDeviceListeners = Collections.synchronizedSet(HashSet<(DeviceId) -> Unit>())
|
||||
|
||||
@@ -83,14 +83,20 @@ class Main {
|
||||
private fun queryLocalDiscovery(configuration: Configuration, deviceId: DeviceId): Collection<DeviceAddress> {
|
||||
val lock = Object()
|
||||
val discoveredAddresses = mutableListOf<DeviceAddress>()
|
||||
val handler = LocalDiscoveryHandler(configuration, { message ->
|
||||
synchronized(lock) {
|
||||
if (message.deviceId == deviceId) {
|
||||
discoveredAddresses.addAll(message.addresses)
|
||||
lock.notify()
|
||||
val handler = LocalDiscoveryHandler(
|
||||
configuration,
|
||||
{
|
||||
throw it.exception
|
||||
},
|
||||
{ message ->
|
||||
synchronized(lock) {
|
||||
if (message.deviceId == deviceId) {
|
||||
discoveredAddresses.addAll(message.addresses)
|
||||
lock.notify()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
handler.startListener()
|
||||
handler.sendAnnounceMessage()
|
||||
synchronized(lock) {
|
||||
|
||||
+13
-11
@@ -14,35 +14,37 @@
|
||||
*/
|
||||
package net.syncthing.java.discovery.protocol
|
||||
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.channels.consumeEach
|
||||
import kotlinx.coroutines.launch
|
||||
import net.syncthing.java.core.beans.DeviceId
|
||||
import net.syncthing.java.core.configuration.Configuration
|
||||
import net.syncthing.java.core.exception.ExceptionReport
|
||||
import net.syncthing.java.core.exception.reportExceptions
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.io.Closeable
|
||||
import java.io.IOException
|
||||
|
||||
internal class LocalDiscoveryHandler(private val configuration: Configuration,
|
||||
private val onMessageReceivedListener: (LocalDiscoveryMessage) -> Unit,
|
||||
private val onMessageFromUnknownDeviceListener: (DeviceId) -> Unit = {}) : Closeable {
|
||||
internal class LocalDiscoveryHandler(
|
||||
private val configuration: Configuration,
|
||||
private val exceptionReportHandler: (ExceptionReport) -> Unit,
|
||||
private val onMessageReceivedListener: (LocalDiscoveryMessage) -> Unit,
|
||||
private val onMessageFromUnknownDeviceListener: (DeviceId) -> Unit = {}
|
||||
) : Closeable {
|
||||
|
||||
private val logger = LoggerFactory.getLogger(javaClass)
|
||||
private val job = Job()
|
||||
|
||||
fun sendAnnounceMessage() {
|
||||
GlobalScope.launch (Dispatchers.IO) {
|
||||
GlobalScope.async (Dispatchers.IO) {
|
||||
LocalDiscoveryUtil.sendAnnounceMessage(
|
||||
ownDeviceId = configuration.localDeviceId,
|
||||
instanceId = configuration.instanceId
|
||||
)
|
||||
}
|
||||
}.reportExceptions("LocalDiscoveryHandler.sendAnnounceMessage", exceptionReportHandler)
|
||||
}
|
||||
|
||||
fun startListener() {
|
||||
GlobalScope.launch (job) {
|
||||
GlobalScope.async (job) {
|
||||
try {
|
||||
LocalDiscoveryUtil.listenForAnnounceMessages().consumeEach { message ->
|
||||
if (message.deviceId == configuration.localDeviceId) {
|
||||
@@ -60,7 +62,7 @@ internal class LocalDiscoveryHandler(private val configuration: Configuration,
|
||||
} catch (ex: IOException) {
|
||||
logger.warn("Failed to listen for announcement messages", ex)
|
||||
}
|
||||
}
|
||||
}.reportExceptions("LocalDiscoveryHandler.startListener", exceptionReportHandler)
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
|
||||
+20
-7
@@ -22,11 +22,14 @@ import kotlinx.coroutines.channels.produce
|
||||
import kotlinx.coroutines.withContext
|
||||
import net.syncthing.java.core.beans.DeviceAddress
|
||||
import net.syncthing.java.core.beans.DeviceId
|
||||
import net.syncthing.java.core.exception.ExceptionDetailException
|
||||
import net.syncthing.java.core.exception.ExceptionDetails
|
||||
import net.syncthing.java.core.utils.NetworkUtils
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.DataOutputStream
|
||||
import java.io.IOException
|
||||
import java.lang.Exception
|
||||
import java.net.*
|
||||
import java.nio.ByteBuffer
|
||||
|
||||
@@ -117,14 +120,24 @@ object LocalDiscoveryUtil {
|
||||
if (broadcastAddress != null) {
|
||||
logger.debug("sending broadcast announce on {}", broadcastAddress)
|
||||
|
||||
DatagramSocket().use { broadcastSocket ->
|
||||
broadcastSocket.broadcast = true
|
||||
try {
|
||||
DatagramSocket().use { broadcastSocket ->
|
||||
broadcastSocket.broadcast = true
|
||||
|
||||
broadcastSocket.send(DatagramPacket(
|
||||
discoveryMessage,
|
||||
discoveryMessage.size,
|
||||
broadcastAddress,
|
||||
LISTENING_PORT))
|
||||
broadcastSocket.send(DatagramPacket(
|
||||
discoveryMessage,
|
||||
discoveryMessage.size,
|
||||
broadcastAddress,
|
||||
LISTENING_PORT))
|
||||
}
|
||||
} catch (ex: Exception) {
|
||||
throw ExceptionDetailException(
|
||||
ex,
|
||||
ExceptionDetails(
|
||||
component = "LocalDiscoveryUtil.sendAnnounceMessage",
|
||||
details = "interface: $networkInterface\naddress: $interfaceAddress\nbroadcast address: $broadcastAddress"
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+16
-3
@@ -1,3 +1,16 @@
|
||||
/*
|
||||
* Copyright (C) 2018 Jonas Lochmann
|
||||
*
|
||||
* This Java file is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package net.syncthing.repository.android
|
||||
|
||||
import net.syncthing.java.core.interfaces.TempRepository
|
||||
@@ -14,7 +27,7 @@ class TempDirectoryLocalRepository(private val directory: File): TempRepository
|
||||
directory.mkdirs()
|
||||
|
||||
// there could be garbage from the previous session which we don't need anymore
|
||||
deleteAllData()
|
||||
deleteAllTempData()
|
||||
}
|
||||
|
||||
override fun pushTempData(data: ByteArray): String {
|
||||
@@ -59,10 +72,10 @@ class TempDirectoryLocalRepository(private val directory: File): TempRepository
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
deleteAllData()
|
||||
deleteAllTempData()
|
||||
}
|
||||
|
||||
fun deleteAllData() {
|
||||
override fun deleteAllTempData() {
|
||||
directory.listFiles().forEach { file ->
|
||||
if (file.isFile) {
|
||||
file.delete()
|
||||
|
||||
+9
@@ -1,5 +1,6 @@
|
||||
/*
|
||||
* Copyright (C) 2016 Davide Imbriaco
|
||||
* Copyright (C) 2018 Jonas Lochmann
|
||||
*
|
||||
* This Java file is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
@@ -227,6 +228,14 @@ class SqlRepository(databaseFolder: File) : Closeable, IndexRepository, TempRepo
|
||||
}
|
||||
}
|
||||
|
||||
override fun deleteAllTempData() {
|
||||
getConnection().use { connection ->
|
||||
connection.prepareStatement("DELETE FROM temporary_data").use { statement ->
|
||||
statement.executeUpdate()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val VERSION = 13
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
apply plugin: 'java-library'
|
||||
apply plugin: 'kotlin'
|
||||
|
||||
dependencies {
|
||||
implementation (project(':syncthing-core')) {
|
||||
exclude group: 'commons-logging', module:'commons-logging'
|
||||
exclude group: 'org.slf4j'
|
||||
exclude group: 'ch.qos.logback'
|
||||
}
|
||||
}
|
||||
+93
@@ -0,0 +1,93 @@
|
||||
/*
|
||||
* 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.repository
|
||||
|
||||
import net.syncthing.java.core.interfaces.TempRepository
|
||||
import java.io.IOException
|
||||
import java.security.MessageDigest
|
||||
import java.security.SecureRandom
|
||||
import java.util.*
|
||||
import javax.crypto.Cipher
|
||||
import javax.crypto.spec.GCMParameterSpec
|
||||
import javax.crypto.spec.SecretKeySpec
|
||||
|
||||
class EncryptedTempRepository(private val otherRepository: TempRepository): TempRepository {
|
||||
companion object {
|
||||
private val secureRandom = SecureRandom()
|
||||
}
|
||||
|
||||
private val keyStorage = Collections.synchronizedMap(mutableMapOf<String, EncryptedTempRepositoryItem>())
|
||||
|
||||
override fun pushTempData(data: ByteArray): String {
|
||||
val (encrypted, config) = encrypt(data)
|
||||
val key = otherRepository.pushTempData(encrypted)
|
||||
|
||||
keyStorage[key] = config
|
||||
|
||||
return key
|
||||
}
|
||||
|
||||
override fun popTempData(key: String) = decrypt(otherRepository.popTempData(key), keyStorage.remove(key)!!)
|
||||
|
||||
override fun deleteTempData(keys: List<String>) {
|
||||
keys.forEach { keyStorage.remove(it) }
|
||||
otherRepository.deleteTempData(keys)
|
||||
}
|
||||
|
||||
override fun deleteAllTempData() {
|
||||
keyStorage.clear()
|
||||
otherRepository.deleteAllTempData()
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
keyStorage.clear()
|
||||
otherRepository.close()
|
||||
}
|
||||
|
||||
private fun encrypt(input: ByteArray): Pair<ByteArray, EncryptedTempRepositoryItem> {
|
||||
val cryptoSpec = EncryptedTempRepositoryItem(
|
||||
key = ByteArray(16).apply { secureRandom.nextBytes(this) },
|
||||
iv = ByteArray(16).apply { secureRandom.nextBytes(this) },
|
||||
sha512 = sha512(input)
|
||||
)
|
||||
|
||||
val output = Cipher.getInstance("AES/GCM/NoPadding").apply {
|
||||
init(
|
||||
Cipher.ENCRYPT_MODE,
|
||||
SecretKeySpec(cryptoSpec.key, "AES"),
|
||||
GCMParameterSpec(128, cryptoSpec.iv)
|
||||
)
|
||||
}.doFinal(input)
|
||||
|
||||
return output to cryptoSpec
|
||||
}
|
||||
|
||||
private fun decrypt(input: ByteArray, config: EncryptedTempRepositoryItem): ByteArray {
|
||||
val output = Cipher.getInstance("AES/GCM/NoPadding").apply {
|
||||
init(
|
||||
Cipher.DECRYPT_MODE,
|
||||
SecretKeySpec(config.key, "AES"),
|
||||
GCMParameterSpec(128, config.iv)
|
||||
)
|
||||
}.doFinal(input)
|
||||
|
||||
if (!sha512(output).contentEquals(config.sha512)) {
|
||||
throw IOException("temporarily file was modified")
|
||||
}
|
||||
|
||||
return output
|
||||
}
|
||||
|
||||
private fun sha512(data: ByteArray): ByteArray = MessageDigest.getInstance("SHA-512").digest(data)
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* 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.repository
|
||||
|
||||
internal class EncryptedTempRepositoryItem(
|
||||
val iv: ByteArray,
|
||||
val key: ByteArray,
|
||||
val sha512: ByteArray
|
||||
)
|
||||
Reference in New Issue
Block a user