14 Commits

Author SHA1 Message Date
l-jonas b96c672a8e Update whatsnew 2018-12-16 07:43:50 +01:00
l-jonas 2a25e9882b Release 0.3.10 2018-12-16 07:43:24 +01:00
Jonas L cb33d8f3e4 Update translations 2018-12-15 15:56:41 +01:00
l-jonas db8e91eafa Fix handling file names with spaces for mime type detection (#134)
This fixes https://github.com/syncthing/syncthing-lite/issues/133
2018-12-13 19:44:44 +01:00
l-jonas f9b91f6ef8 Update whatsnew 2018-12-13 10:34:10 +01:00
l-jonas 01fb92e2c9 Release 0.3.9 2018-12-13 10:33:21 +01:00
l-jonas 4b519e84e3 Update Releasing.md 2018-12-13 10:32:42 +01:00
l-jonas f3d51f0cb9 Add temp repository encryption (#131) 2018-12-13 10:27:07 +01:00
Jonas L fa30beb9d5 Update translations 2018-12-13 10:15:21 +01:00
l-jonas 919fdc31bd Fix crash when accessing closed connections (#129)
* Fix crash when accessing closed connections
2018-12-12 17:43:42 +01:00
l-jonas b3f2af0ee7 Add option to convert file extension to lower case for mime type (#127)
* Add option to convert file extension to lower case for mime type

* Always convert file extension to lower case
2018-12-12 17:20:55 +01:00
l-jonas f33364939b Ignore index updates without matching index info (#125) 2018-12-09 15:39:41 +01:00
l-jonas 1a773daf24 Clear cache handling (#124)
* Remove folder stats from cache when the data is deleted

* Move database deletion off the UI thread

* Remove index info from cache when data is deleted

* Allow observing configuration for changes

* Update folder list on config changes
2018-12-09 15:32:40 +01:00
l-jonas b115a99907 Path validation (#122)
* Remove isTrimmed path validation rule

* Include exact reason in path validation exceptions
2018-12-09 12:00:40 +01:00
30 changed files with 474 additions and 169 deletions
+3 -2
View File
@@ -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
View File
@@ -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)
},
@@ -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
View File
@@ -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
+27 -1
View File
@@ -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>
+27 -1
View File
@@ -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
View File
@@ -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()
}
}
}
}
@@ -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()
@@ -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()
}
@@ -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
}
}
@@ -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()
}
}
}
}
@@ -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 ""
}
}
@@ -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()
@@ -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'
}
}
@@ -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)
}
@@ -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
)