diff --git a/domain-vault/src/main/java/com/github/nullptroma/wallenc/domain/vault/storages/UnlockManager.kt b/domain-vault/src/main/java/com/github/nullptroma/wallenc/domain/vault/storages/UnlockManager.kt index cee2386..d04a1a6 100644 --- a/domain-vault/src/main/java/com/github/nullptroma/wallenc/domain/vault/storages/UnlockManager.kt +++ b/domain-vault/src/main/java/com/github/nullptroma/wallenc/domain/vault/storages/UnlockManager.kt @@ -58,11 +58,17 @@ class UnlockManager( allStorages.removeAt(allStorages.size - 1) allStorages.add(encStorage) } - catch (_: Exception) { - // ключ не подошёл + catch (e: WallencException.Storage.IncorrectKey) { keysToRemove.add(key) allStorages.removeAt(allStorages.size - 1) } + catch (_: WallencException.Storage.EncInfoMissing) { + keysToRemove.add(key) + allStorages.removeAt(allStorages.size - 1) + } + catch (_: Exception) { + allStorages.removeAt(allStorages.size - 1) + } } keymapRepository.delete(*keysToRemove.toTypedArray()) // удалить мёртвые ключи _openedStorages.value = map.toMap() @@ -104,6 +110,21 @@ class UnlockManager( return UUID(bb.long, bb.long) } + override suspend fun rememberKey(storage: IStorage, key: EncryptKey) = withContext(ioDispatcher) { + mutex.withLock { + val encInfo = storage.metaInfo.value.encInfo ?: throw WallencException.Storage.EncInfoMissing() + if (!Encryptor.checkKey(key, encInfo)) { + throw WallencException.Storage.IncorrectKey() + } + keymapRepository.add( + StorageKeyMap( + sourceUuid = storage.uuid, + key = key, + ), + ) + } + } + override suspend fun open( storage: IStorage, key: EncryptKey, diff --git a/domain-vault/src/main/java/com/github/nullptroma/wallenc/domain/vault/storages/common/BaseStorage.kt b/domain-vault/src/main/java/com/github/nullptroma/wallenc/domain/vault/storages/common/BaseStorage.kt index daa956e..047f716 100644 --- a/domain-vault/src/main/java/com/github/nullptroma/wallenc/domain/vault/storages/common/BaseStorage.kt +++ b/domain-vault/src/main/java/com/github/nullptroma/wallenc/domain/vault/storages/common/BaseStorage.kt @@ -3,6 +3,7 @@ package com.github.nullptroma.wallenc.domain.vault.storages.common import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import com.github.nullptroma.wallenc.domain.common.impl.CommonStorageMetaInfo import com.github.nullptroma.wallenc.domain.datatypes.StorageEncryptionInfo +import com.github.nullptroma.wallenc.domain.datatypes.StorageMetaLoadState import com.github.nullptroma.wallenc.domain.errors.WallencException import com.github.nullptroma.wallenc.domain.interfaces.IStorage import com.github.nullptroma.wallenc.domain.interfaces.IStorageAccessor @@ -39,6 +40,10 @@ abstract class BaseStorage( final override val metaInfo: StateFlow get() = _metaInfo + private val _metaLoadState = MutableStateFlow(StorageMetaLoadState.Loading) + final override val metaLoadState: StateFlow + get() = _metaLoadState + final override val size: StateFlow get() = accessor.size @@ -71,32 +76,39 @@ abstract class BaseStorage( } private suspend fun readMetaInfo() = withContext(ioDispatcher) { - val meta = try { - accessor.openReadSystemFile(metaInfoFileName).use { input -> - val bytes = input.readBytes() - when { - bytes.isEmpty() -> { - val default = CommonStorageMetaInfo() - updateMetaInfo(default) - default - } - else -> try { - jackson.readValue(bytes, CommonStorageMetaInfo::class.java) - } catch (_: Exception) { - // Битый JSON — не перезаписываем файл на диске - CommonStorageMetaInfo() - } + val (meta, state) = loadMetaFromDisk() + _metaInfo.value = meta + _metaLoadState.value = state + } + + private suspend fun loadMetaFromDisk(): Pair { + return try { + val bytes = accessor.openReadSystemFile(metaInfoFileName).use { it.readBytes() } + when { + bytes.isEmpty() -> { + val default = CommonStorageMetaInfo() + updateMetaInfo(default) + default to StorageMetaLoadState.Ready + } + else -> try { + jackson.readValue(bytes, CommonStorageMetaInfo::class.java) to StorageMetaLoadState.Ready + } catch (_: Exception) { + CommonStorageMetaInfo() to StorageMetaLoadState.Unavailable } } } catch (_: WallencException.Storage.FileNotFound) { val default = CommonStorageMetaInfo() updateMetaInfo(default) - default + default to StorageMetaLoadState.Ready } catch (_: Exception) { - // Сеть/IO — оставляем дефолт в памяти, существующий файл не трогаем - CommonStorageMetaInfo() + CommonStorageMetaInfo() to StorageMetaLoadState.Unavailable + } + } + + private suspend fun requireMetaReady() { + if (_metaLoadState.value != StorageMetaLoadState.Ready) { + throw WallencException.Storage.NotAvailable() } - _metaInfo.value = meta } private suspend fun updateMetaInfo(meta: IStorageMetaInfo) = withContext(ioDispatcher) { @@ -108,6 +120,7 @@ abstract class BaseStorage( } final override suspend fun rename(newName: String) = withContext(ioDispatcher) { + requireMetaReady() val cur = metaInfo.value updateMetaInfo( CommonStorageMetaInfo( @@ -118,6 +131,7 @@ abstract class BaseStorage( } final override suspend fun setEncInfo(encInfo: StorageEncryptionInfo?) = withContext(ioDispatcher) { + requireMetaReady() val cur = metaInfo.value updateMetaInfo( CommonStorageMetaInfo( diff --git a/domain-vault/src/main/java/com/github/nullptroma/wallenc/domain/vault/storages/encrypt/EncryptedStorageAccessor.kt b/domain-vault/src/main/java/com/github/nullptroma/wallenc/domain/vault/storages/encrypt/EncryptedStorageAccessor.kt index 1db31c8..a887cc6 100644 --- a/domain-vault/src/main/java/com/github/nullptroma/wallenc/domain/vault/storages/encrypt/EncryptedStorageAccessor.kt +++ b/domain-vault/src/main/java/com/github/nullptroma/wallenc/domain/vault/storages/encrypt/EncryptedStorageAccessor.kt @@ -1,5 +1,7 @@ package com.github.nullptroma.wallenc.domain.vault.storages.encrypt +import com.github.nullptroma.wallenc.domain.errors.WallencException +import com.github.nullptroma.wallenc.domain.vault.storages.common.readSystemFileBytesOrEmpty import com.github.nullptroma.wallenc.domain.vault.utils.CloseHandledStreamExtension.Companion.onClosed import com.github.nullptroma.wallenc.domain.vault.utils.CloseHandledStreamExtension.Companion.onClosing import com.github.nullptroma.wallenc.domain.common.impl.CommonDirectory @@ -28,6 +30,7 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock +import java.io.ByteArrayInputStream import java.io.InputStream import java.io.OutputStream import java.time.Instant @@ -272,7 +275,11 @@ class EncryptedStorageAccessor( override suspend fun openReadSystemFile(name: String): InputStream = scope.run { val path = Path(systemHiddenDirName, name).pathString - openRead(path) + try { + openRead(path) + } catch (_: WallencException.Storage.FileNotFound) { + ByteArrayInputStream(ByteArray(0)) + } } override suspend fun openWriteSystemFile(name: String): OutputStream = scope.run { @@ -284,7 +291,7 @@ class EncryptedStorageAccessor( } override suspend fun readSyncJournal(): List { - val bytes = openReadSystemFile(SYNC_JOURNAL_FILENAME).use { it.readBytes() } + val bytes = readSystemFileBytesOrEmpty { openReadSystemFile(SYNC_JOURNAL_FILENAME) } if (bytes.isEmpty()) { return emptyList() } @@ -316,7 +323,7 @@ class EncryptedStorageAccessor( } override suspend fun readSyncLock(): StorageSyncLock? { - val bytes = openReadSystemFile(SYNC_LOCK_FILENAME).use { it.readBytes() } + val bytes = readSystemFileBytesOrEmpty { openReadSystemFile(SYNC_LOCK_FILENAME) } if (bytes.isEmpty()) { return null } diff --git a/domain-vault/src/main/java/com/github/nullptroma/wallenc/domain/vault/vaults/yandex/YandexVault.kt b/domain-vault/src/main/java/com/github/nullptroma/wallenc/domain/vault/vaults/yandex/YandexVault.kt index 2527477..dfd5be9 100644 --- a/domain-vault/src/main/java/com/github/nullptroma/wallenc/domain/vault/vaults/yandex/YandexVault.kt +++ b/domain-vault/src/main/java/com/github/nullptroma/wallenc/domain/vault/vaults/yandex/YandexVault.kt @@ -4,6 +4,7 @@ import com.github.nullptroma.wallenc.domain.vault.network.yandexdisk.YandexDiskA import com.github.nullptroma.wallenc.domain.vault.network.yandexdisk.repository.YandexDiskRepository import com.github.nullptroma.wallenc.domain.vault.storages.yandex.YandexStorage import com.github.nullptroma.wallenc.domain.datatypes.StorageEncryptionInfo +import com.github.nullptroma.wallenc.domain.datatypes.StorageMetaLoadState import com.github.nullptroma.wallenc.domain.interfaces.IStorage import com.github.nullptroma.wallenc.vault.contract.CloudBrand import com.github.nullptroma.wallenc.vault.contract.DescribedVault @@ -132,7 +133,10 @@ class YandexVault( if (attempt > 0) { delay(STORAGE_INIT_RETRY_DELAY_MS * attempt) } - if (runCatching { storage.init() }.isSuccess) { + if ( + runCatching { storage.init() }.isSuccess && + storage.metaLoadState.value == StorageMetaLoadState.Ready + ) { return storage } } diff --git a/domain/src/main/java/com/github/nullptroma/wallenc/domain/datatypes/StorageMetaLoadState.kt b/domain/src/main/java/com/github/nullptroma/wallenc/domain/datatypes/StorageMetaLoadState.kt new file mode 100644 index 0000000..d0386d3 --- /dev/null +++ b/domain/src/main/java/com/github/nullptroma/wallenc/domain/datatypes/StorageMetaLoadState.kt @@ -0,0 +1,7 @@ +package com.github.nullptroma.wallenc.domain.datatypes + +enum class StorageMetaLoadState { + Loading, + Ready, + Unavailable, +} diff --git a/domain/src/main/java/com/github/nullptroma/wallenc/domain/interfaces/IStorage.kt b/domain/src/main/java/com/github/nullptroma/wallenc/domain/interfaces/IStorage.kt index dfe8f98..8c4d8d4 100644 --- a/domain/src/main/java/com/github/nullptroma/wallenc/domain/interfaces/IStorage.kt +++ b/domain/src/main/java/com/github/nullptroma/wallenc/domain/interfaces/IStorage.kt @@ -1,6 +1,7 @@ package com.github.nullptroma.wallenc.domain.interfaces import com.github.nullptroma.wallenc.domain.datatypes.StorageEncryptionInfo +import com.github.nullptroma.wallenc.domain.datatypes.StorageMetaLoadState import com.github.nullptroma.wallenc.domain.tasks.TaskProgress import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.StateFlow @@ -14,6 +15,7 @@ sealed interface IStorageInfo { val numberOfFiles: StateFlow val isEmpty: Flow val metaInfo: StateFlow + val metaLoadState: StateFlow val isVirtualStorage: Boolean } diff --git a/domain/src/main/java/com/github/nullptroma/wallenc/domain/interfaces/IUnlockManager.kt b/domain/src/main/java/com/github/nullptroma/wallenc/domain/interfaces/IUnlockManager.kt index 5972d77..e4f2be8 100644 --- a/domain/src/main/java/com/github/nullptroma/wallenc/domain/interfaces/IUnlockManager.kt +++ b/domain/src/main/java/com/github/nullptroma/wallenc/domain/interfaces/IUnlockManager.kt @@ -16,6 +16,8 @@ interface IUnlockManager { fun getOpenedStorageKey(uuid: UUID): EncryptKey? suspend fun open(storage: IStorage, key: EncryptKey, rememberPassword: Boolean = true): IStorage + /** Сохранить ключ для auto-open без открытия виртуального storage. */ + suspend fun rememberKey(storage: IStorage, key: EncryptKey) suspend fun close(storage: IStorage) suspend fun close(uuid: UUID) } \ No newline at end of file diff --git a/usecases/src/main/java/com/github/nullptroma/wallenc/usecases/ManageStoragesEncryptionUseCase.kt b/usecases/src/main/java/com/github/nullptroma/wallenc/usecases/ManageStoragesEncryptionUseCase.kt index 4b93d07..8161d8b 100644 --- a/usecases/src/main/java/com/github/nullptroma/wallenc/usecases/ManageStoragesEncryptionUseCase.kt +++ b/usecases/src/main/java/com/github/nullptroma/wallenc/usecases/ManageStoragesEncryptionUseCase.kt @@ -46,6 +46,14 @@ class ManageStoragesEncryptionUseCase @Inject constructor( } } + suspend fun rememberStorageKey(storage: IStorageInfo, key: EncryptKey) { + if (storage is IStorage) { + unlockManager.rememberKey(storage, key) + return + } + throw IllegalStateException("Unsupported storage type") + } + suspend fun openStorage(storage: IStorageInfo, key: EncryptKey, rememberPassword: Boolean): IStorageInfo { if (storage is IStorage) return unlockManager.open(storage, key, rememberPassword) throw IllegalStateException("Unsupported storage type") diff --git a/usecases/src/test/java/com/github/nullptroma/wallenc/usecases/fakes/FakeStorage.kt b/usecases/src/test/java/com/github/nullptroma/wallenc/usecases/fakes/FakeStorage.kt index 2b5c3a0..9a3442b 100644 --- a/usecases/src/test/java/com/github/nullptroma/wallenc/usecases/fakes/FakeStorage.kt +++ b/usecases/src/test/java/com/github/nullptroma/wallenc/usecases/fakes/FakeStorage.kt @@ -1,6 +1,7 @@ package com.github.nullptroma.wallenc.usecases.fakes import com.github.nullptroma.wallenc.domain.datatypes.StorageEncryptionInfo +import com.github.nullptroma.wallenc.domain.datatypes.StorageMetaLoadState import com.github.nullptroma.wallenc.domain.datatypes.StorageSyncJournalEntry import com.github.nullptroma.wallenc.domain.interfaces.IStorage import com.github.nullptroma.wallenc.domain.interfaces.IStorageAccessor @@ -23,6 +24,8 @@ class FakeStorage( override val numberOfFiles: StateFlow = MutableStateFlow(0) override val isEmpty: Flow = flowOf(true) override val metaInfo: StateFlow = MutableStateFlow(meta) + override val metaLoadState: StateFlow = + MutableStateFlow(StorageMetaLoadState.Ready) override val isVirtualStorage: Boolean = false override val accessor: IStorageAccessor = accessorImpl diff --git a/usecases/src/test/java/com/github/nullptroma/wallenc/usecases/fakes/FakeVaultsManager.kt b/usecases/src/test/java/com/github/nullptroma/wallenc/usecases/fakes/FakeVaultsManager.kt index e98a72a..7f954f1 100644 --- a/usecases/src/test/java/com/github/nullptroma/wallenc/usecases/fakes/FakeVaultsManager.kt +++ b/usecases/src/test/java/com/github/nullptroma/wallenc/usecases/fakes/FakeVaultsManager.kt @@ -24,6 +24,8 @@ class FakeUnlockManager : IUnlockManager { override suspend fun open(storage: IStorage, key: EncryptKey, rememberPassword: Boolean): IStorage = storage + override suspend fun rememberKey(storage: IStorage, key: EncryptKey) = Unit + override suspend fun close(storage: IStorage) = Unit override suspend fun close(uuid: UUID) = Unit