From 99b953635eeb9de327198588c20f27272001b1ab Mon Sep 17 00:00:00 2001 From: InsanusMokrassar Date: Wed, 9 Mar 2022 13:49:00 +0600 Subject: [PATCH] temp progress on binary correct including --- features/common/common/build.gradle | 1 + .../features/common/common/FileMimeType.kt | 6 + .../common/common/ActualFileMimeType.kt | 8 + .../common/common/ActualFileMimeType.kt | 10 + .../posts/client/ClientWritePostsService.kt | 120 +++++----- .../client/TempFileIdentifierInputProvider.kt | 15 ++ .../services/posts/client/TempUpload.kt | 12 + .../client/ui/create/PostCreateUIModel.kt | 8 + .../client/ui/create/PostCreateUIState.kt | 14 ++ .../services/posts/client/ActualTempUpload.kt | 50 +++++ .../services/posts/client/ActualTempUpload.kt | 38 ++++ services/posts/common/build.gradle | 1 + .../services/posts/common/Constants.kt | 15 ++ .../ServerPostsServiceRoutingConfigurator.kt | 209 ++++++++---------- .../server/TempFileIdentifierInputProvider.kt | 15 ++ 15 files changed, 342 insertions(+), 180 deletions(-) create mode 100644 features/common/common/src/commonMain/kotlin/dev/inmo/postssystem/features/common/common/FileMimeType.kt create mode 100644 features/common/common/src/jsMain/kotlin/dev/inmo/postssystem/features/common/common/ActualFileMimeType.kt create mode 100644 features/common/common/src/jvmMain/kotlin/dev/inmo/postssystem/features/common/common/ActualFileMimeType.kt create mode 100644 services/posts/client/src/commonMain/kotlin/dev/inmo/postssystem/services/posts/client/TempFileIdentifierInputProvider.kt create mode 100644 services/posts/client/src/commonMain/kotlin/dev/inmo/postssystem/services/posts/client/TempUpload.kt create mode 100644 services/posts/client/src/commonMain/kotlin/dev/inmo/postssystem/services/posts/client/ui/create/PostCreateUIModel.kt create mode 100644 services/posts/client/src/commonMain/kotlin/dev/inmo/postssystem/services/posts/client/ui/create/PostCreateUIState.kt create mode 100644 services/posts/client/src/jsMain/kotlin/dev/inmo/postssystem/services/posts/client/ActualTempUpload.kt create mode 100644 services/posts/client/src/jvmMain/kotlin/dev/inmo/postssystem/services/posts/client/ActualTempUpload.kt create mode 100644 services/posts/server/src/jvmMain/kotlin/dev/inmo/postssystem/services/posts/server/TempFileIdentifierInputProvider.kt diff --git a/features/common/common/build.gradle b/features/common/common/build.gradle index 83a1317d..88f2aeb6 100644 --- a/features/common/common/build.gradle +++ b/features/common/common/build.gradle @@ -12,6 +12,7 @@ kotlin { dependencies { api libs.microutils.common api libs.microutils.serialization.typedserializer + api libs.microutils.mimetypes api libs.klock api "io.insert-koin:koin-core:$koin_version" api "com.benasher44:uuid:$uuid_version" diff --git a/features/common/common/src/commonMain/kotlin/dev/inmo/postssystem/features/common/common/FileMimeType.kt b/features/common/common/src/commonMain/kotlin/dev/inmo/postssystem/features/common/common/FileMimeType.kt new file mode 100644 index 00000000..c3feb337 --- /dev/null +++ b/features/common/common/src/commonMain/kotlin/dev/inmo/postssystem/features/common/common/FileMimeType.kt @@ -0,0 +1,6 @@ +package dev.inmo.postssystem.features.common.common + +import dev.inmo.micro_utils.common.MPPFile +import dev.inmo.micro_utils.mime_types.MimeType + +expect val MPPFile.mimeType: MimeType diff --git a/features/common/common/src/jsMain/kotlin/dev/inmo/postssystem/features/common/common/ActualFileMimeType.kt b/features/common/common/src/jsMain/kotlin/dev/inmo/postssystem/features/common/common/ActualFileMimeType.kt new file mode 100644 index 00000000..f0b6365a --- /dev/null +++ b/features/common/common/src/jsMain/kotlin/dev/inmo/postssystem/features/common/common/ActualFileMimeType.kt @@ -0,0 +1,8 @@ +package dev.inmo.postssystem.features.common.common + +import dev.inmo.micro_utils.common.MPPFile +import dev.inmo.micro_utils.mime_types.* + +actual val MPPFile.mimeType: MimeType + get() = findBuiltinMimeType(type) ?: KnownMimeTypes.Any + diff --git a/features/common/common/src/jvmMain/kotlin/dev/inmo/postssystem/features/common/common/ActualFileMimeType.kt b/features/common/common/src/jvmMain/kotlin/dev/inmo/postssystem/features/common/common/ActualFileMimeType.kt new file mode 100644 index 00000000..343eb1f8 --- /dev/null +++ b/features/common/common/src/jvmMain/kotlin/dev/inmo/postssystem/features/common/common/ActualFileMimeType.kt @@ -0,0 +1,10 @@ +package dev.inmo.postssystem.features.common.common + +import dev.inmo.micro_utils.common.MPPFile +import dev.inmo.micro_utils.common.filename +import dev.inmo.micro_utils.mime_types.* +import java.net.URLConnection + +actual val MPPFile.mimeType: MimeType + get() = URLConnection.getFileNameMap().getContentTypeFor(filename.name) ?.let(::findBuiltinMimeType) ?: KnownMimeTypes.Any + diff --git a/services/posts/client/src/commonMain/kotlin/dev/inmo/postssystem/services/posts/client/ClientWritePostsService.kt b/services/posts/client/src/commonMain/kotlin/dev/inmo/postssystem/services/posts/client/ClientWritePostsService.kt index fafc9d8c..cee84895 100644 --- a/services/posts/client/src/commonMain/kotlin/dev/inmo/postssystem/services/posts/client/ClientWritePostsService.kt +++ b/services/posts/client/src/commonMain/kotlin/dev/inmo/postssystem/services/posts/client/ClientWritePostsService.kt @@ -7,6 +7,8 @@ import dev.inmo.micro_utils.ktor.common.encodeHex import dev.inmo.micro_utils.repos.ktor.common.crud.createRouting import dev.inmo.micro_utils.repos.ktor.common.crud.updateRouting import dev.inmo.micro_utils.repos.ktor.common.one_to_many.removeRoute +import dev.inmo.postssystem.features.common.common.FileBasedInputProvider +import dev.inmo.postssystem.features.common.common.SimpleInputProvider import dev.inmo.postssystem.features.content.common.* import dev.inmo.postssystem.features.posts.common.PostId import dev.inmo.postssystem.features.posts.common.RegisteredPost @@ -20,9 +22,15 @@ import kotlinx.serialization.builtins.* class ClientWritePostsService( private val baseUrl: String, - private val unifiedRequester: UnifiedRequester + unifiedRequester: UnifiedRequester ) : WritePostsService { private val root = buildStandardUrl(baseUrl, postsRootPath) + private val unifiedRequester = UnifiedRequester( + unifiedRequester.client, + unifiedRequester.serialFormat.createWithSerializerModuleExtension { + contextual() + } + ) private val contentEitherSerializer = EitherSerializer(ContentId.serializer(), ContentSerializer) private val contentsEitherSerializer = ListSerializer(contentEitherSerializer) @@ -36,83 +44,59 @@ class ClientWritePostsService( root, removeRoute ) + private val tempUploadFullPath = buildStandardUrl( + baseUrl, + postsCreateTempPathPart + ) + + private suspend fun prepareContent(content: Content): Content? { + return (content as? BinaryContent) ?.let { + when (val provider = it.inputProvider) { + is FileBasedInputProvider -> { + val fileId = unifiedRequester.tempUpload( + tempUploadFullPath, + provider.file + ) + it.copy(inputProvider = TempFileIdentifierInputProvider(fileId)) + } + is TempFileIdentifierInputProvider -> it + else -> return@prepareContent null + } + } ?: content + } override suspend fun create(newPost: FullNewPost): RegisteredPost? { - return if (newPost.content.any { it is BinaryContent }) { - val answer = unifiedRequester.client.post(createFullPath) { - formData { - newPost.content.forEachIndexed { i, content -> - when (content) { - is BinaryContent -> append( - i.toString(), - InputProvider(block = content.inputProvider::invoke), - headers { - append(HttpHeaders.ContentType, content.mimeType.raw) - append(HttpHeaders.ContentDisposition, "filename=\"${content.filename.name}\"") - }.build() - ) - else -> append( - i.toString(), - unifiedRequester.serialFormat.encodeHex(ContentSerializer, content) - ) - } - } - } - } - unifiedRequester.serialFormat.decodeFromByteArray(RegisteredPost.serializer().nullable, answer) - } else { - unifiedRequester.unipost( - createFullPath, - contentsSerializer to newPost.content, - RegisteredPost.serializer().nullable - ) + val mappedContent = newPost.content.mapNotNull { + prepareContent(it) } + val mappedPost = newPost.copy( + content = mappedContent + ) + return unifiedRequester.unipost( + createFullPath, + contentsSerializer to mappedPost.content, + RegisteredPost.serializer().nullable + ) } override suspend fun update( postId: PostId, content: List> ): RegisteredPost? { - return if (content.any { it.optionalT2.data is BinaryContent }) { - val answer = unifiedRequester.client.post(createFullPath) { - formData { - content.forEachIndexed { i, eitherContent -> - eitherContent.onFirst { - append( - i.toString(), - unifiedRequester.serialFormat.encodeHex(contentEitherSerializer, it.either()) - ) - }.onSecond { - when (it) { - is BinaryContent -> append( - i.toString(), - InputProvider(block = it.inputProvider::invoke), - headers { - append(HttpHeaders.ContentType, it.mimeType.raw) - append(HttpHeaders.ContentDisposition, "filename=\"${it.filename.name}\"") - }.build() - ) - else -> append( - i.toString(), - unifiedRequester.serialFormat.encodeHex(contentEitherSerializer, it.either()) - ) - } - } - } - } - } - unifiedRequester.serialFormat.decodeFromByteArray(RegisteredPost.serializer().nullable, answer) - } else { - unifiedRequester.unipost( - buildStandardUrl( - root, - updateRouting, - postsPostIdParameter to unifiedRequester.encodeUrlQueryValue(PostId.serializer(), postId) - ), - contentsEitherSerializer to content, - RegisteredPost.serializer().nullable - ) + val mappedContent = content.mapNotNull { + it.mapOnSecond { content -> + prepareContent(content) ?.either() ?: return@mapNotNull null + } ?: it } + return unifiedRequester.unipost( + buildStandardUrl( + root, + updateRouting, + postsPostIdParameter to unifiedRequester.encodeUrlQueryValue(PostId.serializer(), postId) + ), + contentsEitherSerializer to mappedContent, + RegisteredPost.serializer().nullable + ) } override suspend fun remove(postId: PostId) = unifiedRequester.unipost( diff --git a/services/posts/client/src/commonMain/kotlin/dev/inmo/postssystem/services/posts/client/TempFileIdentifierInputProvider.kt b/services/posts/client/src/commonMain/kotlin/dev/inmo/postssystem/services/posts/client/TempFileIdentifierInputProvider.kt new file mode 100644 index 00000000..3005455b --- /dev/null +++ b/services/posts/client/src/commonMain/kotlin/dev/inmo/postssystem/services/posts/client/TempFileIdentifierInputProvider.kt @@ -0,0 +1,15 @@ +package dev.inmo.postssystem.services.posts.client + +import dev.inmo.postssystem.features.common.common.SimpleInputProvider +import dev.inmo.postssystem.features.files.common.FileId +import io.ktor.utils.io.core.Input +import kotlinx.serialization.Serializable + +@Serializable +internal data class TempFileIdentifierInputProvider( + private val tempFile: FileId +) : SimpleInputProvider { + override fun invoke(): Input { + TODO("Not yet implemented") + } +} diff --git a/services/posts/client/src/commonMain/kotlin/dev/inmo/postssystem/services/posts/client/TempUpload.kt b/services/posts/client/src/commonMain/kotlin/dev/inmo/postssystem/services/posts/client/TempUpload.kt new file mode 100644 index 00000000..0948e3de --- /dev/null +++ b/services/posts/client/src/commonMain/kotlin/dev/inmo/postssystem/services/posts/client/TempUpload.kt @@ -0,0 +1,12 @@ +package dev.inmo.postssystem.services.posts.client + +import dev.inmo.micro_utils.common.MPPFile +import dev.inmo.micro_utils.ktor.client.UnifiedRequester +import dev.inmo.postssystem.features.files.common.FileId + +internal expect suspend fun UnifiedRequester.tempUpload( + fullTempUploadDraftPath: String, + file: MPPFile, + onUpload: (Long, Long) -> Unit = { _, _ -> } +): FileId + diff --git a/services/posts/client/src/commonMain/kotlin/dev/inmo/postssystem/services/posts/client/ui/create/PostCreateUIModel.kt b/services/posts/client/src/commonMain/kotlin/dev/inmo/postssystem/services/posts/client/ui/create/PostCreateUIModel.kt new file mode 100644 index 00000000..9922d73b --- /dev/null +++ b/services/posts/client/src/commonMain/kotlin/dev/inmo/postssystem/services/posts/client/ui/create/PostCreateUIModel.kt @@ -0,0 +1,8 @@ +package dev.inmo.postssystem.services.posts.client.ui.create + +import dev.inmo.postssystem.features.common.common.UIModel +import dev.inmo.postssystem.features.content.common.Content + +interface PostCreateUIModel : UIModel { + suspend fun create(content: List) +} diff --git a/services/posts/client/src/commonMain/kotlin/dev/inmo/postssystem/services/posts/client/ui/create/PostCreateUIState.kt b/services/posts/client/src/commonMain/kotlin/dev/inmo/postssystem/services/posts/client/ui/create/PostCreateUIState.kt new file mode 100644 index 00000000..d8b32462 --- /dev/null +++ b/services/posts/client/src/commonMain/kotlin/dev/inmo/postssystem/services/posts/client/ui/create/PostCreateUIState.kt @@ -0,0 +1,14 @@ +package dev.inmo.postssystem.services.posts.client.ui.create + +import kotlinx.serialization.Serializable + +@Serializable +sealed class PostCreateUIState { + @Serializable + object Init : PostCreateUIState() + @Serializable + object Uploading : PostCreateUIState() + @Serializable + object Completed : PostCreateUIState() + +} diff --git a/services/posts/client/src/jsMain/kotlin/dev/inmo/postssystem/services/posts/client/ActualTempUpload.kt b/services/posts/client/src/jsMain/kotlin/dev/inmo/postssystem/services/posts/client/ActualTempUpload.kt new file mode 100644 index 00000000..b9ca1c52 --- /dev/null +++ b/services/posts/client/src/jsMain/kotlin/dev/inmo/postssystem/services/posts/client/ActualTempUpload.kt @@ -0,0 +1,50 @@ +package dev.inmo.postssystem.services.posts.client + +import dev.inmo.micro_utils.common.MPPFile +import dev.inmo.micro_utils.ktor.client.UnifiedRequester +import dev.inmo.postssystem.features.files.common.FileId +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.GlobalScope.coroutineContext +import kotlinx.coroutines.job +import org.w3c.xhr.* + +internal actual suspend fun UnifiedRequester.tempUpload( + fullTempUploadDraftPath: String, + file: MPPFile, + onUpload: (Long, Long) -> Unit +): FileId { + val formData = FormData() + val answer = CompletableDeferred() + + formData.append( + "data", + file + ) + + val request = XMLHttpRequest() + request.responseType = XMLHttpRequestResponseType.TEXT + request.upload.onprogress = { + onUpload(it.loaded.toLong(), it.total.toLong()) + } + request.onload = { + if (request.status == 200.toShort()) { + answer.complete(FileId(request.responseText)) + } else { + answer.completeExceptionally(Exception("Something went wrong")) + } + } + request.onerror = { + answer.completeExceptionally(Exception("Something went wrong")) + } + request.open("POST", fullTempUploadDraftPath, true) + request.send(formData) + + coroutineContext.job.invokeOnCompletion { + runCatching { + request.abort() + } + } + + return answer.await() +} + diff --git a/services/posts/client/src/jvmMain/kotlin/dev/inmo/postssystem/services/posts/client/ActualTempUpload.kt b/services/posts/client/src/jvmMain/kotlin/dev/inmo/postssystem/services/posts/client/ActualTempUpload.kt new file mode 100644 index 00000000..62743962 --- /dev/null +++ b/services/posts/client/src/jvmMain/kotlin/dev/inmo/postssystem/services/posts/client/ActualTempUpload.kt @@ -0,0 +1,38 @@ +package dev.inmo.postssystem.services.posts.client + +import dev.inmo.micro_utils.common.MPPFile +import dev.inmo.micro_utils.common.filename +import dev.inmo.micro_utils.ktor.client.UnifiedRequester +import dev.inmo.micro_utils.ktor.client.inputProvider +import dev.inmo.postssystem.features.common.common.mimeType +import dev.inmo.postssystem.features.files.common.FileId +import io.ktor.client.features.onUpload +import io.ktor.client.request.forms.formData +import io.ktor.client.request.forms.submitFormWithBinaryData +import io.ktor.http.Headers +import io.ktor.http.HttpHeaders + +internal actual suspend fun UnifiedRequester.tempUpload( + fullTempUploadDraftPath: String, + file: MPPFile, + onUpload: (Long, Long) -> Unit +): FileId { + val inputProvider = file.inputProvider() + val fileId = client.submitFormWithBinaryData( + fullTempUploadDraftPath, + formData = formData { + append( + "data", + inputProvider, + Headers.build { + append(HttpHeaders.ContentType, file.mimeType.raw) + append(HttpHeaders.ContentDisposition, "filename=\"${file.filename.string}\"") + } + ) + } + ) { + onUpload(onUpload) + } + return FileId(fileId) +} + diff --git a/services/posts/common/build.gradle b/services/posts/common/build.gradle index f3f4e727..534ba6c3 100644 --- a/services/posts/common/build.gradle +++ b/services/posts/common/build.gradle @@ -12,6 +12,7 @@ kotlin { dependencies { api project(":postssystem.features.common.common") api project(":postssystem.features.posts.common") + api project(":postssystem.features.files.common") api libs.microutils.repos.common api libs.microutils.repos.ktor.client } diff --git a/services/posts/common/src/commonMain/kotlin/dev/inmo/postssystem/services/posts/common/Constants.kt b/services/posts/common/src/commonMain/kotlin/dev/inmo/postssystem/services/posts/common/Constants.kt index a30c0e0c..8784ea95 100644 --- a/services/posts/common/src/commonMain/kotlin/dev/inmo/postssystem/services/posts/common/Constants.kt +++ b/services/posts/common/src/commonMain/kotlin/dev/inmo/postssystem/services/posts/common/Constants.kt @@ -1,5 +1,20 @@ package dev.inmo.postssystem.services.posts.common +import kotlinx.serialization.SerialFormat +import kotlinx.serialization.cbor.Cbor +import kotlinx.serialization.modules.SerializersModule +import kotlinx.serialization.modules.SerializersModuleBuilder + const val postsRootPath = "posts" +const val postsCreateTempPathPart = "temp" const val postsPostIdParameter = "postId" + +fun SerialFormat.createWithSerializerModuleExtension( + configurator: SerializersModuleBuilder.() -> Unit +) = Cbor { + serializersModule = SerializersModule { + include(this@createWithSerializerModuleExtension.serializersModule) + configurator() + } +} diff --git a/services/posts/server/src/jvmMain/kotlin/dev/inmo/postssystem/services/posts/server/ServerPostsServiceRoutingConfigurator.kt b/services/posts/server/src/jvmMain/kotlin/dev/inmo/postssystem/services/posts/server/ServerPostsServiceRoutingConfigurator.kt index 934b32c9..ac62c0a3 100644 --- a/services/posts/server/src/jvmMain/kotlin/dev/inmo/postssystem/services/posts/server/ServerPostsServiceRoutingConfigurator.kt +++ b/services/posts/server/src/jvmMain/kotlin/dev/inmo/postssystem/services/posts/server/ServerPostsServiceRoutingConfigurator.kt @@ -1,6 +1,8 @@ package dev.inmo.postssystem.services.posts.server +import com.benasher44.uuid.uuid4 import dev.inmo.micro_utils.common.* +import dev.inmo.micro_utils.coroutines.launchSafelyWithoutExceptions import dev.inmo.micro_utils.ktor.common.decodeHex import dev.inmo.micro_utils.ktor.server.* import dev.inmo.micro_utils.ktor.server.configurators.ApplicationRoutingConfigurator @@ -11,154 +13,102 @@ import dev.inmo.micro_utils.repos.ktor.common.one_to_many.removeRoute import dev.inmo.micro_utils.repos.ktor.server.crud.configureReadStandardCrudRepoRoutes import dev.inmo.postssystem.features.common.common.FileBasedInputProvider import dev.inmo.postssystem.features.content.common.* +import dev.inmo.postssystem.features.files.common.FileId import dev.inmo.postssystem.features.posts.common.* import dev.inmo.postssystem.services.posts.common.* import io.ktor.application.ApplicationCall import io.ktor.application.call import io.ktor.auth.authenticate +import io.ktor.http.HttpStatusCode import io.ktor.http.content.PartData +import io.ktor.http.content.streamProvider import io.ktor.request.isMultipart import io.ktor.request.receiveMultipart +import io.ktor.response.respond import io.ktor.routing.* import io.ktor.util.asStream import io.ktor.util.pipeline.PipelineContext import io.ktor.utils.io.core.use import io.ktor.utils.io.streams.asInput +import kotlinx.coroutines.* +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock import kotlinx.serialization.builtins.* +import java.io.File +import java.nio.file.Files +import java.nio.file.attribute.FileTime +import java.util.concurrent.TimeUnit class ServerPostsServiceRoutingConfigurator( private val readPostsService: ReadPostsService, private val writePostsService: WritePostsService? = readPostsService as? WritePostsService, + private val scope: CoroutineScope, private val unifiedRouter: UnifiedRouter ) : ApplicationRoutingConfigurator.Element { private val contentEitherSerializer = EitherSerializer(ContentId.serializer(), ContentSerializer) private val contentsEitherSerializer = ListSerializer(contentEitherSerializer) private val contentsSerializer = ListSerializer(ContentSerializer) + private val temporalFilesMap = mutableMapOf() + private val temporalFilesMutex = Mutex() + private val removingJob = scope.launchSafelyWithoutExceptions { + while (isActive) { + val filesWithCreationInfo = temporalFilesMap.mapNotNull { (fileId, file) -> + fileId to ((Files.getAttribute(file.toPath(), "creationTime") as? FileTime) ?.toMillis() ?: return@mapNotNull null) + } + if (filesWithCreationInfo.isEmpty()) { + delay(TimeUnit.HOURS.toMillis(1L)) + continue + } + var min = filesWithCreationInfo.first() + for (fileWithCreationInfo in filesWithCreationInfo) { + if (fileWithCreationInfo.second < min.second) { + min = fileWithCreationInfo + } + } + delay(System.currentTimeMillis() - (min.second + TimeUnit.HOURS.toMillis(1))) + temporalFilesMutex.withLock { + temporalFilesMap.remove(min.first) + } ?.delete() + } + } + + private suspend fun mapBinary(content: BinaryContent): BinaryContent? { + val provider = content.inputProvider + if (provider is TempFileIdentifierInputProvider) { + val newProvider = temporalFilesMutex.withLock { + temporalFilesMap.remove(provider.tempFile) + } ?.let(::FileBasedInputProvider) + + if (newProvider != null) { + return content.copy( + inputProvider = newProvider + ) + } + } + return null + } + private suspend fun PipelineContext.receiveContents(): List { return unifiedRouter.run { - if (call.request.isMultipart()) { - val multipart = call.receiveMultipart() - val list = mutableListOf>() - - var part = multipart.readPart() - - while (part != null) { - val name = part.name - val capturedPart = part - when { - name == null -> {} - capturedPart is PartData.FormItem -> { - list.add( - name to unifiedRouter.serialFormat.decodeHex( - ContentSerializer, - capturedPart.value - ) - ) - } - capturedPart is PartData.FileItem -> { - val filename = capturedPart.originalFileName ?.let(::FileName) ?: error("File name is unknown for default part") - val mimeType = capturedPart.contentType ?.let { - findBuiltinMimeType("${it.contentType}/${it.contentSubtype}") - } ?: error("File type is unknown for default part") - val resultInput = MPPFile.createTempFile( - filename.nameWithoutExtension.let { - var resultName = it - while (resultName.length < 3) { - resultName += "_" - } - resultName - }, - ".${filename.extension}" - ).apply { - outputStream().use { fileStream -> - capturedPart.provider().asStream().copyTo(fileStream) - } - } - - list.add( - name to BinaryContent( - filename, - mimeType, - FileBasedInputProvider(resultInput) - ) - ) - } - else -> {} - } - - part = multipart.readPart() - } - - list.sortedBy { it.first }.map { it.second } - } else { - uniload(contentsSerializer) + uniload(contentsSerializer).mapNotNull { + mapBinary(it as? BinaryContent ?: return@mapNotNull it) } } } private suspend fun PipelineContext.receiveContentsEithers(): List> { return unifiedRouter.run { - if (call.request.isMultipart()) { - val multipart = call.receiveMultipart() - val list = mutableListOf>>() - - var part = multipart.readPart() - - while (part != null) { - val name = part.name - val capturedPart = part - when { - name == null -> {} - capturedPart is PartData.FormItem -> { - list.add( - name to unifiedRouter.serialFormat.decodeHex( - contentEitherSerializer, - capturedPart.value - ) - ) - } - capturedPart is PartData.FileItem -> { - val filename = capturedPart.originalFileName ?.let(::FileName) ?: error("File name is unknown for default part") - val mimeType = capturedPart.contentType ?.let { - findBuiltinMimeType("${it.contentType}/${it.contentSubtype}") - } ?: error("File type is unknown for default part") - val resultInput = MPPFile.createTempFile( - filename.nameWithoutExtension.let { - var resultName = it - while (resultName.length < 3) { - resultName += "_" - } - resultName - }, - ".${filename.extension}" - ).apply { - outputStream().use { fileStream -> - capturedPart.provider().asStream().copyTo(fileStream) - } - } - - list.add( - name to BinaryContent( - filename, - mimeType, - FileBasedInputProvider(resultInput) - ).either() - ) - } - else -> {} - } - - part = multipart.readPart() - } - - list.sortedBy { it.first }.map { it.second } - } else { - uniload(contentsEitherSerializer) + uniload(contentsEitherSerializer).mapNotNull { + it.mapOnSecond { + mapBinary(it as? BinaryContent ?: return@mapOnSecond null) ?.either() + } ?: it } } } + + override fun Route.invoke() { authenticate { route(postsRootPath) { @@ -205,6 +155,41 @@ class ServerPostsServiceRoutingConfigurator( ) } + post(postsCreateTempPathPart) { + val multipart = call.receiveMultipart() + + var fileInfo: Pair? = null + var part = multipart.readPart() + while (part != null) { + if (part is PartData.FileItem) { + break + } + part = multipart.readPart() + } + part ?.let { + if (it is PartData.FileItem) { + val fileId = FileId(uuid4().toString()) + val fileName = it.originalFileName ?.let { FileName(it) } ?: return@let + fileInfo = fileId to File.createTempFile(fileId.string, ".${fileName.extension}").apply { + outputStream().use { outputStream -> + it.streamProvider().use { + it.copyTo(outputStream) + } + } + deleteOnExit() + } + } + } + + fileInfo ?.also { (fileId, file) -> + temporalFilesMutex.withLock { + temporalFilesMap[fileId] = file + } + call.respond(fileId.string) + } ?: call.respond(HttpStatusCode.BadRequest) + + } + } } diff --git a/services/posts/server/src/jvmMain/kotlin/dev/inmo/postssystem/services/posts/server/TempFileIdentifierInputProvider.kt b/services/posts/server/src/jvmMain/kotlin/dev/inmo/postssystem/services/posts/server/TempFileIdentifierInputProvider.kt new file mode 100644 index 00000000..325a1322 --- /dev/null +++ b/services/posts/server/src/jvmMain/kotlin/dev/inmo/postssystem/services/posts/server/TempFileIdentifierInputProvider.kt @@ -0,0 +1,15 @@ +package dev.inmo.postssystem.services.posts.server + +import dev.inmo.postssystem.features.common.common.SimpleInputProvider +import dev.inmo.postssystem.features.files.common.FileId +import io.ktor.utils.io.core.Input +import kotlinx.serialization.Serializable + +@Serializable +internal data class TempFileIdentifierInputProvider( + val tempFile: FileId +) : SimpleInputProvider { + override fun invoke(): Input { + TODO("Not yet implemented") + } +}