temp progress on binary correct including

This commit is contained in:
InsanusMokrassar 2022-03-09 13:49:00 +06:00
parent 186bfd7ac0
commit 99b953635e
15 changed files with 342 additions and 180 deletions

View File

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

View File

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

View File

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

View File

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

View File

@ -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,84 +44,60 @@ 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 -> val mappedPost = newPost.copy(
when (content) { content = mappedContent
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( return unifiedRequester.unipost(
i.toString(),
unifiedRequester.serialFormat.encodeHex(ContentSerializer, content)
)
}
}
}
}
unifiedRequester.serialFormat.decodeFromByteArray(RegisteredPost.serializer().nullable, answer)
} else {
unifiedRequester.unipost(
createFullPath, createFullPath,
contentsSerializer to newPost.content, contentsSerializer to mappedPost.content,
RegisteredPost.serializer().nullable 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())
)
} }
} return unifiedRequester.unipost(
}
}
}
unifiedRequester.serialFormat.decodeFromByteArray(RegisteredPost.serializer().nullable, answer)
} else {
unifiedRequester.unipost(
buildStandardUrl( buildStandardUrl(
root, root,
updateRouting, updateRouting,
postsPostIdParameter to unifiedRequester.encodeUrlQueryValue(PostId.serializer(), postId) postsPostIdParameter to unifiedRequester.encodeUrlQueryValue(PostId.serializer(), postId)
), ),
contentsEitherSerializer to content, contentsEitherSerializer to mappedContent,
RegisteredPost.serializer().nullable RegisteredPost.serializer().nullable
) )
} }
}
override suspend fun remove(postId: PostId) = unifiedRequester.unipost( override suspend fun remove(postId: PostId) = unifiedRequester.unipost(
removeFullPath, removeFullPath,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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,153 +13,101 @@ 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 {
@ -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)
}
} }
} }

View File

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