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

139 lines
4.9 KiB
Kotlin
Raw Normal View History

2019-09-19 22:04:23 +02:00
package moe.kageru.kodeshare
import io.ktor.application.ApplicationCall
import io.ktor.application.call
import io.ktor.http.ContentType
2019-09-22 18:26:28 +02:00
import io.ktor.http.HttpHeaders
2019-09-19 22:04:23 +02:00
import io.ktor.http.HttpStatusCode
import io.ktor.http.content.PartData
import io.ktor.http.content.forEachPart
import io.ktor.http.content.streamProvider
2019-09-22 18:26:28 +02:00
import io.ktor.locations.KtorExperimentalLocationsAPI
2019-09-19 22:04:23 +02:00
import io.ktor.locations.Location
import io.ktor.locations.get
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
2019-09-29 11:50:14 +02:00
import moe.kageru.kodeshare.config.ServerSpec
import moe.kageru.kodeshare.config.config
import moe.kageru.kodeshare.pages.AboutPage
2019-09-22 18:26:28 +02:00
import moe.kageru.kodeshare.pages.Css
import moe.kageru.kodeshare.pages.Homepage
2019-09-29 08:17:31 +02:00
import moe.kageru.kodeshare.pages.PastePage
2019-09-19 22:04:23 +02:00
import moe.kageru.kodeshare.persistence.PasteDao
2019-09-29 08:17:31 +02:00
import org.joda.time.DateTime
2019-09-19 22:04:23 +02:00
2019-09-22 18:26:28 +02:00
@KtorExperimentalLocationsAPI
2019-09-19 22:04:23 +02:00
@ExperimentalStdlibApi
object Routes {
fun Routing.createRoutes() {
get("/") {
2019-09-29 08:17:31 +02:00
call.respond(HttpStatusCode.OK, Homepage.content)
2019-09-22 18:26:28 +02:00
}
get("/style.css") {
call.respondText(Css.default, ContentType.Text.CSS)
2019-09-19 22:04:23 +02:00
}
post("/") {
2019-09-22 18:26:28 +02:00
call.handlePost()
2019-09-19 22:04:23 +02:00
}
2019-09-29 10:15:42 +02:00
get("/favicon.ico") {
call.respond(HttpStatusCode.NotFound)
}
2019-09-29 11:50:14 +02:00
get("/about") {
call.respond(HttpStatusCode.OK, AboutPage.content)
}
2019-09-19 22:04:23 +02:00
get<PasteRequest> { req ->
2019-09-22 18:26:28 +02:00
call.handleGet(req)
2019-09-19 22:04:23 +02:00
}
2019-09-29 08:17:31 +02:00
get<RawPasteRequest> { req ->
call.handleRaw(req)
}
2019-09-29 09:51:15 +02:00
get<TypedPasteRequest> { req ->
call.handleGet(req)
}
2019-09-19 22:04:23 +02:00
}
2019-09-22 18:26:28 +02:00
@ExperimentalStdlibApi
private suspend fun ApplicationCall.handlePost() {
receiveMultipart().forEachPart { part ->
when (part) {
is PartData.FileItem -> {
val content = part.streamProvider().use { it.readBytes().decodeToString() }
2019-09-29 09:51:15 +02:00
respondToUpload(content)?.let { uri ->
Log.info("Saving new file paste with uri $uri")
2019-09-29 08:17:31 +02:00
}
2019-09-22 18:26:28 +02:00
}
is PartData.FormItem -> {
val content = part.value
2019-09-29 09:51:15 +02:00
respondToUpload(content)?.let { uri ->
Log.info("Saving new text paste with uri $uri")
2019-09-29 08:17:31 +02:00
}
2019-09-22 18:26:28 +02:00
}
is PartData.BinaryItem -> {
Log.warn("Received binary item from upload form. This shouldn’t happen.")
}
2019-09-19 22:04:23 +02:00
}
}
}
2019-09-29 09:51:15 +02:00
private suspend fun ApplicationCall.respondToUpload(content: String): String? {
2019-09-22 18:26:28 +02:00
content.ifBlank {
2019-09-29 08:17:31 +02:00
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")
2019-09-22 18:26:28 +02:00
return null
}
2019-09-29 09:51:15 +02:00
val uri = PasteDao.insert(Paste(content, DateTime.now(), Paste.randomUri())).data.uri
2019-09-22 18:26:28 +02:00
// 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.
2019-09-29 09:51:15 +02:00
response.headers.append(HttpHeaders.Location, uri)
2019-09-29 11:50:14 +02:00
respond(HttpStatusCode.Found, "${config[ServerSpec.domain]}/$uri")
2019-09-29 09:51:15 +02:00
return uri
2019-09-22 18:26:28 +02:00
}
2019-09-29 09:51:15 +02:00
private suspend fun ApplicationCall.handleGet(req: AbstractPasteRequest) {
val uri = req.uri
Log.info("Retrieving paste $uri")
PasteDao.selectByUri(uri)?.data?.let { paste ->
2019-09-29 08:17:31 +02:00
respond(
HttpStatusCode.OK,
2019-09-29 09:51:15 +02:00
PastePage.build(paste.content, req.filetype)
2019-09-29 08:17:31 +02:00
)
2019-09-29 09:51:15 +02:00
} ?: respond(HttpStatusCode.NotFound, "nothing found for id $uri")
2019-09-29 08:17:31 +02:00
}
private suspend fun ApplicationCall.handleRaw(req: RawPasteRequest) {
2019-09-29 09:51:15 +02:00
val uri = req.uri
Log.info("Retrieving raw paste $uri")
PasteDao.selectByUri(uri)?.data?.let { paste ->
2019-09-29 08:17:31 +02:00
respondText(paste.content, ContentType.Text.Plain)
2019-09-29 09:51:15 +02:00
} ?: respond(HttpStatusCode.NotFound, "nothing found for id $uri")
2019-09-22 18:26:28 +02:00
}
2019-09-19 22:04:23 +02:00
}
2019-09-29 08:17:31 +02:00
@KtorExperimentalLocationsAPI
2019-09-29 09:51:15 +02:00
// tfw we can’t do {id}.{filetype} here because reasons:tm:
@Location("/{uri}/{filetype}")
data class TypedPasteRequest(override val uri: String, override val filetype: String) : AbstractPasteRequest()
@KtorExperimentalLocationsAPI
@Location("/r/{uri}")
data class RawPasteRequest(override val uri: String) : AbstractPasteRequest()
2019-09-29 08:17:31 +02:00
2019-09-22 18:26:28 +02:00
@KtorExperimentalLocationsAPI
2019-09-29 09:51:15 +02:00
@Location("/{uri}")
data class PasteRequest(override val uri: String) : AbstractPasteRequest()
abstract class AbstractPasteRequest {
abstract val uri: String
open val filetype: String? = null
}