mirror of
https://github.com/InsanusMokrassar/MicroUtils.git
synced 2024-11-16 13:23:49 +00:00
commit
ae3a5bf45d
@ -1,5 +1,10 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 0.9.14
|
||||||
|
|
||||||
|
* `Ktor`:
|
||||||
|
* Add temporal files uploading functionality (for clients to upload and for server to receive)
|
||||||
|
|
||||||
## 0.9.13
|
## 0.9.13
|
||||||
|
|
||||||
* `Versions`:
|
* `Versions`:
|
||||||
|
@ -14,5 +14,5 @@ crypto_js_version=4.1.1
|
|||||||
# Project data
|
# Project data
|
||||||
|
|
||||||
group=dev.inmo
|
group=dev.inmo
|
||||||
version=0.9.13
|
version=0.9.14
|
||||||
android_code_version=103
|
android_code_version=104
|
||||||
|
@ -0,0 +1,19 @@
|
|||||||
|
package dev.inmo.micro_utils.ktor.client
|
||||||
|
|
||||||
|
import dev.inmo.micro_utils.common.MPPFile
|
||||||
|
import dev.inmo.micro_utils.ktor.common.*
|
||||||
|
import io.ktor.client.HttpClient
|
||||||
|
|
||||||
|
expect suspend fun HttpClient.tempUpload(
|
||||||
|
fullTempUploadDraftPath: String,
|
||||||
|
file: MPPFile,
|
||||||
|
onUpload: (uploaded: Long, count: Long) -> Unit = { _, _ -> }
|
||||||
|
): TemporalFileId
|
||||||
|
|
||||||
|
suspend fun UnifiedRequester.tempUpload(
|
||||||
|
fullTempUploadDraftPath: String,
|
||||||
|
file: MPPFile,
|
||||||
|
onUpload: (uploaded: Long, count: Long) -> Unit = { _, _ -> }
|
||||||
|
): TemporalFileId = client.tempUpload(
|
||||||
|
fullTempUploadDraftPath, file, onUpload
|
||||||
|
)
|
@ -0,0 +1,58 @@
|
|||||||
|
package dev.inmo.micro_utils.ktor.client
|
||||||
|
|
||||||
|
import dev.inmo.micro_utils.common.MPPFile
|
||||||
|
import dev.inmo.micro_utils.ktor.common.TemporalFileId
|
||||||
|
import io.ktor.client.HttpClient
|
||||||
|
import kotlinx.coroutines.*
|
||||||
|
import org.w3c.xhr.*
|
||||||
|
|
||||||
|
suspend fun tempUpload(
|
||||||
|
fullTempUploadDraftPath: String,
|
||||||
|
file: MPPFile,
|
||||||
|
onUpload: (Long, Long) -> Unit
|
||||||
|
): TemporalFileId {
|
||||||
|
val formData = FormData()
|
||||||
|
val answer = CompletableDeferred<TemporalFileId>()
|
||||||
|
|
||||||
|
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(TemporalFileId(request.responseText))
|
||||||
|
} else {
|
||||||
|
answer.completeExceptionally(Exception("Something went wrong: $it"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
request.onerror = {
|
||||||
|
answer.completeExceptionally(Exception("Something went wrong: $it"))
|
||||||
|
}
|
||||||
|
request.open("POST", fullTempUploadDraftPath, true)
|
||||||
|
request.send(formData)
|
||||||
|
|
||||||
|
val handle = currentCoroutineContext().job.invokeOnCompletion {
|
||||||
|
runCatching {
|
||||||
|
request.abort()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return runCatching {
|
||||||
|
answer.await()
|
||||||
|
}.also {
|
||||||
|
handle.dispose()
|
||||||
|
}.getOrThrow()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
actual suspend fun HttpClient.tempUpload(
|
||||||
|
fullTempUploadDraftPath: String,
|
||||||
|
file: MPPFile,
|
||||||
|
onUpload: (uploaded: Long, count: Long) -> Unit
|
||||||
|
): TemporalFileId = dev.inmo.micro_utils.ktor.client.tempUpload(fullTempUploadDraftPath, file, onUpload)
|
@ -0,0 +1,39 @@
|
|||||||
|
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.TemporalFileId
|
||||||
|
import io.ktor.client.HttpClient
|
||||||
|
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
|
||||||
|
import java.net.URLConnection
|
||||||
|
|
||||||
|
internal val MPPFile.mimeType: String
|
||||||
|
get() = URLConnection.getFileNameMap().getContentTypeFor(filename.name) ?: "*/*"
|
||||||
|
|
||||||
|
actual suspend fun HttpClient.tempUpload(
|
||||||
|
fullTempUploadDraftPath: String,
|
||||||
|
file: MPPFile,
|
||||||
|
onUpload: (Long, Long) -> Unit
|
||||||
|
): TemporalFileId {
|
||||||
|
val inputProvider = file.inputProvider()
|
||||||
|
val fileId = submitFormWithBinaryData<String>(
|
||||||
|
fullTempUploadDraftPath,
|
||||||
|
formData = formData {
|
||||||
|
append(
|
||||||
|
"data",
|
||||||
|
inputProvider,
|
||||||
|
Headers.build {
|
||||||
|
append(HttpHeaders.ContentType, file.mimeType)
|
||||||
|
append(HttpHeaders.ContentDisposition, "filename=\"${file.filename.string}\"")
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
onUpload(onUpload)
|
||||||
|
}
|
||||||
|
return TemporalFileId(fileId)
|
||||||
|
}
|
@ -13,6 +13,7 @@ kotlin {
|
|||||||
api internalProject("micro_utils.common")
|
api internalProject("micro_utils.common")
|
||||||
api libs.kt.serialization.cbor
|
api libs.kt.serialization.cbor
|
||||||
api libs.klock
|
api libs.klock
|
||||||
|
api libs.uuid
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,8 @@
|
|||||||
|
package dev.inmo.micro_utils.ktor.common
|
||||||
|
|
||||||
|
import kotlin.jvm.JvmInline
|
||||||
|
|
||||||
|
const val DefaultTemporalFilesSubPath = "temp_upload"
|
||||||
|
|
||||||
|
@JvmInline
|
||||||
|
value class TemporalFileId(val string: String)
|
@ -92,6 +92,10 @@ class UnifiedRouter(
|
|||||||
call.respond(HttpStatusCode.BadRequest, "Request query parameters must contains $field")
|
call.respond(HttpStatusCode.BadRequest, "Request query parameters must contains $field")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val default = defaultUnifiedRouter
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val defaultUnifiedRouter = UnifiedRouter()
|
val defaultUnifiedRouter = UnifiedRouter()
|
||||||
|
@ -0,0 +1,132 @@
|
|||||||
|
package dev.inmo.micro_utils.ktor.server
|
||||||
|
|
||||||
|
import com.benasher44.uuid.uuid4
|
||||||
|
import dev.inmo.micro_utils.common.FileName
|
||||||
|
import dev.inmo.micro_utils.common.MPPFile
|
||||||
|
import dev.inmo.micro_utils.coroutines.launchSafelyWithoutExceptions
|
||||||
|
import dev.inmo.micro_utils.ktor.common.DefaultTemporalFilesSubPath
|
||||||
|
import dev.inmo.micro_utils.ktor.common.TemporalFileId
|
||||||
|
import dev.inmo.micro_utils.ktor.server.configurators.ApplicationRoutingConfigurator
|
||||||
|
import io.ktor.application.call
|
||||||
|
import io.ktor.http.HttpStatusCode
|
||||||
|
import io.ktor.http.content.PartData
|
||||||
|
import io.ktor.http.content.streamProvider
|
||||||
|
import io.ktor.request.receiveMultipart
|
||||||
|
import io.ktor.response.respond
|
||||||
|
import io.ktor.routing.Route
|
||||||
|
import io.ktor.routing.post
|
||||||
|
import kotlinx.coroutines.*
|
||||||
|
import kotlinx.coroutines.flow.*
|
||||||
|
import kotlinx.coroutines.sync.Mutex
|
||||||
|
import kotlinx.coroutines.sync.withLock
|
||||||
|
import java.io.File
|
||||||
|
import java.nio.file.Files
|
||||||
|
import java.nio.file.attribute.FileTime
|
||||||
|
|
||||||
|
class TemporalFilesRoutingConfigurator(
|
||||||
|
private val subpath: String = DefaultTemporalFilesSubPath,
|
||||||
|
private val unifiedRouter: UnifiedRouter = UnifiedRouter.default,
|
||||||
|
private val temporalFilesUtilizer: TemporalFilesUtilizer = TemporalFilesUtilizer
|
||||||
|
) : ApplicationRoutingConfigurator.Element {
|
||||||
|
interface TemporalFilesUtilizer {
|
||||||
|
fun start(filesMap: MutableMap<TemporalFileId, MPPFile>, filesMutex: Mutex, onNewFileFlow: Flow<TemporalFileId>): Job
|
||||||
|
|
||||||
|
companion object : TemporalFilesUtilizer {
|
||||||
|
class ByTimerUtilizer(
|
||||||
|
private val removeMillis: Long,
|
||||||
|
private val scope: CoroutineScope
|
||||||
|
) : TemporalFilesUtilizer {
|
||||||
|
override fun start(
|
||||||
|
filesMap: MutableMap<TemporalFileId, MPPFile>,
|
||||||
|
filesMutex: Mutex,
|
||||||
|
onNewFileFlow: Flow<TemporalFileId>
|
||||||
|
): Job = scope.launchSafelyWithoutExceptions {
|
||||||
|
while (isActive) {
|
||||||
|
val filesWithCreationInfo = filesMap.mapNotNull { (fileId, file) ->
|
||||||
|
fileId to ((Files.getAttribute(file.toPath(), "creationTime") as? FileTime) ?.toMillis() ?: return@mapNotNull null)
|
||||||
|
}
|
||||||
|
if (filesWithCreationInfo.isEmpty()) {
|
||||||
|
delay(removeMillis)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
var min = filesWithCreationInfo.first()
|
||||||
|
for (fileWithCreationInfo in filesWithCreationInfo) {
|
||||||
|
if (fileWithCreationInfo.second < min.second) {
|
||||||
|
min = fileWithCreationInfo
|
||||||
|
}
|
||||||
|
}
|
||||||
|
delay(System.currentTimeMillis() - (min.second + removeMillis))
|
||||||
|
filesMutex.withLock {
|
||||||
|
filesMap.remove(min.first)
|
||||||
|
} ?.delete()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun start(
|
||||||
|
filesMap: MutableMap<TemporalFileId, MPPFile>,
|
||||||
|
filesMutex: Mutex,
|
||||||
|
onNewFileFlow: Flow<TemporalFileId>
|
||||||
|
): Job = Job()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val temporalFilesMap = mutableMapOf<TemporalFileId, MPPFile>()
|
||||||
|
private val temporalFilesMutex = Mutex()
|
||||||
|
private val filesFlow = MutableSharedFlow<TemporalFileId>()
|
||||||
|
val utilizerJob = temporalFilesUtilizer.start(temporalFilesMap, temporalFilesMutex, filesFlow.asSharedFlow())
|
||||||
|
|
||||||
|
override fun Route.invoke() {
|
||||||
|
post(subpath) {
|
||||||
|
unifiedRouter.apply {
|
||||||
|
val multipart = call.receiveMultipart()
|
||||||
|
|
||||||
|
var fileInfo: Pair<TemporalFileId, MPPFile>? = 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 = TemporalFileId(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)
|
||||||
|
launchSafelyWithoutExceptions { filesFlow.emit(fileId) }
|
||||||
|
} ?: call.respond(HttpStatusCode.BadRequest)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun removeTemporalFile(temporalFileId: TemporalFileId) {
|
||||||
|
temporalFilesMutex.withLock {
|
||||||
|
temporalFilesMap.remove(temporalFileId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getTemporalFile(temporalFileId: TemporalFileId) = temporalFilesMap[temporalFileId]
|
||||||
|
|
||||||
|
suspend fun getAndRemoveTemporalFile(temporalFileId: TemporalFileId) = temporalFilesMutex.withLock {
|
||||||
|
temporalFilesMap.remove(temporalFileId)
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user