Yandex штуки

This commit is contained in:
2026-05-03 22:03:47 +03:00
parent be1ba29f4d
commit ad985679ee
11 changed files with 113 additions and 38 deletions

View File

@@ -0,0 +1,5 @@
package com.github.nullptroma.wallenc.data.network.yandexdisk
import java.io.IOException
class YandexDiskAuthException(message: String? = null) : IOException(message)

View File

@@ -1,10 +1,13 @@
package com.github.nullptroma.wallenc.data.network.yandexdisk.repository
import android.util.Log
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.fasterxml.jackson.module.kotlin.readValue
import com.github.nullptroma.wallenc.data.network.yandexdisk.YandexDiskApi
import com.github.nullptroma.wallenc.data.network.yandexdisk.YandexDiskAuthException
import com.github.nullptroma.wallenc.data.network.yandexdisk.dto.CustomPropertiesPatchDto
import com.github.nullptroma.wallenc.data.network.yandexdisk.dto.DiskInfoDto
import com.github.nullptroma.wallenc.data.network.yandexdisk.dto.EmbeddedResourceListDto
import com.github.nullptroma.wallenc.data.network.yandexdisk.dto.LinkDto
import com.github.nullptroma.wallenc.data.network.yandexdisk.dto.OperationStatusDto
import com.github.nullptroma.wallenc.data.network.yandexdisk.dto.ResourceDto
@@ -22,8 +25,6 @@ import retrofit2.Response
import java.io.IOException
import java.io.InputStream
class YandexDiskAuthException(message: String? = null) : IOException(message)
class YandexDiskRepository(
private val api: YandexDiskApi,
private val rawHttp: okhttp3.OkHttpClient,
@@ -36,7 +37,15 @@ class YandexDiskRepository(
suspend fun list(path: String, limit: Int, offset: Int, sort: String? = null): ResourceDto =
withContext(ioDispatcher) {
try {
wrapAuth { api.listResources(path, limit, offset, sort) }
} catch (e: HttpException) {
if (e.code() == 404) {
ResourceDto(embedded = EmbeddedResourceListDto(items = emptyList()))
} else {
throw e
}
}
}
suspend fun get(path: String): ResourceDto = withContext(ioDispatcher) {
@@ -47,7 +56,7 @@ class YandexDiskRepository(
val resp = wrapAuth { api.createFolder(path) }
when (resp.code()) {
201 -> Unit
409 -> Unit // уже существует
409 -> Unit
else -> throw failure("createFolder", resp)
}
}
@@ -170,10 +179,19 @@ class YandexDiskRepository(
try {
return block()
} catch (e: HttpException) {
if (e.code() == 401) throw YandexDiskAuthException(e.message())
when (e.code()) {
401 -> {
Log.w(TAG, "Disk API 401: ${e.message()}")
throw YandexDiskAuthException(e.message())
}
404 -> throw e
else -> {
Log.w(TAG, "Disk API HTTP ${e.code()}: ${e.message()}")
throw e
}
}
}
}
private fun failure(op: String, resp: Response<ResponseBody>): IOException {
val msg = resp.errorBody()?.string() ?: resp.message()
@@ -184,6 +202,7 @@ class YandexDiskRepository(
jackson.readValue(body.string())
companion object {
private const val TAG = "YandexDiskRepo"
private val jackson = jacksonObjectMapper().apply { findAndRegisterModules() }
private val OCTET_STREAM = "application/octet-stream".toMediaType()
private const val OPERATION_POLL_DELAY_MS = 300L

View File

@@ -86,10 +86,8 @@ abstract class BaseStorage(
private suspend fun updateMetaInfo(meta: IStorageMetaInfo) = withContext(ioDispatcher) {
val writer = accessor.openWriteSystemFile(metaInfoFileName)
try {
writer.use { writer ->
jackson.writeValue(writer, meta)
} finally {
writer.close()
}
_metaInfo.value = meta
}

View File

@@ -1,6 +1,6 @@
package com.github.nullptroma.wallenc.data.storages.yandex
import com.github.nullptroma.wallenc.data.network.yandexdisk.repository.YandexDiskAuthException
import com.github.nullptroma.wallenc.data.network.yandexdisk.YandexDiskAuthException
import com.github.nullptroma.wallenc.data.network.yandexdisk.dto.ResourceDto
import com.github.nullptroma.wallenc.data.network.yandexdisk.repository.YandexDiskRepository
import com.github.nullptroma.wallenc.data.utils.CloseHandledStreamExtension.Companion.onClosed
@@ -23,6 +23,7 @@ import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.stateIn
import android.util.Log
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import java.io.ByteArrayOutputStream
@@ -72,13 +73,16 @@ class YandexStorageAccessor(
try {
scanSizeAndNumOfFiles()
_storageReady.value = true
Log.d(TAG, "init ok storageUuid=$storageUuid")
} catch (e: YandexDiskAuthException) {
reportAuthFailure()
_storageReady.value = false
Log.w(TAG, "init auth failed storageUuid=$storageUuid", e)
throw e
} catch (_: Exception) {
} catch (e: Exception) {
_storageReady.value = false
throw Exception("Yandex storage init failed")
Log.w(TAG, "init failed storageUuid=$storageUuid", e)
throw Exception("Yandex storage init failed", e)
}
}
@@ -432,6 +436,7 @@ class YandexStorageAccessor(
}
companion object {
private const val TAG = "YandexStorageAcc"
private const val SYSTEM_HIDDEN_DIRNAME = "wallenc-yandex-system"
private const val DATA_PAGE_LENGTH = 10
private const val API_LIST_LIMIT = 200

View File

@@ -1,6 +1,6 @@
package com.github.nullptroma.wallenc.data.vaults.yandex
import com.github.nullptroma.wallenc.data.network.yandexdisk.repository.YandexDiskAuthException
import com.github.nullptroma.wallenc.data.network.yandexdisk.YandexDiskAuthException
import com.github.nullptroma.wallenc.data.network.yandexdisk.repository.YandexDiskRepository
import com.github.nullptroma.wallenc.data.storages.yandex.YandexStorage
import com.github.nullptroma.wallenc.domain.datatypes.StorageEncryptionInfo
@@ -12,6 +12,7 @@ import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import android.util.Log
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.util.UUID
@@ -64,12 +65,15 @@ class YandexVault(
_availableSpace.value = (total - used).coerceAtLeast(0L)
_vaultReachable.value = true
_storages.value = loadStoragesList()
} catch (_: YandexDiskAuthException) {
Log.d(TAG, "refresh ok uuid=$uuid storages=${_storages.value.size}")
} catch (e: YandexDiskAuthException) {
_vaultReachable.value = false
_storages.value = emptyList()
} catch (_: Exception) {
Log.w(TAG, "refresh auth failed uuid=$uuid: ${e.message}")
} catch (e: Exception) {
_vaultReachable.value = false
_storages.value = emptyList()
Log.w(TAG, "refresh failed uuid=$uuid", e)
}
}
@@ -96,8 +100,8 @@ class YandexVault(
try {
storage.init()
out.add(storage)
} catch (_: Exception) {
// пропускаем битое/частично созданное хранилище
} catch (e: Exception) {
Log.w(TAG, "skip broken storage uuid=$storageUuid: ${e.message}")
}
}
if (items.size < APP_LIST_LIMIT) break
@@ -108,6 +112,7 @@ class YandexVault(
override suspend fun createStorage(): IStorage = withContext(ioDispatcher) {
val id = UUID.randomUUID()
Log.d(TAG, "createStorage start vault=$uuid storage=$id")
repo.createFolder("app:/$id")
val storage = YandexStorage(
uuid = id,
@@ -121,6 +126,7 @@ class YandexVault(
)
storage.init()
_storages.value = _storages.value + storage
Log.d(TAG, "createStorage done storage=$id")
storage
}
@@ -132,11 +138,13 @@ class YandexVault(
override suspend fun remove(storage: IStorage) = withContext(ioDispatcher) {
if (storage !is YandexStorage) return@withContext
Log.d(TAG, "remove storage=${storage.uuid}")
repo.delete("app:/${storage.uuid}", permanently = true)
_storages.value = _storages.value.filter { it.uuid != storage.uuid }
}
private companion object {
private const val TAG = "YandexVault"
private const val APP_LIST_LIMIT = 200
}
}

View File

@@ -44,9 +44,9 @@ room-ktx = { group = "androidx.room", name = "room-ktx", version.ref = "room" }
room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "room" }
# Retrofit
retrofit = { group="com.squareup.retrofit2", name = "retrofit", version.ref = "retrofit"}
retrofit-converter-scalars = { group="com.squareup.retrofit2", name = "converter-scalars", version.ref = "retrofit"}
retrofit-converter-jackson = { group="com.squareup.retrofit2", name = "converter-jackson", version.ref = "retrofit"}
retrofit = { group = "com.squareup.retrofit2", name = "retrofit", version.ref = "retrofit" }
retrofit-converter-scalars = { group = "com.squareup.retrofit2", name = "converter-scalars", version.ref = "retrofit" }
retrofit-converter-jackson = { group = "com.squareup.retrofit2", name = "converter-jackson", version.ref = "retrofit" }
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
junit = { group = "junit", name = "junit", version.ref = "junit" }

View File

@@ -23,6 +23,7 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.launch
import java.util.UUID
import kotlin.system.measureTimeMillis
@@ -32,7 +33,7 @@ import kotlin.system.measureTimeMillis
*/
abstract class AbstractVaultBrowserViewModel(
storagesFlow: Flow<List<IStorage>>,
private val canAddStorage: Boolean,
private val vaultAvailabilityFlow: Flow<Boolean>,
private val resolveCreateVaultUuid: () -> UUID?,
private val removeStorageUseCase: RemoveStorageUseCase,
private val getOpenedStoragesUseCase: GetOpenedStoragesUseCase,
@@ -43,7 +44,7 @@ abstract class AbstractVaultBrowserViewModel(
private val taskOrchestrator: ITaskOrchestrator,
private val logger: ILogger,
) : ViewModelBase<VaultBrowserScreenState>(
VaultBrowserScreenState(storagesList = emptyList(), isLoading = true, canAddStorage = canAddStorage),
VaultBrowserScreenState(storagesList = emptyList(), isLoading = true, addStorageFabEnabled = false),
) {
private val _messages = MutableSharedFlow<String>()
@@ -63,6 +64,14 @@ abstract class AbstractVaultBrowserViewModel(
init {
collectFlows(storagesFlow)
viewModelScope.launch {
vaultAvailabilityFlow
.distinctUntilChanged()
.collect { available ->
updateState(state.value.copy(addStorageFabEnabled = available))
logger.debug(TAG, "vault availability → add FAB enabled=$available")
}
}
}
private fun updateStateLoading() {
@@ -125,20 +134,36 @@ abstract class AbstractVaultBrowserViewModel(
}
fun createStorage() {
if (!state.value.canAddStorage) return
if (!state.value.addStorageFabEnabled) {
logger.debug(TAG, "createStorage ignored (vault unavailable or FAB disabled)")
return
}
logger.debug(TAG, "createStorage: enqueue task")
taskOrchestrator.enqueue(
title = "Create storage",
dispatcher = Dispatchers.IO,
work = { ctx ->
try {
ctx.log(TaskLogLevel.Info, "Creating storage…")
val uuid = resolveCreateVaultUuid()
?: throw IllegalStateException("Vault is not available")
manageVaultUseCase.createStorage(uuid)
logger.debug(TAG, "createStorage: vaultUuid=$uuid")
val storage = manageVaultUseCase.createStorage(uuid)
ctx.log(TaskLogLevel.Info, "Storage created")
logger.debug(TAG, "createStorage: done storageUuid=${storage.uuid}")
} catch (e: Exception) {
logger.debug(TAG, "createStorage failed: ${e.stackTraceToString()}")
ctx.log(TaskLogLevel.Error, e.message ?: e.toString())
throw e
}
},
)
}
private companion object {
private const val TAG = "VaultBrowser"
}
private val storageOpMutex = Any()
private val runningStorages = mutableSetOf<UUID>()

View File

@@ -34,7 +34,9 @@ class LocalVaultViewModel @Inject constructor(
storagesFlow = vaultsManager.vaults
.map { vaults -> vaults.described().locals.firstOrNull() }
.flatMapLatest { v -> v?.storages ?: flowOf(emptyList()) },
canAddStorage = true,
vaultAvailabilityFlow = vaultsManager.vaults
.map { vaults -> vaults.described().locals.firstOrNull() }
.flatMapLatest { v -> v?.isAvailable ?: flowOf(false) },
resolveCreateVaultUuid = { vaultsManager.vaults.value.described().locals.firstOrNull()?.uuid },
removeStorageUseCase = removeStorageUseCase,
getOpenedStoragesUseCase = getOpenedStoragesUseCase,

View File

@@ -11,6 +11,8 @@ import com.github.nullptroma.wallenc.domain.usecases.RenameStorageUseCase
import com.github.nullptroma.wallenc.domain.usecases.StorageFileManagementUseCase
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import java.util.UUID
import javax.inject.Inject
@@ -28,8 +30,9 @@ class RemoteVaultViewModel @Inject constructor(
logger: ILogger,
) : AbstractVaultBrowserViewModel(
storagesFlow = manageVaultUseCase.storagesOf(savedStateHandle.requireVaultUuid()),
canAddStorage = false,
resolveCreateVaultUuid = { null },
vaultAvailabilityFlow = manageVaultUseCase.observe(savedStateHandle.requireVaultUuid())
.flatMapLatest { v -> v?.isAvailable ?: flowOf(false) },
resolveCreateVaultUuid = { savedStateHandle.requireVaultUuid() },
removeStorageUseCase = removeStorageUseCase,
getOpenedStoragesUseCase = getOpenedStoragesUseCase,
storageFileManagementUseCase = storageFileManagementUseCase,

View File

@@ -50,13 +50,22 @@ fun VaultBrowserScreen(
modifier = modifier,
contentWindowInsets = WindowInsets(0.dp),
floatingActionButton = {
if (uiState.canAddStorage) {
val fabEnabled = uiState.addStorageFabEnabled
FloatingActionButton(
onClick = { viewModel.createStorage() },
onClick = { if (fabEnabled) viewModel.createStorage() },
containerColor = if (fabEnabled) {
MaterialTheme.colorScheme.primaryContainer
} else {
MaterialTheme.colorScheme.surfaceVariant
},
contentColor = if (fabEnabled) {
MaterialTheme.colorScheme.onPrimaryContainer
} else {
MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.55f)
},
) {
Icon(Icons.Filled.Add, contentDescription = null)
}
}
},
) { innerPadding ->
LazyColumn(

View File

@@ -6,5 +6,6 @@ import com.github.nullptroma.wallenc.domain.interfaces.IStorageInfo
data class VaultBrowserScreenState(
val storagesList: List<Tree<IStorageInfo>>,
val isLoading: Boolean,
val canAddStorage: Boolean = false,
/** FAB «добавить storage»: активна только когда vault доступен (сеть/API/путь). */
val addStorageFabEnabled: Boolean = false,
)