177 lines
7.1 KiB
Kotlin
177 lines
7.1 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.configureReadCRUDRepoRoutes
|
|
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.receive
|
|
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 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
|
|
) : ApplicationRoutingConfigurator.Element {
|
|
|
|
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 call.receive<ContentsWrapper>().content.mapNotNull {
|
|
mapBinary(it as? BinaryContent ?: return@mapNotNull it)
|
|
}
|
|
}
|
|
|
|
private suspend fun PipelineContext<Unit, ApplicationCall>.receiveContentsEithers(): List<Either<ContentId, Content>> {
|
|
return call.receive<ContentsEithersWrapper>().content.mapNotNull {
|
|
it.mapOnSecond {
|
|
mapBinary(it as? BinaryContent ?: return@mapOnSecond null) ?.either()
|
|
} ?: it
|
|
}
|
|
}
|
|
|
|
|
|
|
|
override fun Route.invoke() {
|
|
authenticate {
|
|
route(postsRootPath) {
|
|
configureReadCRUDRepoRoutes(
|
|
readPostsService
|
|
) { PostId(it.toLong()) }
|
|
|
|
writePostsService ?.let {
|
|
|
|
post(createRouting) {
|
|
val data = receiveContents()
|
|
|
|
call.respond(
|
|
writePostsService.create(FullNewPost(data)) ?: HttpStatusCode.NoContent
|
|
)
|
|
}
|
|
|
|
post(updateRouting) {
|
|
call.respond(
|
|
writePostsService.update(
|
|
call.getQueryParameterOrSendError(postsPostIdParameter)?.toLong()?.let(::PostId) ?: return@post,
|
|
receiveContentsEithers()
|
|
) ?: HttpStatusCode.NoContent
|
|
)
|
|
}
|
|
|
|
post(removeRoute) {
|
|
call.respond(
|
|
writePostsService.remove(
|
|
call.receive()
|
|
)
|
|
)
|
|
}
|
|
|
|
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)
|
|
|
|
}
|
|
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|