Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b96c672a8e | |||
| 2a25e9882b | |||
| cb33d8f3e4 | |||
| db8e91eafa | |||
| f9b91f6ef8 | |||
| 01fb92e2c9 | |||
| 4b519e84e3 | |||
| f3d51f0cb9 | |||
| fa30beb9d5 | |||
| 919fdc31bd | |||
| b3f2af0ee7 | |||
| f33364939b | |||
| 1a773daf24 | |||
| b115a99907 |
+3
-2
@@ -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 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
|
||||
|
||||
+3
-2
@@ -18,8 +18,8 @@ android {
|
||||
applicationId "net.syncthing.lite"
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 26
|
||||
versionCode 18
|
||||
versionName "0.3.8"
|
||||
versionCode 20
|
||||
versionName "0.3.10"
|
||||
multiDexEnabled true
|
||||
playAccountConfig = playAccountConfigs.defaultAccountConfig
|
||||
}
|
||||
@@ -88,4 +88,5 @@ dependencies {
|
||||
implementation 'com.github.apl-devs:appintro:v4.2.3'
|
||||
|
||||
implementation project(':syncthing-repository-android')
|
||||
implementation project(':syncthing-temp-repository-encryption')
|
||||
}
|
||||
|
||||
@@ -102,12 +102,10 @@ class MainActivity : SyncthingActivity() {
|
||||
}
|
||||
|
||||
private fun cleanCacheAndIndex() {
|
||||
GlobalScope.launch (Dispatchers.Main) {
|
||||
launch {
|
||||
libraryHandler.libraryManager.withLibrary {
|
||||
it.syncthingClient.clearCacheAndIndex()
|
||||
}
|
||||
|
||||
recreate()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ class FileMenuDialogFragment: BottomSheetDialogFragment() {
|
||||
Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
|
||||
addCategory(Intent.CATEGORY_OPENABLE)
|
||||
|
||||
type = MimeType.getFromUrl(fileSpec.fileName)
|
||||
type = MimeType.getFromFilename(fileSpec.fileName)
|
||||
|
||||
putExtra(Intent.EXTRA_TITLE, fileSpec.fileName)
|
||||
},
|
||||
|
||||
+1
-1
@@ -88,7 +88,7 @@ class DownloadFileDialogFragment : DialogFragment() {
|
||||
dismissAllowingStateLoss()
|
||||
|
||||
if (outputUri == null) {
|
||||
val mimeType = MimeType.getFromUrl(fileSpec.fileName)
|
||||
val mimeType = MimeType.getFromFilename(fileSpec.fileName)
|
||||
|
||||
try {
|
||||
context!!.startActivity(
|
||||
|
||||
@@ -8,6 +8,7 @@ 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
|
||||
@@ -49,7 +50,11 @@ class LibraryInstance (
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
@@ -58,7 +63,7 @@ class LibraryInstance (
|
||||
repository = SqliteIndexRepository(
|
||||
database = RepositoryDatabase.with(context),
|
||||
closeDatabaseOnClose = false,
|
||||
clearTempStorageHook = { tempRepository.deleteAllData() }
|
||||
clearTempStorageHook = { tempRepository.deleteAllTempData() }
|
||||
),
|
||||
tempRepository = tempRepository,
|
||||
exceptionReportHandler = { ex ->
|
||||
|
||||
@@ -159,7 +159,7 @@ class SyncthingProvider : DocumentsProvider() {
|
||||
if (fileInfo.isDirectory())
|
||||
Document.MIME_TYPE_DIR
|
||||
else
|
||||
MimeType.getFromUrl(fileInfo.fileName)
|
||||
MimeType.getFromFilename(fileInfo.fileName)
|
||||
)
|
||||
add(Document.COLUMN_LAST_MODIFIED, fileInfo.lastModified)
|
||||
add(Document.COLUMN_FLAGS, 0)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package net.syncthing.lite.utils
|
||||
|
||||
import android.webkit.MimeTypeMap
|
||||
import net.syncthing.java.core.utils.PathUtils
|
||||
|
||||
object MimeType {
|
||||
private const val DEFAULT_MIME_TYPE = "application/octet-stream"
|
||||
@@ -11,7 +12,7 @@ object MimeType {
|
||||
return mimeType ?: DEFAULT_MIME_TYPE
|
||||
}
|
||||
|
||||
fun getFromUrl(url: String) = getFromExtension(
|
||||
MimeTypeMap.getFileExtensionFromUrl(url)
|
||||
fun getFromFilename(path: String) = getFromExtension(
|
||||
PathUtils.getFileExtensionFromFilename(path).toLowerCase()
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,10 +1 @@
|
||||
- showing more detailed connection status in the device list
|
||||
- selective folder sharing (after this update, you have to confirm each folder once)
|
||||
- better error handling
|
||||
- bugfixes
|
||||
- fix crash if mime type of a file can not be determined
|
||||
- fix crash in the intro at the second step after a screen rotation
|
||||
- internal changes
|
||||
- updated build tools and some dependencies
|
||||
- locks for changing and saving the configuration
|
||||
- removed old connection status change listening API
|
||||
- fix file type detection for file names with umlauts and/ or spaces
|
||||
|
||||
@@ -40,16 +40,42 @@ 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_warning_reconnect_problem">
|
||||
Datorită modului în care această aplicație și serverul Syncthing funcționează,
|
||||
nu se poate face reconectarea timp de câteva minute după ce aplicația a fost oprită (ștearsă din lista de aplicații care rulează)
|
||||
sau conexiunea a fost întreruptă.
|
||||
Aceasta limitare nu se aplica la conexiunile descoperite local.
|
||||
</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>
|
||||
</resources>
|
||||
<string name="toast_error">O eroare s-a produs in Syncthing Lite. Puteți vedea detalii în setările Syncthing Lite.</string>
|
||||
</resources>
|
||||
|
||||
@@ -36,16 +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>
|
||||
</resources>
|
||||
<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>
|
||||
|
||||
+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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
@@ -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
|
||||
@@ -33,8 +35,6 @@ 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,
|
||||
@@ -42,28 +42,33 @@ class IndexHandler(
|
||||
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,
|
||||
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 {
|
||||
@@ -124,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)
|
||||
}
|
||||
@@ -135,7 +140,7 @@ class IndexHandler(
|
||||
}
|
||||
|
||||
updatedIndexInfos.forEach {
|
||||
onIndexRecordAcquiredEvents.send(
|
||||
indexInfoUpdateEvents.send(
|
||||
IndexRecordAcquiredEvent(
|
||||
folderId = it.folderId,
|
||||
indexInfo = it,
|
||||
@@ -176,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()
|
||||
+15
-7
@@ -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)
|
||||
@@ -17,6 +18,8 @@ object IndexMessageProcessor {
|
||||
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)
|
||||
|
||||
@@ -31,16 +34,20 @@ object IndexMessageProcessor {
|
||||
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))
|
||||
}
|
||||
|
||||
@@ -59,4 +66,5 @@ object IndexMessageProcessor {
|
||||
}
|
||||
|
||||
data class Result(val newIndexInfo: IndexInfo, val updatedFiles: List<FileInfo>, val newFolderStats: FolderStats)
|
||||
class IndexInfoNotFoundException: RuntimeException()
|
||||
}
|
||||
|
||||
+11
-5
@@ -23,7 +23,6 @@ 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
|
||||
@@ -34,9 +33,9 @@ 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,
|
||||
exceptionReportHandler: (ExceptionReport) -> Unit
|
||||
) {
|
||||
@@ -82,7 +81,14 @@ class IndexMessageQueueProcessor (
|
||||
init {
|
||||
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
|
||||
|
||||
logger.warn("could not find index info for index update")
|
||||
}
|
||||
}
|
||||
}.reportExceptions("IndexMessageQueueProcessor.indexUpdateProcessingQueue", exceptionReportHandler)
|
||||
|
||||
@@ -138,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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+33
-23
@@ -4,6 +4,8 @@ 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
|
||||
@@ -25,12 +27,12 @@ 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()
|
||||
@@ -44,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
|
||||
)
|
||||
)
|
||||
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 {
|
||||
@@ -68,40 +74,42 @@ 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()
|
||||
|
||||
val localDeviceName: String
|
||||
get() = config.localDeviceName
|
||||
get() = configChannel.value.localDeviceName
|
||||
|
||||
val folders: Set<FolderInfo>
|
||||
get() = config.folders
|
||||
get() = configChannel.value.folders
|
||||
|
||||
val peers: Set<DeviceInfo>
|
||||
get() = config.peers
|
||||
get() = configChannel.value.peers
|
||||
|
||||
suspend fun update(operation: suspend (Config) -> Config): Boolean {
|
||||
modifyLock.withLock {
|
||||
val oldConfig = config
|
||||
val newConfig = operation(config)
|
||||
val oldConfig = configChannel.value
|
||||
val newConfig = operation(oldConfig)
|
||||
|
||||
if (oldConfig != newConfig) {
|
||||
config = newConfig
|
||||
configChannel.send(newConfig)
|
||||
isSaved = false
|
||||
|
||||
return true
|
||||
@@ -121,7 +129,7 @@ class Configuration(configFolder: File = DefaultConfigFolder) {
|
||||
|
||||
private suspend fun persist() {
|
||||
saveLock.withLock {
|
||||
val (config1, isConfig1Saved) = modifyLock.withLock { config to isSaved }
|
||||
val (config1, isConfig1Saved) = modifyLock.withLock { configChannel.value to isSaved }
|
||||
|
||||
if (isConfig1Saved) {
|
||||
return
|
||||
@@ -140,13 +148,15 @@ class Configuration(configFolder: File = DefaultConfigFolder) {
|
||||
)
|
||||
|
||||
modifyLock.withLock {
|
||||
if (config1 === config) {
|
||||
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)"
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -34,42 +34,57 @@ 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)) {
|
||||
fun throwException(reason: String) {
|
||||
throw ExceptionDetailException(
|
||||
IllegalArgumentException("provided path is invalid"),
|
||||
IllegalArgumentException("provided path is invalid because it $reason"),
|
||||
ExceptionDetails(
|
||||
component = "PathUtils",
|
||||
details = "processed path: $path"
|
||||
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)) {
|
||||
fun throwException(reason: String) {
|
||||
throw ExceptionDetailException(
|
||||
IllegalArgumentException("provided filename is invalid"),
|
||||
IllegalArgumentException("provided filename is invalid because the filename $reason"),
|
||||
ExceptionDetails(
|
||||
component = "PathUtils",
|
||||
details = "processed filename: $filename"
|
||||
details = "processed filename: \"$filename\""
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
if (filename.isBlank()) {
|
||||
throwException("is blank")
|
||||
}
|
||||
|
||||
if (containsPathSeparator(filename)) {
|
||||
throwException("contains a path separator")
|
||||
}
|
||||
}
|
||||
|
||||
fun isParent(path: String): Boolean {
|
||||
@@ -115,4 +130,12 @@ object PathUtils {
|
||||
|
||||
return dir.removeSuffix(PATH_SEPARATOR) + file
|
||||
}
|
||||
|
||||
fun getFileExtensionFromFilename(filename: String): String {
|
||||
assertFilenameValid(filename)
|
||||
|
||||
val dotIndex = filename.lastIndexOf(".")
|
||||
|
||||
return if (dotIndex != 0) filename.substring(dotIndex + 1) else ""
|
||||
}
|
||||
}
|
||||
|
||||
+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