full reborn
This commit is contained in:
18
features/files/client/build.gradle
Normal file
18
features/files/client/build.gradle
Normal file
@@ -0,0 +1,18 @@
|
||||
plugins {
|
||||
id "org.jetbrains.kotlin.multiplatform"
|
||||
id "org.jetbrains.kotlin.plugin.serialization"
|
||||
id "com.android.library"
|
||||
}
|
||||
|
||||
apply from: "$mppProjectWithSerializationPresetPath"
|
||||
|
||||
kotlin {
|
||||
sourceSets {
|
||||
commonMain {
|
||||
dependencies {
|
||||
api project(":postssystem.features.files.common")
|
||||
api project(":postssystem.features.common.client")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,48 @@
|
||||
package dev.inmo.postssystem.features.files.client
|
||||
|
||||
import dev.inmo.postssystem.features.files.common.*
|
||||
import dev.inmo.postssystem.features.files.common.storage.FilesStorage
|
||||
import dev.inmo.micro_utils.ktor.client.UnifiedRequester
|
||||
import dev.inmo.micro_utils.ktor.common.buildStandardUrl
|
||||
import dev.inmo.micro_utils.repos.ReadCRUDRepo
|
||||
import dev.inmo.micro_utils.repos.ktor.client.crud.KtorReadStandardCrudRepo
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.request.post
|
||||
import io.ktor.client.statement.HttpResponse
|
||||
import io.ktor.client.statement.readBytes
|
||||
import kotlinx.serialization.BinaryFormat
|
||||
import kotlinx.serialization.builtins.nullable
|
||||
|
||||
class ClientFilesStorage(
|
||||
baseUrl: String,
|
||||
private val client: HttpClient,
|
||||
private val serialFormat: BinaryFormat
|
||||
) : FilesStorage, ReadCRUDRepo<MetaFileInfoStorageWrapper, FileId> by KtorReadStandardCrudRepo(
|
||||
buildStandardUrl(baseUrl, filesRootPathPart),
|
||||
UnifiedRequester(client, serialFormat),
|
||||
MetaFileInfoStorageWrapper.serializer(),
|
||||
MetaFileInfoStorageWrapper.serializer().nullable,
|
||||
FileId.serializer()
|
||||
) {
|
||||
private val unifiedRequester = UnifiedRequester(client, serialFormat)
|
||||
private val fullFilesPath = buildStandardUrl(baseUrl, filesRootPathPart)
|
||||
private val fullFilesGetBytesPath = buildStandardUrl(
|
||||
fullFilesPath,
|
||||
filesGetFilesPathPart
|
||||
)
|
||||
|
||||
override suspend fun getBytes(id: FileId): ByteArray = client.post<HttpResponse>(fullFilesGetBytesPath) {
|
||||
body = serialFormat.encodeToByteArray(FileId.serializer(), id)
|
||||
}.readBytes()
|
||||
|
||||
override suspend fun getFullFileInfo(
|
||||
id: FileId
|
||||
): FullFileInfoStorageWrapper? = unifiedRequester.uniget(
|
||||
buildStandardUrl(
|
||||
fullFilesPath,
|
||||
filesGetFullFileInfoPathPart,
|
||||
filesFileIdParameter to unifiedRequester.encodeUrlQueryValue(FileId.serializer(), id)
|
||||
),
|
||||
FullFileInfoStorageWrapper.serializer().nullable
|
||||
)
|
||||
}
|
1
features/files/client/src/main/AndroidManifest.xml
Normal file
1
features/files/client/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1 @@
|
||||
<manifest package="dev.inmo.postssystem.features.files.client"/>
|
19
features/files/common/build.gradle
Normal file
19
features/files/common/build.gradle
Normal file
@@ -0,0 +1,19 @@
|
||||
plugins {
|
||||
id "org.jetbrains.kotlin.multiplatform"
|
||||
id "org.jetbrains.kotlin.plugin.serialization"
|
||||
id "com.android.library"
|
||||
}
|
||||
|
||||
apply from: "$mppProjectWithSerializationPresetPath"
|
||||
|
||||
kotlin {
|
||||
sourceSets {
|
||||
commonMain {
|
||||
dependencies {
|
||||
api project(":postssystem.features.common.common")
|
||||
api "dev.inmo:micro_utils.mime_types:$microutils_version"
|
||||
api "dev.inmo:micro_utils.repos.common:$microutils_version"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,6 @@
|
||||
package dev.inmo.postssystem.features.files.common
|
||||
|
||||
const val filesRootPathPart = "files"
|
||||
const val filesGetFilesPathPart = "getFiles"
|
||||
const val filesGetFullFileInfoPathPart = "getFullFileInfo"
|
||||
const val filesFileIdParameter = "fileId"
|
@@ -0,0 +1,35 @@
|
||||
package dev.inmo.postssystem.features.files.common
|
||||
|
||||
import dev.inmo.micro_utils.common.*
|
||||
import dev.inmo.micro_utils.mime_types.MimeType
|
||||
import dev.inmo.micro_utils.serialization.typed_serializer.TypedSerializer
|
||||
import kotlinx.serialization.KSerializer
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable(FileInfoSerializer::class)
|
||||
sealed interface FileInfo {
|
||||
val name: FileName
|
||||
val mimeType: MimeType
|
||||
|
||||
companion object {
|
||||
fun serializer(): KSerializer<FileInfo> = FileInfoSerializer
|
||||
}
|
||||
}
|
||||
|
||||
object FileInfoSerializer : KSerializer<FileInfo> by TypedSerializer(
|
||||
"meta" to MetaFileInfo.serializer(),
|
||||
"full" to FullFileInfo.serializer(),
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class MetaFileInfo(override val name: FileName, override val mimeType: MimeType) : FileInfo
|
||||
|
||||
@Serializable
|
||||
data class FullFileInfo(
|
||||
override val name: FileName,
|
||||
override val mimeType: MimeType,
|
||||
@Serializable(ByteArrayAllocatorSerializer::class)
|
||||
val byteArrayAllocator: ByteArrayAllocator
|
||||
) : FileInfo
|
||||
|
||||
fun FullFileInfo.toMetaFileInfo() = MetaFileInfo(name, mimeType)
|
@@ -0,0 +1,21 @@
|
||||
package dev.inmo.postssystem.features.files.common
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlin.jvm.JvmInline
|
||||
|
||||
|
||||
@Serializable
|
||||
@JvmInline
|
||||
value class FileId(val string: String) {
|
||||
override fun toString(): String = string.toString()
|
||||
}
|
||||
|
||||
@Serializable
|
||||
sealed class FileInfoStorageWrapper {
|
||||
abstract val id: FileId
|
||||
abstract val fileInfo: FileInfo
|
||||
}
|
||||
@Serializable
|
||||
data class MetaFileInfoStorageWrapper(override val id: FileId, override val fileInfo: MetaFileInfo) : FileInfoStorageWrapper()
|
||||
@Serializable
|
||||
data class FullFileInfoStorageWrapper(override val id: FileId, override val fileInfo: FullFileInfo) : FileInfoStorageWrapper()
|
@@ -0,0 +1,9 @@
|
||||
package dev.inmo.postssystem.features.files.common.storage
|
||||
|
||||
import dev.inmo.postssystem.features.files.common.*
|
||||
import dev.inmo.micro_utils.repos.ReadCRUDRepo
|
||||
|
||||
interface FilesStorage : ReadCRUDRepo<MetaFileInfoStorageWrapper, FileId> {
|
||||
suspend fun getBytes(id: FileId): ByteArray
|
||||
suspend fun getFullFileInfo(id: FileId): FullFileInfoStorageWrapper?
|
||||
}
|
@@ -0,0 +1,8 @@
|
||||
package dev.inmo.postssystem.features.files.common.storage
|
||||
|
||||
interface FullFilesStorage : FilesStorage, WriteFilesStorage
|
||||
|
||||
class DefaultFullFilesStorage(
|
||||
filesStorage: FilesStorage,
|
||||
writeFilesStorage: WriteFilesStorage
|
||||
) : FullFilesStorage, FilesStorage by filesStorage, WriteFilesStorage by writeFilesStorage
|
@@ -0,0 +1,6 @@
|
||||
package dev.inmo.postssystem.features.files.common.storage
|
||||
|
||||
import dev.inmo.postssystem.features.files.common.*
|
||||
import dev.inmo.micro_utils.repos.WriteCRUDRepo
|
||||
|
||||
interface WriteFilesStorage : WriteCRUDRepo<FullFileInfoStorageWrapper, FileId, FullFileInfo>
|
@@ -0,0 +1,63 @@
|
||||
package dev.inmo.postssystem.features.files.common
|
||||
|
||||
import dev.inmo.postssystem.features.files.common.storage.FilesStorage
|
||||
import dev.inmo.micro_utils.pagination.*
|
||||
import dev.inmo.micro_utils.repos.ReadKeyValueRepo
|
||||
import java.io.File
|
||||
|
||||
class DiskFilesStorage(
|
||||
private val filesFolder: File,
|
||||
private val metasKeyValueRepo: ReadKeyValueRepo<FileId, MetaFileInfo>
|
||||
) : FilesStorage {
|
||||
private val FileId.file
|
||||
get() = File(filesFolder, string)
|
||||
|
||||
init {
|
||||
if (!filesFolder.exists()) {
|
||||
filesFolder.mkdirs()
|
||||
} else {
|
||||
require(filesFolder.isDirectory) { "$filesFolder must be a directory" }
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun FileId.meta(): MetaFileInfoStorageWrapper? {
|
||||
return MetaFileInfoStorageWrapper(
|
||||
this,
|
||||
metasKeyValueRepo.get(this) ?: return null
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun getBytes(id: FileId): ByteArray = id.file.readBytes()
|
||||
|
||||
override suspend fun getFullFileInfo(id: FileId): FullFileInfoStorageWrapper? = getById(
|
||||
id
|
||||
) ?.let {
|
||||
FullFileInfoStorageWrapper(
|
||||
id,
|
||||
FullFileInfo(
|
||||
it.fileInfo.name,
|
||||
it.fileInfo.mimeType
|
||||
) {
|
||||
id.file.readBytes()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun contains(id: FileId): Boolean = metasKeyValueRepo.contains(id)
|
||||
|
||||
override suspend fun count(): Long = metasKeyValueRepo.count()
|
||||
|
||||
override suspend fun getById(id: FileId): MetaFileInfoStorageWrapper? = id.meta()
|
||||
|
||||
override suspend fun getByPagination(pagination: Pagination): PaginationResult<MetaFileInfoStorageWrapper> {
|
||||
val keys = metasKeyValueRepo.keys(pagination)
|
||||
return keys.changeResults(
|
||||
keys.results.mapNotNull {
|
||||
MetaFileInfoStorageWrapper(
|
||||
it,
|
||||
metasKeyValueRepo.get(it) ?: return@mapNotNull null
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
@@ -0,0 +1,15 @@
|
||||
package dev.inmo.postssystem.features.files.common
|
||||
|
||||
import dev.inmo.micro_utils.repos.KeyValueRepo
|
||||
import dev.inmo.micro_utils.repos.mappers.withMapper
|
||||
import kotlinx.serialization.StringFormat
|
||||
|
||||
fun MetasKeyValueRepo(
|
||||
serialFormat: StringFormat,
|
||||
originalRepo: KeyValueRepo<String, String>
|
||||
) = originalRepo.withMapper<FileId, MetaFileInfo, String, String>(
|
||||
{ string },
|
||||
{ serialFormat.encodeToString(MetaFileInfo.serializer(), this) },
|
||||
{ FileId(this) },
|
||||
{ serialFormat.decodeFromString(MetaFileInfo.serializer(), this) }
|
||||
)
|
@@ -0,0 +1,69 @@
|
||||
package dev.inmo.postssystem.features.files.common
|
||||
|
||||
import com.benasher44.uuid.uuid4
|
||||
import dev.inmo.postssystem.features.files.common.storage.WriteFilesStorage
|
||||
import dev.inmo.micro_utils.repos.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
import java.io.File
|
||||
|
||||
class WriteDistFilesStorage(
|
||||
private val filesFolder: File,
|
||||
private val metasKeyValueRepo: WriteKeyValueRepo<FileId, MetaFileInfo>
|
||||
) : WriteFilesStorage {
|
||||
private val FileId.file
|
||||
get() = File(filesFolder, string)
|
||||
|
||||
private val _deletedObjectsIdsFlow = MutableSharedFlow<FileId>()
|
||||
private val _newObjectsFlow = MutableSharedFlow<FullFileInfoStorageWrapper>()
|
||||
private val _updatedObjectsFlow = MutableSharedFlow<FullFileInfoStorageWrapper>()
|
||||
override val deletedObjectsIdsFlow: Flow<FileId> = _deletedObjectsIdsFlow.asSharedFlow()
|
||||
override val newObjectsFlow: Flow<FullFileInfoStorageWrapper> = _newObjectsFlow.asSharedFlow()
|
||||
override val updatedObjectsFlow: Flow<FullFileInfoStorageWrapper> = _updatedObjectsFlow.asSharedFlow()
|
||||
|
||||
init {
|
||||
if (!filesFolder.exists()) {
|
||||
filesFolder.mkdirs()
|
||||
} else {
|
||||
require(filesFolder.isDirectory) { "$filesFolder must be a directory" }
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun create(values: List<FullFileInfo>): List<FullFileInfoStorageWrapper> = values.map {
|
||||
var newId: FileId
|
||||
var file: File
|
||||
do {
|
||||
newId = FileId(uuid4().toString())
|
||||
file = newId.file
|
||||
} while (file.exists())
|
||||
metasKeyValueRepo.set(newId, it.toMetaFileInfo())
|
||||
file.writeBytes(it.byteArrayAllocator())
|
||||
FullFileInfoStorageWrapper(newId, it)
|
||||
}
|
||||
|
||||
override suspend fun deleteById(ids: List<FileId>) {
|
||||
ids.forEach {
|
||||
if (it.file.delete()) {
|
||||
metasKeyValueRepo.unset(it)
|
||||
_deletedObjectsIdsFlow.emit(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun update(
|
||||
id: FileId,
|
||||
value: FullFileInfo
|
||||
): FullFileInfoStorageWrapper? = id.file.takeIf { it.exists() } ?.writeBytes(value.byteArrayAllocator()) ?.let {
|
||||
val result = FullFileInfoStorageWrapper(id, value.copy())
|
||||
|
||||
metasKeyValueRepo.set(id, value.toMetaFileInfo())
|
||||
_updatedObjectsFlow.emit(result)
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
override suspend fun update(
|
||||
values: List<UpdatedValuePair<FileId, FullFileInfo>>
|
||||
): List<FullFileInfoStorageWrapper> = values.mapNotNull { (id, file) ->
|
||||
update(id, file)
|
||||
}
|
||||
}
|
1
features/files/common/src/main/AndroidManifest.xml
Normal file
1
features/files/common/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1 @@
|
||||
<manifest package="dev.inmo.postssystem.features.files.common"/>
|
17
features/files/server/build.gradle
Normal file
17
features/files/server/build.gradle
Normal file
@@ -0,0 +1,17 @@
|
||||
plugins {
|
||||
id "org.jetbrains.kotlin.multiplatform"
|
||||
id "org.jetbrains.kotlin.plugin.serialization"
|
||||
}
|
||||
|
||||
apply from: "$mppJavaProjectPresetPath"
|
||||
|
||||
kotlin {
|
||||
sourceSets {
|
||||
commonMain {
|
||||
dependencies {
|
||||
api project(":postssystem.features.files.common")
|
||||
api project(":postssystem.features.common.server")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,57 @@
|
||||
package dev.inmo.postssystem.features.files.server
|
||||
|
||||
import dev.inmo.postssystem.features.files.common.*
|
||||
import dev.inmo.postssystem.features.files.common.storage.*
|
||||
import dev.inmo.micro_utils.ktor.server.*
|
||||
import dev.inmo.micro_utils.ktor.server.configurators.ApplicationRoutingConfigurator
|
||||
import dev.inmo.micro_utils.repos.ktor.server.crud.configureReadStandardCrudRepoRoutes
|
||||
import dev.inmo.micro_utils.repos.ktor.server.crud.configureWriteStandardCrudRepoRoutes
|
||||
import io.ktor.application.call
|
||||
import io.ktor.auth.authenticate
|
||||
import io.ktor.response.respondBytes
|
||||
import io.ktor.routing.*
|
||||
import kotlinx.serialization.builtins.nullable
|
||||
|
||||
class FilesRoutingConfigurator(
|
||||
private val filesStorage: FilesStorage,
|
||||
private val writeFilesStorage: WriteFilesStorage?
|
||||
) : ApplicationRoutingConfigurator.Element {
|
||||
constructor(fullFilesStorage: FullFilesStorage) : this(fullFilesStorage, fullFilesStorage)
|
||||
|
||||
override fun Route.invoke() {
|
||||
authenticate {
|
||||
route(filesRootPathPart) {
|
||||
configureReadStandardCrudRepoRoutes(
|
||||
filesStorage,
|
||||
MetaFileInfoStorageWrapper.serializer(),
|
||||
MetaFileInfoStorageWrapper.serializer().nullable,
|
||||
FileId.serializer()
|
||||
)
|
||||
writeFilesStorage ?.let {
|
||||
configureWriteStandardCrudRepoRoutes(
|
||||
writeFilesStorage,
|
||||
FullFileInfoStorageWrapper.serializer(),
|
||||
FullFileInfoStorageWrapper.serializer().nullable,
|
||||
FullFileInfo.serializer(),
|
||||
FileId.serializer()
|
||||
)
|
||||
}
|
||||
post(filesGetFilesPathPart) {
|
||||
call.respondBytes(
|
||||
filesStorage.getBytes(
|
||||
call.uniload(FileId.serializer())
|
||||
)
|
||||
)
|
||||
}
|
||||
get(filesGetFullFileInfoPathPart) {
|
||||
call.unianswer(
|
||||
FullFileInfoStorageWrapper.serializer().nullable,
|
||||
filesStorage.getFullFileInfo(
|
||||
call.decodeUrlQueryValueOrSendError(filesFileIdParameter, FileId.serializer()) ?: return@get
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user