feat(storage): добавлены маршруты и экраны для управления текстовыми секретами и 2FA токенами
This commit is contained in:
@@ -16,5 +16,6 @@ kotlin {
|
||||
dependencies {
|
||||
implementation(project(":domain"))
|
||||
implementation(libs.kotlinx.coroutines.core)
|
||||
implementation(libs.kotlinx.serialization.json)
|
||||
testImplementation(libs.junit)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.github.nullptroma.wallenc.usecases
|
||||
|
||||
import com.github.nullptroma.wallenc.domain.interfaces.IStorage
|
||||
import com.github.nullptroma.wallenc.domain.interfaces.IVaultsManager
|
||||
import java.util.UUID
|
||||
|
||||
class FindStorageUseCase(
|
||||
private val vaultsManager: IVaultsManager,
|
||||
) {
|
||||
fun find(storageUuid: UUID): IStorage? {
|
||||
return vaultsManager.vaults.value
|
||||
.flatMap { it.storages.value }
|
||||
.firstOrNull { it.uuid == storageUuid }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
package com.github.nullptroma.wallenc.usecases
|
||||
|
||||
import com.github.nullptroma.wallenc.domain.datatypes.TextSecretEntryRecord
|
||||
import com.github.nullptroma.wallenc.domain.datatypes.TextSecretRecord
|
||||
import com.github.nullptroma.wallenc.domain.interfaces.IStorage
|
||||
import com.github.nullptroma.wallenc.domain.interfaces.IStorageInfo
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.serialization.json.JsonArray
|
||||
import kotlinx.serialization.json.JsonElement
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.json.JsonPrimitive
|
||||
import kotlinx.serialization.json.buildJsonArray
|
||||
import kotlinx.serialization.json.buildJsonObject
|
||||
import kotlinx.serialization.json.contentOrNull
|
||||
import kotlinx.serialization.json.jsonArray
|
||||
import kotlinx.serialization.json.jsonPrimitive
|
||||
import java.util.UUID
|
||||
|
||||
class ManageTextSecretsUseCase {
|
||||
private val mutex = Mutex()
|
||||
|
||||
suspend fun list(storageInfo: IStorageInfo): List<TextSecretRecord> = mutex.withLock {
|
||||
val storage = storageInfo as? IStorage ?: return@withLock emptyList()
|
||||
readAll(storage)
|
||||
}
|
||||
|
||||
suspend fun get(storageInfo: IStorageInfo, id: String): TextSecretRecord? = mutex.withLock {
|
||||
val storage = storageInfo as? IStorage ?: return@withLock null
|
||||
readAll(storage).firstOrNull { it.id == id }
|
||||
}
|
||||
|
||||
suspend fun create(storageInfo: IStorageInfo, title: String, items: List<TextSecretEntryRecord>): TextSecretRecord =
|
||||
mutex.withLock {
|
||||
val storage = storageInfo as? IStorage ?: error("Storage is not writable")
|
||||
val next = TextSecretRecord(
|
||||
id = UUID.randomUUID().toString(),
|
||||
title = title.trim(),
|
||||
items = items.normalizeItems(),
|
||||
)
|
||||
val updated = readAll(storage).toMutableList().apply { add(next) }
|
||||
writeAll(storage, updated)
|
||||
next
|
||||
}
|
||||
|
||||
suspend fun update(storageInfo: IStorageInfo, secret: TextSecretRecord): Boolean = mutex.withLock {
|
||||
val storage = storageInfo as? IStorage ?: return@withLock false
|
||||
val current = readAll(storage)
|
||||
val index = current.indexOfFirst { it.id == secret.id }
|
||||
if (index < 0) return@withLock false
|
||||
val updatedSecret = secret.copy(
|
||||
title = secret.title.trim(),
|
||||
items = secret.items.normalizeItems(),
|
||||
)
|
||||
val updated = current.toMutableList().apply { this[index] = updatedSecret }
|
||||
writeAll(storage, updated)
|
||||
true
|
||||
}
|
||||
|
||||
suspend fun delete(storageInfo: IStorageInfo, id: String): Boolean = mutex.withLock {
|
||||
val storage = storageInfo as? IStorage ?: return@withLock false
|
||||
val current = readAll(storage)
|
||||
val updated = current.filterNot { it.id == id }
|
||||
if (updated.size == current.size) return@withLock false
|
||||
writeAll(storage, updated)
|
||||
true
|
||||
}
|
||||
|
||||
private suspend fun readAll(storage: IStorage): List<TextSecretRecord> {
|
||||
return StorageDomainJsonIo.readArray(storage, StorageDomainDataFiles.TEXT_SECRETS_FILE)
|
||||
.mapNotNull { parseSecret(it) }
|
||||
}
|
||||
|
||||
private suspend fun writeAll(storage: IStorage, records: List<TextSecretRecord>) {
|
||||
StorageDomainJsonIo.writeArray(
|
||||
storage = storage,
|
||||
fileName = StorageDomainDataFiles.TEXT_SECRETS_FILE,
|
||||
data = records.map { encodeSecret(it) },
|
||||
)
|
||||
}
|
||||
|
||||
private fun parseSecret(element: JsonElement): TextSecretRecord? {
|
||||
val obj = element as? JsonObject ?: return null
|
||||
val id = obj["id"]?.jsonPrimitive?.contentOrNull?.takeIf { it.isNotBlank() } ?: return null
|
||||
val title = obj["title"]?.jsonPrimitive?.contentOrNull ?: return null
|
||||
val itemsElement = obj["items"] ?: JsonArray(emptyList())
|
||||
val items = (itemsElement as? JsonArray)?.mapNotNull { parseItem(it) } ?: emptyList()
|
||||
return TextSecretRecord(
|
||||
id = id,
|
||||
title = title,
|
||||
items = items,
|
||||
)
|
||||
}
|
||||
|
||||
private fun parseItem(element: JsonElement): TextSecretEntryRecord? {
|
||||
val obj = element as? JsonObject ?: return null
|
||||
val value = obj["value"]?.jsonPrimitive?.contentOrNull ?: return null
|
||||
val label = obj["label"]?.jsonPrimitive?.contentOrNull
|
||||
return TextSecretEntryRecord(
|
||||
label = label,
|
||||
value = value,
|
||||
)
|
||||
}
|
||||
|
||||
private fun encodeSecret(record: TextSecretRecord): JsonElement = buildJsonObject {
|
||||
put("id", JsonPrimitive(record.id))
|
||||
put("title", JsonPrimitive(record.title))
|
||||
put(
|
||||
"items",
|
||||
buildJsonArray {
|
||||
record.items.forEach { item ->
|
||||
add(
|
||||
buildJsonObject {
|
||||
item.label?.let { put("label", JsonPrimitive(it)) }
|
||||
put("value", JsonPrimitive(item.value))
|
||||
},
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private fun List<TextSecretEntryRecord>.normalizeItems(): List<TextSecretEntryRecord> =
|
||||
this.mapNotNull { item ->
|
||||
val value = item.value.trim()
|
||||
if (value.isBlank()) {
|
||||
null
|
||||
} else {
|
||||
TextSecretEntryRecord(
|
||||
label = item.label?.trim().takeUnless { it.isNullOrBlank() },
|
||||
value = value,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
package com.github.nullptroma.wallenc.usecases
|
||||
|
||||
import com.github.nullptroma.wallenc.domain.datatypes.TwoFaTokenRecord
|
||||
import com.github.nullptroma.wallenc.domain.interfaces.IStorage
|
||||
import com.github.nullptroma.wallenc.domain.interfaces.IStorageInfo
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.serialization.json.JsonElement
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.json.JsonPrimitive
|
||||
import kotlinx.serialization.json.buildJsonObject
|
||||
import kotlinx.serialization.json.contentOrNull
|
||||
import kotlinx.serialization.json.jsonPrimitive
|
||||
import java.util.UUID
|
||||
|
||||
class ManageTwoFaTokensUseCase {
|
||||
private val mutex = Mutex()
|
||||
|
||||
suspend fun list(storageInfo: IStorageInfo): List<TwoFaTokenRecord> = mutex.withLock {
|
||||
val storage = storageInfo as? IStorage ?: return@withLock emptyList()
|
||||
readAll(storage)
|
||||
}
|
||||
|
||||
suspend fun get(storageInfo: IStorageInfo, id: String): TwoFaTokenRecord? = mutex.withLock {
|
||||
val storage = storageInfo as? IStorage ?: return@withLock null
|
||||
readAll(storage).firstOrNull { it.id == id }
|
||||
}
|
||||
|
||||
suspend fun create(
|
||||
storageInfo: IStorageInfo,
|
||||
issuer: String,
|
||||
account: String,
|
||||
secret: String,
|
||||
notes: String? = null,
|
||||
): TwoFaTokenRecord = mutex.withLock {
|
||||
val storage = storageInfo as? IStorage ?: error("Storage is not writable")
|
||||
val next = TwoFaTokenRecord(
|
||||
id = UUID.randomUUID().toString(),
|
||||
issuer = issuer.trim(),
|
||||
account = account.trim(),
|
||||
secret = secret.trim(),
|
||||
notes = notes?.trim().takeUnless { it.isNullOrBlank() },
|
||||
)
|
||||
val updated = readAll(storage).toMutableList().apply { add(next) }
|
||||
writeAll(storage, updated)
|
||||
next
|
||||
}
|
||||
|
||||
suspend fun update(storageInfo: IStorageInfo, token: TwoFaTokenRecord): Boolean = mutex.withLock {
|
||||
val storage = storageInfo as? IStorage ?: return@withLock false
|
||||
val updatedToken = token.copy(
|
||||
issuer = token.issuer.trim(),
|
||||
account = token.account.trim(),
|
||||
secret = token.secret.trim(),
|
||||
notes = token.notes?.trim().takeUnless { it.isNullOrBlank() },
|
||||
)
|
||||
val current = readAll(storage)
|
||||
val index = current.indexOfFirst { it.id == token.id }
|
||||
if (index < 0) return@withLock false
|
||||
val updated = current.toMutableList().apply { this[index] = updatedToken }
|
||||
writeAll(storage, updated)
|
||||
true
|
||||
}
|
||||
|
||||
suspend fun delete(storageInfo: IStorageInfo, id: String): Boolean = mutex.withLock {
|
||||
val storage = storageInfo as? IStorage ?: return@withLock false
|
||||
val current = readAll(storage)
|
||||
val updated = current.filterNot { it.id == id }
|
||||
if (updated.size == current.size) return@withLock false
|
||||
writeAll(storage, updated)
|
||||
true
|
||||
}
|
||||
|
||||
private suspend fun readAll(storage: IStorage): List<TwoFaTokenRecord> {
|
||||
return StorageDomainJsonIo.readArray(storage, StorageDomainDataFiles.TWO_FA_TOKENS_FILE)
|
||||
.mapNotNull { parseToken(it) }
|
||||
}
|
||||
|
||||
private suspend fun writeAll(storage: IStorage, records: List<TwoFaTokenRecord>) {
|
||||
StorageDomainJsonIo.writeArray(
|
||||
storage = storage,
|
||||
fileName = StorageDomainDataFiles.TWO_FA_TOKENS_FILE,
|
||||
data = records.map { record -> encodeToken(record) },
|
||||
)
|
||||
}
|
||||
|
||||
private fun parseToken(element: JsonElement): TwoFaTokenRecord? {
|
||||
val obj = element as? JsonObject ?: return null
|
||||
val id = obj["id"]?.jsonPrimitive?.contentOrNull?.takeIf { it.isNotBlank() } ?: return null
|
||||
val issuer = obj["issuer"]?.jsonPrimitive?.contentOrNull ?: return null
|
||||
val account = obj["account"]?.jsonPrimitive?.contentOrNull ?: return null
|
||||
val secret = obj["secret"]?.jsonPrimitive?.contentOrNull ?: return null
|
||||
val notes = obj["notes"]?.jsonPrimitive?.contentOrNull
|
||||
return TwoFaTokenRecord(
|
||||
id = id,
|
||||
issuer = issuer,
|
||||
account = account,
|
||||
secret = secret,
|
||||
notes = notes,
|
||||
)
|
||||
}
|
||||
|
||||
private fun encodeToken(record: TwoFaTokenRecord): JsonElement = buildJsonObject {
|
||||
put("id", JsonPrimitive(record.id))
|
||||
put("issuer", JsonPrimitive(record.issuer))
|
||||
put("account", JsonPrimitive(record.account))
|
||||
put("secret", JsonPrimitive(record.secret))
|
||||
record.notes?.let { put("notes", JsonPrimitive(it)) }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.github.nullptroma.wallenc.usecases
|
||||
|
||||
/**
|
||||
* Единая точка с путями обычных JSON-файлов пользовательских доменных данных.
|
||||
*/
|
||||
object StorageDomainDataFiles {
|
||||
const val TWO_FA_TOKENS_FILE = "/two-fa-tokens.json"
|
||||
const val TEXT_SECRETS_FILE = "/text-secrets.json"
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package com.github.nullptroma.wallenc.usecases
|
||||
|
||||
import com.github.nullptroma.wallenc.domain.interfaces.IStorage
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonArray
|
||||
import kotlinx.serialization.json.JsonElement
|
||||
import kotlinx.serialization.json.buildJsonArray
|
||||
|
||||
internal object StorageDomainJsonIo {
|
||||
val json: Json = Json {
|
||||
prettyPrint = true
|
||||
ignoreUnknownKeys = true
|
||||
explicitNulls = false
|
||||
}
|
||||
|
||||
suspend fun readArray(storage: IStorage, fileName: String): JsonArray {
|
||||
return try {
|
||||
val text = storage.accessor.openRead(fileName).use { stream ->
|
||||
stream.readBytes().decodeToString()
|
||||
}
|
||||
if (text.isBlank()) {
|
||||
JsonArray(emptyList())
|
||||
} else {
|
||||
when (val parsed = json.parseToJsonElement(text)) {
|
||||
is JsonArray -> parsed
|
||||
else -> JsonArray(emptyList())
|
||||
}
|
||||
}
|
||||
} catch (_: Exception) {
|
||||
JsonArray(emptyList())
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun writeArray(storage: IStorage, fileName: String, data: List<JsonElement>) {
|
||||
val payload = buildJsonArray { data.forEach { add(it) } }
|
||||
storage.accessor.openWrite(fileName).use { stream ->
|
||||
stream.write(json.encodeToString(JsonArray.serializer(), payload).encodeToByteArray())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,222 @@
|
||||
package com.github.nullptroma.wallenc.usecases
|
||||
|
||||
import com.github.nullptroma.wallenc.domain.datatypes.DataPage
|
||||
import com.github.nullptroma.wallenc.domain.datatypes.StorageEncryptionInfo
|
||||
import com.github.nullptroma.wallenc.domain.datatypes.TextSecretEntryRecord
|
||||
import com.github.nullptroma.wallenc.domain.datatypes.StorageSyncJournalEntry
|
||||
import com.github.nullptroma.wallenc.domain.datatypes.StorageSyncLock
|
||||
import com.github.nullptroma.wallenc.domain.interfaces.IDirectory
|
||||
import com.github.nullptroma.wallenc.domain.interfaces.IFile
|
||||
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.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharedFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.emptyFlow
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertNotNull
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
import java.time.Instant
|
||||
import java.util.UUID
|
||||
|
||||
class StorageDomainUseCasesTest {
|
||||
|
||||
@Test
|
||||
fun twoFaCrudWorksAndPersists() = runBlocking {
|
||||
val storage = FakeStorage()
|
||||
val useCase = ManageTwoFaTokensUseCase()
|
||||
|
||||
val created = useCase.create(
|
||||
storageInfo = storage,
|
||||
issuer = "GitHub",
|
||||
account = "user@example.com",
|
||||
secret = "SECRET",
|
||||
notes = "primary",
|
||||
)
|
||||
assertNotNull(created.id)
|
||||
assertEquals(1, useCase.list(storage).size)
|
||||
|
||||
val updated = useCase.update(
|
||||
storageInfo = storage,
|
||||
token = created.copy(issuer = "GitHubUpdated"),
|
||||
)
|
||||
assertTrue(updated)
|
||||
assertEquals("GitHubUpdated", useCase.get(storage, created.id)?.issuer)
|
||||
|
||||
val removed = useCase.delete(storage, created.id)
|
||||
assertTrue(removed)
|
||||
assertTrue(useCase.list(storage).isEmpty())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun twoFaInvalidJsonFallsBackToEmptyList() = runBlocking {
|
||||
val storage = FakeStorage().apply {
|
||||
setDomainFile(StorageDomainDataFiles.TWO_FA_TOKENS_FILE, "not-json")
|
||||
}
|
||||
val useCase = ManageTwoFaTokensUseCase()
|
||||
assertTrue(useCase.list(storage).isEmpty())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun textSecretsCrudWorksWithOptionalLabels() = runBlocking {
|
||||
val storage = FakeStorage()
|
||||
val useCase = ManageTextSecretsUseCase()
|
||||
|
||||
val created = useCase.create(
|
||||
storageInfo = storage,
|
||||
title = "Server Credentials",
|
||||
items = listOf(
|
||||
TextSecretEntryRecord(label = "username", value = "admin"),
|
||||
TextSecretEntryRecord(label = null, value = "password"),
|
||||
),
|
||||
)
|
||||
assertEquals(1, useCase.list(storage).size)
|
||||
|
||||
val updated = useCase.update(
|
||||
storageInfo = storage,
|
||||
secret = created.copy(
|
||||
title = "Prod Credentials",
|
||||
items = created.items + TextSecretEntryRecord(label = "url", value = "https://x.dev"),
|
||||
),
|
||||
)
|
||||
assertTrue(updated)
|
||||
val loaded = useCase.get(storage, created.id)
|
||||
assertEquals("Prod Credentials", loaded?.title)
|
||||
assertEquals(3, loaded?.items?.size)
|
||||
|
||||
val deleted = useCase.delete(storage, created.id)
|
||||
assertTrue(deleted)
|
||||
assertNull(useCase.get(storage, created.id))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun textSecretsInvalidJsonFallsBackToEmptyList() = runBlocking {
|
||||
val storage = FakeStorage().apply {
|
||||
setDomainFile(StorageDomainDataFiles.TEXT_SECRETS_FILE, "{broken")
|
||||
}
|
||||
val useCase = ManageTextSecretsUseCase()
|
||||
assertTrue(useCase.list(storage).isEmpty())
|
||||
}
|
||||
}
|
||||
|
||||
private class FakeStorage : IStorage {
|
||||
private val accessorImpl = FakeStorageAccessor()
|
||||
|
||||
override val uuid: UUID = UUID.randomUUID()
|
||||
override val isAvailable: StateFlow<Boolean> = MutableStateFlow(true)
|
||||
override val size: StateFlow<Long?> = MutableStateFlow(0L)
|
||||
override val numberOfFiles: StateFlow<Int?> = MutableStateFlow(0)
|
||||
override val isEmpty: Flow<Boolean?> = flowOf(true)
|
||||
override val metaInfo: StateFlow<IStorageMetaInfo> = MutableStateFlow(FakeMetaInfo())
|
||||
override val isVirtualStorage: Boolean = false
|
||||
override val accessor: IStorageAccessor = accessorImpl
|
||||
|
||||
override suspend fun rename(newName: String) = Unit
|
||||
|
||||
override suspend fun setEncInfo(encInfo: StorageEncryptionInfo?) = Unit
|
||||
|
||||
override suspend fun clearAllContent(onProgress: suspend (TaskProgress) -> Unit) = Unit
|
||||
|
||||
fun setDomainFile(path: String, value: String) {
|
||||
accessorImpl.dataFiles[path] = value.encodeToByteArray()
|
||||
}
|
||||
}
|
||||
|
||||
private class FakeMetaInfo : IStorageMetaInfo {
|
||||
override val encInfo: StorageEncryptionInfo? = null
|
||||
override val name: String? = "Fake"
|
||||
override val lastModified: Instant = Instant.now()
|
||||
}
|
||||
|
||||
private class FakeStorageAccessor : IStorageAccessor {
|
||||
val dataFiles: MutableMap<String, ByteArray> = mutableMapOf()
|
||||
private val systemFiles: MutableMap<String, ByteArray> = mutableMapOf()
|
||||
|
||||
override val size: StateFlow<Long?> = MutableStateFlow(0L)
|
||||
override val numberOfFiles: StateFlow<Int?> = MutableStateFlow(0)
|
||||
override val isAvailable: StateFlow<Boolean> = MutableStateFlow(true)
|
||||
override val filesUpdates: SharedFlow<DataPage<IFile>> = MutableSharedFlow()
|
||||
override val dirsUpdates: SharedFlow<DataPage<IDirectory>> = MutableSharedFlow()
|
||||
|
||||
override suspend fun getAllFiles(): List<IFile> = emptyList()
|
||||
|
||||
override suspend fun getFiles(path: String): List<IFile> = emptyList()
|
||||
|
||||
override fun getFilesFlow(path: String): Flow<DataPage<IFile>> = emptyFlow()
|
||||
|
||||
override suspend fun getAllDirs(): List<IDirectory> = emptyList()
|
||||
|
||||
override suspend fun getDirs(path: String): List<IDirectory> = emptyList()
|
||||
|
||||
override fun getDirsFlow(path: String): Flow<DataPage<IDirectory>> = emptyFlow()
|
||||
|
||||
override suspend fun getFileInfo(path: String): IFile {
|
||||
error("Not implemented in tests")
|
||||
}
|
||||
|
||||
override suspend fun getDirInfo(path: String): IDirectory {
|
||||
error("Not implemented in tests")
|
||||
}
|
||||
|
||||
override suspend fun setHidden(path: String, hidden: Boolean) = Unit
|
||||
|
||||
override suspend fun touchFile(path: String) = Unit
|
||||
|
||||
override suspend fun touchDir(path: String) = Unit
|
||||
|
||||
override suspend fun delete(path: String) = Unit
|
||||
|
||||
override suspend fun openWrite(path: String): OutputStream {
|
||||
return object : ByteArrayOutputStream() {
|
||||
override fun close() {
|
||||
dataFiles[path] = toByteArray()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun openRead(path: String): InputStream {
|
||||
val bytes = dataFiles[path] ?: throw IllegalStateException("File not found: $path")
|
||||
return ByteArrayInputStream(bytes)
|
||||
}
|
||||
|
||||
override suspend fun moveToTrash(path: String) = Unit
|
||||
|
||||
override suspend fun openReadSystemFile(name: String): InputStream {
|
||||
val bytes = systemFiles[name] ?: ByteArray(0)
|
||||
return ByteArrayInputStream(bytes)
|
||||
}
|
||||
|
||||
override suspend fun openWriteSystemFile(name: String): OutputStream {
|
||||
return object : ByteArrayOutputStream() {
|
||||
override fun close() {
|
||||
systemFiles[name] = toByteArray()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun readSyncJournal(): List<StorageSyncJournalEntry> = emptyList()
|
||||
|
||||
override suspend fun appendSyncJournal(entries: List<StorageSyncJournalEntry>) = Unit
|
||||
|
||||
override suspend fun rewriteSyncJournal(entries: List<StorageSyncJournalEntry>) = Unit
|
||||
|
||||
override suspend fun readSyncLock(): StorageSyncLock? = null
|
||||
|
||||
override suspend fun tryAcquireSyncLock(holderId: String, leaseUntil: Instant): Boolean = true
|
||||
|
||||
override suspend fun releaseSyncLock(holderId: String) = Unit
|
||||
|
||||
override suspend fun forceClearSyncLock() = Unit
|
||||
}
|
||||
Reference in New Issue
Block a user