diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8ce82b06c8a..1558532d2ca 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -9,9 +9,6 @@ jobs: - uses: actions/setup-java@v1 with: java-version: 11 - - name: Fix android 32.0.0 dx - continue-on-error: true - run: cd /usr/local/lib/android/sdk/build-tools/32.0.0/ && mv d8 dx && cd lib && mv d8.jar dx.jar - name: Rewrite version run: | branch="`echo "${{ github.ref }}" | grep -o "[^/]*$"`" @@ -20,9 +17,8 @@ jobs: mv gradle.properties.tmp gradle.properties - name: Build run: ./gradlew build -# - name: Publish -# continue-on-error: true -# run: ./gradlew --no-parallel publishAllPublicationsToGithubPackagesRepository -# env: -# GITHUBPACKAGES_USER: ${{ github.actor }} -# GITHUBPACKAGES_PASSWORD: ${{ secrets.GITHUB_TOKEN }} + - name: Publish + continue-on-error: true + run: ./gradlew publishAllPublicationsToGiteaRepository + env: + GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }} diff --git a/CHANGELOG.md b/CHANGELOG.md index ad0031c5228..47ff8f9da13 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,16 @@ # Changelog +## 0.14.3 + +* `Common`: + * New type `Progress` +* `Ktor`: + * `Client`: + * New universal `uniUpload` extension for `HttpClient` + * `Server`: + * New universal `handleUniUpload` extension for `ApplicationCall` + * Add extensions `download` and `downloadToTemporalFile` + ## 0.14.2 * `Versions`: diff --git a/common/src/commonMain/kotlin/dev/inmo/micro_utils/common/Progress.kt b/common/src/commonMain/kotlin/dev/inmo/micro_utils/common/Progress.kt new file mode 100644 index 00000000000..dbce92508f0 --- /dev/null +++ b/common/src/commonMain/kotlin/dev/inmo/micro_utils/common/Progress.kt @@ -0,0 +1,37 @@ +package dev.inmo.micro_utils.common + +import kotlinx.serialization.Serializable +import kotlin.jvm.JvmInline + +@Serializable +@JvmInline +value class Progress private constructor( + val of1: Double +) { + val of1Float + get() = of1.toFloat() + val of100 + get() = of1 * 100 + val of100Float + get() = of100.toFloat() + val of100Int + get() = of100.toInt() + + init { + require(of1 in rangeOfValues) { + "Progress main value should be in $rangeOfValues, but incoming value is $of1" + } + } + + companion object { + val rangeOfValues = 0.0 .. 1.0 + + val START = Progress(rangeOfValues.start) + val COMPLETED = Progress(rangeOfValues.endInclusive) + + operator fun invoke(of1: Double) = Progress(of1.coerceIn(rangeOfValues)) + operator fun invoke(part: Number, total: Number) = Progress( + part.toDouble() / total.toDouble() + ) + } +} diff --git a/common/src/commonMain/kotlin/dev/inmo/micro_utils/common/ProgressOperations.kt b/common/src/commonMain/kotlin/dev/inmo/micro_utils/common/ProgressOperations.kt new file mode 100644 index 00000000000..0d71c3325cc --- /dev/null +++ b/common/src/commonMain/kotlin/dev/inmo/micro_utils/common/ProgressOperations.kt @@ -0,0 +1,80 @@ +@file:Suppress( + "RemoveRedundantCallsOfConversionMethods", + "RedundantVisibilityModifier", +) + +package dev.inmo.micro_utils.common + +import kotlin.Byte +import kotlin.Double +import kotlin.Float +import kotlin.Int +import kotlin.Long +import kotlin.Short +import kotlin.Suppress + +public operator fun Progress.plus(other: Progress): Progress = Progress(of1 + other.of1) + +public operator fun Progress.minus(other: Progress): Progress = Progress(of1 - other.of1) + +public operator fun Progress.plus(i: Byte): Progress = Progress((of1 + i).toDouble()) + +public operator fun Progress.minus(i: Byte): Progress = Progress((of1 - i).toDouble()) + +public operator fun Progress.times(i: Byte): Progress = Progress((of1 * i).toDouble()) + +public operator fun Progress.div(i: Byte): Progress = Progress((of1 / i).toDouble()) + +public operator fun Progress.rem(i: Byte): Progress = Progress((of1 % i).toDouble()) + +public operator fun Progress.plus(i: Short): Progress = Progress((of1 + i).toDouble()) + +public operator fun Progress.minus(i: Short): Progress = Progress((of1 - i).toDouble()) + +public operator fun Progress.times(i: Short): Progress = Progress((of1 * i).toDouble()) + +public operator fun Progress.div(i: Short): Progress = Progress((of1 / i).toDouble()) + +public operator fun Progress.rem(i: Short): Progress = Progress((of1 % i).toDouble()) + +public operator fun Progress.plus(i: Int): Progress = Progress((of1 + i).toDouble()) + +public operator fun Progress.minus(i: Int): Progress = Progress((of1 - i).toDouble()) + +public operator fun Progress.times(i: Int): Progress = Progress((of1 * i).toDouble()) + +public operator fun Progress.div(i: Int): Progress = Progress((of1 / i).toDouble()) + +public operator fun Progress.rem(i: Int): Progress = Progress((of1 % i).toDouble()) + +public operator fun Progress.plus(i: Long): Progress = Progress((of1 + i).toDouble()) + +public operator fun Progress.minus(i: Long): Progress = Progress((of1 - i).toDouble()) + +public operator fun Progress.times(i: Long): Progress = Progress((of1 * i).toDouble()) + +public operator fun Progress.div(i: Long): Progress = Progress((of1 / i).toDouble()) + +public operator fun Progress.rem(i: Long): Progress = Progress((of1 % i).toDouble()) + +public operator fun Progress.plus(i: Float): Progress = Progress((of1 + i).toDouble()) + +public operator fun Progress.minus(i: Float): Progress = Progress((of1 - i).toDouble()) + +public operator fun Progress.times(i: Float): Progress = Progress((of1 * i).toDouble()) + +public operator fun Progress.div(i: Float): Progress = Progress((of1 / i).toDouble()) + +public operator fun Progress.rem(i: Float): Progress = Progress((of1 % i).toDouble()) + +public operator fun Progress.plus(i: Double): Progress = Progress((of1 + i).toDouble()) + +public operator fun Progress.minus(i: Double): Progress = Progress((of1 - i).toDouble()) + +public operator fun Progress.times(i: Double): Progress = Progress((of1 * i).toDouble()) + +public operator fun Progress.div(i: Double): Progress = Progress((of1 / i).toDouble()) + +public operator fun Progress.rem(i: Double): Progress = Progress((of1 % i).toDouble()) + +public operator fun Progress.compareTo(other: Progress): Int = (of1 - other.of1).toInt() diff --git a/gradle.properties b/gradle.properties index e6f4943cdba..48cf61f1249 100644 --- a/gradle.properties +++ b/gradle.properties @@ -14,5 +14,5 @@ crypto_js_version=4.1.1 # Project data group=dev.inmo -version=0.14.2 -android_code_version=163 +version=0.14.3 +android_code_version=164 diff --git a/ktor/client/build.gradle b/ktor/client/build.gradle index 19c17926dfe..f3ac97bea11 100644 --- a/ktor/client/build.gradle +++ b/ktor/client/build.gradle @@ -15,5 +15,9 @@ kotlin { api libs.ktor.client } } + + androidMain { + dependsOn jvmMain + } } } diff --git a/ktor/client/src/commonMain/kotlin/dev/inmo/micro_utils/ktor/client/OnUploadCallback.kt b/ktor/client/src/commonMain/kotlin/dev/inmo/micro_utils/ktor/client/OnUploadCallback.kt new file mode 100644 index 00000000000..abd79c93b2b --- /dev/null +++ b/ktor/client/src/commonMain/kotlin/dev/inmo/micro_utils/ktor/client/OnUploadCallback.kt @@ -0,0 +1,3 @@ +package dev.inmo.micro_utils.ktor.client + +typealias OnUploadCallback = suspend (uploaded: Long, count: Long) -> Unit diff --git a/ktor/client/src/commonMain/kotlin/dev/inmo/micro_utils/ktor/client/TemporalUpload.kt b/ktor/client/src/commonMain/kotlin/dev/inmo/micro_utils/ktor/client/TemporalUpload.kt index d09ddd3436c..ab0d0de9ab7 100644 --- a/ktor/client/src/commonMain/kotlin/dev/inmo/micro_utils/ktor/client/TemporalUpload.kt +++ b/ktor/client/src/commonMain/kotlin/dev/inmo/micro_utils/ktor/client/TemporalUpload.kt @@ -7,5 +7,5 @@ import io.ktor.client.HttpClient expect suspend fun HttpClient.tempUpload( fullTempUploadDraftPath: String, file: MPPFile, - onUpload: (uploaded: Long, count: Long) -> Unit = { _, _ -> } + onUpload: OnUploadCallback = { _, _ -> } ): TemporalFileId diff --git a/ktor/client/src/commonMain/kotlin/dev/inmo/micro_utils/ktor/client/UniUpload.kt b/ktor/client/src/commonMain/kotlin/dev/inmo/micro_utils/ktor/client/UniUpload.kt new file mode 100644 index 00000000000..6bef13f2f33 --- /dev/null +++ b/ktor/client/src/commonMain/kotlin/dev/inmo/micro_utils/ktor/client/UniUpload.kt @@ -0,0 +1,31 @@ +package dev.inmo.micro_utils.ktor.client + +import dev.inmo.micro_utils.common.FileName +import dev.inmo.micro_utils.ktor.common.LambdaInputProvider +import io.ktor.client.HttpClient +import io.ktor.http.Headers +import kotlinx.serialization.DeserializationStrategy +import kotlinx.serialization.StringFormat +import kotlinx.serialization.json.Json + +data class UniUploadFileInfo( + val fileName: FileName, + val mimeType: String, + val inputAllocator: LambdaInputProvider +) + +/** + * Will execute submitting of multipart data request + * + * @param data [Map] where keys will be used as names for multipart parts and values as values. If you will pass + * [dev.inmo.micro_utils.common.MPPFile] (File from JS or JVM platform). Also you may pass [UniUploadFileInfo] as value + * in case you wish to pass other source of multipart binary data than regular file + */ +expect suspend fun HttpClient.uniUpload( + url: String, + data: Map, + resultDeserializer: DeserializationStrategy, + headers: Headers = Headers.Empty, + stringFormat: StringFormat = Json, + onUpload: OnUploadCallback = { _, _ -> } +): T? diff --git a/ktor/client/src/jsMain/kotlin/dev/inmo/micro_utils/ktor/client/ActualTemporalUpload.kt b/ktor/client/src/jsMain/kotlin/dev/inmo/micro_utils/ktor/client/ActualTemporalUpload.kt index 33c8a4910f5..35a9f89a6eb 100644 --- a/ktor/client/src/jsMain/kotlin/dev/inmo/micro_utils/ktor/client/ActualTemporalUpload.kt +++ b/ktor/client/src/jsMain/kotlin/dev/inmo/micro_utils/ktor/client/ActualTemporalUpload.kt @@ -1,6 +1,8 @@ package dev.inmo.micro_utils.ktor.client import dev.inmo.micro_utils.common.MPPFile +import dev.inmo.micro_utils.coroutines.LinkedSupervisorJob +import dev.inmo.micro_utils.coroutines.launchSafelyWithoutExceptions import dev.inmo.micro_utils.ktor.common.TemporalFileId import io.ktor.client.HttpClient import kotlinx.coroutines.* @@ -12,10 +14,11 @@ import org.w3c.xhr.XMLHttpRequest.Companion.DONE suspend fun tempUpload( fullTempUploadDraftPath: String, file: MPPFile, - onUpload: (Long, Long) -> Unit + onUpload: OnUploadCallback ): TemporalFileId { val formData = FormData() val answer = CompletableDeferred(currentCoroutineContext().job) + val subscope = CoroutineScope(currentCoroutineContext().LinkedSupervisorJob()) formData.append( "data", @@ -25,7 +28,7 @@ suspend fun tempUpload( val request = XMLHttpRequest() request.responseType = XMLHttpRequestResponseType.TEXT request.upload.onprogress = { - onUpload(it.loaded.toLong(), it.total.toLong()) + subscope.launchSafelyWithoutExceptions { onUpload(it.loaded.toLong(), it.total.toLong()) } } request.onload = { if (request.status == 200.toShort()) { @@ -48,12 +51,14 @@ suspend fun tempUpload( } } - return answer.await() + return answer.await().also { + subscope.cancel() + } } actual suspend fun HttpClient.tempUpload( fullTempUploadDraftPath: String, file: MPPFile, - onUpload: (uploaded: Long, count: Long) -> Unit + onUpload: OnUploadCallback ): TemporalFileId = dev.inmo.micro_utils.ktor.client.tempUpload(fullTempUploadDraftPath, file, onUpload) diff --git a/ktor/client/src/jsMain/kotlin/dev/inmo/micro_utils/ktor/client/ActualUniUpload.kt b/ktor/client/src/jsMain/kotlin/dev/inmo/micro_utils/ktor/client/ActualUniUpload.kt new file mode 100644 index 00000000000..e2a10effbe2 --- /dev/null +++ b/ktor/client/src/jsMain/kotlin/dev/inmo/micro_utils/ktor/client/ActualUniUpload.kt @@ -0,0 +1,96 @@ +package dev.inmo.micro_utils.ktor.client + +import dev.inmo.micro_utils.common.MPPFile +import dev.inmo.micro_utils.common.Progress +import dev.inmo.micro_utils.coroutines.LinkedSupervisorJob +import dev.inmo.micro_utils.coroutines.launchSafelyWithoutExceptions +import io.ktor.client.HttpClient +import io.ktor.http.Headers +import io.ktor.utils.io.core.readBytes +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.cancel +import kotlinx.coroutines.currentCoroutineContext +import kotlinx.coroutines.job +import kotlinx.serialization.DeserializationStrategy +import kotlinx.serialization.StringFormat +import kotlinx.serialization.encodeToString +import org.khronos.webgl.Int8Array +import org.w3c.files.Blob +import org.w3c.xhr.FormData +import org.w3c.xhr.TEXT +import org.w3c.xhr.XMLHttpRequest +import org.w3c.xhr.XMLHttpRequestResponseType + +/** + * Will execute submitting of multipart data request + * + * @param data [Map] where keys will be used as names for multipart parts and values as values. If you will pass + * [dev.inmo.micro_utils.common.MPPFile] (File from JS or JVM platform). Also you may pass [UniUploadFileInfo] as value + * in case you wish to pass other source of multipart binary data than regular file + */ +actual suspend fun HttpClient.uniUpload( + url: String, + data: Map, + resultDeserializer: DeserializationStrategy, + headers: Headers, + stringFormat: StringFormat, + onUpload: OnUploadCallback +): T? { + val formData = FormData() + val answer = CompletableDeferred(currentCoroutineContext().job) + val subscope = CoroutineScope(currentCoroutineContext().LinkedSupervisorJob()) + + data.forEach { (k, v) -> + when (v) { + is MPPFile -> formData.append( + k, + v + ) + is UniUploadFileInfo -> formData.append( + k, + Blob(arrayOf(Int8Array(v.inputAllocator().readBytes().toTypedArray()))), + v.fileName.name + ) + else -> formData.append( + k, + stringFormat.encodeToString(v) + ) + } + } + + val request = XMLHttpRequest() + headers.forEach { s, strings -> + request.setRequestHeader(s, strings.joinToString()) + } + request.responseType = XMLHttpRequestResponseType.TEXT + request.upload.onprogress = { + subscope.launchSafelyWithoutExceptions { onUpload(it.loaded.toLong(), it.total.toLong()) } + } + request.onload = { + if (request.status == 200.toShort()) { + answer.complete( + stringFormat.decodeFromString(resultDeserializer, request.responseText) + ) + } else { + answer.completeExceptionally(Exception("Something went wrong: $it")) + } + } + request.onerror = { + answer.completeExceptionally(Exception("Something went wrong: $it")) + } + request.open("POST", url, true) + request.send(formData) + + answer.invokeOnCompletion { + runCatching { + if (request.readyState != XMLHttpRequest.DONE) { + request.abort() + } + } + } + + return answer.await().also { + subscope.cancel() + } +} 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 index 1b7790568f3..ad8a9ee406a 100644 --- 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 @@ -4,6 +4,8 @@ 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()) { +fun MPPFile.inputProviderSync(): InputProvider = InputProvider(length()) { inputStream().asInput() } + +actual suspend fun MPPFile.inputProvider(): InputProvider = inputProviderSync() diff --git a/ktor/client/src/jvmMain/kotlin/dev/inmo/micro_utils/ktor/client/ActualTemporalUpload.kt b/ktor/client/src/jvmMain/kotlin/dev/inmo/micro_utils/ktor/client/ActualTemporalUpload.kt index 0dcd0dde1f4..3341ef88b83 100644 --- a/ktor/client/src/jvmMain/kotlin/dev/inmo/micro_utils/ktor/client/ActualTemporalUpload.kt +++ b/ktor/client/src/jvmMain/kotlin/dev/inmo/micro_utils/ktor/client/ActualTemporalUpload.kt @@ -18,7 +18,7 @@ internal val MPPFile.mimeType: String actual suspend fun HttpClient.tempUpload( fullTempUploadDraftPath: String, file: MPPFile, - onUpload: (Long, Long) -> Unit + onUpload: OnUploadCallback ): TemporalFileId { val inputProvider = file.inputProvider() val fileId = submitFormWithBinaryData( diff --git a/ktor/client/src/jvmMain/kotlin/dev/inmo/micro_utils/ktor/client/ActualUniUpload.kt b/ktor/client/src/jvmMain/kotlin/dev/inmo/micro_utils/ktor/client/ActualUniUpload.kt new file mode 100644 index 00000000000..27661c479f0 --- /dev/null +++ b/ktor/client/src/jvmMain/kotlin/dev/inmo/micro_utils/ktor/client/ActualUniUpload.kt @@ -0,0 +1,101 @@ +package dev.inmo.micro_utils.ktor.client + +import dev.inmo.micro_utils.common.Progress +import io.ktor.client.HttpClient +import io.ktor.client.engine.mergeHeaders +import io.ktor.client.plugins.onUpload +import io.ktor.client.request.HttpRequestBuilder +import io.ktor.client.request.forms.InputProvider +import io.ktor.client.request.forms.formData +import io.ktor.client.request.forms.submitForm +import io.ktor.client.request.forms.submitFormWithBinaryData +import io.ktor.client.request.headers +import io.ktor.client.statement.bodyAsText +import io.ktor.http.Headers +import io.ktor.http.HttpHeaders +import io.ktor.http.HttpStatusCode +import io.ktor.http.Parameters +import io.ktor.http.content.PartData +import kotlinx.serialization.DeserializationStrategy +import kotlinx.serialization.StringFormat +import kotlinx.serialization.encodeToString +import java.io.File + +/** + * Will execute submitting of multipart data request + * + * @param data [Map] where keys will be used as names for multipart parts and values as values. If you will pass + * [dev.inmo.micro_utils.common.MPPFile] (File from JS or JVM platform). Also you may pass [UniUploadFileInfo] as value + * in case you wish to pass other source of multipart binary data than regular file + */ +actual suspend fun HttpClient.uniUpload( + url: String, + data: Map, + resultDeserializer: DeserializationStrategy, + headers: Headers, + stringFormat: StringFormat, + onUpload: OnUploadCallback +): T? { + val withBinary = data.values.any { it is File || it is UniUploadFileInfo } + + val formData = formData { + data.forEach { (k, v) -> + when (v) { + is File -> append( + k, + v.inputProviderSync(), + Headers.build { + append(HttpHeaders.ContentType, v.mimeType) + append(HttpHeaders.ContentDisposition, "filename=\"${v.name}\"") + } + ) + is UniUploadFileInfo -> append( + k, + InputProvider(block = v.inputAllocator), + Headers.build { + append(HttpHeaders.ContentType, v.mimeType) + append(HttpHeaders.ContentDisposition, "filename=\"${v.fileName.name}\"") + } + ) + else -> append( + k, + stringFormat.encodeToString(v) + ) + } + } + } + + val requestBuilder: HttpRequestBuilder.() -> Unit = { + headers { + appendAll(headers) + } + onUpload { bytesSentTotal, contentLength -> + onUpload(bytesSentTotal, contentLength) + } + } + + val response = if (withBinary) { + submitFormWithBinaryData( + url, + formData, + block = requestBuilder + ) + } else { + submitForm( + url, + Parameters.build { + formData.forEach { + val formItem = (it as PartData.FormItem) + append(it.name!!, it.value) + } + }, + block = requestBuilder + ) + } + + return if (response.status == HttpStatusCode.OK) { + stringFormat.decodeFromString(resultDeserializer, response.bodyAsText()) + } else { + null + } +} diff --git a/ktor/common/src/commonMain/kotlin/dev/inmo/micro_utils/ktor/common/InputProvider.kt b/ktor/common/src/commonMain/kotlin/dev/inmo/micro_utils/ktor/common/InputProvider.kt new file mode 100644 index 00000000000..c03a604c2e1 --- /dev/null +++ b/ktor/common/src/commonMain/kotlin/dev/inmo/micro_utils/ktor/common/InputProvider.kt @@ -0,0 +1,5 @@ +package dev.inmo.micro_utils.ktor.common + +import io.ktor.utils.io.core.Input + +typealias LambdaInputProvider = () -> Input diff --git a/ktor/server/src/jvmMain/kotlin/dev/inmo/micro_utils/ktor/server/DownloadFileItem.kt b/ktor/server/src/jvmMain/kotlin/dev/inmo/micro_utils/ktor/server/DownloadFileItem.kt new file mode 100644 index 00000000000..2056a6e704b --- /dev/null +++ b/ktor/server/src/jvmMain/kotlin/dev/inmo/micro_utils/ktor/server/DownloadFileItem.kt @@ -0,0 +1,56 @@ +package dev.inmo.micro_utils.ktor.server + +import com.benasher44.uuid.uuid4 +import io.ktor.http.content.PartData +import io.ktor.utils.io.copyTo +import io.ktor.utils.io.core.copyTo +import io.ktor.utils.io.jvm.javaio.copyTo +import io.ktor.utils.io.streams.asOutput +import java.io.File + +fun PartData.FileItem.download(target: File) { + provider().use { input -> + target.outputStream().use { + input.copyTo(it.asOutput()) + } + } +} + +fun PartData.FileItem.downloadToTemporalFile(): File { + val outputFile = File.createTempFile(uuid4().toString(), ".temp").apply { + deleteOnExit() + } + download(outputFile) + return outputFile +} + +fun PartData.BinaryItem.download(target: File) { + provider().use { input -> + target.outputStream().use { + input.copyTo(it.asOutput()) + } + } +} + +fun PartData.BinaryItem.downloadToTemporalFile(): File { + val outputFile = File.createTempFile(uuid4().toString(), ".temp").apply { + deleteOnExit() + } + download(outputFile) + return outputFile +} + +suspend fun PartData.BinaryChannelItem.download(target: File) { + val input = provider() + target.outputStream().use { + input.copyTo(it) + } +} + +suspend fun PartData.BinaryChannelItem.downloadToTemporalFile(): File { + val outputFile = File.createTempFile(uuid4().toString(), ".temp").apply { + deleteOnExit() + } + download(outputFile) + return outputFile +} diff --git a/ktor/server/src/jvmMain/kotlin/dev/inmo/micro_utils/ktor/server/QueryParameters.kt b/ktor/server/src/jvmMain/kotlin/dev/inmo/micro_utils/ktor/server/QueryParameters.kt new file mode 100644 index 00000000000..053d316c631 --- /dev/null +++ b/ktor/server/src/jvmMain/kotlin/dev/inmo/micro_utils/ktor/server/QueryParameters.kt @@ -0,0 +1,25 @@ +package dev.inmo.micro_utils.ktor.server + +import io.ktor.http.HttpStatusCode +import io.ktor.server.application.ApplicationCall +import io.ktor.server.response.respond + +suspend fun ApplicationCall.getParameterOrSendError( + field: String +) = parameters[field].also { + if (it == null) { + respond(HttpStatusCode.BadRequest, "Request must contains $field") + } +} + +fun ApplicationCall.getQueryParameter( + field: String +) = request.queryParameters[field] + +suspend fun ApplicationCall.getQueryParameterOrSendError( + field: String +) = getQueryParameter(field).also { + if (it == null) { + respond(HttpStatusCode.BadRequest, "Request query parameters must contains $field") + } +} 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/UniloadMultipart.kt similarity index 63% rename from ktor/server/src/jvmMain/kotlin/dev/inmo/micro_utils/ktor/server/ServerRoutingShortcuts.kt rename to ktor/server/src/jvmMain/kotlin/dev/inmo/micro_utils/ktor/server/UniloadMultipart.kt index 968a3607d11..27071b701b6 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/UniloadMultipart.kt @@ -2,22 +2,34 @@ 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.http.* import io.ktor.http.content.* import io.ktor.server.application.ApplicationCall -import io.ktor.server.application.call -import io.ktor.server.request.receive import io.ktor.server.request.receiveMultipart -import io.ktor.server.response.respond -import io.ktor.server.response.respondBytes -import io.ktor.server.routing.Route -import io.ktor.server.websocket.WebSocketServerSession -import io.ktor.util.pipeline.PipelineContext import io.ktor.utils.io.core.* -import kotlinx.coroutines.flow.Flow -import kotlinx.serialization.DeserializationStrategy -import kotlinx.serialization.SerializationStrategy +import kotlinx.coroutines.currentCoroutineContext +import kotlinx.coroutines.isActive + +/** + * Server-side part which receives [dev.inmo.micro_utils.ktor.client.uniUpload] request + */ +suspend inline fun ApplicationCall.handleUniUpload( + onFormItem: (PartData.FormItem) -> Unit = {}, + onBinaryContent: (PartData.BinaryItem) -> Unit = {}, + onBinaryChannelItem: (PartData.BinaryChannelItem) -> Unit = {}, + onFileItem: (PartData.FileItem) -> Unit = {} +) { + val multipartData = receiveMultipart() + + while (currentCoroutineContext().isActive) { + val partData = multipartData.readPart() ?: break + when (partData) { + is PartData.FormItem -> onFormItem(partData) + is PartData.FileItem -> onFileItem(partData) + is PartData.BinaryItem -> onBinaryContent(partData) + is PartData.BinaryChannelItem -> onBinaryChannelItem(partData) + } + } +} suspend fun ApplicationCall.uniloadMultipart( onFormItem: (PartData.FormItem) -> Unit = {}, @@ -25,21 +37,16 @@ suspend fun ApplicationCall.uniloadMultipart( onBinaryChannelItem: (PartData.BinaryChannelItem) -> 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) - is PartData.BinaryChannelItem -> onBinaryChannelItem(it) + handleUniUpload( + onFormItem, + onBinaryContent, + onBinaryChannelItem + ) { + when (it.name) { + "bytes" -> resultInput = it.provider() + else -> onCustomFileItem(it) } } @@ -89,23 +96,3 @@ suspend fun ApplicationCall.uniloadMultipartFile( resultInput ?: error("Bytes has not been received") } - -suspend fun ApplicationCall.getParameterOrSendError( - field: String -) = parameters[field].also { - if (it == null) { - respond(HttpStatusCode.BadRequest, "Request must contains $field") - } -} - -fun ApplicationCall.getQueryParameter( - field: String -) = request.queryParameters[field] - -suspend fun ApplicationCall.getQueryParameterOrSendError( - field: String -) = getQueryParameter(field).also { - if (it == null) { - respond(HttpStatusCode.BadRequest, "Request query parameters must contains $field") - } -} diff --git a/repos/common/build.gradle b/repos/common/build.gradle index 7a315ab290a..e2b157e2674 100644 --- a/repos/common/build.gradle +++ b/repos/common/build.gradle @@ -28,6 +28,7 @@ kotlin { api internalProject("micro_utils.common") api internalProject("micro_utils.coroutines") } + dependsOn jvmMain } } }