From 9ceb8bd934242e31ae9af46fa9f5f1ddd321b68e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9F=D1=8B=D1=82=D0=BA=D0=BE=D0=B2=20=D0=A0=D0=BE=D0=BC?= =?UTF-8?q?=D0=B0=D0=BD?= Date: Mon, 11 May 2026 20:54:15 +0300 Subject: [PATCH] =?UTF-8?q?=D0=9F=D0=B5=D1=80=D0=B5=D0=BD=D0=BE=D1=81=20lo?= =?UTF-8?q?calVault=20=D0=B2=20domain-storage?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../app/di/modules/data/SingletonModule.kt | 4 +- .../infrastructure/vaults/local/LocalVault.kt | 73 +++++++++++++++---- .../vaults/local/LocalVaultIdStore.kt | 30 -------- 3 files changed, 58 insertions(+), 49 deletions(-) rename {infrastructure-android => domain-storage}/src/main/java/com/github/nullptroma/wallenc/infrastructure/vaults/local/LocalVault.kt (58%) delete mode 100644 infrastructure-android/src/main/java/com/github/nullptroma/wallenc/infrastructure/vaults/local/LocalVaultIdStore.kt diff --git a/app/src/main/java/com/github/nullptroma/wallenc/app/di/modules/data/SingletonModule.kt b/app/src/main/java/com/github/nullptroma/wallenc/app/di/modules/data/SingletonModule.kt index 14d113e..6af0186 100644 --- a/app/src/main/java/com/github/nullptroma/wallenc/app/di/modules/data/SingletonModule.kt +++ b/app/src/main/java/com/github/nullptroma/wallenc/app/di/modules/data/SingletonModule.kt @@ -18,7 +18,6 @@ import com.github.nullptroma.wallenc.infrastructure.ports.YandexAccountStore import com.github.nullptroma.wallenc.task.runtime.TaskOrchestrator import com.github.nullptroma.wallenc.infrastructure.vaults.VaultsManager import com.github.nullptroma.wallenc.infrastructure.vaults.local.LocalVault -import com.github.nullptroma.wallenc.infrastructure.vaults.local.LocalVaultIdStore import com.github.nullptroma.wallenc.domain.interfaces.IUnlockManager import com.github.nullptroma.wallenc.domain.interfaces.IVault import com.github.nullptroma.wallenc.domain.interfaces.IVaultsManager @@ -92,8 +91,7 @@ class SingletonModule { @ApplicationContext context: Context, ): IVault = LocalVault( ioDispatcher = ioDispatcher, - context = context, - idStore = LocalVaultIdStore(context), + vaultRoot = context.getExternalFilesDir("LocalVault"), ) @Provides diff --git a/infrastructure-android/src/main/java/com/github/nullptroma/wallenc/infrastructure/vaults/local/LocalVault.kt b/domain-storage/src/main/java/com/github/nullptroma/wallenc/infrastructure/vaults/local/LocalVault.kt similarity index 58% rename from infrastructure-android/src/main/java/com/github/nullptroma/wallenc/infrastructure/vaults/local/LocalVault.kt rename to domain-storage/src/main/java/com/github/nullptroma/wallenc/infrastructure/vaults/local/LocalVault.kt index 93ae8a3..508c3aa 100644 --- a/infrastructure-android/src/main/java/com/github/nullptroma/wallenc/infrastructure/vaults/local/LocalVault.kt +++ b/domain-storage/src/main/java/com/github/nullptroma/wallenc/infrastructure/vaults/local/LocalVault.kt @@ -1,9 +1,8 @@ package com.github.nullptroma.wallenc.infrastructure.vaults.local -import android.content.Context -import com.github.nullptroma.wallenc.infrastructure.storages.local.LocalStorage import com.github.nullptroma.wallenc.domain.datatypes.StorageEncryptionInfo import com.github.nullptroma.wallenc.domain.interfaces.IStorage +import com.github.nullptroma.wallenc.infrastructure.storages.local.LocalStorage import com.github.nullptroma.wallenc.vault.contract.DescribedVault import com.github.nullptroma.wallenc.vault.contract.VaultDescriptor import kotlinx.coroutines.CoroutineDispatcher @@ -13,6 +12,7 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import java.io.File +import java.io.FileOutputStream import java.util.UUID import kotlin.io.path.Path import kotlin.io.path.createDirectory @@ -20,11 +20,13 @@ import kotlin.io.path.pathString class LocalVault( private val ioDispatcher: CoroutineDispatcher, - context: Context, - idStore: LocalVaultIdStore, + private val vaultRoot: File?, ) : DescribedVault { - override val uuid: UUID = idStore.getOrCreate() + override val uuid: UUID = vaultRoot?.let { root -> + root.mkdirs() + readOrCreateVaultUuid(File(root, UUID_FILE_NAME)) + } ?: UUID.randomUUID() override val descriptor: VaultDescriptor = VaultDescriptor.LocalDevice(uuid) @@ -40,39 +42,42 @@ class LocalVault( private val _availableSpace = MutableStateFlow(null) override val availableSpace: StateFlow = _availableSpace - private val path = MutableStateFlow(null) + private val path = MutableStateFlow(vaultRoot) init { CoroutineScope(ioDispatcher).launch { - path.value = context.getExternalFilesDir("LocalVault") _isAvailable.value = path.value != null - readStorages() + if (path.value != null) { + readStorages() + } } } private suspend fun readStorages() { val path = path.value - if (path == null || !_isAvailable.value) + if (path == null || !_isAvailable.value) { throw Exception("Not available") + } val dirs = path.listFiles()?.filter { it.isDirectory } if (dirs != null) { _storages.value = dirs.map { - val uuid = UUID.fromString(it.name) - LocalStorage(uuid, it.path, ioDispatcher).apply { init() } + val storageUuid = UUID.fromString(it.name) + LocalStorage(storageUuid, it.path, ioDispatcher).apply { init() } } } } override suspend fun createStorage(): LocalStorage = withContext(ioDispatcher) { val path = path.value - if (path == null || !_isAvailable.value) + if (path == null || !_isAvailable.value) { throw Exception("Not available") + } - val uuid = UUID.randomUUID() - val next = Path(path.path, uuid.toString()) + val storageUuid = UUID.randomUUID() + val next = Path(path.path, storageUuid.toString()) next.createDirectory() - val newStorage = LocalStorage(uuid, next.pathString, ioDispatcher) + val newStorage = LocalStorage(storageUuid, next.pathString, ioDispatcher) newStorage.init() _storages.value = _storages.value.toMutableList().apply { add(newStorage) @@ -90,8 +95,9 @@ class LocalVault( override suspend fun remove(storage: IStorage) = withContext(ioDispatcher) { val path = path.value - if (path == null || !_isAvailable.value) + if (path == null || !_isAvailable.value) { throw Exception("Not available") + } val curStorages = _storages.value.toMutableList() val index = curStorages.indexOfFirst { it.uuid == storage.uuid } @@ -102,4 +108,39 @@ class LocalVault( File(localStorage.absolutePath).deleteRecursively() } } + + private companion object { + const val UUID_FILE_NAME = ".uuid" + + private val uuidLock = Any() + + private fun readOrCreateVaultUuid(idFile: File): UUID = synchronized(uuidLock) { + if (idFile.exists()) { + idFile.bufferedReader().use { reader -> + val line = reader.readLine()?.trim() + if (!line.isNullOrEmpty()) { + runCatching { UUID.fromString(line) }.getOrNull()?.let { return@synchronized it } + } + } + } + val generated = UUID.randomUUID() + val parent = idFile.parentFile ?: throw IllegalStateException("No parent for $idFile") + parent.mkdirs() + val tmp = File.createTempFile("vault-uuid-", ".tmp", parent) + try { + FileOutputStream(tmp).use { fos -> + fos.write(generated.toString().toByteArray(Charsets.UTF_8)) + fos.fd.sync() + } + if (!tmp.renameTo(idFile)) { + tmp.copyTo(idFile, overwrite = true) + } + } finally { + if (tmp.exists()) { + tmp.delete() + } + } + return@synchronized generated + } + } } diff --git a/infrastructure-android/src/main/java/com/github/nullptroma/wallenc/infrastructure/vaults/local/LocalVaultIdStore.kt b/infrastructure-android/src/main/java/com/github/nullptroma/wallenc/infrastructure/vaults/local/LocalVaultIdStore.kt deleted file mode 100644 index 0e13cf2..0000000 --- a/infrastructure-android/src/main/java/com/github/nullptroma/wallenc/infrastructure/vaults/local/LocalVaultIdStore.kt +++ /dev/null @@ -1,30 +0,0 @@ -package com.github.nullptroma.wallenc.infrastructure.vaults.local - -import android.content.Context -import java.util.UUID - -/** - * Хранит/восстанавливает идентификатор единственного локального vault'а - * в [android.content.SharedPreferences]. При первом обращении генерирует новый UUID - * и записывает его синхронно (`commit`), чтобы к моменту, когда другие подсистемы - * (DB, шифр-ключи) начнут связывать с ним записи, значение уже было персистентно. - */ -class LocalVaultIdStore(context: Context) { - - private val prefs = context.applicationContext.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) - - /** Возвращает существующий идентификатор или, если его нет, генерирует и сохраняет новый. */ - fun getOrCreate(): UUID { - prefs.getString(KEY_LOCAL_VAULT_UUID, null)?.let { stored -> - runCatching { UUID.fromString(stored) }.getOrNull()?.let { return it } - } - val generated = UUID.randomUUID() - prefs.edit().putString(KEY_LOCAL_VAULT_UUID, generated.toString()).commit() - return generated - } - - private companion object { - const val PREFS_NAME = "wallenc.vaults" - const val KEY_LOCAL_VAULT_UUID = "local_vault_uuid" - } -}