kodeshare/src/main/kotlin/moe/kageru/kodeshare/Routes.kt

152 lines
5.2 KiB
Kotlin

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.PartData
import io.ktor.http.content.forEachPart
import io.ktor.http.content.streamProvider
import io.ktor.locations.KtorExperimentalLocationsAPI
import io.ktor.locations.Location
import io.ktor.locations.get
import io.ktor.locations.head
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.head
import io.ktor.routing.post
import moe.kageru.kodeshare.config.ServerSpec
import moe.kageru.kodeshare.config.config
import moe.kageru.kodeshare.pages.AboutPage
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("/favicon.ico") {
call.respond(HttpStatusCode.NotFound)
}
get("/about") {
call.respond(HttpStatusCode.OK, AboutPage.content)
}
get<HtmlPasteRequest> { req ->
call.handleGet(req, raw = false)
}
get<RawPasteRequest> { req ->
call.handleGet(req, raw = true)
}
head("/") {
call.respond(HttpStatusCode.OK)
}
head<RawPasteRequest> { req ->
call.handleHead(req, true)
}
head<HtmlPasteRequest> { req ->
call.handleHead(req)
}
}
private fun splitPath(uri: String): Pair<String, String?> {
return if (uri.contains('.')) {
val (name, ext) = uri.split(".", limit = 2)
Pair(name, ext)
} else {
Pair(uri, null)
}
}
private suspend fun ApplicationCall.handleHead(paste: PasteRequest, raw: Boolean = false) {
val uri = splitPath(paste.uri).first
if (PasteDao.selectByUri(uri) != null) {
respond(HttpStatusCode.OK, if (raw) ContentType.Text.Plain else ContentType.Text.Html)
} else {
respond(HttpStatusCode.NotFound)
}
}
@ExperimentalStdlibApi
private suspend fun ApplicationCall.handlePost() {
receiveMultipart().forEachPart { part ->
when (part) {
is PartData.FileItem -> {
val content = part.streamProvider().use { it.readBytes().decodeToString() }
processUpload(content)?.let { uri ->
Log.info("Saving new file paste with uri $uri")
}
}
is PartData.FormItem -> {
val content = part.value
processUpload(content)?.let { uri ->
Log.info("Saving new text paste with uri $uri")
}
}
is PartData.BinaryItem -> {
Log.warn("Received binary item from upload form. This shouldn’t happen.")
}
}
}
}
private suspend fun ApplicationCall.processUpload(content: String): String? {
content.ifBlank {
Log.info("Rejecting blank paste")
respond(HttpStatusCode.BadRequest, "Empty pastes are not allowed")
return null
}
if (content.length > 1024 * 1024) {
Log.info("Rejecting paste over 1M")
respond(HttpStatusCode.BadRequest, "Pastes are limited to 1 MiB")
return null
}
val uri = PasteDao.insert(Paste(content, DateTime.now(), Paste.randomUri())).data.uri
// 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, uri)
respond(HttpStatusCode.Found, "${config[ServerSpec.pasteurl].ifBlank { config[ServerSpec.domain] }}$uri\n")
return uri
}
private suspend fun ApplicationCall.handleGet(req: PasteRequest, raw: Boolean) {
val (uri, ext) = splitPath(req.uri)
Log.info("Retrieving paste $uri")
PasteDao.selectByUri(uri)?.data?.let { paste ->
if (raw) {
respondText(paste.content, ContentType.Text.Plain)
} else {
respond(HttpStatusCode.OK, PastePage.build(paste.content, paste.uri, ext))
}
} ?: respond(HttpStatusCode.NotFound, "nothing found for id $uri\n")
}
}
@KtorExperimentalLocationsAPI
@Location("/r/{uri}")
data class RawPasteRequest(override val uri: String) : PasteRequest
@KtorExperimentalLocationsAPI
@Location("/{uri}")
data class HtmlPasteRequest(override val uri: String) : PasteRequest
interface PasteRequest {
val uri: String
}