temp progress on binary correct including
This commit is contained in:
parent
186bfd7ac0
commit
99b953635e
@ -12,6 +12,7 @@ kotlin {
|
|||||||
dependencies {
|
dependencies {
|
||||||
api libs.microutils.common
|
api libs.microutils.common
|
||||||
api libs.microutils.serialization.typedserializer
|
api libs.microutils.serialization.typedserializer
|
||||||
|
api libs.microutils.mimetypes
|
||||||
api libs.klock
|
api libs.klock
|
||||||
api "io.insert-koin:koin-core:$koin_version"
|
api "io.insert-koin:koin-core:$koin_version"
|
||||||
api "com.benasher44:uuid:$uuid_version"
|
api "com.benasher44:uuid:$uuid_version"
|
||||||
|
@ -0,0 +1,6 @@
|
|||||||
|
package dev.inmo.postssystem.features.common.common
|
||||||
|
|
||||||
|
import dev.inmo.micro_utils.common.MPPFile
|
||||||
|
import dev.inmo.micro_utils.mime_types.MimeType
|
||||||
|
|
||||||
|
expect val MPPFile.mimeType: MimeType
|
@ -0,0 +1,8 @@
|
|||||||
|
package dev.inmo.postssystem.features.common.common
|
||||||
|
|
||||||
|
import dev.inmo.micro_utils.common.MPPFile
|
||||||
|
import dev.inmo.micro_utils.mime_types.*
|
||||||
|
|
||||||
|
actual val MPPFile.mimeType: MimeType
|
||||||
|
get() = findBuiltinMimeType(type) ?: KnownMimeTypes.Any
|
||||||
|
|
@ -0,0 +1,10 @@
|
|||||||
|
package dev.inmo.postssystem.features.common.common
|
||||||
|
|
||||||
|
import dev.inmo.micro_utils.common.MPPFile
|
||||||
|
import dev.inmo.micro_utils.common.filename
|
||||||
|
import dev.inmo.micro_utils.mime_types.*
|
||||||
|
import java.net.URLConnection
|
||||||
|
|
||||||
|
actual val MPPFile.mimeType: MimeType
|
||||||
|
get() = URLConnection.getFileNameMap().getContentTypeFor(filename.name) ?.let(::findBuiltinMimeType) ?: KnownMimeTypes.Any
|
||||||
|
|
@ -7,6 +7,8 @@ import dev.inmo.micro_utils.ktor.common.encodeHex
|
|||||||
import dev.inmo.micro_utils.repos.ktor.common.crud.createRouting
|
import dev.inmo.micro_utils.repos.ktor.common.crud.createRouting
|
||||||
import dev.inmo.micro_utils.repos.ktor.common.crud.updateRouting
|
import dev.inmo.micro_utils.repos.ktor.common.crud.updateRouting
|
||||||
import dev.inmo.micro_utils.repos.ktor.common.one_to_many.removeRoute
|
import dev.inmo.micro_utils.repos.ktor.common.one_to_many.removeRoute
|
||||||
|
import dev.inmo.postssystem.features.common.common.FileBasedInputProvider
|
||||||
|
import dev.inmo.postssystem.features.common.common.SimpleInputProvider
|
||||||
import dev.inmo.postssystem.features.content.common.*
|
import dev.inmo.postssystem.features.content.common.*
|
||||||
import dev.inmo.postssystem.features.posts.common.PostId
|
import dev.inmo.postssystem.features.posts.common.PostId
|
||||||
import dev.inmo.postssystem.features.posts.common.RegisteredPost
|
import dev.inmo.postssystem.features.posts.common.RegisteredPost
|
||||||
@ -20,9 +22,15 @@ import kotlinx.serialization.builtins.*
|
|||||||
|
|
||||||
class ClientWritePostsService(
|
class ClientWritePostsService(
|
||||||
private val baseUrl: String,
|
private val baseUrl: String,
|
||||||
private val unifiedRequester: UnifiedRequester
|
unifiedRequester: UnifiedRequester
|
||||||
) : WritePostsService {
|
) : WritePostsService {
|
||||||
private val root = buildStandardUrl(baseUrl, postsRootPath)
|
private val root = buildStandardUrl(baseUrl, postsRootPath)
|
||||||
|
private val unifiedRequester = UnifiedRequester(
|
||||||
|
unifiedRequester.client,
|
||||||
|
unifiedRequester.serialFormat.createWithSerializerModuleExtension {
|
||||||
|
contextual()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
private val contentEitherSerializer = EitherSerializer(ContentId.serializer(), ContentSerializer)
|
private val contentEitherSerializer = EitherSerializer(ContentId.serializer(), ContentSerializer)
|
||||||
private val contentsEitherSerializer = ListSerializer(contentEitherSerializer)
|
private val contentsEitherSerializer = ListSerializer(contentEitherSerializer)
|
||||||
@ -36,83 +44,59 @@ class ClientWritePostsService(
|
|||||||
root,
|
root,
|
||||||
removeRoute
|
removeRoute
|
||||||
)
|
)
|
||||||
|
private val tempUploadFullPath = buildStandardUrl(
|
||||||
|
baseUrl,
|
||||||
|
postsCreateTempPathPart
|
||||||
|
)
|
||||||
|
|
||||||
|
private suspend fun prepareContent(content: Content): Content? {
|
||||||
|
return (content as? BinaryContent) ?.let {
|
||||||
|
when (val provider = it.inputProvider) {
|
||||||
|
is FileBasedInputProvider -> {
|
||||||
|
val fileId = unifiedRequester.tempUpload(
|
||||||
|
tempUploadFullPath,
|
||||||
|
provider.file
|
||||||
|
)
|
||||||
|
it.copy(inputProvider = TempFileIdentifierInputProvider(fileId))
|
||||||
|
}
|
||||||
|
is TempFileIdentifierInputProvider -> it
|
||||||
|
else -> return@prepareContent null
|
||||||
|
}
|
||||||
|
} ?: content
|
||||||
|
}
|
||||||
|
|
||||||
override suspend fun create(newPost: FullNewPost): RegisteredPost? {
|
override suspend fun create(newPost: FullNewPost): RegisteredPost? {
|
||||||
return if (newPost.content.any { it is BinaryContent }) {
|
val mappedContent = newPost.content.mapNotNull {
|
||||||
val answer = unifiedRequester.client.post<ByteArray>(createFullPath) {
|
prepareContent(it)
|
||||||
formData {
|
|
||||||
newPost.content.forEachIndexed { i, content ->
|
|
||||||
when (content) {
|
|
||||||
is BinaryContent -> append(
|
|
||||||
i.toString(),
|
|
||||||
InputProvider(block = content.inputProvider::invoke),
|
|
||||||
headers {
|
|
||||||
append(HttpHeaders.ContentType, content.mimeType.raw)
|
|
||||||
append(HttpHeaders.ContentDisposition, "filename=\"${content.filename.name}\"")
|
|
||||||
}.build()
|
|
||||||
)
|
|
||||||
else -> append(
|
|
||||||
i.toString(),
|
|
||||||
unifiedRequester.serialFormat.encodeHex(ContentSerializer, content)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
unifiedRequester.serialFormat.decodeFromByteArray(RegisteredPost.serializer().nullable, answer)
|
|
||||||
} else {
|
|
||||||
unifiedRequester.unipost(
|
|
||||||
createFullPath,
|
|
||||||
contentsSerializer to newPost.content,
|
|
||||||
RegisteredPost.serializer().nullable
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
val mappedPost = newPost.copy(
|
||||||
|
content = mappedContent
|
||||||
|
)
|
||||||
|
return unifiedRequester.unipost(
|
||||||
|
createFullPath,
|
||||||
|
contentsSerializer to mappedPost.content,
|
||||||
|
RegisteredPost.serializer().nullable
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun update(
|
override suspend fun update(
|
||||||
postId: PostId,
|
postId: PostId,
|
||||||
content: List<Either<ContentId, Content>>
|
content: List<Either<ContentId, Content>>
|
||||||
): RegisteredPost? {
|
): RegisteredPost? {
|
||||||
return if (content.any { it.optionalT2.data is BinaryContent }) {
|
val mappedContent = content.mapNotNull {
|
||||||
val answer = unifiedRequester.client.post<ByteArray>(createFullPath) {
|
it.mapOnSecond { content ->
|
||||||
formData {
|
prepareContent(content) ?.either() ?: return@mapNotNull null
|
||||||
content.forEachIndexed { i, eitherContent ->
|
} ?: it
|
||||||
eitherContent.onFirst {
|
|
||||||
append(
|
|
||||||
i.toString(),
|
|
||||||
unifiedRequester.serialFormat.encodeHex(contentEitherSerializer, it.either())
|
|
||||||
)
|
|
||||||
}.onSecond {
|
|
||||||
when (it) {
|
|
||||||
is BinaryContent -> append(
|
|
||||||
i.toString(),
|
|
||||||
InputProvider(block = it.inputProvider::invoke),
|
|
||||||
headers {
|
|
||||||
append(HttpHeaders.ContentType, it.mimeType.raw)
|
|
||||||
append(HttpHeaders.ContentDisposition, "filename=\"${it.filename.name}\"")
|
|
||||||
}.build()
|
|
||||||
)
|
|
||||||
else -> append(
|
|
||||||
i.toString(),
|
|
||||||
unifiedRequester.serialFormat.encodeHex(contentEitherSerializer, it.either())
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
unifiedRequester.serialFormat.decodeFromByteArray(RegisteredPost.serializer().nullable, answer)
|
|
||||||
} else {
|
|
||||||
unifiedRequester.unipost(
|
|
||||||
buildStandardUrl(
|
|
||||||
root,
|
|
||||||
updateRouting,
|
|
||||||
postsPostIdParameter to unifiedRequester.encodeUrlQueryValue(PostId.serializer(), postId)
|
|
||||||
),
|
|
||||||
contentsEitherSerializer to content,
|
|
||||||
RegisteredPost.serializer().nullable
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
return unifiedRequester.unipost(
|
||||||
|
buildStandardUrl(
|
||||||
|
root,
|
||||||
|
updateRouting,
|
||||||
|
postsPostIdParameter to unifiedRequester.encodeUrlQueryValue(PostId.serializer(), postId)
|
||||||
|
),
|
||||||
|
contentsEitherSerializer to mappedContent,
|
||||||
|
RegisteredPost.serializer().nullable
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun remove(postId: PostId) = unifiedRequester.unipost(
|
override suspend fun remove(postId: PostId) = unifiedRequester.unipost(
|
||||||
|
@ -0,0 +1,15 @@
|
|||||||
|
package dev.inmo.postssystem.services.posts.client
|
||||||
|
|
||||||
|
import dev.inmo.postssystem.features.common.common.SimpleInputProvider
|
||||||
|
import dev.inmo.postssystem.features.files.common.FileId
|
||||||
|
import io.ktor.utils.io.core.Input
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
internal data class TempFileIdentifierInputProvider(
|
||||||
|
private val tempFile: FileId
|
||||||
|
) : SimpleInputProvider {
|
||||||
|
override fun invoke(): Input {
|
||||||
|
TODO("Not yet implemented")
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,12 @@
|
|||||||
|
package dev.inmo.postssystem.services.posts.client
|
||||||
|
|
||||||
|
import dev.inmo.micro_utils.common.MPPFile
|
||||||
|
import dev.inmo.micro_utils.ktor.client.UnifiedRequester
|
||||||
|
import dev.inmo.postssystem.features.files.common.FileId
|
||||||
|
|
||||||
|
internal expect suspend fun UnifiedRequester.tempUpload(
|
||||||
|
fullTempUploadDraftPath: String,
|
||||||
|
file: MPPFile,
|
||||||
|
onUpload: (Long, Long) -> Unit = { _, _ -> }
|
||||||
|
): FileId
|
||||||
|
|
@ -0,0 +1,8 @@
|
|||||||
|
package dev.inmo.postssystem.services.posts.client.ui.create
|
||||||
|
|
||||||
|
import dev.inmo.postssystem.features.common.common.UIModel
|
||||||
|
import dev.inmo.postssystem.features.content.common.Content
|
||||||
|
|
||||||
|
interface PostCreateUIModel : UIModel<PostCreateUIState> {
|
||||||
|
suspend fun create(content: List<Content>)
|
||||||
|
}
|
@ -0,0 +1,14 @@
|
|||||||
|
package dev.inmo.postssystem.services.posts.client.ui.create
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
sealed class PostCreateUIState {
|
||||||
|
@Serializable
|
||||||
|
object Init : PostCreateUIState()
|
||||||
|
@Serializable
|
||||||
|
object Uploading : PostCreateUIState()
|
||||||
|
@Serializable
|
||||||
|
object Completed : PostCreateUIState()
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,50 @@
|
|||||||
|
package dev.inmo.postssystem.services.posts.client
|
||||||
|
|
||||||
|
import dev.inmo.micro_utils.common.MPPFile
|
||||||
|
import dev.inmo.micro_utils.ktor.client.UnifiedRequester
|
||||||
|
import dev.inmo.postssystem.features.files.common.FileId
|
||||||
|
import kotlinx.coroutines.CompletableDeferred
|
||||||
|
import kotlinx.coroutines.GlobalScope.coroutineContext
|
||||||
|
import kotlinx.coroutines.job
|
||||||
|
import org.w3c.xhr.*
|
||||||
|
|
||||||
|
internal actual suspend fun UnifiedRequester.tempUpload(
|
||||||
|
fullTempUploadDraftPath: String,
|
||||||
|
file: MPPFile,
|
||||||
|
onUpload: (Long, Long) -> Unit
|
||||||
|
): FileId {
|
||||||
|
val formData = FormData()
|
||||||
|
val answer = CompletableDeferred<FileId>()
|
||||||
|
|
||||||
|
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(FileId(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)
|
||||||
|
|
||||||
|
coroutineContext.job.invokeOnCompletion {
|
||||||
|
runCatching {
|
||||||
|
request.abort()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return answer.await()
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,38 @@
|
|||||||
|
package dev.inmo.postssystem.services.posts.client
|
||||||
|
|
||||||
|
import dev.inmo.micro_utils.common.MPPFile
|
||||||
|
import dev.inmo.micro_utils.common.filename
|
||||||
|
import dev.inmo.micro_utils.ktor.client.UnifiedRequester
|
||||||
|
import dev.inmo.micro_utils.ktor.client.inputProvider
|
||||||
|
import dev.inmo.postssystem.features.common.common.mimeType
|
||||||
|
import dev.inmo.postssystem.features.files.common.FileId
|
||||||
|
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
|
||||||
|
|
||||||
|
internal actual suspend fun UnifiedRequester.tempUpload(
|
||||||
|
fullTempUploadDraftPath: String,
|
||||||
|
file: MPPFile,
|
||||||
|
onUpload: (Long, Long) -> Unit
|
||||||
|
): FileId {
|
||||||
|
val inputProvider = file.inputProvider()
|
||||||
|
val fileId = client.submitFormWithBinaryData<String>(
|
||||||
|
fullTempUploadDraftPath,
|
||||||
|
formData = formData {
|
||||||
|
append(
|
||||||
|
"data",
|
||||||
|
inputProvider,
|
||||||
|
Headers.build {
|
||||||
|
append(HttpHeaders.ContentType, file.mimeType.raw)
|
||||||
|
append(HttpHeaders.ContentDisposition, "filename=\"${file.filename.string}\"")
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
onUpload(onUpload)
|
||||||
|
}
|
||||||
|
return FileId(fileId)
|
||||||
|
}
|
||||||
|
|
@ -12,6 +12,7 @@ kotlin {
|
|||||||
dependencies {
|
dependencies {
|
||||||
api project(":postssystem.features.common.common")
|
api project(":postssystem.features.common.common")
|
||||||
api project(":postssystem.features.posts.common")
|
api project(":postssystem.features.posts.common")
|
||||||
|
api project(":postssystem.features.files.common")
|
||||||
api libs.microutils.repos.common
|
api libs.microutils.repos.common
|
||||||
api libs.microutils.repos.ktor.client
|
api libs.microutils.repos.ktor.client
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,20 @@
|
|||||||
package dev.inmo.postssystem.services.posts.common
|
package dev.inmo.postssystem.services.posts.common
|
||||||
|
|
||||||
|
import kotlinx.serialization.SerialFormat
|
||||||
|
import kotlinx.serialization.cbor.Cbor
|
||||||
|
import kotlinx.serialization.modules.SerializersModule
|
||||||
|
import kotlinx.serialization.modules.SerializersModuleBuilder
|
||||||
|
|
||||||
const val postsRootPath = "posts"
|
const val postsRootPath = "posts"
|
||||||
|
const val postsCreateTempPathPart = "temp"
|
||||||
|
|
||||||
const val postsPostIdParameter = "postId"
|
const val postsPostIdParameter = "postId"
|
||||||
|
|
||||||
|
fun SerialFormat.createWithSerializerModuleExtension(
|
||||||
|
configurator: SerializersModuleBuilder.() -> Unit
|
||||||
|
) = Cbor {
|
||||||
|
serializersModule = SerializersModule {
|
||||||
|
include(this@createWithSerializerModuleExtension.serializersModule)
|
||||||
|
configurator()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
package dev.inmo.postssystem.services.posts.server
|
package dev.inmo.postssystem.services.posts.server
|
||||||
|
|
||||||
|
import com.benasher44.uuid.uuid4
|
||||||
import dev.inmo.micro_utils.common.*
|
import dev.inmo.micro_utils.common.*
|
||||||
|
import dev.inmo.micro_utils.coroutines.launchSafelyWithoutExceptions
|
||||||
import dev.inmo.micro_utils.ktor.common.decodeHex
|
import dev.inmo.micro_utils.ktor.common.decodeHex
|
||||||
import dev.inmo.micro_utils.ktor.server.*
|
import dev.inmo.micro_utils.ktor.server.*
|
||||||
import dev.inmo.micro_utils.ktor.server.configurators.ApplicationRoutingConfigurator
|
import dev.inmo.micro_utils.ktor.server.configurators.ApplicationRoutingConfigurator
|
||||||
@ -11,154 +13,102 @@ import dev.inmo.micro_utils.repos.ktor.common.one_to_many.removeRoute
|
|||||||
import dev.inmo.micro_utils.repos.ktor.server.crud.configureReadStandardCrudRepoRoutes
|
import dev.inmo.micro_utils.repos.ktor.server.crud.configureReadStandardCrudRepoRoutes
|
||||||
import dev.inmo.postssystem.features.common.common.FileBasedInputProvider
|
import dev.inmo.postssystem.features.common.common.FileBasedInputProvider
|
||||||
import dev.inmo.postssystem.features.content.common.*
|
import dev.inmo.postssystem.features.content.common.*
|
||||||
|
import dev.inmo.postssystem.features.files.common.FileId
|
||||||
import dev.inmo.postssystem.features.posts.common.*
|
import dev.inmo.postssystem.features.posts.common.*
|
||||||
import dev.inmo.postssystem.services.posts.common.*
|
import dev.inmo.postssystem.services.posts.common.*
|
||||||
import io.ktor.application.ApplicationCall
|
import io.ktor.application.ApplicationCall
|
||||||
import io.ktor.application.call
|
import io.ktor.application.call
|
||||||
import io.ktor.auth.authenticate
|
import io.ktor.auth.authenticate
|
||||||
|
import io.ktor.http.HttpStatusCode
|
||||||
import io.ktor.http.content.PartData
|
import io.ktor.http.content.PartData
|
||||||
|
import io.ktor.http.content.streamProvider
|
||||||
import io.ktor.request.isMultipart
|
import io.ktor.request.isMultipart
|
||||||
import io.ktor.request.receiveMultipart
|
import io.ktor.request.receiveMultipart
|
||||||
|
import io.ktor.response.respond
|
||||||
import io.ktor.routing.*
|
import io.ktor.routing.*
|
||||||
import io.ktor.util.asStream
|
import io.ktor.util.asStream
|
||||||
import io.ktor.util.pipeline.PipelineContext
|
import io.ktor.util.pipeline.PipelineContext
|
||||||
import io.ktor.utils.io.core.use
|
import io.ktor.utils.io.core.use
|
||||||
import io.ktor.utils.io.streams.asInput
|
import io.ktor.utils.io.streams.asInput
|
||||||
|
import kotlinx.coroutines.*
|
||||||
|
import kotlinx.coroutines.sync.Mutex
|
||||||
|
import kotlinx.coroutines.sync.withLock
|
||||||
import kotlinx.serialization.builtins.*
|
import kotlinx.serialization.builtins.*
|
||||||
|
import java.io.File
|
||||||
|
import java.nio.file.Files
|
||||||
|
import java.nio.file.attribute.FileTime
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
class ServerPostsServiceRoutingConfigurator(
|
class ServerPostsServiceRoutingConfigurator(
|
||||||
private val readPostsService: ReadPostsService,
|
private val readPostsService: ReadPostsService,
|
||||||
private val writePostsService: WritePostsService? = readPostsService as? WritePostsService,
|
private val writePostsService: WritePostsService? = readPostsService as? WritePostsService,
|
||||||
|
private val scope: CoroutineScope,
|
||||||
private val unifiedRouter: UnifiedRouter
|
private val unifiedRouter: UnifiedRouter
|
||||||
) : ApplicationRoutingConfigurator.Element {
|
) : ApplicationRoutingConfigurator.Element {
|
||||||
private val contentEitherSerializer = EitherSerializer(ContentId.serializer(), ContentSerializer)
|
private val contentEitherSerializer = EitherSerializer(ContentId.serializer(), ContentSerializer)
|
||||||
private val contentsEitherSerializer = ListSerializer(contentEitherSerializer)
|
private val contentsEitherSerializer = ListSerializer(contentEitherSerializer)
|
||||||
private val contentsSerializer = ListSerializer(ContentSerializer)
|
private val contentsSerializer = ListSerializer(ContentSerializer)
|
||||||
|
|
||||||
|
private val temporalFilesMap = mutableMapOf<FileId, MPPFile>()
|
||||||
|
private val temporalFilesMutex = Mutex()
|
||||||
|
private val removingJob = scope.launchSafelyWithoutExceptions {
|
||||||
|
while (isActive) {
|
||||||
|
val filesWithCreationInfo = temporalFilesMap.mapNotNull { (fileId, file) ->
|
||||||
|
fileId to ((Files.getAttribute(file.toPath(), "creationTime") as? FileTime) ?.toMillis() ?: return@mapNotNull null)
|
||||||
|
}
|
||||||
|
if (filesWithCreationInfo.isEmpty()) {
|
||||||
|
delay(TimeUnit.HOURS.toMillis(1L))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
var min = filesWithCreationInfo.first()
|
||||||
|
for (fileWithCreationInfo in filesWithCreationInfo) {
|
||||||
|
if (fileWithCreationInfo.second < min.second) {
|
||||||
|
min = fileWithCreationInfo
|
||||||
|
}
|
||||||
|
}
|
||||||
|
delay(System.currentTimeMillis() - (min.second + TimeUnit.HOURS.toMillis(1)))
|
||||||
|
temporalFilesMutex.withLock {
|
||||||
|
temporalFilesMap.remove(min.first)
|
||||||
|
} ?.delete()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun mapBinary(content: BinaryContent): BinaryContent? {
|
||||||
|
val provider = content.inputProvider
|
||||||
|
if (provider is TempFileIdentifierInputProvider) {
|
||||||
|
val newProvider = temporalFilesMutex.withLock {
|
||||||
|
temporalFilesMap.remove(provider.tempFile)
|
||||||
|
} ?.let(::FileBasedInputProvider)
|
||||||
|
|
||||||
|
if (newProvider != null) {
|
||||||
|
return content.copy(
|
||||||
|
inputProvider = newProvider
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
private suspend fun PipelineContext<Unit, ApplicationCall>.receiveContents(): List<Content> {
|
private suspend fun PipelineContext<Unit, ApplicationCall>.receiveContents(): List<Content> {
|
||||||
return unifiedRouter.run {
|
return unifiedRouter.run {
|
||||||
if (call.request.isMultipart()) {
|
uniload(contentsSerializer).mapNotNull {
|
||||||
val multipart = call.receiveMultipart()
|
mapBinary(it as? BinaryContent ?: return@mapNotNull it)
|
||||||
val list = mutableListOf<Pair<String, Content>>()
|
|
||||||
|
|
||||||
var part = multipart.readPart()
|
|
||||||
|
|
||||||
while (part != null) {
|
|
||||||
val name = part.name
|
|
||||||
val capturedPart = part
|
|
||||||
when {
|
|
||||||
name == null -> {}
|
|
||||||
capturedPart is PartData.FormItem -> {
|
|
||||||
list.add(
|
|
||||||
name to unifiedRouter.serialFormat.decodeHex(
|
|
||||||
ContentSerializer,
|
|
||||||
capturedPart.value
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
capturedPart is PartData.FileItem -> {
|
|
||||||
val filename = capturedPart.originalFileName ?.let(::FileName) ?: error("File name is unknown for default part")
|
|
||||||
val mimeType = capturedPart.contentType ?.let {
|
|
||||||
findBuiltinMimeType("${it.contentType}/${it.contentSubtype}")
|
|
||||||
} ?: error("File type is unknown for default part")
|
|
||||||
val resultInput = MPPFile.createTempFile(
|
|
||||||
filename.nameWithoutExtension.let {
|
|
||||||
var resultName = it
|
|
||||||
while (resultName.length < 3) {
|
|
||||||
resultName += "_"
|
|
||||||
}
|
|
||||||
resultName
|
|
||||||
},
|
|
||||||
".${filename.extension}"
|
|
||||||
).apply {
|
|
||||||
outputStream().use { fileStream ->
|
|
||||||
capturedPart.provider().asStream().copyTo(fileStream)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
list.add(
|
|
||||||
name to BinaryContent(
|
|
||||||
filename,
|
|
||||||
mimeType,
|
|
||||||
FileBasedInputProvider(resultInput)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
else -> {}
|
|
||||||
}
|
|
||||||
|
|
||||||
part = multipart.readPart()
|
|
||||||
}
|
|
||||||
|
|
||||||
list.sortedBy { it.first }.map { it.second }
|
|
||||||
} else {
|
|
||||||
uniload(contentsSerializer)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun PipelineContext<Unit, ApplicationCall>.receiveContentsEithers(): List<Either<ContentId, Content>> {
|
private suspend fun PipelineContext<Unit, ApplicationCall>.receiveContentsEithers(): List<Either<ContentId, Content>> {
|
||||||
return unifiedRouter.run {
|
return unifiedRouter.run {
|
||||||
if (call.request.isMultipart()) {
|
uniload(contentsEitherSerializer).mapNotNull {
|
||||||
val multipart = call.receiveMultipart()
|
it.mapOnSecond {
|
||||||
val list = mutableListOf<Pair<String, Either<ContentId, Content>>>()
|
mapBinary(it as? BinaryContent ?: return@mapOnSecond null) ?.either()
|
||||||
|
} ?: it
|
||||||
var part = multipart.readPart()
|
|
||||||
|
|
||||||
while (part != null) {
|
|
||||||
val name = part.name
|
|
||||||
val capturedPart = part
|
|
||||||
when {
|
|
||||||
name == null -> {}
|
|
||||||
capturedPart is PartData.FormItem -> {
|
|
||||||
list.add(
|
|
||||||
name to unifiedRouter.serialFormat.decodeHex(
|
|
||||||
contentEitherSerializer,
|
|
||||||
capturedPart.value
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
capturedPart is PartData.FileItem -> {
|
|
||||||
val filename = capturedPart.originalFileName ?.let(::FileName) ?: error("File name is unknown for default part")
|
|
||||||
val mimeType = capturedPart.contentType ?.let {
|
|
||||||
findBuiltinMimeType("${it.contentType}/${it.contentSubtype}")
|
|
||||||
} ?: error("File type is unknown for default part")
|
|
||||||
val resultInput = MPPFile.createTempFile(
|
|
||||||
filename.nameWithoutExtension.let {
|
|
||||||
var resultName = it
|
|
||||||
while (resultName.length < 3) {
|
|
||||||
resultName += "_"
|
|
||||||
}
|
|
||||||
resultName
|
|
||||||
},
|
|
||||||
".${filename.extension}"
|
|
||||||
).apply {
|
|
||||||
outputStream().use { fileStream ->
|
|
||||||
capturedPart.provider().asStream().copyTo(fileStream)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
list.add(
|
|
||||||
name to BinaryContent(
|
|
||||||
filename,
|
|
||||||
mimeType,
|
|
||||||
FileBasedInputProvider(resultInput)
|
|
||||||
).either()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
else -> {}
|
|
||||||
}
|
|
||||||
|
|
||||||
part = multipart.readPart()
|
|
||||||
}
|
|
||||||
|
|
||||||
list.sortedBy { it.first }.map { it.second }
|
|
||||||
} else {
|
|
||||||
uniload(contentsEitherSerializer)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
override fun Route.invoke() {
|
override fun Route.invoke() {
|
||||||
authenticate {
|
authenticate {
|
||||||
route(postsRootPath) {
|
route(postsRootPath) {
|
||||||
@ -205,6 +155,41 @@ class ServerPostsServiceRoutingConfigurator(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
post(postsCreateTempPathPart) {
|
||||||
|
val multipart = call.receiveMultipart()
|
||||||
|
|
||||||
|
var fileInfo: Pair<FileId, 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 = FileId(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)
|
||||||
|
} ?: call.respond(HttpStatusCode.BadRequest)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,15 @@
|
|||||||
|
package dev.inmo.postssystem.services.posts.server
|
||||||
|
|
||||||
|
import dev.inmo.postssystem.features.common.common.SimpleInputProvider
|
||||||
|
import dev.inmo.postssystem.features.files.common.FileId
|
||||||
|
import io.ktor.utils.io.core.Input
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
internal data class TempFileIdentifierInputProvider(
|
||||||
|
val tempFile: FileId
|
||||||
|
) : SimpleInputProvider {
|
||||||
|
override fun invoke(): Input {
|
||||||
|
TODO("Not yet implemented")
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user