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