Перенос localVault в domain-storage

This commit is contained in:
2026-05-11 20:54:15 +03:00
parent 3928ac5409
commit 9ceb8bd934
3 changed files with 58 additions and 49 deletions

View File

@@ -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.task.runtime.TaskOrchestrator
import com.github.nullptroma.wallenc.infrastructure.vaults.VaultsManager 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.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.IUnlockManager
import com.github.nullptroma.wallenc.domain.interfaces.IVault import com.github.nullptroma.wallenc.domain.interfaces.IVault
import com.github.nullptroma.wallenc.domain.interfaces.IVaultsManager import com.github.nullptroma.wallenc.domain.interfaces.IVaultsManager
@@ -92,8 +91,7 @@ class SingletonModule {
@ApplicationContext context: Context, @ApplicationContext context: Context,
): IVault = LocalVault( ): IVault = LocalVault(
ioDispatcher = ioDispatcher, ioDispatcher = ioDispatcher,
context = context, vaultRoot = context.getExternalFilesDir("LocalVault"),
idStore = LocalVaultIdStore(context),
) )
@Provides @Provides

View File

@@ -1,9 +1,8 @@
package com.github.nullptroma.wallenc.infrastructure.vaults.local 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.datatypes.StorageEncryptionInfo
import com.github.nullptroma.wallenc.domain.interfaces.IStorage 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.DescribedVault
import com.github.nullptroma.wallenc.vault.contract.VaultDescriptor import com.github.nullptroma.wallenc.vault.contract.VaultDescriptor
import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineDispatcher
@@ -13,6 +12,7 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.io.File import java.io.File
import java.io.FileOutputStream
import java.util.UUID import java.util.UUID
import kotlin.io.path.Path import kotlin.io.path.Path
import kotlin.io.path.createDirectory import kotlin.io.path.createDirectory
@@ -20,11 +20,13 @@ import kotlin.io.path.pathString
class LocalVault( class LocalVault(
private val ioDispatcher: CoroutineDispatcher, private val ioDispatcher: CoroutineDispatcher,
context: Context, private val vaultRoot: File?,
idStore: LocalVaultIdStore,
) : DescribedVault { ) : 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) override val descriptor: VaultDescriptor = VaultDescriptor.LocalDevice(uuid)
@@ -40,39 +42,42 @@ class LocalVault(
private val _availableSpace = MutableStateFlow<Long?>(null) private val _availableSpace = MutableStateFlow<Long?>(null)
override val availableSpace: StateFlow<Long?> = _availableSpace override val availableSpace: StateFlow<Long?> = _availableSpace
private val path = MutableStateFlow<File?>(null) private val path = MutableStateFlow<File?>(vaultRoot)
init { init {
CoroutineScope(ioDispatcher).launch { CoroutineScope(ioDispatcher).launch {
path.value = context.getExternalFilesDir("LocalVault")
_isAvailable.value = path.value != null _isAvailable.value = path.value != null
readStorages() if (path.value != null) {
readStorages()
}
} }
} }
private suspend fun readStorages() { private suspend fun readStorages() {
val path = path.value val path = path.value
if (path == null || !_isAvailable.value) if (path == null || !_isAvailable.value) {
throw Exception("Not available") throw Exception("Not available")
}
val dirs = path.listFiles()?.filter { it.isDirectory } val dirs = path.listFiles()?.filter { it.isDirectory }
if (dirs != null) { if (dirs != null) {
_storages.value = dirs.map { _storages.value = dirs.map {
val uuid = UUID.fromString(it.name) val storageUuid = UUID.fromString(it.name)
LocalStorage(uuid, it.path, ioDispatcher).apply { init() } LocalStorage(storageUuid, it.path, ioDispatcher).apply { init() }
} }
} }
} }
override suspend fun createStorage(): LocalStorage = withContext(ioDispatcher) { override suspend fun createStorage(): LocalStorage = withContext(ioDispatcher) {
val path = path.value val path = path.value
if (path == null || !_isAvailable.value) if (path == null || !_isAvailable.value) {
throw Exception("Not available") throw Exception("Not available")
}
val uuid = UUID.randomUUID() val storageUuid = UUID.randomUUID()
val next = Path(path.path, uuid.toString()) val next = Path(path.path, storageUuid.toString())
next.createDirectory() next.createDirectory()
val newStorage = LocalStorage(uuid, next.pathString, ioDispatcher) val newStorage = LocalStorage(storageUuid, next.pathString, ioDispatcher)
newStorage.init() newStorage.init()
_storages.value = _storages.value.toMutableList().apply { _storages.value = _storages.value.toMutableList().apply {
add(newStorage) add(newStorage)
@@ -90,8 +95,9 @@ class LocalVault(
override suspend fun remove(storage: IStorage) = withContext(ioDispatcher) { override suspend fun remove(storage: IStorage) = withContext(ioDispatcher) {
val path = path.value val path = path.value
if (path == null || !_isAvailable.value) if (path == null || !_isAvailable.value) {
throw Exception("Not available") throw Exception("Not available")
}
val curStorages = _storages.value.toMutableList() val curStorages = _storages.value.toMutableList()
val index = curStorages.indexOfFirst { it.uuid == storage.uuid } val index = curStorages.indexOfFirst { it.uuid == storage.uuid }
@@ -102,4 +108,39 @@ class LocalVault(
File(localStorage.absolutePath).deleteRecursively() 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
}
}
} }

View File

@@ -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"
}
}