From 49ee38a93637e67bbe017f11d116931ecb29b3d2 Mon Sep 17 00:00:00 2001 From: InsanusMokrassar Date: Thu, 26 Nov 2020 15:03:40 +0600 Subject: [PATCH] FileStandardKeyValueRepo --- CHANGELOG.md | 4 + .../repos/FileStandardKeyValueRepo.kt | 168 ++++++++++++++++++ 2 files changed, 172 insertions(+) create mode 100644 repos/common/src/jvmMain/kotlin/dev/inmo/micro_utils/repos/FileStandardKeyValueRepo.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index 9076a6f3a37..f69a5c3bdd6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ * `Pagination` got new extension: `Pagination#isFirstPage` * `Coroutines`: * New extension `FlowCollector#invoke` has been added +* `Repos` + * `Common` + * `JVM` (and `Android` since `Android API 26`): + * `FileStandardKeyValueRepo` has been added ## 0.4.5 diff --git a/repos/common/src/jvmMain/kotlin/dev/inmo/micro_utils/repos/FileStandardKeyValueRepo.kt b/repos/common/src/jvmMain/kotlin/dev/inmo/micro_utils/repos/FileStandardKeyValueRepo.kt new file mode 100644 index 00000000000..3e698df4e29 --- /dev/null +++ b/repos/common/src/jvmMain/kotlin/dev/inmo/micro_utils/repos/FileStandardKeyValueRepo.kt @@ -0,0 +1,168 @@ +package dev.inmo.micro_utils.repos + +import dev.inmo.micro_utils.pagination.* +import dev.inmo.micro_utils.pagination.utils.reverse +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.* +import java.io.File +import java.nio.file.FileSystems +import java.nio.file.Path +import java.nio.file.StandardWatchEventKinds.* + +private inline val String.isAbsolute + get() = startsWith(File.separator) + +class FileReadStandardKeyValueRepo( + private val folder: File +) : ReadStandardKeyValueRepo { + init { + folder.mkdirs() + } + + override suspend fun get(k: String): File? { + val file = File(folder, k) + if (file.exists()) { + return file + } + return null + } + + override suspend fun values(pagination: Pagination, reversed: Boolean): PaginationResult { + val count = count() + val resultPagination = if (reversed) pagination.reverse(count) else pagination + val filesPaths = folder.list() ?.copyOfRange(resultPagination.firstIndex, resultPagination.lastIndex) ?: return emptyPaginationResult() + if (reversed) { + filesPaths.reverse() + } + return filesPaths.map { File(folder, it) }.createPaginationResult( + resultPagination, + count + ) + } + + override suspend fun keys(pagination: Pagination, reversed: Boolean): PaginationResult { + val count = count() + val resultPagination = if (reversed) pagination.reverse(count) else pagination + val filesPaths = folder.list() ?.copyOfRange(resultPagination.firstIndex, resultPagination.lastIndex) ?: return emptyPaginationResult() + if (reversed) { + filesPaths.reverse() + } + return filesPaths.toList().createPaginationResult( + resultPagination, + count + ) + } + + override suspend fun keys( + v: File, + pagination: Pagination, + reversed: Boolean + ): PaginationResult { + val resultPagination = if (reversed) pagination.reverse(1L) else pagination + return if (resultPagination.isFirstPage) { + val fileSubpath = v.absolutePath.removePrefix(folder.absolutePath) + if (fileSubpath == v.absolutePath) { + emptyList() + } else { + listOf(fileSubpath) + } + } else { + emptyList() + }.createPaginationResult(resultPagination, 1L) + } + + override suspend fun contains(key: String): Boolean { + return File(folder, key).exists() + } + + override suspend fun count(): Long = folder.list() ?.size ?.toLong() ?: 0L +} + +/** + * Files watching will not correctly works on Android with version of API lower than API 26 + */ +class FileWriteStandardKeyValueRepo( + private val folder: File, + filesChangedProcessingScope: CoroutineScope? = null +) : WriteStandardKeyValueRepo { + private val _onNewValue = MutableSharedFlow>() + override val onNewValue: Flow> = _onNewValue.asSharedFlow() + private val _onValueRemoved = MutableSharedFlow() + override val onValueRemoved: Flow = _onValueRemoved.asSharedFlow() + + init { + folder.mkdirs() + filesChangedProcessingScope ?.let { + it.launch { + try { + val folderPath = folder.toPath() + while (isActive) { + val key = try { + folderPath.register(FileSystems.getDefault().newWatchService(), ENTRY_CREATE, ENTRY_MODIFY, ENTRY_DELETE) + } catch (e: Exception) { + // add verbose way to show that file watching is not working + return@launch + } + + for (event in key.pollEvents()) { + val relativeFilePath = (event.context() as? Path) ?: continue + val file = relativeFilePath.toFile() + val relativePath = file.toRelativeString(folder) + + when (event.kind()) { + ENTRY_CREATE, ENTRY_MODIFY -> { + launch { + _onNewValue.emit(relativePath to file) + } + } + ENTRY_DELETE -> { + launch { + _onValueRemoved.emit(relativePath) + } + } + } + } + + if (key.isValid || folder.exists()) { + continue + } + break + } + } catch (e: Throwable) { + // add verbose way to notify that this functionality is disabled + } + } + } + } + + override suspend fun set(toSet: Map) { + supervisorScope { + toSet.map { (filename, fileSource) -> + launch { + val file = File(folder, filename) + + file.delete() + fileSource.copyTo(file, overwrite = true) + _onNewValue.emit(filename to file) + } + } + }.joinAll() + } + + override suspend fun unset(toUnset: List) { + toUnset.forEach { + val file = File(folder, it) + if (file.exists()) { + file.delete() + _onValueRemoved.emit(it) + } + } + } +} + +class FileStandardKeyValueRepo( + folder: File, + filesChangedProcessingScope: CoroutineScope? = null +) : StandardKeyValueRepo, + WriteStandardKeyValueRepo by FileWriteStandardKeyValueRepo(folder, filesChangedProcessingScope), + ReadStandardKeyValueRepo by FileReadStandardKeyValueRepo(folder) \ No newline at end of file