diff --git a/data/src/main/java/com/github/nullptroma/wallenc/data/storages/common/BaseStorage.kt b/data/src/main/java/com/github/nullptroma/wallenc/data/storages/common/BaseStorage.kt new file mode 100644 index 0000000..3af8417 --- /dev/null +++ b/data/src/main/java/com/github/nullptroma/wallenc/data/storages/common/BaseStorage.kt @@ -0,0 +1,149 @@ +package com.github.nullptroma.wallenc.data.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.interfaces.IStorage +import com.github.nullptroma.wallenc.domain.interfaces.IStorageAccessor +import com.github.nullptroma.wallenc.domain.interfaces.IStorageMetaInfo +import com.github.nullptroma.wallenc.domain.tasks.TaskProgress +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.ensureActive +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.withContext +import java.io.InputStream +import java.util.UUID + +/** + * Общий «скелет» storage'а: единая логика meta-info, rename, setEncInfo, + * clearAllContent и делегирования размера/доступности к [accessor]. + * + * Подклассы определяют только как создаётся [accessor], значение + * [isVirtualStorage] и (при необходимости) расширяют [init] своими шагами + * (например, проверкой ключа или инициализацией внешней связи). + */ +abstract class BaseStorage( + override val uuid: UUID, + private val ioDispatcher: CoroutineDispatcher, + metaInfoFilePostfix: String, +) : IStorage { + + protected val metaInfoFileName: String = "${metaInfoUuidPart()}$metaInfoFilePostfix" + + private val _metaInfo = MutableStateFlow(CommonStorageMetaInfo()) + final override val metaInfo: StateFlow + get() = _metaInfo + + final override val size: StateFlow + get() = accessor.size + + final override val numberOfFiles: StateFlow + get() = accessor.numberOfFiles + + final override val isAvailable: StateFlow + get() = accessor.isAvailable + + final override val isEmpty: Flow + get() = accessor.numberOfFiles.map { n -> n?.let { it == 0 } } + + abstract override val accessor: IStorageAccessor + + /** + * Базовая реализация [IStorageAccessor] передаёт UUID полностью; подклассы + * могут переопределить, чтобы сохранить совместимость с уже существующими + * именами файлов (например, [com.github.nullptroma.wallenc.data.storages.encrypt.EncryptedStorage] + * раньше использовал первые 8 символов). + */ + protected open fun metaInfoUuidPart(): String = uuid.toString() + + /** + * Запускается единожды при старте storage'а. Подклассы могут переопределить, + * добавив свои шаги (init accessor'а, проверка ключа и т.п.). Обязательно + * должен в какой-то момент вызвать [readMetaInfo]. + */ + open suspend fun init() { + readMetaInfo() + } + + private suspend fun readMetaInfo() = withContext(ioDispatcher) { + var meta: CommonStorageMetaInfo + var reader: InputStream? = null + try { + reader = accessor.openReadSystemFile(metaInfoFileName) + meta = jackson.readValue(reader, CommonStorageMetaInfo::class.java) + } catch (_: Exception) { + // чтение не удалось — пишем дефолт, чтобы файл появился + meta = CommonStorageMetaInfo() + updateMetaInfo(meta) + } finally { + reader?.close() + } + _metaInfo.value = meta + } + + private suspend fun updateMetaInfo(meta: IStorageMetaInfo) = withContext(ioDispatcher) { + val writer = accessor.openWriteSystemFile(metaInfoFileName) + try { + jackson.writeValue(writer, meta) + } finally { + writer.close() + } + _metaInfo.value = meta + } + + final override suspend fun rename(newName: String) = withContext(ioDispatcher) { + val cur = metaInfo.value + updateMetaInfo( + CommonStorageMetaInfo( + encInfo = cur.encInfo, + name = newName, + ), + ) + } + + final override suspend fun setEncInfo(encInfo: StorageEncryptionInfo?) = withContext(ioDispatcher) { + val cur = metaInfo.value + updateMetaInfo( + CommonStorageMetaInfo( + encInfo = encInfo, + name = cur.name, + ), + ) + } + + final override suspend fun clearAllContent(onProgress: suspend (TaskProgress) -> Unit) = withContext(ioDispatcher) { + val files = accessor.getAllFiles() + val dirs = accessor.getAllDirs() + val paths = buildList { + addAll(files.map { it.metaInfo.path }) + addAll(dirs.map { it.metaInfo.path }) + } + .filter { it != "/" && it.isNotBlank() } + .sortedByDescending { it.length } + val total = paths.size + if (total == 0) { + onProgress(TaskProgress(1f, null)) + return@withContext + } + paths.forEachIndexed { index, path -> + accessor.delete(path) + if (index % PROGRESS_REPORT_INTERVAL == 0 || index == paths.lastIndex) { + onProgress( + TaskProgress( + fraction = (index + 1).toFloat() / total, + label = null, + ), + ) + coroutineContext.ensureActive() + } + } + } + + companion object { + private const val PROGRESS_REPORT_INTERVAL = 16 + private val jackson = jacksonObjectMapper().apply { findAndRegisterModules() } + } +} diff --git a/data/src/main/java/com/github/nullptroma/wallenc/data/storages/encrypt/EncryptedStorage.kt b/data/src/main/java/com/github/nullptroma/wallenc/data/storages/encrypt/EncryptedStorage.kt index 12afe8e..a005ac9 100644 --- a/data/src/main/java/com/github/nullptroma/wallenc/data/storages/encrypt/EncryptedStorage.kt +++ b/data/src/main/java/com/github/nullptroma/wallenc/data/storages/encrypt/EncryptedStorage.kt @@ -1,66 +1,51 @@ package com.github.nullptroma.wallenc.data.storages.encrypt -import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper -import com.github.nullptroma.wallenc.domain.common.impl.CommonStorageMetaInfo +import com.github.nullptroma.wallenc.data.storages.common.BaseStorage import com.github.nullptroma.wallenc.domain.datatypes.EncryptKey -import com.github.nullptroma.wallenc.domain.datatypes.StorageEncryptionInfo import com.github.nullptroma.wallenc.domain.encrypt.Encryptor import com.github.nullptroma.wallenc.domain.interfaces.IStorage -import com.github.nullptroma.wallenc.domain.interfaces.IStorageMetaInfo -import com.github.nullptroma.wallenc.domain.tasks.TaskProgress +import com.github.nullptroma.wallenc.domain.interfaces.IStorageAccessor import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.DisposableHandle import kotlinx.coroutines.Job -import kotlinx.coroutines.ensureActive -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.map import kotlinx.coroutines.withContext -import java.io.InputStream import java.util.UUID class EncryptedStorage private constructor( private val source: IStorage, private val key: EncryptKey, ioDispatcher: CoroutineDispatcher, - override val uuid: UUID = UUID.randomUUID() -) : IStorage, DisposableHandle { + uuid: UUID = UUID.randomUUID() +) : BaseStorage( + uuid = uuid, + ioDispatcher = ioDispatcher, + metaInfoFilePostfix = STORAGE_INFO_FILE_POSTFIX, +), DisposableHandle { + private val job = Job() private val scope = CoroutineScope(ioDispatcher + job) + private val encInfo = source.metaInfo.value.encInfo ?: throw Exception("Storage is not encrypted") // TODO - private val metaInfoFileName: String = "${uuid.toString().take(8)}$STORAGE_INFO_FILE_POSTFIX" - override val size: StateFlow - get() = accessor.size - override val numberOfFiles: StateFlow - get() = accessor.numberOfFiles - override val isEmpty: Flow - get() = accessor.numberOfFiles.map { n -> n?.let { it == 0 } } - - private val _metaInfo = MutableStateFlow( - CommonStorageMetaInfo() - ) - override val metaInfo: StateFlow - get() = _metaInfo override val isVirtualStorage: Boolean = true - override val isAvailable: StateFlow - get() = source.isAvailable - override val accessor: EncryptedStorageAccessor = + private val _accessor: EncryptedStorageAccessor = EncryptedStorageAccessor( source = source.accessor, pathIv = encInfo.pathIv, key = key, systemHiddenDirName = "${uuid.toString().take(8)}$SYSTEM_HIDDEN_DIRNAME_POSTFIX", - scope = scope + scope = scope, ) + override val accessor: IStorageAccessor = _accessor - private suspend fun init() { + override fun metaInfoUuidPart(): String = uuid.toString().take(8) + + override suspend fun init() { checkKey() - readMetaInfo() + super.init() } private fun checkKey() { @@ -68,89 +53,12 @@ class EncryptedStorage private constructor( throw Exception("Incorrect key") // TODO } - private suspend fun readMetaInfo() = scope.run { - var meta: CommonStorageMetaInfo - var reader: InputStream? = null - try { - reader = accessor.openReadSystemFile(metaInfoFileName) - meta = jackson.readValue(reader, CommonStorageMetaInfo::class.java) - } catch (e: Exception) { - // чтение не удалось, значит нужно записать файл - meta = CommonStorageMetaInfo() - updateMetaInfo(meta) - } finally { - reader?.close() - } - _metaInfo.value = meta - } - - private suspend fun updateMetaInfo(meta: IStorageMetaInfo) = scope.run { - val writer = accessor.openWriteSystemFile(metaInfoFileName) - try { - jackson.writeValue(writer, meta) - } catch (e: Exception) { - throw e - } finally { - writer.close() - } - _metaInfo.value = meta - } - - override suspend fun rename(newName: String) = scope.run { - val curMeta = metaInfo.value - updateMetaInfo( - CommonStorageMetaInfo( - encInfo = curMeta.encInfo, - name = newName - ) - ) - } - - override suspend fun setEncInfo(encInfo: StorageEncryptionInfo?) = scope.run { - val curMeta = metaInfo.value - updateMetaInfo( - CommonStorageMetaInfo( - encInfo = encInfo, - name = curMeta.name - ) - ) - } - - override suspend fun clearAllContent(onProgress: suspend (TaskProgress) -> Unit) = scope.run { - val files = accessor.getAllFiles() - val dirs = accessor.getAllDirs() - val paths = buildList { - addAll(files.map { it.metaInfo.path }) - addAll(dirs.map { it.metaInfo.path }) - } - .filter { it != "/" && it.isNotBlank() } - .sortedByDescending { it.length } - val total = paths.size - if (total == 0) { - onProgress(TaskProgress(1f, null)) - return@run - } - paths.forEachIndexed { index, path -> - accessor.delete(path) - if (index % PROGRESS_REPORT_INTERVAL == 0 || index == paths.lastIndex) { - onProgress( - TaskProgress( - fraction = (index + 1).toFloat() / total, - label = null, - ), - ) - coroutineContext.ensureActive() - } - } - } - override fun dispose() { - accessor.dispose() + _accessor.dispose() job.cancel() } companion object { - private const val PROGRESS_REPORT_INTERVAL = 16 suspend fun create( source: IStorage, key: EncryptKey, @@ -174,6 +82,5 @@ class EncryptedStorage private constructor( private const val SYSTEM_HIDDEN_DIRNAME_POSTFIX = "-enc-dir" const val STORAGE_INFO_FILE_POSTFIX = ".enc-meta" - private val jackson = jacksonObjectMapper().apply { findAndRegisterModules() } } -} \ No newline at end of file +} diff --git a/data/src/main/java/com/github/nullptroma/wallenc/data/storages/encrypt/EncryptedStorageAccessor.kt b/data/src/main/java/com/github/nullptroma/wallenc/data/storages/encrypt/EncryptedStorageAccessor.kt index 8a1b57c..6864503 100644 --- a/data/src/main/java/com/github/nullptroma/wallenc/data/storages/encrypt/EncryptedStorageAccessor.kt +++ b/data/src/main/java/com/github/nullptroma/wallenc/data/storages/encrypt/EncryptedStorageAccessor.kt @@ -4,7 +4,7 @@ import com.github.nullptroma.wallenc.data.utils.CloseHandledStreamExtension.Comp import com.github.nullptroma.wallenc.domain.common.impl.CommonDirectory import com.github.nullptroma.wallenc.domain.common.impl.CommonFile import com.github.nullptroma.wallenc.domain.common.impl.CommonMetaInfo -import com.github.nullptroma.wallenc.domain.datatypes.DataPackage +import com.github.nullptroma.wallenc.domain.datatypes.DataPage import com.github.nullptroma.wallenc.domain.datatypes.EncryptKey import com.github.nullptroma.wallenc.domain.encrypt.Encryptor import com.github.nullptroma.wallenc.domain.encrypt.EncryptorWithStaticIv @@ -41,11 +41,11 @@ class EncryptedStorageAccessor( override val isAvailable: StateFlow = source.isAvailable - private val _filesUpdates = MutableSharedFlow>>() - override val filesUpdates: SharedFlow>> = _filesUpdates + private val _filesUpdates = MutableSharedFlow>() + override val filesUpdates: SharedFlow> = _filesUpdates - private val _dirsUpdates = MutableSharedFlow>>() - override val dirsUpdates: SharedFlow>> = _dirsUpdates + private val _dirsUpdates = MutableSharedFlow>() + override val dirsUpdates: SharedFlow> = _dirsUpdates private val dataEncryptor = Encryptor(key.toAesKey()) private val pathEncryptor: EncryptorWithStaticIv? = if(pathIv != null) EncryptorWithStaticIv(key.toAesKey(), pathIv) else null @@ -58,26 +58,32 @@ class EncryptedStorageAccessor( private fun collectSourceState() { scope.launch { launch { - source.filesUpdates.collect { - val files = it.data.map(::decryptEntity).filterSystemHiddenFiles() + source.filesUpdates.collect { page -> + val files = page.data.map(::decryptEntity).filterSystemHiddenFiles() _filesUpdates.emit( - DataPackage( - data = files, - isLoading = it.isLoading, - isError = it.isError + DataPage( + list = files, + isLoading = page.isLoading, + isError = page.isError, + hasNext = page.hasNext, + pageLength = page.pageLength, + pageIndex = page.pageIndex, ) ) } } launch { - source.dirsUpdates.collect { - val dirs = it.data.map(::decryptEntity).filterSystemHiddenDirs() + source.dirsUpdates.collect { page -> + val dirs = page.data.map(::decryptEntity).filterSystemHiddenDirs() _dirsUpdates.emit( - DataPackage( - data = dirs, - isLoading = it.isLoading, - isError = it.isError + DataPage( + list = dirs, + isLoading = page.isLoading, + isError = page.isError, + hasNext = page.hasNext, + pageLength = page.pageLength, + pageIndex = page.pageIndex, ) ) } @@ -178,12 +184,15 @@ class EncryptedStorageAccessor( return source.getFiles(encryptPath(path)).map(::decryptEntity).filterSystemHiddenFiles() } - override fun getFilesFlow(path: String): Flow>> { - val flow = source.getFilesFlow(encryptPath(path)).map { - DataPackage( - data = it.data.map(::decryptEntity).filterSystemHiddenFiles(), - isLoading = it.isLoading, - isError = it.isError + override fun getFilesFlow(path: String): Flow> { + val flow = source.getFilesFlow(encryptPath(path)).map { page -> + DataPage( + list = page.data.map(::decryptEntity).filterSystemHiddenFiles(), + isLoading = page.isLoading, + isError = page.isError, + hasNext = page.hasNext, + pageLength = page.pageLength, + pageIndex = page.pageIndex, ) } return flow @@ -197,13 +206,16 @@ class EncryptedStorageAccessor( return source.getDirs(encryptPath(path)).map(::decryptEntity).filterSystemHiddenDirs() } - override fun getDirsFlow(path: String): Flow>> { - val flow = source.getDirsFlow(encryptPath(path)).map { - DataPackage( + override fun getDirsFlow(path: String): Flow> { + val flow = source.getDirsFlow(encryptPath(path)).map { page -> + DataPage( // включать все папки, кроме системной - data = it.data.map(::decryptEntity).filterSystemHiddenDirs(), - isLoading = it.isLoading, - isError = it.isError + list = page.data.map(::decryptEntity).filterSystemHiddenDirs(), + isLoading = page.isLoading, + isError = page.isError, + hasNext = page.hasNext, + pageLength = page.pageLength, + pageIndex = page.pageIndex, ) } return flow @@ -255,12 +267,12 @@ class EncryptedStorageAccessor( dataEncryptor.dispose() } - suspend fun openReadSystemFile(name: String): InputStream = scope.run { + override suspend fun openReadSystemFile(name: String): InputStream = scope.run { val path = Path(systemHiddenDirName, name).pathString return@run openRead(path) } - suspend fun openWriteSystemFile(name: String): OutputStream = scope.run { + override suspend fun openWriteSystemFile(name: String): OutputStream = scope.run { val path = Path(systemHiddenDirName, name).pathString systemHiddenFilesIsActual = false return@run openWrite(path).onClosing { diff --git a/data/src/main/java/com/github/nullptroma/wallenc/data/storages/local/LocalStorage.kt b/data/src/main/java/com/github/nullptroma/wallenc/data/storages/local/LocalStorage.kt index b44f1f4..05739d0 100644 --- a/data/src/main/java/com/github/nullptroma/wallenc/data/storages/local/LocalStorage.kt +++ b/data/src/main/java/com/github/nullptroma/wallenc/data/storages/local/LocalStorage.kt @@ -1,132 +1,29 @@ package com.github.nullptroma.wallenc.data.storages.local -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.interfaces.IStorage +import com.github.nullptroma.wallenc.data.storages.common.BaseStorage import com.github.nullptroma.wallenc.domain.interfaces.IStorageAccessor -import com.github.nullptroma.wallenc.domain.interfaces.IStorageMetaInfo -import com.github.nullptroma.wallenc.domain.tasks.TaskProgress import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.ensureActive -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.withContext -import java.io.InputStream import java.util.UUID - class LocalStorage( - override val uuid: UUID, + uuid: UUID, val absolutePath: String, - private val ioDispatcher: CoroutineDispatcher, -) : IStorage { - override val size: StateFlow - get() = accessor.size - override val numberOfFiles: StateFlow - get() = accessor.numberOfFiles - override val isEmpty: Flow - get() = accessor.numberOfFiles.map { n -> n?.let { it == 0 } } - - private val _metaInfo = MutableStateFlow( - CommonStorageMetaInfo() - ) - override val metaInfo: StateFlow - get() = _metaInfo - - override val isAvailable: StateFlow - get() = accessor.isAvailable + ioDispatcher: CoroutineDispatcher, +) : BaseStorage( + uuid = uuid, + ioDispatcher = ioDispatcher, + metaInfoFilePostfix = STORAGE_INFO_FILE_POSTFIX, +) { private val _accessor = LocalStorageAccessor(absolutePath, ioDispatcher) override val accessor: IStorageAccessor = _accessor override val isVirtualStorage: Boolean = false - private val metaInfoFileName: String = "$uuid$STORAGE_INFO_FILE_POSTFIX" - suspend fun init() { + override suspend fun init() { _accessor.init() - readMetaInfo() - } - - private suspend fun readMetaInfo() = withContext(ioDispatcher) { - var meta: CommonStorageMetaInfo - var reader: InputStream? = null - try { - reader = _accessor.openReadSystemFile(metaInfoFileName) - meta = jackson.readValue(reader, CommonStorageMetaInfo::class.java) - } - catch(e: Exception) { - // чтение не удалось, значит нужно записать файл - meta = CommonStorageMetaInfo() - updateMetaInfo(meta) - } - finally { - reader?.close() - } - _metaInfo.value = meta - } - - private suspend fun updateMetaInfo(meta: IStorageMetaInfo) = withContext(ioDispatcher) { - val writer = _accessor.openWriteSystemFile(metaInfoFileName) - try { - jackson.writeValue(writer, meta) - } - catch (e: Exception) { - throw e - } - finally { - writer.close() - } - _metaInfo.value = meta - } - - override suspend fun rename(newName: String) = withContext(ioDispatcher) { - val curMeta = metaInfo.value - updateMetaInfo(CommonStorageMetaInfo( - encInfo = curMeta.encInfo, - name = newName - )) - } - - override suspend fun setEncInfo(encInfo: StorageEncryptionInfo?) = withContext(ioDispatcher) { - val curMeta = metaInfo.value - updateMetaInfo(CommonStorageMetaInfo( - encInfo = encInfo, - name = curMeta.name - )) - } - - override suspend fun clearAllContent(onProgress: suspend (TaskProgress) -> Unit) = withContext(ioDispatcher) { - val files = accessor.getAllFiles() - val dirs = accessor.getAllDirs() - val paths = buildList { - addAll(files.map { it.metaInfo.path }) - addAll(dirs.map { it.metaInfo.path }) - } - .filter { it != "/" && it.isNotBlank() } - .sortedByDescending { it.length } - val total = paths.size - if (total == 0) { - onProgress(TaskProgress(1f, null)) - return@withContext - } - paths.forEachIndexed { index, path -> - accessor.delete(path) - if (index % PROGRESS_REPORT_INTERVAL == 0 || index == paths.lastIndex) { - onProgress( - TaskProgress( - fraction = (index + 1).toFloat() / total, - label = null, - ), - ) - coroutineContext.ensureActive() - } - } + super.init() } companion object { - private const val PROGRESS_REPORT_INTERVAL = 16 const val STORAGE_INFO_FILE_POSTFIX = ".storage-info" - private val jackson = jacksonObjectMapper().apply { findAndRegisterModules() } } -} \ No newline at end of file +} diff --git a/data/src/main/java/com/github/nullptroma/wallenc/data/storages/local/LocalStorageAccessor.kt b/data/src/main/java/com/github/nullptroma/wallenc/data/storages/local/LocalStorageAccessor.kt index 49637ec..9b7555d 100644 --- a/data/src/main/java/com/github/nullptroma/wallenc/data/storages/local/LocalStorageAccessor.kt +++ b/data/src/main/java/com/github/nullptroma/wallenc/data/storages/local/LocalStorageAccessor.kt @@ -7,7 +7,6 @@ import com.github.nullptroma.wallenc.data.utils.CloseHandledStreamExtension.Comp import com.github.nullptroma.wallenc.domain.common.impl.CommonDirectory import com.github.nullptroma.wallenc.domain.common.impl.CommonFile import com.github.nullptroma.wallenc.domain.common.impl.CommonMetaInfo -import com.github.nullptroma.wallenc.domain.datatypes.DataPackage import com.github.nullptroma.wallenc.domain.datatypes.DataPage import com.github.nullptroma.wallenc.domain.interfaces.IDirectory import com.github.nullptroma.wallenc.domain.interfaces.IFile @@ -297,7 +296,7 @@ class LocalStorageAccessor( return@withContext list } - override fun getFilesFlow(path: String): Flow>> = flow { + override fun getFilesFlow(path: String): Flow> = flow { if (!checkAvailable()) return@flow @@ -352,7 +351,7 @@ class LocalStorageAccessor( return@withContext list } - override fun getDirsFlow(path: String): Flow>> = flow { + override fun getDirsFlow(path: String): Flow> = flow { if (!checkAvailable()) return@flow @@ -531,7 +530,7 @@ class LocalStorageAccessor( writeMeta(pair.metaFile, newMeta) } - suspend fun openReadSystemFile(name: String): InputStream = withContext(ioDispatcher) { + override suspend fun openReadSystemFile(name: String): InputStream = withContext(ioDispatcher) { val dirPath = _filesystemBasePath.resolve(SYSTEM_HIDDEN_DIRNAME) val path = dirPath.resolve(name) val file = path.toFile() @@ -543,7 +542,7 @@ class LocalStorageAccessor( return@withContext file.inputStream() } - suspend fun openWriteSystemFile(name: String): OutputStream = withContext(ioDispatcher) { + override suspend fun openWriteSystemFile(name: String): OutputStream = withContext(ioDispatcher) { val dirPath = _filesystemBasePath.resolve(SYSTEM_HIDDEN_DIRNAME) val path = dirPath.resolve(name) val file = path.toFile() diff --git a/data/src/main/java/com/github/nullptroma/wallenc/data/vaults/VaultsManager.kt b/data/src/main/java/com/github/nullptroma/wallenc/data/vaults/VaultsManager.kt index ddad8af..dcfe0d1 100644 --- a/data/src/main/java/com/github/nullptroma/wallenc/data/vaults/VaultsManager.kt +++ b/data/src/main/java/com/github/nullptroma/wallenc/data/vaults/VaultsManager.kt @@ -18,14 +18,19 @@ import com.github.nullptroma.wallenc.vaultapi.VaultRegistrar import com.github.nullptroma.wallenc.vaultapi.VaultRegistration import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.withContext import java.util.UUID +@OptIn(ExperimentalCoroutinesApi::class) class VaultsManager( private val ioDispatcher: CoroutineDispatcher, context: Context, @@ -58,9 +63,12 @@ class VaultsManager( .map { remote -> listOf(localVault) + remote } .stateIn(scope, SharingStarted.Eagerly, listOf(localVault)) - // Поведение Phase 1 — UnlockManager работает только с локальными storages. - // Расширение до combine(local + remote) пойдёт во Phase 2. - override val allStorages: StateFlow> = localVault.storages + override val allStorages: StateFlow> = vaults + .flatMapLatest { vs -> + if (vs.isEmpty()) flowOf(emptyList()) + else combine(vs.map { it.storages }) { arr -> arr.toList().flatten() } + } + .stateIn(scope, SharingStarted.Eagerly, emptyList()) override val unlockManager: IUnlockManager = UnlockManager( keymapRepository = keyRepo, diff --git a/data/src/main/java/com/github/nullptroma/wallenc/data/vaults/local/LocalVault.kt b/data/src/main/java/com/github/nullptroma/wallenc/data/vaults/local/LocalVault.kt index 4c00ca7..59cf5bb 100644 --- a/data/src/main/java/com/github/nullptroma/wallenc/data/vaults/local/LocalVault.kt +++ b/data/src/main/java/com/github/nullptroma/wallenc/data/vaults/local/LocalVault.kt @@ -34,11 +34,11 @@ class LocalVault( private val _isAvailable = MutableStateFlow(false) override val isAvailable: StateFlow = _isAvailable - private val _totalSpace = MutableStateFlow(null) - override val totalSpace: StateFlow = _totalSpace + private val _totalSpace = MutableStateFlow(null) + override val totalSpace: StateFlow = _totalSpace - private val _availableSpace = MutableStateFlow(null) - override val availableSpace: StateFlow = _availableSpace + private val _availableSpace = MutableStateFlow(null) + override val availableSpace: StateFlow = _availableSpace private val path = MutableStateFlow(null) diff --git a/data/src/main/java/com/github/nullptroma/wallenc/data/vaults/yandex/YandexVault.kt b/data/src/main/java/com/github/nullptroma/wallenc/data/vaults/yandex/YandexVault.kt index df9bfb5..9e11992 100644 --- a/data/src/main/java/com/github/nullptroma/wallenc/data/vaults/yandex/YandexVault.kt +++ b/data/src/main/java/com/github/nullptroma/wallenc/data/vaults/yandex/YandexVault.kt @@ -31,11 +31,11 @@ class YandexVault( private val _isAvailable = MutableStateFlow(true) override val isAvailable: StateFlow = _isAvailable - private val _totalSpace = MutableStateFlow(null) - override val totalSpace: StateFlow = _totalSpace + private val _totalSpace = MutableStateFlow(null) + override val totalSpace: StateFlow = _totalSpace - private val _availableSpace = MutableStateFlow(null) - override val availableSpace: StateFlow = _availableSpace + private val _availableSpace = MutableStateFlow(null) + override val availableSpace: StateFlow = _availableSpace override suspend fun createStorage(): IStorage = throw UnsupportedOperationException("Yandex.Disk REST integration is not connected yet") diff --git a/domain/src/main/java/com/github/nullptroma/wallenc/domain/interfaces/IStorageAccessor.kt b/domain/src/main/java/com/github/nullptroma/wallenc/domain/interfaces/IStorageAccessor.kt index 466b104..6efa973 100644 --- a/domain/src/main/java/com/github/nullptroma/wallenc/domain/interfaces/IStorageAccessor.kt +++ b/domain/src/main/java/com/github/nullptroma/wallenc/domain/interfaces/IStorageAccessor.kt @@ -1,6 +1,6 @@ package com.github.nullptroma.wallenc.domain.interfaces -import com.github.nullptroma.wallenc.domain.datatypes.DataPackage +import com.github.nullptroma.wallenc.domain.datatypes.DataPage import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.StateFlow @@ -11,8 +11,8 @@ interface IStorageAccessor { val size: StateFlow val numberOfFiles: StateFlow val isAvailable: StateFlow - val filesUpdates: SharedFlow>> - val dirsUpdates: SharedFlow>> + val filesUpdates: SharedFlow> + val dirsUpdates: SharedFlow> suspend fun getAllFiles(): List suspend fun getFiles(path: String): List @@ -21,7 +21,7 @@ interface IStorageAccessor { * @param path Путь к директории * @return Поток файлов */ - fun getFilesFlow(path: String): Flow>> + fun getFilesFlow(path: String): Flow> suspend fun getAllDirs(): List suspend fun getDirs(path: String): List @@ -30,7 +30,7 @@ interface IStorageAccessor { * @param path Путь к директории * @return Поток директорий */ - fun getDirsFlow(path: String): Flow>> + fun getDirsFlow(path: String): Flow> suspend fun getFileInfo(path: String): IFile suspend fun getDirInfo(path: String): IDirectory suspend fun setHidden(path: String, hidden: Boolean) @@ -40,4 +40,12 @@ interface IStorageAccessor { suspend fun openWrite(path: String): OutputStream suspend fun openRead(path: String): InputStream suspend fun moveToTrash(path: String) + + /** + * Системный sidecar-файл для логических нужд хранилища (мета, ключи и т.п.). + * Конкретный accessor решает, где он физически живёт, но он не должен + * попадать в выдачу [getFiles]/[getDirs]/[size]/[numberOfFiles]. + */ + suspend fun openReadSystemFile(name: String): InputStream + suspend fun openWriteSystemFile(name: String): OutputStream } \ No newline at end of file diff --git a/domain/src/main/java/com/github/nullptroma/wallenc/domain/interfaces/IVault.kt b/domain/src/main/java/com/github/nullptroma/wallenc/domain/interfaces/IVault.kt index b398670..b97a59e 100644 --- a/domain/src/main/java/com/github/nullptroma/wallenc/domain/interfaces/IVault.kt +++ b/domain/src/main/java/com/github/nullptroma/wallenc/domain/interfaces/IVault.kt @@ -11,8 +11,8 @@ import kotlinx.coroutines.flow.StateFlow interface IVault : IVaultInfo { val storages: StateFlow> val isAvailable: StateFlow - val totalSpace: StateFlow - val availableSpace: StateFlow + val totalSpace: StateFlow + val availableSpace: StateFlow suspend fun createStorage(): IStorage suspend fun createStorage(enc: StorageEncryptionInfo): IStorage