diff --git a/src/main/kotlin/moe/kageru/kodeshare/Kodeshare.kt b/src/main/kotlin/moe/kageru/kodeshare/Kodeshare.kt index 19c56c5..21552ef 100644 --- a/src/main/kotlin/moe/kageru/kodeshare/Kodeshare.kt +++ b/src/main/kotlin/moe/kageru/kodeshare/Kodeshare.kt @@ -8,12 +8,22 @@ import io.ktor.routing.routing import io.ktor.server.engine.embeddedServer import io.ktor.server.netty.Netty import moe.kageru.kodeshare.Routes.createRoutes -import org.joda.time.DateTime +import moe.kageru.kodeshare.config.ServerSpec +import moe.kageru.kodeshare.config.config +import moe.kageru.kodeshare.persistence.PasteDao @KtorExperimentalLocationsAPI @ExperimentalStdlibApi fun main() { - embeddedServer(Netty, 9092) { + /* + * This is meant as a health check against the DB + * so we notice errors at startup, + * not when first accessing it + */ + println(PasteDao.select(1)?.id) + val port = config[ServerSpec.port] + Log.info("Kodeshare running on port $port") + embeddedServer(Netty, port) { install(DefaultHeaders) install(Locations) routing { @@ -21,5 +31,3 @@ fun main() { } }.start(wait = true) } - -data class Paste(val content: String, val created: DateTime) \ No newline at end of file diff --git a/src/main/kotlin/moe/kageru/kodeshare/Paste.kt b/src/main/kotlin/moe/kageru/kodeshare/Paste.kt new file mode 100644 index 0000000..34feada --- /dev/null +++ b/src/main/kotlin/moe/kageru/kodeshare/Paste.kt @@ -0,0 +1,18 @@ +package moe.kageru.kodeshare + +import moe.kageru.kodeshare.persistence.PasteDao +import org.joda.time.DateTime + +data class Paste(val content: String, val created: DateTime, val uri: String) { + companion object { + private val alphabet = ('a'..'z') + ('A'..'Z') + ('0'..'9') + + tailrec fun randomUri(): String { + val uri = List(6) { alphabet.random() }.joinToString("") + if (PasteDao.selectByUri(uri) == null) { + return uri + } + return randomUri() + } + } +} diff --git a/src/main/kotlin/moe/kageru/kodeshare/Routes.kt b/src/main/kotlin/moe/kageru/kodeshare/Routes.kt index c351c04..1cb0a7c 100644 --- a/src/main/kotlin/moe/kageru/kodeshare/Routes.kt +++ b/src/main/kotlin/moe/kageru/kodeshare/Routes.kt @@ -5,15 +5,12 @@ 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 @@ -45,6 +42,9 @@ object Routes { get { req -> call.handleRaw(req) } + get { req -> + call.handleGet(req) + } } @ExperimentalStdlibApi @@ -53,14 +53,14 @@ object Routes { 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") + respondToUpload(content)?.let { uri -> + Log.info("Saving new file paste with uri $uri") } } is PartData.FormItem -> { val content = part.value - respondToUpload(content)?.let { id -> - Log.info("Saving new text paste with ID $id") + respondToUpload(content)?.let { uri -> + Log.info("Saving new text paste with uri $uri") } } is PartData.BinaryItem -> { @@ -70,7 +70,7 @@ object Routes { } } - private suspend fun ApplicationCall.respondToUpload(content: String): Long? { + private suspend fun ApplicationCall.respondToUpload(content: String): String? { content.ifBlank { Log.info("Rejecting blank paste") respond(HttpStatusCode.BadRequest, "Empty pastes are not allowed") @@ -81,39 +81,49 @@ object Routes { respond(HttpStatusCode.BadRequest, "Pastes are limited to 1MB each") return null } - val id = PasteDao.insert(Paste(content, DateTime.now())).id + 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, "$id") - respond(HttpStatusCode.Found, "$id") - return id + response.headers.append(HttpHeaders.Location, uri) + respond(HttpStatusCode.Found, uri) + return uri } - private suspend fun ApplicationCall.handleGet(req: PasteRequest) { - val id = req.id - Log.info("Retrieving paste $id") - PasteDao.select(id)?.data?.let { paste -> + private suspend fun ApplicationCall.handleGet(req: AbstractPasteRequest) { + val uri = req.uri + Log.info("Retrieving paste $uri") + PasteDao.selectByUri(uri)?.data?.let { paste -> respond( HttpStatusCode.OK, - PastePage.build(paste.content) + PastePage.build(paste.content, req.filetype) ) - } ?: respond(HttpStatusCode.NotFound, "nothing found for id $id") + } ?: respond(HttpStatusCode.NotFound, "nothing found for id $uri") } private suspend fun ApplicationCall.handleRaw(req: RawPasteRequest) { - val id = req.id - Log.info("Retrieving raw paste $id") - PasteDao.select(id)?.data?.let { paste -> + val uri = req.uri + Log.info("Retrieving raw paste $uri") + PasteDao.selectByUri(uri)?.data?.let { paste -> respondText(paste.content, ContentType.Text.Plain) - } ?: respond(HttpStatusCode.NotFound, "nothing found for id $id") + } ?: respond(HttpStatusCode.NotFound, "nothing found for id $uri") } } @KtorExperimentalLocationsAPI -@Location("/r/{id}") -data class RawPasteRequest(val id: Long) +// 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("/{id}") -data class PasteRequest(val id: Long) +@Location("/r/{uri}") +data class RawPasteRequest(override val uri: String) : AbstractPasteRequest() + +@KtorExperimentalLocationsAPI +@Location("/{uri}") +data class PasteRequest(override val uri: String) : AbstractPasteRequest() + +abstract class AbstractPasteRequest { + abstract val uri: String + open val filetype: String? = null +} diff --git a/src/main/kotlin/moe/kageru/kodeshare/config/Config.kt b/src/main/kotlin/moe/kageru/kodeshare/config/Config.kt index dceebfe..3fcf7e8 100644 --- a/src/main/kotlin/moe/kageru/kodeshare/config/Config.kt +++ b/src/main/kotlin/moe/kageru/kodeshare/config/Config.kt @@ -3,8 +3,10 @@ package moe.kageru.kodeshare.config import com.uchuhimo.konf.Config import com.uchuhimo.konf.ConfigSpec -val config = Config { addSpec(DatabaseSpec) } - .from.properties.file("kodeshare.properties") +val config = Config { + addSpec(DatabaseSpec) + addSpec(ServerSpec) +}.from.properties.file("kodeshare.properties") .from.env() object DatabaseSpec : ConfigSpec() { @@ -12,4 +14,8 @@ object DatabaseSpec : ConfigSpec() { val password by required() val user by optional("kodeshare") val database by optional("kode") +} + +object ServerSpec : ConfigSpec() { + val port by optional(9092) } \ No newline at end of file diff --git a/src/main/kotlin/moe/kageru/kodeshare/pages/Css.kt b/src/main/kotlin/moe/kageru/kodeshare/pages/Css.kt index 64b1898..ee00bcb 100644 --- a/src/main/kotlin/moe/kageru/kodeshare/pages/Css.kt +++ b/src/main/kotlin/moe/kageru/kodeshare/pages/Css.kt @@ -32,7 +32,7 @@ object Css { maxWidth = 100.pct } // this doesn’t inherit the style from anything else for some reason - rule(".hljs") { + rule(".hljs, pre, code") { width = 100.pct height = 100.pct textAlign = TextAlign.left diff --git a/src/main/kotlin/moe/kageru/kodeshare/pages/PastePage.kt b/src/main/kotlin/moe/kageru/kodeshare/pages/PastePage.kt index 00e45e9..fc25e9b 100644 --- a/src/main/kotlin/moe/kageru/kodeshare/pages/PastePage.kt +++ b/src/main/kotlin/moe/kageru/kodeshare/pages/PastePage.kt @@ -5,7 +5,7 @@ import io.ktor.http.HttpStatusCode import kotlinx.html.* object PastePage { - fun build(content: String) = HtmlContent(HttpStatusCode.OK) { + fun build(content: String, type: String?) = HtmlContent(HttpStatusCode.OK) { head { link(rel = "stylesheet", href = "/style.css", type = "text/css") link( @@ -18,7 +18,7 @@ object PastePage { } body { pre { - code { + code(classes = type) { +content } } diff --git a/src/main/kotlin/moe/kageru/kodeshare/persistence/PasteDao.kt b/src/main/kotlin/moe/kageru/kodeshare/persistence/PasteDao.kt index c4b7e10..003546c 100644 --- a/src/main/kotlin/moe/kageru/kodeshare/persistence/PasteDao.kt +++ b/src/main/kotlin/moe/kageru/kodeshare/persistence/PasteDao.kt @@ -16,11 +16,16 @@ object PasteDao { val id = PasteTable.insert { it[content] = paste.content it[created] = paste.created + it[uri] = paste.uri }[PasteTable.id] Transient(id, paste) } - init { + fun selectByUri(uri: String): Transient? = transaction { + PasteTable.select { PasteTable.uri.eq(uri) }.firstOrNull() + }?.toPaste() + + init { val source = MariaDbDataSource().apply { userName = config[DatabaseSpec.user] setPassword(config[DatabaseSpec.password]) @@ -29,7 +34,7 @@ object PasteDao { } Database.connect(source) transaction { - SchemaUtils.create(PasteTable) + SchemaUtils.createMissingTablesAndColumns(PasteTable) } } } @@ -38,14 +43,16 @@ private fun ResultRow.toPaste() = Transient( get(PasteTable.id), Paste( content = get(PasteTable.content), - created = get(PasteTable.created) + created = get(PasteTable.created), + uri = get(PasteTable.uri) ) ) private object PasteTable : Table() { - val id = long("id").primaryKey().autoIncrement().index() + val id = long("id").primaryKey().autoIncrement().uniqueIndex() val content = text("content") val created = datetime("created").index() + val uri = varchar("uri", 10).uniqueIndex() } /*