feat(sync): перевёл группы синхронизации на Room и добавил контроль совместимости

This commit is contained in:
2026-05-17 18:03:14 +03:00
parent 15f13577c8
commit e562e4d9e9
28 changed files with 518 additions and 159 deletions

View File

@@ -4,24 +4,28 @@ import androidx.room.Database
import androidx.room.RoomDatabase
import com.github.nullptroma.wallenc.infrastructure.db.app.dao.StorageKeyMapDao
import com.github.nullptroma.wallenc.infrastructure.db.app.dao.StorageMetaInfoDao
import com.github.nullptroma.wallenc.infrastructure.db.app.dao.StorageSyncGroupDao
import com.github.nullptroma.wallenc.infrastructure.db.app.dao.YandexAccountDao
import com.github.nullptroma.wallenc.infrastructure.db.app.model.DbStorageKeyMap
import com.github.nullptroma.wallenc.infrastructure.db.app.model.DbStorageMetaInfo
import com.github.nullptroma.wallenc.infrastructure.db.app.model.DbStorageSyncGroup
import com.github.nullptroma.wallenc.infrastructure.db.app.model.DbYandexAccount
interface IAppDb {
val storageKeyMapDao: StorageKeyMapDao
val storageMetaInfoDao: StorageMetaInfoDao
val storageSyncGroupDao: StorageSyncGroupDao
val yandexAccountDao: YandexAccountDao
}
@Database(
entities = [DbStorageKeyMap::class, DbStorageMetaInfo::class, DbYandexAccount::class],
version = 4,
entities = [DbStorageKeyMap::class, DbStorageMetaInfo::class, DbYandexAccount::class, DbStorageSyncGroup::class],
version = 5,
exportSchema = false
)
abstract class AppDb : IAppDb, RoomDatabase() {
abstract override val storageKeyMapDao: StorageKeyMapDao
abstract override val storageMetaInfoDao: StorageMetaInfoDao
abstract override val storageSyncGroupDao: StorageSyncGroupDao
abstract override val yandexAccountDao: YandexAccountDao
}

View File

@@ -0,0 +1,19 @@
package com.github.nullptroma.wallenc.infrastructure.db.app.dao
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import com.github.nullptroma.wallenc.infrastructure.db.app.model.DbStorageSyncGroup
@Dao
interface StorageSyncGroupDao {
@Query("SELECT * FROM storage_sync_groups")
suspend fun getAll(): List<DbStorageSyncGroup>
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun upsert(group: DbStorageSyncGroup)
@Query("DELETE FROM storage_sync_groups WHERE group_id = :groupId")
suspend fun deleteById(groupId: String)
}

View File

@@ -0,0 +1,13 @@
package com.github.nullptroma.wallenc.infrastructure.db.app.model
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity(tableName = "storage_sync_groups")
data class DbStorageSyncGroup(
@PrimaryKey @ColumnInfo(name = "group_id") val id: String,
@ColumnInfo(name = "storage_uuids_csv") val storageUuidsCsv: String,
@ColumnInfo(name = "encryption_kind") val encryptionKind: String,
@ColumnInfo(name = "encryption_secret") val encryptionSecret: String?,
)

View File

@@ -0,0 +1,91 @@
package com.github.nullptroma.wallenc.infrastructure.db.app.repository
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 com.github.nullptroma.wallenc.infrastructure.db.app.dao.StorageSyncGroupDao
import com.github.nullptroma.wallenc.infrastructure.db.app.model.DbStorageSyncGroup
import com.github.nullptroma.wallenc.infrastructure.utils.IProvider
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.withContext
import java.util.UUID
class StorageSyncGroupRepository(
private val dao: StorageSyncGroupDao,
private val ioDispatcher: CoroutineDispatcher,
) : IStorageSyncGroupStore {
private val allGroupsProvider: IProvider<List<StorageSyncGroup>> = object : IProvider<List<StorageSyncGroup>> {
override suspend fun get(): List<StorageSyncGroup> = dao.getAll().mapNotNull(::toDomain)
override suspend fun set(value: List<StorageSyncGroup>) {
val desiredIds = value.map { it.id }.toSet()
val current = dao.getAll()
current
.asSequence()
.filter { it.id !in desiredIds }
.forEach { dao.deleteById(it.id) }
value.forEach { group ->
dao.upsert(toDb(group))
}
}
override suspend fun clear() {
dao.getAll().forEach { dao.deleteById(it.id) }
}
}
override suspend fun getGroups(): List<StorageSyncGroup> = withContext(ioDispatcher) {
allGroupsProvider.get().orEmpty()
}
override suspend fun putGroup(group: StorageSyncGroup) = withContext(ioDispatcher) {
val all = allGroupsProvider.get().orEmpty().toMutableList()
val idx = all.indexOfFirst { it.id == group.id }
if (idx >= 0) {
all[idx] = group
} else {
all.add(group)
}
allGroupsProvider.set(all)
}
override suspend fun removeGroup(groupId: String) = withContext(ioDispatcher) {
val all = allGroupsProvider.get().orEmpty().filterNot { it.id == groupId }
allGroupsProvider.set(all)
}
private fun toDb(group: StorageSyncGroup): DbStorageSyncGroup = DbStorageSyncGroup(
id = group.id,
storageUuidsCsv = group.storageUuids.joinToString(",") { it.toString() },
encryptionKind = group.encryptionKind.name,
encryptionSecret = group.encryptionSecret,
)
private fun toDomain(db: DbStorageSyncGroup): StorageSyncGroup? {
val kind = runCatching {
StorageSyncGroupEncryptionKind.valueOf(db.encryptionKind)
}.getOrElse {
StorageSyncGroupEncryptionKind.UNSET
}
if (db.id.isBlank()) {
return null
}
val uuids = db.storageUuidsCsv
.split(",")
.mapNotNull { token ->
val value = token.trim()
if (value.isBlank()) {
null
} else {
runCatching { UUID.fromString(value) }.getOrNull()
}
}
.toSet()
return StorageSyncGroup(
id = db.id,
storageUuids = uuids,
encryptionKind = kind,
encryptionSecret = db.encryptionSecret?.takeIf { it.isNotBlank() },
)
}
}