Локальное хранилище теперь читает файлы и создаёт .wallenc-meta
This commit is contained in:
@@ -12,7 +12,7 @@ android {
|
|||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId = "com.github.nullptroma.wallenc.app"
|
applicationId = "com.github.nullptroma.wallenc.app"
|
||||||
minSdk = 24
|
minSdk = 26
|
||||||
targetSdk = 34
|
targetSdk = 34
|
||||||
versionCode = 1
|
versionCode = 1
|
||||||
versionName = "1.0"
|
versionName = "1.0"
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package com.github.nullptroma.wallenc.app.di.modules.domain
|
|||||||
|
|
||||||
import com.github.nullptroma.wallenc.domain.models.IVaultsManager
|
import com.github.nullptroma.wallenc.domain.models.IVaultsManager
|
||||||
import com.github.nullptroma.wallenc.domain.usecases.GetAllRawStoragesUseCase
|
import com.github.nullptroma.wallenc.domain.usecases.GetAllRawStoragesUseCase
|
||||||
|
import com.github.nullptroma.wallenc.domain.usecases.StorageFileManagementUseCase
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
import dagger.Provides
|
import dagger.Provides
|
||||||
import dagger.hilt.InstallIn
|
import dagger.hilt.InstallIn
|
||||||
@@ -16,4 +17,10 @@ class UseCasesModule {
|
|||||||
fun provideGetAllRawStoragesUseCase(vaultsManager: IVaultsManager): GetAllRawStoragesUseCase {
|
fun provideGetAllRawStoragesUseCase(vaultsManager: IVaultsManager): GetAllRawStoragesUseCase {
|
||||||
return GetAllRawStoragesUseCase(vaultsManager)
|
return GetAllRawStoragesUseCase(vaultsManager)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
fun provideStorageFileManagementUseCase(): StorageFileManagementUseCase {
|
||||||
|
return StorageFileManagementUseCase()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -9,7 +9,7 @@ android {
|
|||||||
compileSdk = 34
|
compileSdk = 34
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
minSdk = 24
|
minSdk = 26
|
||||||
|
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
consumerProguardFiles("consumer-rules.pro")
|
consumerProguardFiles("consumer-rules.pro")
|
||||||
@@ -34,6 +34,10 @@ android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
// jackson
|
||||||
|
implementation(libs.jackson.module.kotlin)
|
||||||
|
implementation(libs.jackson.datatype.jsr310)
|
||||||
|
|
||||||
// Timber
|
// Timber
|
||||||
implementation(libs.timber)
|
implementation(libs.timber)
|
||||||
|
|
||||||
@@ -45,9 +49,8 @@ dependencies {
|
|||||||
|
|
||||||
// Retrofit
|
// Retrofit
|
||||||
implementation(libs.retrofit)
|
implementation(libs.retrofit)
|
||||||
implementation(libs.retrofit.converter.gson)
|
|
||||||
implementation(libs.retrofit.converter.scalars)
|
implementation(libs.retrofit.converter.scalars)
|
||||||
implementation(libs.google.gson)
|
implementation(libs.retrofit.converter.jackson)
|
||||||
|
|
||||||
implementation(libs.androidx.core.ktx)
|
implementation(libs.androidx.core.ktx)
|
||||||
implementation(libs.androidx.appcompat)
|
implementation(libs.androidx.appcompat)
|
||||||
|
|||||||
@@ -1,6 +1,14 @@
|
|||||||
package com.github.nullptroma.wallenc.data.vaults.local
|
package com.github.nullptroma.wallenc.data.vaults.local
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.core.JacksonException
|
||||||
|
import com.fasterxml.jackson.core.JsonParseException
|
||||||
|
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
||||||
|
import com.fasterxml.jackson.module.kotlin.readValue
|
||||||
|
import com.github.nullptroma.wallenc.data.vaults.local.entity.LocalDirectory
|
||||||
|
import com.github.nullptroma.wallenc.data.vaults.local.entity.LocalFile
|
||||||
|
import com.github.nullptroma.wallenc.data.vaults.local.entity.LocalMetaInfo
|
||||||
import com.github.nullptroma.wallenc.domain.datatypes.DataPackage
|
import com.github.nullptroma.wallenc.domain.datatypes.DataPackage
|
||||||
|
import com.github.nullptroma.wallenc.domain.datatypes.DataPage
|
||||||
import com.github.nullptroma.wallenc.domain.models.IDirectory
|
import com.github.nullptroma.wallenc.domain.models.IDirectory
|
||||||
import com.github.nullptroma.wallenc.domain.models.IFile
|
import com.github.nullptroma.wallenc.domain.models.IFile
|
||||||
import com.github.nullptroma.wallenc.domain.models.IStorageAccessor
|
import com.github.nullptroma.wallenc.domain.models.IStorageAccessor
|
||||||
@@ -11,90 +19,256 @@ import kotlinx.coroutines.flow.MutableSharedFlow
|
|||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.SharedFlow
|
import kotlinx.coroutines.flow.SharedFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.flow
|
||||||
|
import kotlinx.coroutines.flow.flowOn
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
import timber.log.Timber
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import java.io.OutputStream
|
import java.io.OutputStream
|
||||||
|
import java.nio.file.Path
|
||||||
|
import java.time.LocalDateTime
|
||||||
import kotlin.io.path.Path
|
import kotlin.io.path.Path
|
||||||
|
import kotlin.io.path.absolute
|
||||||
import kotlin.io.path.fileSize
|
import kotlin.io.path.fileSize
|
||||||
|
import kotlin.io.path.pathString
|
||||||
|
import kotlin.io.path.relativeTo
|
||||||
|
|
||||||
class LocalStorageAccessor(
|
class LocalStorageAccessor(
|
||||||
private val absolutePath: String,
|
absolutePath: String,
|
||||||
private val ioDispatcher: CoroutineDispatcher
|
private val ioDispatcher: CoroutineDispatcher
|
||||||
) : IStorageAccessor {
|
) : IStorageAccessor {
|
||||||
private val _size = MutableStateFlow<Long?>(null)
|
private val _absolutePath: Path = Path(absolutePath).normalize().absolute()
|
||||||
private val _numberOfFiles = MutableStateFlow<Int?>(null)
|
private val _jackson = jacksonObjectMapper().apply { findAndRegisterModules() }
|
||||||
private val _isAvailable = MutableStateFlow(false)
|
|
||||||
private val _filesUpdates = MutableSharedFlow<DataPackage<IFile>>()
|
|
||||||
private val _dirsUpdates = MutableSharedFlow<DataPackage<IDirectory>>()
|
|
||||||
|
|
||||||
override val size: StateFlow<Long?>
|
private val _size = MutableStateFlow<Long?>(null)
|
||||||
get() = _size
|
override val size: StateFlow<Long?> = _size
|
||||||
override val numberOfFiles: StateFlow<Int?>
|
|
||||||
get() = _numberOfFiles
|
private val _numberOfFiles = MutableStateFlow<Int?>(null)
|
||||||
override val isAvailable: StateFlow<Boolean>
|
override val numberOfFiles: StateFlow<Int?> = _numberOfFiles
|
||||||
get() = _isAvailable
|
|
||||||
override val filesUpdates: SharedFlow<DataPackage<IFile>>
|
private val _isAvailable = MutableStateFlow(false)
|
||||||
get() = _filesUpdates
|
override val isAvailable: StateFlow<Boolean> = _isAvailable
|
||||||
override val dirsUpdates: SharedFlow<DataPackage<IDirectory>>
|
|
||||||
get() = _dirsUpdates
|
private val _filesUpdates = MutableSharedFlow<DataPackage<IFile>>()
|
||||||
|
override val filesUpdates: SharedFlow<DataPackage<IFile>> = _filesUpdates
|
||||||
|
|
||||||
|
private val _dirsUpdates = MutableSharedFlow<DataPackage<IDirectory>>()
|
||||||
|
override val dirsUpdates: SharedFlow<DataPackage<IDirectory>> = _dirsUpdates
|
||||||
|
|
||||||
init {
|
init {
|
||||||
|
// запускам сканирование хранилища
|
||||||
CoroutineScope(ioDispatcher).launch {
|
CoroutineScope(ioDispatcher).launch {
|
||||||
scanStorage()
|
Timber.d("Local storage path: $_absolutePath")
|
||||||
|
updateSizeAndNumOfFiles()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Проверяет существование корневого пути Storage в файловой системе, изменяет _isAvailable
|
||||||
|
*/
|
||||||
private fun checkAvailable(): Boolean {
|
private fun checkAvailable(): Boolean {
|
||||||
_isAvailable.value = File(absolutePath).exists()
|
_isAvailable.value = _absolutePath.toFile().exists()
|
||||||
return _isAvailable.value
|
return _isAvailable.value
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun forAllFiles(dir: File, callback: (File) -> Unit) {
|
/**
|
||||||
if (dir.exists() == false)
|
* Перебирает все файлы в файловой системе
|
||||||
|
* @param dir стартовый каталог
|
||||||
|
* @param maxDepth максимальная глубина (отрицательное для бесконечной)
|
||||||
|
* @param callback метод обратного вызова для каждого файла и директории
|
||||||
|
*/
|
||||||
|
private suspend fun scanFileSystem(
|
||||||
|
dir: File,
|
||||||
|
maxDepth: Int,
|
||||||
|
callback: suspend (File) -> Unit,
|
||||||
|
useCallbackForSelf: Boolean = true
|
||||||
|
) {
|
||||||
|
if (!dir.exists())
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
||||||
|
val children = dir.listFiles()
|
||||||
|
if (children != null) {
|
||||||
|
// вызвать коллбек для каждого элемента директории
|
||||||
|
for (child in children) {
|
||||||
|
callback(child)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (maxDepth != 0) {
|
||||||
|
val nextMaxDepth = if (maxDepth > 0) maxDepth - 1 else maxDepth
|
||||||
|
for (child in children) {
|
||||||
|
if (child.isDirectory) {
|
||||||
|
scanFileSystem(child, nextMaxDepth, callback, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (useCallbackForSelf)
|
||||||
callback(dir)
|
callback(dir)
|
||||||
|
|
||||||
val nextDirs = dir.listFiles()
|
|
||||||
if (nextDirs != null) {
|
|
||||||
for (nextDir in nextDirs) {
|
|
||||||
forAllFiles(nextDir, callback)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun scanStorage() = withContext(ioDispatcher) {
|
/**
|
||||||
_isAvailable.value = File(absolutePath).exists()
|
* Перебирает все файлы и каталоги в relativePath и возвращает с мета-информацией
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
private suspend fun scanStorage(
|
||||||
|
baseStoragePath: String,
|
||||||
|
maxDepth: Int,
|
||||||
|
fileCallback: suspend (LocalFile) -> Unit = {},
|
||||||
|
dirCallback: suspend (LocalDirectory) -> Unit = {}
|
||||||
|
) {
|
||||||
|
if (!checkAvailable())
|
||||||
|
throw Exception("Not available")
|
||||||
|
val basePath = Path(_absolutePath.pathString, baseStoragePath)
|
||||||
|
scanFileSystem(basePath.toFile(), maxDepth, { file ->
|
||||||
|
val filePath = Path(file.absolutePath)
|
||||||
|
|
||||||
|
// если это файл с мета-информацией - пропустить
|
||||||
|
if (filePath.pathString.endsWith(META_INFO_POSTFIX)) {
|
||||||
|
// Если не удаётся прочитать метаданные или они указывают на несуществующий файл - удалить
|
||||||
|
try {
|
||||||
|
val reader = file.bufferedReader()
|
||||||
|
val meta : LocalMetaInfo = _jackson.readValue(reader)
|
||||||
|
val fileInMeta = File(Path(_absolutePath.pathString, meta.path).pathString)
|
||||||
|
if (!fileInMeta.exists())
|
||||||
|
file.delete()
|
||||||
|
} catch (e: JacksonException) {
|
||||||
|
file.delete()
|
||||||
|
}
|
||||||
|
return@scanFileSystem
|
||||||
|
}
|
||||||
|
|
||||||
|
val metaFilePath = Path(
|
||||||
|
if (file.isFile) {
|
||||||
|
file.absolutePath + META_INFO_POSTFIX
|
||||||
|
} else {
|
||||||
|
Path(file.absolutePath, META_INFO_POSTFIX).pathString
|
||||||
|
}
|
||||||
|
)
|
||||||
|
val metaFile = metaFilePath.toFile()
|
||||||
|
val metaInfo: LocalMetaInfo
|
||||||
|
val storageFilePath = "/" + filePath.relativeTo(_absolutePath)
|
||||||
|
|
||||||
|
if (!metaFile.exists()) {
|
||||||
|
metaInfo = createNewLocalMetaInfo(storageFilePath, filePath.fileSize())
|
||||||
|
_jackson.writeValue(metaFile, metaInfo)
|
||||||
|
} else {
|
||||||
|
var readMeta: LocalMetaInfo
|
||||||
|
try {
|
||||||
|
val reader = metaFile.bufferedReader()
|
||||||
|
readMeta = _jackson.readValue(reader)
|
||||||
|
} catch (e: JacksonException) {
|
||||||
|
// если файл повреждён - пересоздать
|
||||||
|
readMeta = createNewLocalMetaInfo(storageFilePath, filePath.fileSize())
|
||||||
|
_jackson.writeValue(metaFile, readMeta)
|
||||||
|
}
|
||||||
|
metaInfo = readMeta
|
||||||
|
}
|
||||||
|
|
||||||
|
if (file.isFile) {
|
||||||
|
fileCallback(LocalFile(metaInfo))
|
||||||
|
} else {
|
||||||
|
dirCallback(LocalDirectory(metaInfo, null))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Создаёт LocalMetaInfo, не требуя наличие файла в файловой системе
|
||||||
|
* @param storagePath полный путь в Storage
|
||||||
|
* @param size размер файла
|
||||||
|
*/
|
||||||
|
private fun createNewLocalMetaInfo(storagePath: String, size: Long): LocalMetaInfo {
|
||||||
|
return LocalMetaInfo(
|
||||||
|
size = size,
|
||||||
|
isDeleted = false,
|
||||||
|
isHidden = false,
|
||||||
|
lastModified = LocalDateTime.now(),
|
||||||
|
path = storagePath
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Считает файлы и их размер. Не бросает исключения, если файлы недоступны
|
||||||
|
* @throws none Если возникла ошибка, оставляет размер и количества файлов равными null
|
||||||
|
*/
|
||||||
|
private suspend fun updateSizeAndNumOfFiles() {
|
||||||
|
if (!checkAvailable()) {
|
||||||
|
_size.value = null
|
||||||
|
_numberOfFiles.value = null
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
var size = 0L
|
var size = 0L
|
||||||
var numOfFiles = 0
|
var numOfFiles = 0
|
||||||
|
|
||||||
forAllFiles(File(absolutePath)) {
|
scanStorage(baseStoragePath = "/", maxDepth = -1, fileCallback = {
|
||||||
if (it.isFile) {
|
size += it.metaInfo.size
|
||||||
numOfFiles++
|
numOfFiles++
|
||||||
size += Path(it.path).fileSize()
|
})
|
||||||
}
|
|
||||||
}
|
|
||||||
_size.value = size
|
_size.value = size
|
||||||
_numberOfFiles.value = numOfFiles
|
_numberOfFiles.value = numOfFiles
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getAllFiles(): List<IFile> = withContext(ioDispatcher) {
|
override suspend fun getAllFiles(): List<IFile> = withContext(ioDispatcher) {
|
||||||
if(checkAvailable() == false)
|
if (!checkAvailable())
|
||||||
return@withContext listOf()
|
return@withContext listOf()
|
||||||
|
|
||||||
val list = mutableListOf<IFile>()
|
val list = mutableListOf<IFile>()
|
||||||
return@withContext listOf()
|
scanStorage(baseStoragePath = "/", maxDepth = -1, fileCallback = {
|
||||||
|
list.add(it)
|
||||||
|
})
|
||||||
|
return@withContext list
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getFiles(path: String): List<IFile> = withContext(ioDispatcher) {
|
override suspend fun getFiles(path: String): List<IFile> = withContext(ioDispatcher) {
|
||||||
TODO("Not yet implemented")
|
if (!checkAvailable())
|
||||||
|
return@withContext listOf()
|
||||||
|
|
||||||
|
val list = mutableListOf<IFile>()
|
||||||
|
scanStorage(baseStoragePath = path, maxDepth = 0, fileCallback = {
|
||||||
|
list.add(it)
|
||||||
|
})
|
||||||
|
return@withContext list
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getFilesFlow(path: String): Flow<DataPackage<IFile>> {
|
override fun getFilesFlow(path: String): Flow<DataPackage<List<IFile>>> = flow {
|
||||||
TODO("Not yet implemented")
|
if (!checkAvailable())
|
||||||
|
return@flow
|
||||||
|
|
||||||
|
val buf = mutableListOf<IFile>()
|
||||||
|
var pageNumber = 0
|
||||||
|
scanStorage(baseStoragePath = path, maxDepth = 0, fileCallback = {
|
||||||
|
if(buf.size == DATA_PAGE_LENGTH) {
|
||||||
|
val page = DataPage(
|
||||||
|
list = buf.toList(),
|
||||||
|
isLoading = false,
|
||||||
|
isError = false,
|
||||||
|
hasNext = true,
|
||||||
|
pageLength = DATA_PAGE_LENGTH,
|
||||||
|
pageIndex = pageNumber++
|
||||||
|
)
|
||||||
|
emit(page)
|
||||||
|
buf.clear()
|
||||||
}
|
}
|
||||||
|
buf.add(it)
|
||||||
|
})
|
||||||
|
// отправка последней страницы
|
||||||
|
val page = DataPage(
|
||||||
|
list = buf.toList(),
|
||||||
|
isLoading = false,
|
||||||
|
isError = false,
|
||||||
|
hasNext = false,
|
||||||
|
pageLength = DATA_PAGE_LENGTH,
|
||||||
|
pageIndex = pageNumber++
|
||||||
|
)
|
||||||
|
emit(page)
|
||||||
|
}.flowOn(ioDispatcher)
|
||||||
|
|
||||||
override suspend fun getAllDirs(): List<IDirectory> = withContext(ioDispatcher) {
|
override suspend fun getAllDirs(): List<IDirectory> = withContext(ioDispatcher) {
|
||||||
TODO("Not yet implemented")
|
TODO("Not yet implemented")
|
||||||
@@ -104,7 +278,7 @@ class LocalStorageAccessor(
|
|||||||
TODO("Not yet implemented")
|
TODO("Not yet implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getDirsFlow(path: String): Flow<DataPackage<IDirectory>> {
|
override fun getDirsFlow(path: String): Flow<DataPackage<List<IDirectory>>> {
|
||||||
TODO("Not yet implemented")
|
TODO("Not yet implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -139,4 +313,9 @@ class LocalStorageAccessor(
|
|||||||
override suspend fun moveToTrash(path: String) = withContext(ioDispatcher) {
|
override suspend fun moveToTrash(path: String) = withContext(ioDispatcher) {
|
||||||
TODO("Not yet implemented")
|
TODO("Not yet implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val META_INFO_POSTFIX = ".wallenc-meta"
|
||||||
|
private const val DATA_PAGE_LENGTH = 10
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -18,12 +18,23 @@ import kotlin.io.path.createDirectory
|
|||||||
import kotlin.io.path.pathString
|
import kotlin.io.path.pathString
|
||||||
|
|
||||||
class LocalVault(private val ioDispatcher: CoroutineDispatcher, context: Context) : IVault {
|
class LocalVault(private val ioDispatcher: CoroutineDispatcher, context: Context) : IVault {
|
||||||
private val _path = MutableStateFlow<File?>(null)
|
override val type: VaultType = VaultType.LOCAL
|
||||||
private val _storages = MutableStateFlow(listOf<IStorage>())
|
override val uuid: UUID
|
||||||
private val _totalSpace = MutableStateFlow(null)
|
get() = TODO("Not yet implemented")
|
||||||
private val _availableSpace = MutableStateFlow(null)
|
|
||||||
private val _isAvailable = MutableStateFlow(false)
|
|
||||||
|
|
||||||
|
private val _storages = MutableStateFlow(listOf<IStorage>())
|
||||||
|
override val storages: StateFlow<List<IStorage>> = _storages
|
||||||
|
|
||||||
|
private val _isAvailable = MutableStateFlow(false)
|
||||||
|
override val isAvailable: StateFlow<Boolean> = _isAvailable
|
||||||
|
|
||||||
|
private val _totalSpace = MutableStateFlow(null)
|
||||||
|
override val totalSpace: StateFlow<Int?> = _totalSpace
|
||||||
|
|
||||||
|
private val _availableSpace = MutableStateFlow(null)
|
||||||
|
override val availableSpace: StateFlow<Int?> = _availableSpace
|
||||||
|
|
||||||
|
private val _path = MutableStateFlow<File?>(null)
|
||||||
|
|
||||||
init {
|
init {
|
||||||
CoroutineScope(ioDispatcher).launch {
|
CoroutineScope(ioDispatcher).launch {
|
||||||
@@ -35,7 +46,7 @@ class LocalVault(private val ioDispatcher: CoroutineDispatcher, context: Context
|
|||||||
|
|
||||||
private fun readStorages() {
|
private fun readStorages() {
|
||||||
val path = _path.value
|
val path = _path.value
|
||||||
if (path == null || _isAvailable.value == false)
|
if (path == null || !_isAvailable.value)
|
||||||
return
|
return
|
||||||
|
|
||||||
val dirs = path.listFiles()?.filter { it.isDirectory }
|
val dirs = path.listFiles()?.filter { it.isDirectory }
|
||||||
@@ -49,7 +60,7 @@ class LocalVault(private val ioDispatcher: CoroutineDispatcher, context: Context
|
|||||||
|
|
||||||
override suspend fun createStorage(): IStorage = withContext(ioDispatcher) {
|
override suspend fun createStorage(): IStorage = withContext(ioDispatcher) {
|
||||||
val path = _path.value
|
val path = _path.value
|
||||||
if (path == null || _isAvailable.value == false)
|
if (path == null || !_isAvailable.value)
|
||||||
throw Exception("Not available")
|
throw Exception("Not available")
|
||||||
|
|
||||||
val uuid = UUID.randomUUID()
|
val uuid = UUID.randomUUID()
|
||||||
@@ -78,16 +89,4 @@ class LocalVault(private val ioDispatcher: CoroutineDispatcher, context: Context
|
|||||||
override suspend fun remove(storage: IStorage) = withContext(ioDispatcher) {
|
override suspend fun remove(storage: IStorage) = withContext(ioDispatcher) {
|
||||||
TODO("Not yet implemented")
|
TODO("Not yet implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
override val type: VaultType = VaultType.LOCAL
|
|
||||||
override val uuid: UUID
|
|
||||||
get() = TODO("Not yet implemented")
|
|
||||||
override val storages: StateFlow<List<IStorage>>
|
|
||||||
get() = _storages
|
|
||||||
override val isAvailable: StateFlow<Boolean>
|
|
||||||
get() = _isAvailable
|
|
||||||
override val totalSpace: StateFlow<Int?>
|
|
||||||
get() = _totalSpace
|
|
||||||
override val availableSpace: StateFlow<Int?>
|
|
||||||
get() = _availableSpace
|
|
||||||
}
|
}
|
||||||
@@ -2,7 +2,7 @@ package com.github.nullptroma.wallenc.data.vaults.local.entity
|
|||||||
|
|
||||||
import com.github.nullptroma.wallenc.domain.models.IDirectory
|
import com.github.nullptroma.wallenc.domain.models.IDirectory
|
||||||
|
|
||||||
class LocalDirectory(
|
data class LocalDirectory(
|
||||||
override val metaInfo: LocalMetaInfo,
|
override val metaInfo: LocalMetaInfo,
|
||||||
override val elementsCount: Int
|
override val elementsCount: Int?
|
||||||
) : IDirectory
|
) : IDirectory
|
||||||
@@ -2,4 +2,4 @@ package com.github.nullptroma.wallenc.data.vaults.local.entity
|
|||||||
|
|
||||||
import com.github.nullptroma.wallenc.domain.models.IFile
|
import com.github.nullptroma.wallenc.domain.models.IFile
|
||||||
|
|
||||||
class LocalFile(override val metaInfo: LocalMetaInfo) : IFile
|
data class LocalFile(override val metaInfo: LocalMetaInfo) : IFile
|
||||||
@@ -3,20 +3,10 @@ package com.github.nullptroma.wallenc.data.vaults.local.entity
|
|||||||
import com.github.nullptroma.wallenc.domain.models.IMetaInfo
|
import com.github.nullptroma.wallenc.domain.models.IMetaInfo
|
||||||
import java.time.LocalDateTime
|
import java.time.LocalDateTime
|
||||||
|
|
||||||
class LocalMetaInfo : IMetaInfo {
|
data class LocalMetaInfo(
|
||||||
override val name: String
|
override val size: Long,
|
||||||
override val size: Int
|
override val isDeleted: Boolean,
|
||||||
get() = TODO("Not yet implemented")
|
override val isHidden: Boolean,
|
||||||
override val isDeleted: Boolean
|
override val lastModified: LocalDateTime,
|
||||||
get() = TODO("Not yet implemented")
|
|
||||||
override val isHidden: Boolean
|
|
||||||
get() = TODO("Not yet implemented")
|
|
||||||
override val lastModified: LocalDateTime
|
|
||||||
get() = TODO("Not yet implemented")
|
|
||||||
override val path: String
|
override val path: String
|
||||||
get() = TODO("Not yet implemented")
|
) : IMetaInfo
|
||||||
|
|
||||||
init {
|
|
||||||
name = ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
package com.github.nullptroma.wallenc.domain.datatypes
|
package com.github.nullptroma.wallenc.domain.datatypes
|
||||||
|
|
||||||
open class DataPackage<T>(
|
sealed class DataPackage<T>(
|
||||||
val data: T,
|
val data: T,
|
||||||
val isLoading: Boolean? = false,
|
val isLoading: Boolean? = false,
|
||||||
val isError: Boolean? = false
|
val isError: Boolean? = false
|
||||||
|
|||||||
@@ -2,7 +2,9 @@ package com.github.nullptroma.wallenc.domain.datatypes
|
|||||||
|
|
||||||
class DataPage<T>(
|
class DataPage<T>(
|
||||||
list: List<T>,
|
list: List<T>,
|
||||||
|
isLoading: Boolean? = false,
|
||||||
|
isError: Boolean? = false,
|
||||||
val hasNext: Boolean? = false,
|
val hasNext: Boolean? = false,
|
||||||
val pageLength: Int,
|
val pageLength: Int,
|
||||||
val pageNumber: Int
|
val pageIndex: Int
|
||||||
) : DataPackage<List<T>>(list)
|
) : DataPackage<List<T>>(data = list, isLoading = isLoading, isError = isError)
|
||||||
@@ -2,5 +2,5 @@ package com.github.nullptroma.wallenc.domain.models
|
|||||||
|
|
||||||
interface IDirectory {
|
interface IDirectory {
|
||||||
val metaInfo: IMetaInfo
|
val metaInfo: IMetaInfo
|
||||||
val elementsCount: Int
|
val elementsCount: Int?
|
||||||
}
|
}
|
||||||
@@ -4,8 +4,7 @@ import java.time.LocalDateTime
|
|||||||
|
|
||||||
|
|
||||||
interface IMetaInfo {
|
interface IMetaInfo {
|
||||||
val name: String
|
val size: Long
|
||||||
val size: Int
|
|
||||||
val isDeleted: Boolean
|
val isDeleted: Boolean
|
||||||
val isHidden: Boolean
|
val isHidden: Boolean
|
||||||
val lastModified: LocalDateTime
|
val lastModified: LocalDateTime
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ interface IStorageAccessor {
|
|||||||
* @param path Путь к директории
|
* @param path Путь к директории
|
||||||
* @return Поток файлов
|
* @return Поток файлов
|
||||||
*/
|
*/
|
||||||
fun getFilesFlow(path: String): Flow<DataPackage<IFile>>
|
fun getFilesFlow(path: String): Flow<DataPackage<List<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<IDirectory>>
|
fun getDirsFlow(path: String): Flow<DataPackage<List<IDirectory>>>
|
||||||
|
|
||||||
suspend fun touchFile(path: String)
|
suspend fun touchFile(path: String)
|
||||||
suspend fun touchDir(path: String)
|
suspend fun touchDir(path: String)
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package com.github.nullptroma.wallenc.domain.usecases
|
||||||
|
|
||||||
|
import com.github.nullptroma.wallenc.domain.models.IFile
|
||||||
|
import com.github.nullptroma.wallenc.domain.models.IStorage
|
||||||
|
|
||||||
|
class StorageFileManagementUseCase {
|
||||||
|
private var _storage: IStorage? = null
|
||||||
|
|
||||||
|
fun setStorage(storage: IStorage) {
|
||||||
|
_storage = storage
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun getAllFiles(): List<IFile> {
|
||||||
|
val storage = _storage ?: return listOf()
|
||||||
|
return storage.accessor.getAllFiles()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
[versions]
|
[versions]
|
||||||
agp = "8.7.1"
|
agp = "8.7.1"
|
||||||
|
jacksonModuleKotlin = "2.18.2"
|
||||||
kotlin = "2.0.10"
|
kotlin = "2.0.10"
|
||||||
coreKtx = "1.15.0"
|
coreKtx = "1.15.0"
|
||||||
junit = "4.13.2"
|
junit = "4.13.2"
|
||||||
@@ -19,12 +20,13 @@ daggerHilt = "2.52"
|
|||||||
ksp = "2.0.10-1.0.24"
|
ksp = "2.0.10-1.0.24"
|
||||||
room = "2.6.1"
|
room = "2.6.1"
|
||||||
retrofit = "2.11.0"
|
retrofit = "2.11.0"
|
||||||
gson = "2.11.0"
|
|
||||||
appcompat = "1.7.0"
|
appcompat = "1.7.0"
|
||||||
material = "1.12.0"
|
material = "1.12.0"
|
||||||
runtimeAndroid = "1.7.5"
|
runtimeAndroid = "1.7.5"
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
|
jackson-datatype-jsr310 = { module = "com.fasterxml.jackson.datatype:jackson-datatype-jsr310" }
|
||||||
|
jackson-module-kotlin = { module = "com.fasterxml.jackson.module:jackson-module-kotlin", version.ref = "jacksonModuleKotlin" }
|
||||||
kotlin-reflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = "kotlinReflect" }
|
kotlin-reflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = "kotlinReflect" }
|
||||||
kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinxCoroutinesCore" }
|
kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinxCoroutinesCore" }
|
||||||
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" }
|
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" }
|
||||||
@@ -47,8 +49,7 @@ room-compiler = { group = "androidx.room", name = "room-compiler", version.ref =
|
|||||||
# Retrofit
|
# Retrofit
|
||||||
retrofit = { group="com.squareup.retrofit2", name = "retrofit", 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-scalars = { group="com.squareup.retrofit2", name = "converter-scalars", version.ref = "retrofit"}
|
||||||
retrofit-converter-gson = { group="com.squareup.retrofit2", name = "converter-gson", version.ref = "retrofit"}
|
retrofit-converter-jackson = { group="com.squareup.retrofit2", name = "converter-jackson", version.ref = "retrofit"}
|
||||||
google-gson = { group="com.google.code.gson", name = "gson", version.ref = "gson"}
|
|
||||||
|
|
||||||
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
|
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
|
||||||
junit = { group = "junit", name = "junit", version.ref = "junit" }
|
junit = { group = "junit", name = "junit", version.ref = "junit" }
|
||||||
@@ -74,8 +75,9 @@ androidx-runtime-android = { group = "androidx.compose.runtime", name = "runtime
|
|||||||
android-application = { id = "com.android.application", version.ref = "agp" }
|
android-application = { id = "com.android.application", version.ref = "agp" }
|
||||||
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
|
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
|
||||||
compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
|
compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
|
||||||
jetbrains-kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
|
|
||||||
jetbrains-kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
|
jetbrains-kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
|
||||||
|
jetbrains-kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
|
||||||
dagger-hilt = { id = "com.google.dagger.hilt.android", version.ref = "daggerHilt" }
|
dagger-hilt = { id = "com.google.dagger.hilt.android", version.ref = "daggerHilt" }
|
||||||
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
|
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
|
||||||
android-library = { id = "com.android.library", version.ref = "agp" }
|
android-library = { id = "com.android.library", version.ref = "agp" }
|
||||||
|
kotlin-parcelize = { id = "kotlin-parcelize" }
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ plugins {
|
|||||||
alias(libs.plugins.compose.compiler)
|
alias(libs.plugins.compose.compiler)
|
||||||
alias(libs.plugins.dagger.hilt)
|
alias(libs.plugins.dagger.hilt)
|
||||||
alias(libs.plugins.jetbrains.kotlin.serialization)
|
alias(libs.plugins.jetbrains.kotlin.serialization)
|
||||||
id("kotlin-parcelize")
|
alias(libs.plugins.kotlin.parcelize)
|
||||||
alias(libs.plugins.ksp)
|
alias(libs.plugins.ksp)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.github.nullptroma.wallenc.presentation.screens.main.screens.local.vault
|
package com.github.nullptroma.wallenc.presentation.screens.main.screens.local.vault
|
||||||
|
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
@@ -7,6 +8,7 @@ import androidx.compose.material3.Card
|
|||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
@@ -20,12 +22,17 @@ fun LocalVaultScreen(modifier: Modifier = Modifier,
|
|||||||
val uiState by viewModel.state.collectAsStateWithLifecycle()
|
val uiState by viewModel.state.collectAsStateWithLifecycle()
|
||||||
LazyColumn(modifier = modifier) {
|
LazyColumn(modifier = modifier) {
|
||||||
items(uiState.storagesList) {
|
items(uiState.storagesList) {
|
||||||
Card {
|
Card(modifier = Modifier.clickable {
|
||||||
|
viewModel.printAllFilesToLog(it)
|
||||||
|
}) {
|
||||||
|
val available = it.isAvailable.collectAsStateWithLifecycle()
|
||||||
|
val numOfFiles = it.isAvailable.collectAsStateWithLifecycle()
|
||||||
|
val size = it.isAvailable.collectAsStateWithLifecycle()
|
||||||
Column {
|
Column {
|
||||||
Text(it.uuid.toString())
|
Text(it.uuid.toString())
|
||||||
Text("IsAvailable: ${it.isAvailable.value}")
|
Text("IsAvailable: $available")
|
||||||
Text("Files: ${it.numberOfFiles.value}")
|
Text("Files: $numOfFiles")
|
||||||
Text("Size: ${it.size.value}")
|
Text("Size: $size")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +1,24 @@
|
|||||||
package com.github.nullptroma.wallenc.presentation.screens.main.screens.local.vault
|
package com.github.nullptroma.wallenc.presentation.screens.main.screens.local.vault
|
||||||
|
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import com.github.nullptroma.wallenc.domain.models.IStorage
|
||||||
import com.github.nullptroma.wallenc.domain.usecases.GetAllRawStoragesUseCase
|
import com.github.nullptroma.wallenc.domain.usecases.GetAllRawStoragesUseCase
|
||||||
|
import com.github.nullptroma.wallenc.domain.usecases.StorageFileManagementUseCase
|
||||||
import com.github.nullptroma.wallenc.presentation.viewmodel.ViewModelBase
|
import com.github.nullptroma.wallenc.presentation.viewmodel.ViewModelBase
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class LocalVaultViewModel @Inject constructor(private val getAllRawStoragesUseCase: GetAllRawStoragesUseCase) :
|
class LocalVaultViewModel @Inject constructor(
|
||||||
|
private val _getAllRawStoragesUseCase: GetAllRawStoragesUseCase,
|
||||||
|
private val _storageFileManagementUseCase: StorageFileManagementUseCase
|
||||||
|
) :
|
||||||
ViewModelBase<LocalVaultScreenState>(LocalVaultScreenState(listOf())) {
|
ViewModelBase<LocalVaultScreenState>(LocalVaultScreenState(listOf())) {
|
||||||
init {
|
init {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
getAllRawStoragesUseCase.localStorage.storages.collect {
|
_getAllRawStoragesUseCase.localStorage.storages.collect {
|
||||||
val newState = state.value.copy(
|
val newState = state.value.copy(
|
||||||
storagesList = it
|
storagesList = it
|
||||||
)
|
)
|
||||||
@@ -20,4 +26,15 @@ class LocalVaultViewModel @Inject constructor(private val getAllRawStoragesUseCa
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun printAllFilesToLog(storage: IStorage) {
|
||||||
|
_storageFileManagementUseCase.setStorage(storage)
|
||||||
|
viewModelScope.launch {
|
||||||
|
val files = _storageFileManagementUseCase.getAllFiles()
|
||||||
|
for (file in files) {
|
||||||
|
Timber.tag("File")
|
||||||
|
Timber.d(file.metaInfo.toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user