207 lines
8.5 KiB
Kotlin
207 lines
8.5 KiB
Kotlin
package dev.inmo.postssystem.services.posts.server
|
|
|
|
import com.benasher44.uuid.uuid4
|
|
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.server.*
|
|
import dev.inmo.micro_utils.ktor.server.configurators.ApplicationRoutingConfigurator
|
|
import dev.inmo.micro_utils.mime_types.findBuiltinMimeType
|
|
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.one_to_many.removeRoute
|
|
import dev.inmo.micro_utils.repos.ktor.server.crud.configureReadStandardCrudRepoRoutes
|
|
import dev.inmo.postssystem.features.common.common.FileBasedInputProvider
|
|
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.services.posts.common.*
|
|
import io.ktor.http.HttpStatusCode
|
|
import io.ktor.http.content.PartData
|
|
import io.ktor.http.content.streamProvider
|
|
import io.ktor.server.application.ApplicationCall
|
|
import io.ktor.server.application.call
|
|
import io.ktor.server.auth.authenticate
|
|
import io.ktor.server.request.receiveMultipart
|
|
import io.ktor.server.response.respond
|
|
import io.ktor.server.routing.*
|
|
import io.ktor.util.asStream
|
|
import io.ktor.util.pipeline.PipelineContext
|
|
import io.ktor.utils.io.core.use
|
|
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.modules.polymorphic
|
|
import java.io.File
|
|
import java.nio.file.Files
|
|
import java.nio.file.attribute.FileTime
|
|
import java.util.concurrent.TimeUnit
|
|
|
|
class ServerPostsServiceRoutingConfigurator(
|
|
private val readPostsService: ReadPostsService,
|
|
private val writePostsService: WritePostsService? = readPostsService as? WritePostsService,
|
|
private val scope: CoroutineScope,
|
|
unifiedRouter: UnifiedRouter
|
|
) : ApplicationRoutingConfigurator.Element {
|
|
private val unifiedRouter = UnifiedRouter(
|
|
serialFormat = unifiedRouter.serialFormat.createWithSerializerModuleExtension {
|
|
polymorphic(Content::class) {
|
|
subclass(BinaryContent::class, BinaryContentSerializer(TempFileIdentifierInputProvider::class, TempFileIdentifierInputProvider.serializer()))
|
|
}
|
|
}
|
|
)
|
|
private val contentEitherSerializer = EitherSerializer(ContentId.serializer(), ContentSerializer)
|
|
private val contentsEitherSerializer = ListSerializer(contentEitherSerializer)
|
|
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> {
|
|
return unifiedRouter.run {
|
|
uniload(contentsSerializer).mapNotNull {
|
|
mapBinary(it as? BinaryContent ?: return@mapNotNull it)
|
|
}
|
|
}
|
|
}
|
|
|
|
private suspend fun PipelineContext<Unit, ApplicationCall>.receiveContentsEithers(): List<Either<ContentId, Content>> {
|
|
return unifiedRouter.run {
|
|
uniload(contentsEitherSerializer).mapNotNull {
|
|
it.mapOnSecond {
|
|
mapBinary(it as? BinaryContent ?: return@mapOnSecond null) ?.either()
|
|
} ?: it
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
|
|
override fun Route.invoke() {
|
|
authenticate {
|
|
route(postsRootPath) {
|
|
configureReadStandardCrudRepoRoutes(
|
|
readPostsService,
|
|
RegisteredPost.serializer(),
|
|
RegisteredPost.serializer().nullable,
|
|
PostId.serializer(),
|
|
unifiedRouter
|
|
)
|
|
|
|
writePostsService ?.let {
|
|
|
|
unifiedRouter.apply {
|
|
|
|
post(createRouting) {
|
|
val data = receiveContents()
|
|
|
|
unianswer(
|
|
RegisteredPost.serializer().nullable,
|
|
writePostsService.create(FullNewPost(data))
|
|
)
|
|
}
|
|
|
|
post(updateRouting) {
|
|
val postId = call.decodeUrlQueryValueOrSendError(postsPostIdParameter, PostId.serializer()) ?: return@post
|
|
val data = receiveContentsEithers()
|
|
|
|
unianswer(
|
|
RegisteredPost.serializer().nullable,
|
|
writePostsService.update(
|
|
postId,
|
|
data
|
|
)
|
|
)
|
|
}
|
|
|
|
post(removeRoute) {
|
|
val postId = uniload(PostId.serializer())
|
|
|
|
unianswer(
|
|
Unit.serializer(),
|
|
writePostsService.remove(postId)
|
|
)
|
|
}
|
|
|
|
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)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|