diff --git a/CHANGELOG.md b/CHANGELOG.md index ac926cb7010..12af78d0f75 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # Changelog +## 0.8.7 + +* `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 * `Common`: 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/gradle.properties b/gradle.properties index 1a4126017ce..d8a06549686 100644 --- a/gradle.properties +++ b/gradle.properties @@ -45,5 +45,5 @@ dokka_version=1.5.31 # Project data group=dev.inmo -version=0.8.6 -android_code_version=86 +version=0.8.7 +android_code_version=87 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 182f4eb66f7..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,16 +1,20 @@ 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> class UnifiedRequester( - private val client: HttpClient = HttpClient(), - private val serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat + val client: HttpClient = HttpClient(), + val serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat ) { suspend fun uniget( url: String, @@ -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 b020d634803..344092679c9 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,22 +1,31 @@ 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.asStream +import io.ktor.util.cio.writeChannel import io.ktor.util.pipeline.PipelineContext +import io.ktor.utils.io.core.* import kotlinx.coroutines.flow.Flow import kotlinx.serialization.* +import java.io.File +import java.io.File.createTempFile class UnifiedRouter( - private val serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat, - private val serialFormatContentType: ContentType = standardKtorSerialFormatContentType + val serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat, + val serialFormatContentType: ContentType = standardKtorSerialFormatContentType ) { fun Route.includeWebsocketHandling( suburl: String, @@ -104,6 +113,127 @@ 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}" + ).apply { + outputStream().use { fileStream -> + it.provider().asStream().copyTo(fileStream) + } + } + } + "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}" + ).apply { + outputStream().use { fileStream -> + it.provider().asStream().copyTo(fileStream) + } + } + } else { + onCustomFileItem(it) + } + } + is PartData.BinaryItem -> onBinaryContent(it) + } + } + + resultInput ?: error("Bytes has not been received") +} + suspend fun ApplicationCall.getParameterOrSendError( field: String ) = parameters[field].also {