From 844a1290941e2d30b69c57ee608522836d3f0468 Mon Sep 17 00:00:00 2001 From: InsanusMokrassar Date: Tue, 22 Nov 2022 12:57:43 +0600 Subject: [PATCH] complete universal uniupload --- .../dev/inmo/micro_utils/common/Progress.kt | 37 ++++++++ .../micro_utils/common/ProgressOperations.kt | 80 +++++++++++++++++ ktor/client/build.gradle | 4 + .../ktor/client/OnUploadCallback.kt | 3 + .../micro_utils/ktor/client/TemporalUpload.kt | 2 +- .../inmo/micro_utils/ktor/client/UniUpload.kt | 4 +- .../ktor/client/ActualTemporalUpload.kt | 2 +- .../ktor/client/ActualUniUpload.kt | 86 +++++++++++++++++++ .../ktor/client/ActualTemporalUpload.kt | 2 +- .../ktor/client/ActualUniUpload.kt | 21 +++-- 10 files changed, 229 insertions(+), 12 deletions(-) create mode 100644 common/src/commonMain/kotlin/dev/inmo/micro_utils/common/Progress.kt create mode 100644 common/src/commonMain/kotlin/dev/inmo/micro_utils/common/ProgressOperations.kt create mode 100644 ktor/client/src/commonMain/kotlin/dev/inmo/micro_utils/ktor/client/OnUploadCallback.kt create mode 100644 ktor/client/src/jsMain/kotlin/dev/inmo/micro_utils/ktor/client/ActualUniUpload.kt 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/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..9a5ec689d21 --- /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 = (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 index 91b524c7f2b..10a85ea73bf 100644 --- 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 @@ -4,7 +4,6 @@ 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 io.ktor.utils.io.core.Input import kotlinx.serialization.DeserializationStrategy import kotlinx.serialization.StringFormat import kotlinx.serialization.json.Json @@ -27,5 +26,6 @@ expect suspend fun HttpClient.uniupload( data: Map, resultDeserializer: DeserializationStrategy, headers: Headers = Headers.Empty, - stringFormat: StringFormat = Json.Default + 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..5ebb93f758c 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 @@ -55,5 +55,5 @@ suspend fun tempUpload( 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..6e6160449e1 --- /dev/null +++ b/ktor/client/src/jsMain/kotlin/dev/inmo/micro_utils/ktor/client/ActualUniUpload.kt @@ -0,0 +1,86 @@ +package dev.inmo.micro_utils.ktor.client + +import dev.inmo.micro_utils.common.MPPFile +import dev.inmo.micro_utils.common.Progress +import io.ktor.client.HttpClient +import io.ktor.http.Headers +import io.ktor.utils.io.core.readBytes +import kotlinx.coroutines.CompletableDeferred +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) + + 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() + request.responseType = XMLHttpRequestResponseType.TEXT + request.upload.onprogress = { + 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() +} 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 index ed394e21075..98342c32854 100644 --- 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 @@ -1,7 +1,8 @@ package dev.inmo.micro_utils.ktor.client -import dev.inmo.micro_utils.ktor.common.input +import dev.inmo.micro_utils.common.Progress import io.ktor.client.HttpClient +import io.ktor.client.plugins.onUpload import io.ktor.client.request.forms.InputProvider import io.ktor.client.request.forms.formData import io.ktor.client.request.forms.submitForm @@ -13,10 +14,8 @@ import io.ktor.http.HttpStatusCode import io.ktor.http.Parameters import io.ktor.http.content.PartData import kotlinx.serialization.DeserializationStrategy -import kotlinx.serialization.InternalSerializationApi import kotlinx.serialization.StringFormat import kotlinx.serialization.encodeToString -import kotlinx.serialization.serializer import java.io.File /** @@ -26,13 +25,13 @@ import java.io.File * [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 */ -@OptIn(InternalSerializationApi::class) actual suspend fun HttpClient.uniupload( url: String, data: Map, resultDeserializer: DeserializationStrategy, headers: Headers, - stringFormat: StringFormat + stringFormat: StringFormat, + onUpload: OnUploadCallback ): T? { val withBinary = data.values.any { it is File || it is UniUploadFileInfo } @@ -67,7 +66,11 @@ actual suspend fun HttpClient.uniupload( submitFormWithBinaryData( url, formData - ) + ) { + onUpload { bytesSentTotal, contentLength -> + onUpload(bytesSentTotal, contentLength) + } + } } else { submitForm( url, @@ -77,7 +80,11 @@ actual suspend fun HttpClient.uniupload( append(it.name!!, it.value) } } - ) + ) { + onUpload { bytesSentTotal, contentLength -> + onUpload(bytesSentTotal, contentLength) + } + } } return if (response.status == HttpStatusCode.OK) {