152 lines
5.2 KiB
Kotlin
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
|
|
}
|