Yandex штуки
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
package com.github.nullptroma.wallenc.data.network.yandexdisk
|
||||
|
||||
import java.io.IOException
|
||||
|
||||
class YandexDiskAuthException(message: String? = null) : IOException(message)
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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" }
|
||||
|
||||
@@ -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>()
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user