feat(sync): перевёл группы синхронизации на Room и добавил контроль совместимости
This commit is contained in:
@@ -8,8 +8,11 @@ class FindStorageUseCase(
|
||||
private val vaultsManager: IVaultsManager,
|
||||
) {
|
||||
fun find(storageUuid: UUID): IStorage? {
|
||||
return vaultsManager.vaults.value
|
||||
.flatMap { it.storages.value }
|
||||
return (
|
||||
vaultsManager.allStorages.value +
|
||||
vaultsManager.unlockManager.openedStorages.value.values
|
||||
)
|
||||
.distinctBy { it.uuid }
|
||||
.firstOrNull { it.uuid == storageUuid }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,8 +2,22 @@ package com.github.nullptroma.wallenc.usecases
|
||||
|
||||
import com.github.nullptroma.wallenc.domain.interfaces.IStorageSyncGroupStore
|
||||
import com.github.nullptroma.wallenc.domain.interfaces.StorageSyncGroup
|
||||
import com.github.nullptroma.wallenc.domain.interfaces.StorageSyncGroupEncryptionKind
|
||||
import java.util.UUID
|
||||
|
||||
data class StorageSyncCompatibilityInput(
|
||||
val isEncrypted: Boolean,
|
||||
val encryptionSecret: String? = null,
|
||||
)
|
||||
|
||||
sealed interface AddStorageToSyncGroupResult {
|
||||
data object Added : AddStorageToSyncGroupResult
|
||||
data object GroupNotFound : AddStorageToSyncGroupResult
|
||||
data object AlreadyInGroup : AddStorageToSyncGroupResult
|
||||
data object IncompatibleEncryption : AddStorageToSyncGroupResult
|
||||
data object MissingEncryptionSecret : AddStorageToSyncGroupResult
|
||||
}
|
||||
|
||||
class ManageStorageSyncGroupsUseCase(
|
||||
private val groupStore: IStorageSyncGroupStore,
|
||||
) {
|
||||
@@ -17,7 +31,12 @@ class ManageStorageSyncGroupsUseCase(
|
||||
index++
|
||||
candidate = "group-$index"
|
||||
}
|
||||
val group = StorageSyncGroup(id = candidate, storageUuids = emptySet())
|
||||
val group = StorageSyncGroup(
|
||||
id = candidate,
|
||||
storageUuids = emptySet(),
|
||||
encryptionKind = StorageSyncGroupEncryptionKind.UNSET,
|
||||
encryptionSecret = null,
|
||||
)
|
||||
groupStore.putGroup(group)
|
||||
return group
|
||||
}
|
||||
@@ -26,17 +45,68 @@ class ManageStorageSyncGroupsUseCase(
|
||||
groupStore.removeGroup(groupId.trim())
|
||||
}
|
||||
|
||||
suspend fun addStorageToGroup(groupId: String, storageUuid: UUID) {
|
||||
val current = getGroups().firstOrNull { it.id == groupId } ?: return
|
||||
suspend fun addStorageToGroup(
|
||||
groupId: String,
|
||||
storageUuid: UUID,
|
||||
compatibility: StorageSyncCompatibilityInput,
|
||||
): AddStorageToSyncGroupResult {
|
||||
val current = getGroups().firstOrNull { it.id == groupId }
|
||||
?: return AddStorageToSyncGroupResult.GroupNotFound
|
||||
|
||||
if (storageUuid in current.storageUuids) {
|
||||
return AddStorageToSyncGroupResult.AlreadyInGroup
|
||||
}
|
||||
|
||||
val encryptedSecret = compatibility.encryptionSecret?.takeIf { it.isNotBlank() }
|
||||
val effectiveEncryption = when {
|
||||
!compatibility.isEncrypted -> StorageSyncGroupEncryptionKind.NONE to null
|
||||
encryptedSecret == null -> return AddStorageToSyncGroupResult.MissingEncryptionSecret
|
||||
else -> StorageSyncGroupEncryptionKind.PASSWORD to encryptedSecret
|
||||
}
|
||||
|
||||
val (nextKind, nextSecret) = when (current.encryptionKind) {
|
||||
StorageSyncGroupEncryptionKind.UNSET -> effectiveEncryption
|
||||
StorageSyncGroupEncryptionKind.NONE -> {
|
||||
if (effectiveEncryption.first != StorageSyncGroupEncryptionKind.NONE) {
|
||||
return AddStorageToSyncGroupResult.IncompatibleEncryption
|
||||
}
|
||||
StorageSyncGroupEncryptionKind.NONE to null
|
||||
}
|
||||
|
||||
StorageSyncGroupEncryptionKind.PASSWORD -> {
|
||||
if (
|
||||
effectiveEncryption.first != StorageSyncGroupEncryptionKind.PASSWORD ||
|
||||
current.encryptionSecret != effectiveEncryption.second
|
||||
) {
|
||||
return AddStorageToSyncGroupResult.IncompatibleEncryption
|
||||
}
|
||||
StorageSyncGroupEncryptionKind.PASSWORD to current.encryptionSecret
|
||||
}
|
||||
}
|
||||
|
||||
groupStore.putGroup(
|
||||
current.copy(storageUuids = current.storageUuids + storageUuid),
|
||||
current.copy(
|
||||
storageUuids = current.storageUuids + storageUuid,
|
||||
encryptionKind = nextKind,
|
||||
encryptionSecret = nextSecret,
|
||||
),
|
||||
)
|
||||
return AddStorageToSyncGroupResult.Added
|
||||
}
|
||||
|
||||
suspend fun removeStorageFromGroup(groupId: String, storageUuid: UUID) {
|
||||
val current = getGroups().firstOrNull { it.id == groupId } ?: return
|
||||
val remaining = current.storageUuids - storageUuid
|
||||
groupStore.putGroup(
|
||||
current.copy(storageUuids = current.storageUuids - storageUuid),
|
||||
current.copy(
|
||||
storageUuids = remaining,
|
||||
encryptionKind = if (remaining.isEmpty()) {
|
||||
StorageSyncGroupEncryptionKind.UNSET
|
||||
} else {
|
||||
current.encryptionKind
|
||||
},
|
||||
encryptionSecret = if (remaining.isEmpty()) null else current.encryptionSecret,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.github.nullptroma.wallenc.usecases
|
||||
|
||||
import com.github.nullptroma.wallenc.domain.datatypes.EncryptKey
|
||||
import com.github.nullptroma.wallenc.domain.interfaces.IStorage
|
||||
import com.github.nullptroma.wallenc.domain.interfaces.StorageSyncGroup
|
||||
import com.github.nullptroma.wallenc.domain.interfaces.StorageSyncGroupEncryptionKind
|
||||
import java.util.Base64
|
||||
import java.util.UUID
|
||||
|
||||
fun storageEncryptionSecret(key: EncryptKey): String =
|
||||
Base64.getEncoder().encodeToString(key.bytes)
|
||||
|
||||
fun isStorageCompatibleWithGroup(
|
||||
storage: IStorage,
|
||||
group: StorageSyncGroup,
|
||||
resolveStorageKey: (UUID) -> EncryptKey?,
|
||||
): Boolean {
|
||||
return when (group.encryptionKind) {
|
||||
StorageSyncGroupEncryptionKind.UNSET -> true
|
||||
StorageSyncGroupEncryptionKind.NONE -> storage.metaInfo.value.encInfo == null
|
||||
StorageSyncGroupEncryptionKind.PASSWORD -> {
|
||||
val groupSecret = group.encryptionSecret ?: return false
|
||||
if (storage.metaInfo.value.encInfo == null) {
|
||||
return false
|
||||
}
|
||||
val currentSecret = resolveStorageKey(storage.uuid)?.let(::storageEncryptionSecret)
|
||||
currentSecret != null && currentSecret == groupSecret
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -72,6 +72,20 @@ class StorageSyncEngine(
|
||||
reportProgress(null, "Storage sync: group \"$groupId\" skipped (need at least 2 storages)")
|
||||
return
|
||||
}
|
||||
val incompatible = storages.filterNot { storage ->
|
||||
isStorageCompatibleWithGroup(
|
||||
storage = storage,
|
||||
group = group,
|
||||
resolveStorageKey = vaultsManager.unlockManager::getOpenedStorageKey,
|
||||
)
|
||||
}
|
||||
if (incompatible.isNotEmpty()) {
|
||||
reportProgress(
|
||||
null,
|
||||
"Storage sync: group \"$groupId\" skipped (incompatible encryption: ${incompatible.size})",
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
var leaseUntil = Instant.now().plusSeconds(SYNC_LOCK_LEASE_SECONDS)
|
||||
val lockedAccessors = mutableListOf<IStorageAccessor>()
|
||||
|
||||
Reference in New Issue
Block a user