refactor(vault): extract BaseStorage and align storage interfaces

Consolidate duplicated meta-info and clear logic into BaseStorage. Promote
system file accessors and DataPage-based flows into IStorageAccessor. Use Long
for vault disk space to support cloud byte counts. Combine local and remote
storages in VaultsManager so UnlockManager sees all backends.

Yandex Disk REST integration (phase B) is deferred to a follow-up change.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-05-03 20:25:59 +03:00
parent 78aa776adc
commit d60cd9053a
10 changed files with 260 additions and 280 deletions

View File

@@ -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<IStorageMetaInfo>(CommonStorageMetaInfo())
final override val metaInfo: StateFlow<IStorageMetaInfo>
get() = _metaInfo
final override val size: StateFlow<Long?>
get() = accessor.size
final override val numberOfFiles: StateFlow<Int?>
get() = accessor.numberOfFiles
final override val isAvailable: StateFlow<Boolean>
get() = accessor.isAvailable
final override val isEmpty: Flow<Boolean?>
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() }
}
}

View File

@@ -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<Long?>
get() = accessor.size
override val numberOfFiles: StateFlow<Int?>
get() = accessor.numberOfFiles
override val isEmpty: Flow<Boolean?>
get() = accessor.numberOfFiles.map { n -> n?.let { it == 0 } }
private val _metaInfo = MutableStateFlow<IStorageMetaInfo>(
CommonStorageMetaInfo()
)
override val metaInfo: StateFlow<IStorageMetaInfo>
get() = _metaInfo
override val isVirtualStorage: Boolean = true
override val isAvailable: StateFlow<Boolean>
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() }
}
}
}

View File

@@ -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<Boolean> = source.isAvailable
private val _filesUpdates = MutableSharedFlow<DataPackage<List<IFile>>>()
override val filesUpdates: SharedFlow<DataPackage<List<IFile>>> = _filesUpdates
private val _filesUpdates = MutableSharedFlow<DataPage<IFile>>()
override val filesUpdates: SharedFlow<DataPage<IFile>> = _filesUpdates
private val _dirsUpdates = MutableSharedFlow<DataPackage<List<IDirectory>>>()
override val dirsUpdates: SharedFlow<DataPackage<List<IDirectory>>> = _dirsUpdates
private val _dirsUpdates = MutableSharedFlow<DataPage<IDirectory>>()
override val dirsUpdates: SharedFlow<DataPage<IDirectory>> = _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<DataPackage<List<IFile>>> {
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<DataPage<IFile>> {
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<DataPackage<List<IDirectory>>> {
val flow = source.getDirsFlow(encryptPath(path)).map {
DataPackage(
override fun getDirsFlow(path: String): Flow<DataPage<IDirectory>> {
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 {

View File

@@ -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<Long?>
get() = accessor.size
override val numberOfFiles: StateFlow<Int?>
get() = accessor.numberOfFiles
override val isEmpty: Flow<Boolean?>
get() = accessor.numberOfFiles.map { n -> n?.let { it == 0 } }
private val _metaInfo = MutableStateFlow<IStorageMetaInfo>(
CommonStorageMetaInfo()
)
override val metaInfo: StateFlow<IStorageMetaInfo>
get() = _metaInfo
override val isAvailable: StateFlow<Boolean>
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() }
}
}
}

View File

@@ -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<DataPackage<List<IFile>>> = flow {
override fun getFilesFlow(path: String): Flow<DataPage<IFile>> = flow {
if (!checkAvailable())
return@flow
@@ -352,7 +351,7 @@ class LocalStorageAccessor(
return@withContext list
}
override fun getDirsFlow(path: String): Flow<DataPackage<List<IDirectory>>> = flow {
override fun getDirsFlow(path: String): Flow<DataPage<IDirectory>> = 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()

View File

@@ -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<List<IStorage>> = localVault.storages
override val allStorages: StateFlow<List<IStorage>> = 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,

View File

@@ -34,11 +34,11 @@ class LocalVault(
private val _isAvailable = MutableStateFlow(false)
override val isAvailable: StateFlow<Boolean> = _isAvailable
private val _totalSpace = MutableStateFlow<Int?>(null)
override val totalSpace: StateFlow<Int?> = _totalSpace
private val _totalSpace = MutableStateFlow<Long?>(null)
override val totalSpace: StateFlow<Long?> = _totalSpace
private val _availableSpace = MutableStateFlow<Int?>(null)
override val availableSpace: StateFlow<Int?> = _availableSpace
private val _availableSpace = MutableStateFlow<Long?>(null)
override val availableSpace: StateFlow<Long?> = _availableSpace
private val path = MutableStateFlow<File?>(null)

View File

@@ -31,11 +31,11 @@ class YandexVault(
private val _isAvailable = MutableStateFlow(true)
override val isAvailable: StateFlow<Boolean> = _isAvailable
private val _totalSpace = MutableStateFlow<Int?>(null)
override val totalSpace: StateFlow<Int?> = _totalSpace
private val _totalSpace = MutableStateFlow<Long?>(null)
override val totalSpace: StateFlow<Long?> = _totalSpace
private val _availableSpace = MutableStateFlow<Int?>(null)
override val availableSpace: StateFlow<Int?> = _availableSpace
private val _availableSpace = MutableStateFlow<Long?>(null)
override val availableSpace: StateFlow<Long?> = _availableSpace
override suspend fun createStorage(): IStorage =
throw UnsupportedOperationException("Yandex.Disk REST integration is not connected yet")