package moe.kageru.kodeshare import io.ktor.application.ApplicationCall import io.ktor.application.call import io.ktor.http.ContentType import io.ktor.http.HttpHeaders import io.ktor.http.HttpStatusCode import io.ktor.http.content.MultiPartData.Empty.readPart import io.ktor.http.content.PartData import io.ktor.http.content.forEachPart import io.ktor.http.content.readAllParts import io.ktor.http.content.streamProvider import io.ktor.locations.KtorExperimentalLocationsAPI import io.ktor.locations.Location import io.ktor.locations.get import io.ktor.request.receive import io.ktor.request.receiveMultipart import io.ktor.response.respond import io.ktor.response.respondText import io.ktor.routing.Routing import io.ktor.routing.get import io.ktor.routing.post import moe.kageru.kodeshare.pages.Css import moe.kageru.kodeshare.pages.Homepage import moe.kageru.kodeshare.pages.PastePage import moe.kageru.kodeshare.persistence.PasteDao import org.joda.time.DateTime @KtorExperimentalLocationsAPI @ExperimentalStdlibApi object Routes { fun Routing.createRoutes() { get("/") { call.respond(HttpStatusCode.OK, Homepage.content) } get("/style.css") { call.respondText(Css.default, ContentType.Text.CSS) } post("/") { call.handlePost() } get { req -> call.handleGet(req) } get { req -> call.handleRaw(req) } } @ExperimentalStdlibApi private suspend fun ApplicationCall.handlePost() { receiveMultipart().forEachPart { part -> when (part) { is PartData.FileItem -> { val content = part.streamProvider().use { it.readBytes().decodeToString() } respondToUpload(content)?.let { id -> Log.info("Saving new file paste with ID $id") } } is PartData.FormItem -> { val content = part.value respondToUpload(content)?.let { id -> Log.info("Saving new text paste with ID $id") } } is PartData.BinaryItem -> { Log.warn("Received binary item from upload form. This shouldn’t happen.") } } } } private suspend fun ApplicationCall.respondToUpload(content: String): Long? { content.ifBlank { Log.info("Rejecting blank paste") respond(HttpStatusCode.BadRequest, "Empty pastes are not allowed") return null } if (content.length > 1 * 1024 * 1024) { Log.info("Rejecting paste over 1MB") respond(HttpStatusCode.BadRequest, "Pastes are limited to 1MB each") return null } val id = PasteDao.insert(Paste(content, DateTime.now())).id // This will show the URL in a terminal when uploading via curl, // while also redirecting browser uploads to the newly created paste. // May seem odd to return code 302, but it seems to be the only way. response.headers.append(HttpHeaders.Location, "$id") respond(HttpStatusCode.Found, "$id") return id } private suspend fun ApplicationCall.handleGet(req: PasteRequest) { val id = req.id Log.info("Retrieving paste $id") PasteDao.select(id)?.data?.let { paste -> respond( HttpStatusCode.OK, PastePage.build(paste.content) ) } ?: respond(HttpStatusCode.NotFound, "nothing found for id $id") } private suspend fun ApplicationCall.handleRaw(req: RawPasteRequest) { val id = req.id Log.info("Retrieving raw paste $id") PasteDao.select(id)?.data?.let { paste -> respondText(paste.content, ContentType.Text.Plain) } ?: respond(HttpStatusCode.NotFound, "nothing found for id $id") } } @KtorExperimentalLocationsAPI @Location("/r/{id}") data class RawPasteRequest(val id: Long) @KtorExperimentalLocationsAPI @Location("/{id}") data class PasteRequest(val id: Long)