temporal files uploading functionality

This commit is contained in:
InsanusMokrassar 2022-03-15 15:33:45 +06:00
parent 03117ac565
commit 21195e1bcb
8 changed files with 260 additions and 0 deletions

View File

@ -2,6 +2,9 @@
## 0.9.14 ## 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`:

View File

@ -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
)

View File

@ -0,0 +1,54 @@
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"))
}
}
request.onerror = {
answer.completeExceptionally(Exception("Something went wrong"))
}
request.open("POST", fullTempUploadDraftPath, true)
request.send(formData)
currentCoroutineContext().job.invokeOnCompletion {
runCatching {
request.abort()
}
}
return answer.await()
}
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)

View File

@ -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)
}

View File

@ -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
} }
} }
} }

View File

@ -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)

View File

@ -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()

View File

@ -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)
}
}