diff --git a/CHANGELOG.md b/CHANGELOG.md index a3da290085c..12af78d0f75 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,8 +5,10 @@ * `Ktor`: * `Client`: * `UnifiedRequester` now have no private fields + * Add preview work with multipart * `Server` * `UnifiedRouter` now have no private fields + * Add preview work with multipart ## 0.8.6 diff --git a/common/src/commonMain/kotlin/dev/inmo/micro_utils/common/MPPFile.kt b/common/src/commonMain/kotlin/dev/inmo/micro_utils/common/MPPFile.kt index 8aada0ca9c7..7aef8ed8c9a 100644 --- a/common/src/commonMain/kotlin/dev/inmo/micro_utils/common/MPPFile.kt +++ b/common/src/commonMain/kotlin/dev/inmo/micro_utils/common/MPPFile.kt @@ -23,11 +23,12 @@ value class FileName(val string: String) { } -@PreviewFeature expect class MPPFile expect val MPPFile.filename: FileName expect val MPPFile.filesize: Long +expect val MPPFile.bytesAllocatorSync: ByteArrayAllocator expect val MPPFile.bytesAllocator: SuspendByteArrayAllocator +fun MPPFile.bytesSync() = bytesAllocatorSync() suspend fun MPPFile.bytes() = bytesAllocator() diff --git a/common/src/jsMain/kotlin/dev/inmo/micro_utils/common/JSMPPFile.kt b/common/src/jsMain/kotlin/dev/inmo/micro_utils/common/JSMPPFile.kt index 249c9a487f3..7e3d865bfb6 100644 --- a/common/src/jsMain/kotlin/dev/inmo/micro_utils/common/JSMPPFile.kt +++ b/common/src/jsMain/kotlin/dev/inmo/micro_utils/common/JSMPPFile.kt @@ -2,8 +2,7 @@ package dev.inmo.micro_utils.common import org.khronos.webgl.ArrayBuffer import org.w3c.dom.ErrorEvent -import org.w3c.files.File -import org.w3c.files.FileReader +import org.w3c.files.* import kotlin.js.Promise /** @@ -24,6 +23,11 @@ fun MPPFile.readBytesPromise() = Promise { success, failure -> reader.readAsArrayBuffer(this) } +fun MPPFile.readBytes(): ByteArray { + val reader = FileReaderSync() + return reader.readAsArrayBuffer(this).toByteArray() +} + private suspend fun MPPFile.dirtyReadBytes(): ByteArray = readBytesPromise().await() /** @@ -40,5 +44,11 @@ actual val MPPFile.filesize: Long * @suppress */ @Warning("That is not optimized version of bytes allocator. Use asyncBytesAllocator everywhere you can") +actual val MPPFile.bytesAllocatorSync: ByteArrayAllocator + get() = ::readBytes +/** + * @suppress + */ +@Warning("That is not optimized version of bytes allocator. Use asyncBytesAllocator everywhere you can") actual val MPPFile.bytesAllocator: SuspendByteArrayAllocator get() = ::dirtyReadBytes diff --git a/common/src/jvmMain/kotlin/dev/inmo/micro_utils/common/JVMMPPFile.kt b/common/src/jvmMain/kotlin/dev/inmo/micro_utils/common/JVMMPPFile.kt index cd132ffd948..e245fe93836 100644 --- a/common/src/jvmMain/kotlin/dev/inmo/micro_utils/common/JVMMPPFile.kt +++ b/common/src/jvmMain/kotlin/dev/inmo/micro_utils/common/JVMMPPFile.kt @@ -22,6 +22,11 @@ actual val MPPFile.filesize: Long /** * @suppress */ +actual val MPPFile.bytesAllocatorSync: ByteArrayAllocator + get() = ::readBytes +/** + * @suppress + */ actual val MPPFile.bytesAllocator: SuspendByteArrayAllocator get() = { doInIO { diff --git a/ktor/client/src/commonMain/kotlin/dev/inmo/micro_utils/ktor/client/MPPFileInputProvider.kt b/ktor/client/src/commonMain/kotlin/dev/inmo/micro_utils/ktor/client/MPPFileInputProvider.kt new file mode 100644 index 00000000000..de8363bb3cb --- /dev/null +++ b/ktor/client/src/commonMain/kotlin/dev/inmo/micro_utils/ktor/client/MPPFileInputProvider.kt @@ -0,0 +1,6 @@ +package dev.inmo.micro_utils.ktor.client + +import dev.inmo.micro_utils.common.MPPFile +import io.ktor.client.request.forms.InputProvider + +expect suspend fun MPPFile.inputProvider(): InputProvider diff --git a/ktor/client/src/commonMain/kotlin/dev/inmo/micro_utils/ktor/client/StandardHttpClientGetPost.kt b/ktor/client/src/commonMain/kotlin/dev/inmo/micro_utils/ktor/client/StandardHttpClientGetPost.kt index 82b7d2710d6..e6725a2ed24 100644 --- a/ktor/client/src/commonMain/kotlin/dev/inmo/micro_utils/ktor/client/StandardHttpClientGetPost.kt +++ b/ktor/client/src/commonMain/kotlin/dev/inmo/micro_utils/ktor/client/StandardHttpClientGetPost.kt @@ -1,9 +1,13 @@ package dev.inmo.micro_utils.ktor.client +import dev.inmo.micro_utils.common.MPPFile +import dev.inmo.micro_utils.common.filename import dev.inmo.micro_utils.ktor.common.* import io.ktor.client.HttpClient -import io.ktor.client.request.get -import io.ktor.client.request.post +import io.ktor.client.request.* +import io.ktor.client.request.forms.* +import io.ktor.http.* +import io.ktor.utils.io.core.ByteReadPacket import kotlinx.serialization.* typealias BodyPair = Pair, T> @@ -31,6 +35,54 @@ class UnifiedRequester( resultDeserializer: DeserializationStrategy ) = client.unipost(url, bodyInfo, resultDeserializer, serialFormat) + suspend fun unimultipart( + url: String, + filename: String, + inputProvider: InputProvider, + resultDeserializer: DeserializationStrategy, + mimetype: String = "*/*", + additionalParametersBuilder: FormBuilder.() -> Unit = {}, + dataHeadersBuilder: HeadersBuilder.() -> Unit = {}, + requestBuilder: HttpRequestBuilder.() -> Unit = {}, + ): ResultType = client.unimultipart(url, filename, inputProvider, resultDeserializer, mimetype, additionalParametersBuilder, dataHeadersBuilder, requestBuilder, serialFormat) + + suspend fun unimultipart( + url: String, + filename: String, + inputProvider: InputProvider, + otherData: BodyPair, + resultDeserializer: DeserializationStrategy, + mimetype: String = "*/*", + additionalParametersBuilder: FormBuilder.() -> Unit = {}, + dataHeadersBuilder: HeadersBuilder.() -> Unit = {}, + requestBuilder: HttpRequestBuilder.() -> Unit = {}, + ): ResultType = client.unimultipart(url, filename, otherData, inputProvider, resultDeserializer, mimetype, additionalParametersBuilder, dataHeadersBuilder, requestBuilder, serialFormat) + + suspend fun unimultipart( + url: String, + mppFile: MPPFile, + resultDeserializer: DeserializationStrategy, + mimetype: String = "*/*", + additionalParametersBuilder: FormBuilder.() -> Unit = {}, + dataHeadersBuilder: HeadersBuilder.() -> Unit = {}, + requestBuilder: HttpRequestBuilder.() -> Unit = {} + ): ResultType = client.unimultipart( + url, mppFile, resultDeserializer, mimetype, additionalParametersBuilder, dataHeadersBuilder, requestBuilder, serialFormat + ) + + suspend fun unimultipart( + url: String, + mppFile: MPPFile, + otherData: BodyPair, + resultDeserializer: DeserializationStrategy, + mimetype: String = "*/*", + additionalParametersBuilder: FormBuilder.() -> Unit = {}, + dataHeadersBuilder: HeadersBuilder.() -> Unit = {}, + requestBuilder: HttpRequestBuilder.() -> Unit = {} + ): ResultType = client.unimultipart( + url, mppFile, otherData, resultDeserializer, mimetype, additionalParametersBuilder, dataHeadersBuilder, requestBuilder, serialFormat + ) + fun createStandardWebsocketFlow( url: String, checkReconnection: (Throwable?) -> Boolean = { true }, @@ -69,3 +121,124 @@ suspend fun HttpClient.unipost( }.let { serialFormat.decodeDefault(resultDeserializer, it) } + +suspend fun HttpClient.unimultipart( + url: String, + filename: String, + inputProvider: InputProvider, + resultDeserializer: DeserializationStrategy, + mimetype: String = "*/*", + additionalParametersBuilder: FormBuilder.() -> Unit = {}, + dataHeadersBuilder: HeadersBuilder.() -> Unit = {}, + requestBuilder: HttpRequestBuilder.() -> Unit = {}, + serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat +): ResultType = submitFormWithBinaryData( + url, + formData = formData { + append( + "bytes", + inputProvider, + Headers.build { + append(HttpHeaders.ContentType, mimetype) + append(HttpHeaders.ContentDisposition, "filename=$filename") + dataHeadersBuilder() + } + ) + additionalParametersBuilder() + } +) { + requestBuilder() +}.let { serialFormat.decodeDefault(resultDeserializer, it) } + +suspend fun HttpClient.unimultipart( + url: String, + filename: String, + otherData: BodyPair, + inputProvider: InputProvider, + resultDeserializer: DeserializationStrategy, + mimetype: String = "*/*", + additionalParametersBuilder: FormBuilder.() -> Unit = {}, + dataHeadersBuilder: HeadersBuilder.() -> Unit = {}, + requestBuilder: HttpRequestBuilder.() -> Unit = {}, + serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat +): ResultType = unimultipart( + url, + filename, + inputProvider, + resultDeserializer, + mimetype, + additionalParametersBuilder = { + val serialized = serialFormat.encodeDefault(otherData.first, otherData.second) + append( + "data", + InputProvider(serialized.size.toLong()) { + ByteReadPacket(serialized) + }, + Headers.build { + append(HttpHeaders.ContentType, ContentType.Application.Cbor.contentType) + append(HttpHeaders.ContentDisposition, "filename=data.bytes") + dataHeadersBuilder() + } + ) + additionalParametersBuilder() + }, + dataHeadersBuilder, + requestBuilder, + serialFormat +) + +suspend fun HttpClient.unimultipart( + url: String, + mppFile: MPPFile, + resultDeserializer: DeserializationStrategy, + mimetype: String = "*/*", + additionalParametersBuilder: FormBuilder.() -> Unit = {}, + dataHeadersBuilder: HeadersBuilder.() -> Unit = {}, + requestBuilder: HttpRequestBuilder.() -> Unit = {}, + serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat +): ResultType = unimultipart( + url, + mppFile.filename.string, + mppFile.inputProvider(), + resultDeserializer, + mimetype, + additionalParametersBuilder, + dataHeadersBuilder, + requestBuilder, + serialFormat +) + +suspend fun HttpClient.unimultipart( + url: String, + mppFile: MPPFile, + otherData: BodyPair, + resultDeserializer: DeserializationStrategy, + mimetype: String = "*/*", + additionalParametersBuilder: FormBuilder.() -> Unit = {}, + dataHeadersBuilder: HeadersBuilder.() -> Unit = {}, + requestBuilder: HttpRequestBuilder.() -> Unit = {}, + serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat +): ResultType = unimultipart( + url, + mppFile, + resultDeserializer, + mimetype, + additionalParametersBuilder = { + val serialized = serialFormat.encodeDefault(otherData.first, otherData.second) + append( + "data", + InputProvider(serialized.size.toLong()) { + ByteReadPacket(serialized) + }, + Headers.build { + append(HttpHeaders.ContentType, ContentType.Application.Cbor.contentType) + append(HttpHeaders.ContentDisposition, "filename=data.bytes") + dataHeadersBuilder() + } + ) + additionalParametersBuilder() + }, + dataHeadersBuilder, + requestBuilder, + serialFormat +) diff --git a/ktor/client/src/jsMain/kotlin/dev/inmo/micro_utils/ktor/client/ActualMPPFileInputProvider.kt b/ktor/client/src/jsMain/kotlin/dev/inmo/micro_utils/ktor/client/ActualMPPFileInputProvider.kt new file mode 100644 index 00000000000..8dafe4a6372 --- /dev/null +++ b/ktor/client/src/jsMain/kotlin/dev/inmo/micro_utils/ktor/client/ActualMPPFileInputProvider.kt @@ -0,0 +1,11 @@ +package dev.inmo.micro_utils.ktor.client + +import dev.inmo.micro_utils.common.* +import io.ktor.client.request.forms.InputProvider +import io.ktor.utils.io.core.ByteReadPacket + +actual suspend fun MPPFile.inputProvider(): InputProvider = bytes().let { + InputProvider(it.size.toLong()) { + ByteReadPacket(it) + } +} diff --git a/ktor/client/src/jvmMain/kotlin/dev/inmo/micro_utils/ktor/client/ActualMPPFileInputProvider.kt b/ktor/client/src/jvmMain/kotlin/dev/inmo/micro_utils/ktor/client/ActualMPPFileInputProvider.kt new file mode 100644 index 00000000000..1b7790568f3 --- /dev/null +++ b/ktor/client/src/jvmMain/kotlin/dev/inmo/micro_utils/ktor/client/ActualMPPFileInputProvider.kt @@ -0,0 +1,9 @@ +package dev.inmo.micro_utils.ktor.client + +import dev.inmo.micro_utils.common.MPPFile +import io.ktor.client.request.forms.InputProvider +import io.ktor.utils.io.streams.asInput + +actual suspend fun MPPFile.inputProvider(): InputProvider = InputProvider(length()) { + inputStream().asInput() +} diff --git a/ktor/common/build.gradle b/ktor/common/build.gradle index 64cbbd21ac2..b07a884bf17 100644 --- a/ktor/common/build.gradle +++ b/ktor/common/build.gradle @@ -10,6 +10,7 @@ kotlin { sourceSets { commonMain { dependencies { + api internalProject("micro_utils.common") api "org.jetbrains.kotlinx:kotlinx-serialization-cbor:$kotlin_serialisation_core_version" api "com.soywiz.korlibs.klock:klock:$klockVersion" } diff --git a/ktor/server/src/jvmMain/kotlin/dev/inmo/micro_utils/ktor/server/ServerRoutingShortcuts.kt b/ktor/server/src/jvmMain/kotlin/dev/inmo/micro_utils/ktor/server/ServerRoutingShortcuts.kt index 4e655ef8abb..4886e22bb20 100644 --- a/ktor/server/src/jvmMain/kotlin/dev/inmo/micro_utils/ktor/server/ServerRoutingShortcuts.kt +++ b/ktor/server/src/jvmMain/kotlin/dev/inmo/micro_utils/ktor/server/ServerRoutingShortcuts.kt @@ -1,18 +1,25 @@ package dev.inmo.micro_utils.ktor.server +import dev.inmo.micro_utils.common.* import dev.inmo.micro_utils.coroutines.safely import dev.inmo.micro_utils.ktor.common.* import io.ktor.application.ApplicationCall import io.ktor.application.call import io.ktor.http.ContentType import io.ktor.http.HttpStatusCode +import io.ktor.http.content.PartData +import io.ktor.http.content.forEachPart import io.ktor.request.receive +import io.ktor.request.receiveMultipart import io.ktor.response.respond import io.ktor.response.respondBytes import io.ktor.routing.Route import io.ktor.util.pipeline.PipelineContext +import io.ktor.utils.io.core.Input +import io.ktor.utils.io.core.readBytes import kotlinx.coroutines.flow.Flow import kotlinx.serialization.* +import java.io.File.createTempFile class UnifiedRouter( val serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat, @@ -104,6 +111,119 @@ suspend fun ApplicationCall.uniload( ) } +suspend fun ApplicationCall.uniloadMultipart( + onFormItem: (PartData.FormItem) -> Unit = {}, + onCustomFileItem: (PartData.FileItem) -> Unit = {}, + onBinaryContent: (PartData.BinaryItem) -> Unit = {} +) = safely { + val multipartData = receiveMultipart() + + var resultInput: Input? = null + + multipartData.forEachPart { + when (it) { + is PartData.FormItem -> onFormItem(it) + is PartData.FileItem -> { + when (it.name) { + "bytes" -> resultInput = it.provider() + else -> onCustomFileItem(it) + } + } + is PartData.BinaryItem -> onBinaryContent(it) + } + } + + resultInput ?: error("Bytes has not been received") +} + +suspend fun ApplicationCall.uniloadMultipart( + deserializer: DeserializationStrategy, + onFormItem: (PartData.FormItem) -> Unit = {}, + onCustomFileItem: (PartData.FileItem) -> Unit = {}, + onBinaryContent: (PartData.BinaryItem) -> Unit = {} +): Pair { + var data: Optional? = null + val resultInput = uniloadMultipart( + onFormItem, + { + if (it.name == "data") { + data = standardKtorSerialFormat.decodeDefault(deserializer, it.provider().readBytes()).optional + } else { + onCustomFileItem(it) + } + }, + onBinaryContent + ) + + val completeData = data ?: error("Data has not been received") + return resultInput to (completeData.dataOrNull().let { it as T }) +} + +suspend fun ApplicationCall.uniloadMultipartFile( + deserializer: DeserializationStrategy, + onFormItem: (PartData.FormItem) -> Unit = {}, + onCustomFileItem: (PartData.FileItem) -> Unit = {}, + onBinaryContent: (PartData.BinaryItem) -> Unit = {}, +) = safely { + val multipartData = receiveMultipart() + + var resultInput: MPPFile? = null + var data: Optional? = null + + multipartData.forEachPart { + when (it) { + is PartData.FormItem -> onFormItem(it) + is PartData.FileItem -> { + when (it.name) { + "bytes" -> { + val name = FileName(it.originalFileName ?: error("File name is unknown for default part")) + resultInput = MPPFile.createTempFile( + name.nameWithoutExtension, + ".${name.extension}" + ) + } + "data" -> data = standardKtorSerialFormat.decodeDefault(deserializer, it.provider().readBytes()).optional + else -> onCustomFileItem(it) + } + } + is PartData.BinaryItem -> onBinaryContent(it) + } + } + + val completeData = data ?: error("Data has not been received") + (resultInput ?: error("Bytes has not been received")) to (completeData.dataOrNull().let { it as T }) +} + +suspend fun ApplicationCall.uniloadMultipartFile( + onFormItem: (PartData.FormItem) -> Unit = {}, + onCustomFileItem: (PartData.FileItem) -> Unit = {}, + onBinaryContent: (PartData.BinaryItem) -> Unit = {}, +) = safely { + val multipartData = receiveMultipart() + + var resultInput: MPPFile? = null + + multipartData.forEachPart { + when (it) { + is PartData.FormItem -> onFormItem(it) + is PartData.FileItem -> { + if (it.name == "bytes") { + val name = FileName(it.originalFileName ?: error("File name is unknown for default part")) + resultInput = MPPFile.createTempFile( + name.nameWithoutExtension, + ".${name.extension}" + ) + } else { + onCustomFileItem(it) + } + } + is PartData.BinaryItem -> onBinaryContent(it) + } + } + + resultInput ?: error("Bytes has not been received") +} + suspend fun ApplicationCall.getParameterOrSendError( field: String ) = parameters[field].also {