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:
@@ -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() }
|
||||
}
|
||||
}
|
||||
@@ -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() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user