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() 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 unifiedRouter.run { uniload(contentsSerializer).mapNotNull { mapBinary(it as? BinaryContent ?: return@mapNotNull it) } } } private suspend fun PipelineContext.receiveContentsEithers(): List> { 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? = 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) } } } } } } }