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 package com.github.nullptroma.wallenc.data.storages.encrypt
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import com.github.nullptroma.wallenc.data.storages.common.BaseStorage
import com.github.nullptroma.wallenc.domain.common.impl.CommonStorageMetaInfo
import com.github.nullptroma.wallenc.domain.datatypes.EncryptKey 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.encrypt.Encryptor
import com.github.nullptroma.wallenc.domain.interfaces.IStorage import com.github.nullptroma.wallenc.domain.interfaces.IStorage
import com.github.nullptroma.wallenc.domain.interfaces.IStorageMetaInfo import com.github.nullptroma.wallenc.domain.interfaces.IStorageAccessor
import com.github.nullptroma.wallenc.domain.tasks.TaskProgress
import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.DisposableHandle import kotlinx.coroutines.DisposableHandle
import kotlinx.coroutines.Job 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 kotlinx.coroutines.withContext
import java.io.InputStream
import java.util.UUID import java.util.UUID
class EncryptedStorage private constructor( class EncryptedStorage private constructor(
private val source: IStorage, private val source: IStorage,
private val key: EncryptKey, private val key: EncryptKey,
ioDispatcher: CoroutineDispatcher, ioDispatcher: CoroutineDispatcher,
override val uuid: UUID = UUID.randomUUID() uuid: UUID = UUID.randomUUID()
) : IStorage, DisposableHandle { ) : BaseStorage(
uuid = uuid,
ioDispatcher = ioDispatcher,
metaInfoFilePostfix = STORAGE_INFO_FILE_POSTFIX,
), DisposableHandle {
private val job = Job() private val job = Job()
private val scope = CoroutineScope(ioDispatcher + job) private val scope = CoroutineScope(ioDispatcher + job)
private val encInfo = private val encInfo =
source.metaInfo.value.encInfo ?: throw Exception("Storage is not encrypted") // TODO 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 isVirtualStorage: Boolean = true
override val isAvailable: StateFlow<Boolean> private val _accessor: EncryptedStorageAccessor =
get() = source.isAvailable
override val accessor: EncryptedStorageAccessor =
EncryptedStorageAccessor( EncryptedStorageAccessor(
source = source.accessor, source = source.accessor,
pathIv = encInfo.pathIv, pathIv = encInfo.pathIv,
key = key, key = key,
systemHiddenDirName = "${uuid.toString().take(8)}$SYSTEM_HIDDEN_DIRNAME_POSTFIX", 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() checkKey()
readMetaInfo() super.init()
} }
private fun checkKey() { private fun checkKey() {
@@ -68,89 +53,12 @@ class EncryptedStorage private constructor(
throw Exception("Incorrect key") // TODO 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() { override fun dispose() {
accessor.dispose() _accessor.dispose()
job.cancel() job.cancel()
} }
companion object { companion object {
private const val PROGRESS_REPORT_INTERVAL = 16
suspend fun create( suspend fun create(
source: IStorage, source: IStorage,
key: EncryptKey, key: EncryptKey,
@@ -174,6 +82,5 @@ class EncryptedStorage private constructor(
private const val SYSTEM_HIDDEN_DIRNAME_POSTFIX = "-enc-dir" private const val SYSTEM_HIDDEN_DIRNAME_POSTFIX = "-enc-dir"
const val STORAGE_INFO_FILE_POSTFIX = ".enc-meta" 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.CommonDirectory
import com.github.nullptroma.wallenc.domain.common.impl.CommonFile import com.github.nullptroma.wallenc.domain.common.impl.CommonFile
import com.github.nullptroma.wallenc.domain.common.impl.CommonMetaInfo 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.datatypes.EncryptKey
import com.github.nullptroma.wallenc.domain.encrypt.Encryptor import com.github.nullptroma.wallenc.domain.encrypt.Encryptor
import com.github.nullptroma.wallenc.domain.encrypt.EncryptorWithStaticIv import com.github.nullptroma.wallenc.domain.encrypt.EncryptorWithStaticIv
@@ -41,11 +41,11 @@ class EncryptedStorageAccessor(
override val isAvailable: StateFlow<Boolean> = source.isAvailable override val isAvailable: StateFlow<Boolean> = source.isAvailable
private val _filesUpdates = MutableSharedFlow<DataPackage<List<IFile>>>() private val _filesUpdates = MutableSharedFlow<DataPage<IFile>>()
override val filesUpdates: SharedFlow<DataPackage<List<IFile>>> = _filesUpdates override val filesUpdates: SharedFlow<DataPage<IFile>> = _filesUpdates
private val _dirsUpdates = MutableSharedFlow<DataPackage<List<IDirectory>>>() private val _dirsUpdates = MutableSharedFlow<DataPage<IDirectory>>()
override val dirsUpdates: SharedFlow<DataPackage<List<IDirectory>>> = _dirsUpdates override val dirsUpdates: SharedFlow<DataPage<IDirectory>> = _dirsUpdates
private val dataEncryptor = Encryptor(key.toAesKey()) private val dataEncryptor = Encryptor(key.toAesKey())
private val pathEncryptor: EncryptorWithStaticIv? = if(pathIv != null) EncryptorWithStaticIv(key.toAesKey(), pathIv) else null private val pathEncryptor: EncryptorWithStaticIv? = if(pathIv != null) EncryptorWithStaticIv(key.toAesKey(), pathIv) else null
@@ -58,26 +58,32 @@ class EncryptedStorageAccessor(
private fun collectSourceState() { private fun collectSourceState() {
scope.launch { scope.launch {
launch { launch {
source.filesUpdates.collect { source.filesUpdates.collect { page ->
val files = it.data.map(::decryptEntity).filterSystemHiddenFiles() val files = page.data.map(::decryptEntity).filterSystemHiddenFiles()
_filesUpdates.emit( _filesUpdates.emit(
DataPackage( DataPage(
data = files, list = files,
isLoading = it.isLoading, isLoading = page.isLoading,
isError = it.isError isError = page.isError,
hasNext = page.hasNext,
pageLength = page.pageLength,
pageIndex = page.pageIndex,
) )
) )
} }
} }
launch { launch {
source.dirsUpdates.collect { source.dirsUpdates.collect { page ->
val dirs = it.data.map(::decryptEntity).filterSystemHiddenDirs() val dirs = page.data.map(::decryptEntity).filterSystemHiddenDirs()
_dirsUpdates.emit( _dirsUpdates.emit(
DataPackage( DataPage(
data = dirs, list = dirs,
isLoading = it.isLoading, isLoading = page.isLoading,
isError = it.isError 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() return source.getFiles(encryptPath(path)).map(::decryptEntity).filterSystemHiddenFiles()
} }
override fun getFilesFlow(path: String): Flow<DataPackage<List<IFile>>> { override fun getFilesFlow(path: String): Flow<DataPage<IFile>> {
val flow = source.getFilesFlow(encryptPath(path)).map { val flow = source.getFilesFlow(encryptPath(path)).map { page ->
DataPackage( DataPage(
data = it.data.map(::decryptEntity).filterSystemHiddenFiles(), list = page.data.map(::decryptEntity).filterSystemHiddenFiles(),
isLoading = it.isLoading, isLoading = page.isLoading,
isError = it.isError isError = page.isError,
hasNext = page.hasNext,
pageLength = page.pageLength,
pageIndex = page.pageIndex,
) )
} }
return flow return flow
@@ -197,13 +206,16 @@ class EncryptedStorageAccessor(
return source.getDirs(encryptPath(path)).map(::decryptEntity).filterSystemHiddenDirs() return source.getDirs(encryptPath(path)).map(::decryptEntity).filterSystemHiddenDirs()
} }
override fun getDirsFlow(path: String): Flow<DataPackage<List<IDirectory>>> { override fun getDirsFlow(path: String): Flow<DataPage<IDirectory>> {
val flow = source.getDirsFlow(encryptPath(path)).map { val flow = source.getDirsFlow(encryptPath(path)).map { page ->
DataPackage( DataPage(
// включать все папки, кроме системной // включать все папки, кроме системной
data = it.data.map(::decryptEntity).filterSystemHiddenDirs(), list = page.data.map(::decryptEntity).filterSystemHiddenDirs(),
isLoading = it.isLoading, isLoading = page.isLoading,
isError = it.isError isError = page.isError,
hasNext = page.hasNext,
pageLength = page.pageLength,
pageIndex = page.pageIndex,
) )
} }
return flow return flow
@@ -255,12 +267,12 @@ class EncryptedStorageAccessor(
dataEncryptor.dispose() 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 val path = Path(systemHiddenDirName, name).pathString
return@run openRead(path) 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 val path = Path(systemHiddenDirName, name).pathString
systemHiddenFilesIsActual = false systemHiddenFilesIsActual = false
return@run openWrite(path).onClosing { return@run openWrite(path).onClosing {

View File

@@ -1,132 +1,29 @@
package com.github.nullptroma.wallenc.data.storages.local package com.github.nullptroma.wallenc.data.storages.local
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import com.github.nullptroma.wallenc.data.storages.common.BaseStorage
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.IStorageAccessor
import com.github.nullptroma.wallenc.domain.interfaces.IStorageMetaInfo
import com.github.nullptroma.wallenc.domain.tasks.TaskProgress
import kotlinx.coroutines.CoroutineDispatcher 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 import java.util.UUID
class LocalStorage( class LocalStorage(
override val uuid: UUID, uuid: UUID,
val absolutePath: String, val absolutePath: String,
private val ioDispatcher: CoroutineDispatcher, ioDispatcher: CoroutineDispatcher,
) : IStorage { ) : BaseStorage(
override val size: StateFlow<Long?> uuid = uuid,
get() = accessor.size ioDispatcher = ioDispatcher,
override val numberOfFiles: StateFlow<Int?> metaInfoFilePostfix = STORAGE_INFO_FILE_POSTFIX,
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
private val _accessor = LocalStorageAccessor(absolutePath, ioDispatcher) private val _accessor = LocalStorageAccessor(absolutePath, ioDispatcher)
override val accessor: IStorageAccessor = _accessor override val accessor: IStorageAccessor = _accessor
override val isVirtualStorage: Boolean = false override val isVirtualStorage: Boolean = false
private val metaInfoFileName: String = "$uuid$STORAGE_INFO_FILE_POSTFIX"
suspend fun init() { override suspend fun init() {
_accessor.init() _accessor.init()
readMetaInfo() super.init()
}
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()
}
}
} }
companion object { companion object {
private const val PROGRESS_REPORT_INTERVAL = 16
const val STORAGE_INFO_FILE_POSTFIX = ".storage-info" 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.CommonDirectory
import com.github.nullptroma.wallenc.domain.common.impl.CommonFile import com.github.nullptroma.wallenc.domain.common.impl.CommonFile
import com.github.nullptroma.wallenc.domain.common.impl.CommonMetaInfo 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.DataPage
import com.github.nullptroma.wallenc.domain.interfaces.IDirectory import com.github.nullptroma.wallenc.domain.interfaces.IDirectory
import com.github.nullptroma.wallenc.domain.interfaces.IFile import com.github.nullptroma.wallenc.domain.interfaces.IFile
@@ -297,7 +296,7 @@ class LocalStorageAccessor(
return@withContext list return@withContext list
} }
override fun getFilesFlow(path: String): Flow<DataPackage<List<IFile>>> = flow { override fun getFilesFlow(path: String): Flow<DataPage<IFile>> = flow {
if (!checkAvailable()) if (!checkAvailable())
return@flow return@flow
@@ -352,7 +351,7 @@ class LocalStorageAccessor(
return@withContext list return@withContext list
} }
override fun getDirsFlow(path: String): Flow<DataPackage<List<IDirectory>>> = flow { override fun getDirsFlow(path: String): Flow<DataPage<IDirectory>> = flow {
if (!checkAvailable()) if (!checkAvailable())
return@flow return@flow
@@ -531,7 +530,7 @@ class LocalStorageAccessor(
writeMeta(pair.metaFile, newMeta) 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 dirPath = _filesystemBasePath.resolve(SYSTEM_HIDDEN_DIRNAME)
val path = dirPath.resolve(name) val path = dirPath.resolve(name)
val file = path.toFile() val file = path.toFile()
@@ -543,7 +542,7 @@ class LocalStorageAccessor(
return@withContext file.inputStream() 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 dirPath = _filesystemBasePath.resolve(SYSTEM_HIDDEN_DIRNAME)
val path = dirPath.resolve(name) val path = dirPath.resolve(name)
val file = path.toFile() 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 com.github.nullptroma.wallenc.vaultapi.VaultRegistration
import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow 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.map
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.util.UUID import java.util.UUID
@OptIn(ExperimentalCoroutinesApi::class)
class VaultsManager( class VaultsManager(
private val ioDispatcher: CoroutineDispatcher, private val ioDispatcher: CoroutineDispatcher,
context: Context, context: Context,
@@ -58,9 +63,12 @@ class VaultsManager(
.map { remote -> listOf(localVault) + remote } .map { remote -> listOf(localVault) + remote }
.stateIn(scope, SharingStarted.Eagerly, listOf(localVault)) .stateIn(scope, SharingStarted.Eagerly, listOf(localVault))
// Поведение Phase 1 — UnlockManager работает только с локальными storages. override val allStorages: StateFlow<List<IStorage>> = vaults
// Расширение до combine(local + remote) пойдёт во Phase 2. .flatMapLatest { vs ->
override val allStorages: StateFlow<List<IStorage>> = localVault.storages 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( override val unlockManager: IUnlockManager = UnlockManager(
keymapRepository = keyRepo, keymapRepository = keyRepo,

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
package com.github.nullptroma.wallenc.domain.interfaces 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.Flow
import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
@@ -11,8 +11,8 @@ interface IStorageAccessor {
val size: StateFlow<Long?> val size: StateFlow<Long?>
val numberOfFiles: StateFlow<Int?> val numberOfFiles: StateFlow<Int?>
val isAvailable: StateFlow<Boolean> val isAvailable: StateFlow<Boolean>
val filesUpdates: SharedFlow<DataPackage<List<IFile>>> val filesUpdates: SharedFlow<DataPage<IFile>>
val dirsUpdates: SharedFlow<DataPackage<List<IDirectory>>> val dirsUpdates: SharedFlow<DataPage<IDirectory>>
suspend fun getAllFiles(): List<IFile> suspend fun getAllFiles(): List<IFile>
suspend fun getFiles(path: String): List<IFile> suspend fun getFiles(path: String): List<IFile>
@@ -21,7 +21,7 @@ interface IStorageAccessor {
* @param path Путь к директории * @param path Путь к директории
* @return Поток файлов * @return Поток файлов
*/ */
fun getFilesFlow(path: String): Flow<DataPackage<List<IFile>>> fun getFilesFlow(path: String): Flow<DataPage<IFile>>
suspend fun getAllDirs(): List<IDirectory> suspend fun getAllDirs(): List<IDirectory>
suspend fun getDirs(path: String): List<IDirectory> suspend fun getDirs(path: String): List<IDirectory>
@@ -30,7 +30,7 @@ interface IStorageAccessor {
* @param path Путь к директории * @param path Путь к директории
* @return Поток директорий * @return Поток директорий
*/ */
fun getDirsFlow(path: String): Flow<DataPackage<List<IDirectory>>> fun getDirsFlow(path: String): Flow<DataPage<IDirectory>>
suspend fun getFileInfo(path: String): IFile suspend fun getFileInfo(path: String): IFile
suspend fun getDirInfo(path: String): IDirectory suspend fun getDirInfo(path: String): IDirectory
suspend fun setHidden(path: String, hidden: Boolean) suspend fun setHidden(path: String, hidden: Boolean)
@@ -40,4 +40,12 @@ interface IStorageAccessor {
suspend fun openWrite(path: String): OutputStream suspend fun openWrite(path: String): OutputStream
suspend fun openRead(path: String): InputStream suspend fun openRead(path: String): InputStream
suspend fun moveToTrash(path: String) suspend fun moveToTrash(path: String)
/**
* Системный sidecar-файл для логических нужд хранилища (мета, ключи и т.п.).
* Конкретный accessor решает, где он физически живёт, но он не должен
* попадать в выдачу [getFiles]/[getDirs]/[size]/[numberOfFiles].
*/
suspend fun openReadSystemFile(name: String): InputStream
suspend fun openWriteSystemFile(name: String): OutputStream
} }

View File

@@ -11,8 +11,8 @@ import kotlinx.coroutines.flow.StateFlow
interface IVault : IVaultInfo { interface IVault : IVaultInfo {
val storages: StateFlow<List<IStorage>> val storages: StateFlow<List<IStorage>>
val isAvailable: StateFlow<Boolean> val isAvailable: StateFlow<Boolean>
val totalSpace: StateFlow<Int?> val totalSpace: StateFlow<Long?>
val availableSpace: StateFlow<Int?> val availableSpace: StateFlow<Long?>
suspend fun createStorage(): IStorage suspend fun createStorage(): IStorage
suspend fun createStorage(enc: StorageEncryptionInfo): IStorage suspend fun createStorage(enc: StorageEncryptionInfo): IStorage