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() 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.receiveContents(): List { return call.receive().content.mapNotNull { mapBinary(it as? BinaryContent ?: return@mapNotNull it) } } private suspend fun PipelineContext.receiveContentsEithers(): List> { return call.receive().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? = 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) } } } } } }