Возможность переподключения к remote vault
This commit is contained in:
@@ -6,6 +6,7 @@ import com.github.nullptroma.wallenc.domain.interfaces.IStorage
|
||||
import com.github.nullptroma.wallenc.domain.interfaces.IStorageInfo
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.merge
|
||||
@@ -29,7 +30,13 @@ class ManageTextSecretsUseCase {
|
||||
val storage = storageInfo as? IStorage ?: return flowOf(emptyList())
|
||||
return merge(
|
||||
flowOf(Unit),
|
||||
storage.accessor.filesUpdates.map { Unit },
|
||||
storage.accessor.filesUpdates
|
||||
.filter { page ->
|
||||
page.data.any { file ->
|
||||
domainFilePathEquals(file.metaInfo.path, StorageDomainDataFiles.TEXT_SECRETS_FILE)
|
||||
}
|
||||
}
|
||||
.map { Unit },
|
||||
).map {
|
||||
mutex.withLock { readAll(storage) }
|
||||
}.distinctUntilChanged()
|
||||
@@ -40,11 +47,16 @@ class ManageTextSecretsUseCase {
|
||||
readAll(storage).firstOrNull { it.id == id }
|
||||
}
|
||||
|
||||
suspend fun create(storageInfo: IStorageInfo, title: String, items: List<TextSecretEntryRecord>): TextSecretRecord =
|
||||
suspend fun create(
|
||||
storageInfo: IStorageInfo,
|
||||
title: String,
|
||||
items: List<TextSecretEntryRecord>,
|
||||
id: String = UUID.randomUUID().toString(),
|
||||
): TextSecretRecord =
|
||||
mutex.withLock {
|
||||
val storage = storageInfo as? IStorage ?: error("Storage is not writable")
|
||||
val next = TextSecretRecord(
|
||||
id = UUID.randomUUID().toString(),
|
||||
id = id,
|
||||
title = title.trim(),
|
||||
items = items.normalizeItems(),
|
||||
)
|
||||
@@ -142,4 +154,7 @@ class ManageTextSecretsUseCase {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun domainFilePathEquals(left: String, right: String): Boolean =
|
||||
left.trimStart('/') == right.trimStart('/')
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import com.github.nullptroma.wallenc.domain.interfaces.IStorage
|
||||
import com.github.nullptroma.wallenc.domain.interfaces.IStorageInfo
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.merge
|
||||
@@ -25,7 +26,13 @@ class ManageTwoFaTokensUseCase {
|
||||
val storage = storageInfo as? IStorage ?: return flowOf(emptyList())
|
||||
return merge(
|
||||
flowOf(Unit),
|
||||
storage.accessor.filesUpdates.map { Unit },
|
||||
storage.accessor.filesUpdates
|
||||
.filter { page ->
|
||||
page.data.any { file ->
|
||||
domainFilePathEquals(file.metaInfo.path, StorageDomainDataFiles.TWO_FA_TOKENS_FILE)
|
||||
}
|
||||
}
|
||||
.map { Unit },
|
||||
).map {
|
||||
mutex.withLock { readAll(storage) }
|
||||
}.distinctUntilChanged()
|
||||
@@ -117,4 +124,7 @@ class ManageTwoFaTokensUseCase {
|
||||
put("secret", JsonPrimitive(record.secret))
|
||||
record.notes?.let { put("notes", JsonPrimitive(it)) }
|
||||
}
|
||||
|
||||
private fun domainFilePathEquals(left: String, right: String): Boolean =
|
||||
left.trimStart('/') == right.trimStart('/')
|
||||
}
|
||||
|
||||
@@ -4,6 +4,6 @@ package com.github.nullptroma.wallenc.usecases
|
||||
* Единая точка с путями обычных JSON-файлов пользовательских доменных данных.
|
||||
*/
|
||||
object StorageDomainDataFiles {
|
||||
const val TWO_FA_TOKENS_FILE = "/two-fa-tokens.json"
|
||||
const val TEXT_SECRETS_FILE = "/text-secrets.json"
|
||||
const val TWO_FA_TOKENS_FILE = "/wallenc-data/two-fa-tokens.json"
|
||||
const val TEXT_SECRETS_FILE = "/wallenc-data/text-secrets.json"
|
||||
}
|
||||
|
||||
@@ -73,7 +73,7 @@ class StorageSyncEngine(
|
||||
return
|
||||
}
|
||||
|
||||
val leaseUntil = Instant.now().plusSeconds(SYNC_LOCK_LEASE_SECONDS)
|
||||
var leaseUntil = Instant.now().plusSeconds(SYNC_LOCK_LEASE_SECONDS)
|
||||
val lockedAccessors = mutableListOf<IStorageAccessor>()
|
||||
try {
|
||||
reportProgress(null, "Storage sync: group \"$groupId\" acquiring locks")
|
||||
@@ -92,6 +92,12 @@ class StorageSyncEngine(
|
||||
|
||||
reportProgress(null, "Storage sync: group \"$groupId\" reading journals")
|
||||
for ((journalIndex, storage) in storages.withIndex()) {
|
||||
leaseUntil = renewLocksIfNeeded(
|
||||
groupId = groupId,
|
||||
lockedAccessors = lockedAccessors,
|
||||
currentLeaseUntil = leaseUntil,
|
||||
reportProgress = reportProgress,
|
||||
) ?: return
|
||||
if (syncGeneration.get() != generationSnapshot) {
|
||||
reportProgress(null, "Storage sync: group \"$groupId\" cancelled by newer run")
|
||||
return
|
||||
@@ -115,6 +121,12 @@ class StorageSyncEngine(
|
||||
|
||||
reportProgress(null, "Storage sync: group \"$groupId\" processing ${mergedEntries.size} entries")
|
||||
for ((pathIndex, merged) in mergedEntries.withIndex()) {
|
||||
leaseUntil = renewLocksIfNeeded(
|
||||
groupId = groupId,
|
||||
lockedAccessors = lockedAccessors,
|
||||
currentLeaseUntil = leaseUntil,
|
||||
reportProgress = reportProgress,
|
||||
) ?: return
|
||||
val path = merged.key
|
||||
val winnerEntry = merged.value
|
||||
reportProgress(null, "Storage sync: group \"$groupId\" entry ${pathIndex + 1}/${mergedEntries.size}")
|
||||
@@ -156,6 +168,30 @@ class StorageSyncEngine(
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun renewLocksIfNeeded(
|
||||
groupId: String,
|
||||
lockedAccessors: List<IStorageAccessor>,
|
||||
currentLeaseUntil: Instant,
|
||||
reportProgress: suspend (fraction: Float?, label: String?) -> Unit,
|
||||
): Instant? {
|
||||
val now = Instant.now()
|
||||
if (currentLeaseUntil.isAfter(now.plusSeconds(SYNC_LOCK_RENEW_MARGIN_SECONDS))) {
|
||||
return currentLeaseUntil
|
||||
}
|
||||
val nextLeaseUntil = now.plusSeconds(SYNC_LOCK_LEASE_SECONDS)
|
||||
reportProgress(null, "Storage sync: group \"$groupId\" renewing locks")
|
||||
for (accessor in lockedAccessors) {
|
||||
val renewed = runCatching {
|
||||
accessor.tryAcquireSyncLock(holderId, nextLeaseUntil)
|
||||
}.getOrElse { false }
|
||||
if (!renewed) {
|
||||
reportProgress(null, "Storage sync: group \"$groupId\" lock renewal failed")
|
||||
return null
|
||||
}
|
||||
}
|
||||
return nextLeaseUntil
|
||||
}
|
||||
|
||||
private fun resolveStorages(uuids: Set<UUID>): List<IStorage> {
|
||||
val byUuid = vaultsManager.allStorages.value.associateBy { it.uuid }
|
||||
return uuids.mapNotNull { byUuid[it] }
|
||||
@@ -226,5 +262,6 @@ class StorageSyncEngine(
|
||||
|
||||
private companion object {
|
||||
private const val SYNC_LOCK_LEASE_SECONDS: Long = 30 * 60
|
||||
private const val SYNC_LOCK_RENEW_MARGIN_SECONDS: Long = 60
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import com.github.nullptroma.wallenc.domain.interfaces.IDirectory
|
||||
import com.github.nullptroma.wallenc.domain.interfaces.IFile
|
||||
import com.github.nullptroma.wallenc.domain.interfaces.IStorage
|
||||
import com.github.nullptroma.wallenc.domain.interfaces.IStorageAccessor
|
||||
import com.github.nullptroma.wallenc.domain.interfaces.IMetaInfo
|
||||
import com.github.nullptroma.wallenc.domain.interfaces.IStorageMetaInfo
|
||||
import com.github.nullptroma.wallenc.domain.tasks.TaskProgress
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
@@ -184,7 +185,13 @@ private class FakeStorageAccessor : IStorageAccessor {
|
||||
return object : ByteArrayOutputStream() {
|
||||
override fun close() {
|
||||
dataFiles[path] = toByteArray()
|
||||
_filesUpdates.tryEmit(DataPage(list = emptyList(), pageLength = 1, pageIndex = 0))
|
||||
_filesUpdates.tryEmit(
|
||||
DataPage(
|
||||
listOf(FakeFile(path)),
|
||||
pageLength = 1,
|
||||
pageIndex = 0,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -223,3 +230,13 @@ private class FakeStorageAccessor : IStorageAccessor {
|
||||
|
||||
override suspend fun forceClearSyncLock() = Unit
|
||||
}
|
||||
|
||||
private class FakeFile(path: String) : IFile {
|
||||
override val metaInfo: IMetaInfo = object : IMetaInfo {
|
||||
override val size: Long = 0L
|
||||
override val isDeleted: Boolean = false
|
||||
override val isHidden: Boolean = false
|
||||
override val lastModified: Instant = Instant.now()
|
||||
override val path: String = path
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user