Use random strings as URIs

This commit is contained in:
kageru 2019-09-29 09:51:15 +02:00
parent 624320b8b1
commit 4919958d8f
7 changed files with 88 additions and 39 deletions

@ -8,12 +8,22 @@ import io.ktor.routing.routing
import io.ktor.server.engine.embeddedServer import io.ktor.server.engine.embeddedServer
import io.ktor.server.netty.Netty import io.ktor.server.netty.Netty
import moe.kageru.kodeshare.Routes.createRoutes 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 @KtorExperimentalLocationsAPI
@ExperimentalStdlibApi @ExperimentalStdlibApi
fun main() { 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(DefaultHeaders)
install(Locations) install(Locations)
routing { routing {
@ -21,5 +31,3 @@ fun main() {
} }
}.start(wait = true) }.start(wait = true)
} }
data class Paste(val content: String, val created: DateTime)

@ -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()
}
}
}

@ -5,15 +5,12 @@ import io.ktor.application.call
import io.ktor.http.ContentType import io.ktor.http.ContentType
import io.ktor.http.HttpHeaders import io.ktor.http.HttpHeaders
import io.ktor.http.HttpStatusCode import io.ktor.http.HttpStatusCode
import io.ktor.http.content.MultiPartData.Empty.readPart
import io.ktor.http.content.PartData import io.ktor.http.content.PartData
import io.ktor.http.content.forEachPart import io.ktor.http.content.forEachPart
import io.ktor.http.content.readAllParts
import io.ktor.http.content.streamProvider import io.ktor.http.content.streamProvider
import io.ktor.locations.KtorExperimentalLocationsAPI import io.ktor.locations.KtorExperimentalLocationsAPI
import io.ktor.locations.Location import io.ktor.locations.Location
import io.ktor.locations.get import io.ktor.locations.get
import io.ktor.request.receive
import io.ktor.request.receiveMultipart import io.ktor.request.receiveMultipart
import io.ktor.response.respond import io.ktor.response.respond
import io.ktor.response.respondText import io.ktor.response.respondText
@ -45,6 +42,9 @@ object Routes {
get<RawPasteRequest> { req -> get<RawPasteRequest> { req ->
call.handleRaw(req) call.handleRaw(req)
} }
get<TypedPasteRequest> { req ->
call.handleGet(req)
}
} }
@ExperimentalStdlibApi @ExperimentalStdlibApi
@ -53,14 +53,14 @@ object Routes {
when (part) { when (part) {
is PartData.FileItem -> { is PartData.FileItem -> {
val content = part.streamProvider().use { it.readBytes().decodeToString() } val content = part.streamProvider().use { it.readBytes().decodeToString() }
respondToUpload(content)?.let { id -> respondToUpload(content)?.let { uri ->
Log.info("Saving new file paste with ID $id") Log.info("Saving new file paste with uri $uri")
} }
} }
is PartData.FormItem -> { is PartData.FormItem -> {
val content = part.value val content = part.value
respondToUpload(content)?.let { id -> respondToUpload(content)?.let { uri ->
Log.info("Saving new text paste with ID $id") Log.info("Saving new text paste with uri $uri")
} }
} }
is PartData.BinaryItem -> { 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 { content.ifBlank {
Log.info("Rejecting blank paste") Log.info("Rejecting blank paste")
respond(HttpStatusCode.BadRequest, "Empty pastes are not allowed") respond(HttpStatusCode.BadRequest, "Empty pastes are not allowed")
@ -81,39 +81,49 @@ object Routes {
respond(HttpStatusCode.BadRequest, "Pastes are limited to 1MB each") respond(HttpStatusCode.BadRequest, "Pastes are limited to 1MB each")
return null 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, // This will show the URL in a terminal when uploading via curl,
// while also redirecting browser uploads to the newly created paste. // 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. // May seem odd to return code 302, but it seems to be the only way.
response.headers.append(HttpHeaders.Location, "$id") response.headers.append(HttpHeaders.Location, uri)
respond(HttpStatusCode.Found, "$id") respond(HttpStatusCode.Found, uri)
return id return uri
} }
private suspend fun ApplicationCall.handleGet(req: PasteRequest) { private suspend fun ApplicationCall.handleGet(req: AbstractPasteRequest) {
val id = req.id val uri = req.uri
Log.info("Retrieving paste $id") Log.info("Retrieving paste $uri")
PasteDao.select(id)?.data?.let { paste -> PasteDao.selectByUri(uri)?.data?.let { paste ->
respond( respond(
HttpStatusCode.OK, 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) { private suspend fun ApplicationCall.handleRaw(req: RawPasteRequest) {
val id = req.id val uri = req.uri
Log.info("Retrieving raw paste $id") Log.info("Retrieving raw paste $uri")
PasteDao.select(id)?.data?.let { paste -> PasteDao.selectByUri(uri)?.data?.let { paste ->
respondText(paste.content, ContentType.Text.Plain) respondText(paste.content, ContentType.Text.Plain)
} ?: respond(HttpStatusCode.NotFound, "nothing found for id $id") } ?: respond(HttpStatusCode.NotFound, "nothing found for id $uri")
} }
} }
@KtorExperimentalLocationsAPI @KtorExperimentalLocationsAPI
@Location("/r/{id}") // tfw we can’t do {id}.{filetype} here because reasons:tm:
data class RawPasteRequest(val id: Long) @Location("/{uri}/{filetype}")
data class TypedPasteRequest(override val uri: String, override val filetype: String) : AbstractPasteRequest()
@KtorExperimentalLocationsAPI @KtorExperimentalLocationsAPI
@Location("/{id}") @Location("/r/{uri}")
data class PasteRequest(val id: Long) 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
}

@ -3,8 +3,10 @@ package moe.kageru.kodeshare.config
import com.uchuhimo.konf.Config import com.uchuhimo.konf.Config
import com.uchuhimo.konf.ConfigSpec import com.uchuhimo.konf.ConfigSpec
val config = Config { addSpec(DatabaseSpec) } val config = Config {
.from.properties.file("kodeshare.properties") addSpec(DatabaseSpec)
addSpec(ServerSpec)
}.from.properties.file("kodeshare.properties")
.from.env() .from.env()
object DatabaseSpec : ConfigSpec() { object DatabaseSpec : ConfigSpec() {
@ -13,3 +15,7 @@ object DatabaseSpec : ConfigSpec() {
val user by optional("kodeshare") val user by optional("kodeshare")
val database by optional("kode") val database by optional("kode")
} }
object ServerSpec : ConfigSpec() {
val port by optional(9092)
}

@ -32,7 +32,7 @@ object Css {
maxWidth = 100.pct maxWidth = 100.pct
} }
// this doesn’t inherit the style from anything else for some reason // this doesn’t inherit the style from anything else for some reason
rule(".hljs") { rule(".hljs, pre, code") {
width = 100.pct width = 100.pct
height = 100.pct height = 100.pct
textAlign = TextAlign.left textAlign = TextAlign.left

@ -5,7 +5,7 @@ import io.ktor.http.HttpStatusCode
import kotlinx.html.* import kotlinx.html.*
object PastePage { object PastePage {
fun build(content: String) = HtmlContent(HttpStatusCode.OK) { fun build(content: String, type: String?) = HtmlContent(HttpStatusCode.OK) {
head { head {
link(rel = "stylesheet", href = "/style.css", type = "text/css") link(rel = "stylesheet", href = "/style.css", type = "text/css")
link( link(
@ -18,7 +18,7 @@ object PastePage {
} }
body { body {
pre { pre {
code { code(classes = type) {
+content +content
} }
} }

@ -16,10 +16,15 @@ object PasteDao {
val id = PasteTable.insert { val id = PasteTable.insert {
it[content] = paste.content it[content] = paste.content
it[created] = paste.created it[created] = paste.created
it[uri] = paste.uri
}[PasteTable.id] }[PasteTable.id]
Transient(id, paste) Transient(id, paste)
} }
fun selectByUri(uri: String): Transient<Paste>? = transaction {
PasteTable.select { PasteTable.uri.eq(uri) }.firstOrNull()
}?.toPaste()
init { init {
val source = MariaDbDataSource().apply { val source = MariaDbDataSource().apply {
userName = config[DatabaseSpec.user] userName = config[DatabaseSpec.user]
@ -29,7 +34,7 @@ object PasteDao {
} }
Database.connect(source) Database.connect(source)
transaction { transaction {
SchemaUtils.create(PasteTable) SchemaUtils.createMissingTablesAndColumns(PasteTable)
} }
} }
} }
@ -38,14 +43,16 @@ private fun ResultRow.toPaste() = Transient(
get(PasteTable.id), get(PasteTable.id),
Paste( Paste(
content = get(PasteTable.content), content = get(PasteTable.content),
created = get(PasteTable.created) created = get(PasteTable.created),
uri = get(PasteTable.uri)
) )
) )
private object PasteTable : Table() { private object PasteTable : Table() {
val id = long("id").primaryKey().autoIncrement().index() val id = long("id").primaryKey().autoIncrement().uniqueIndex()
val content = text("content") val content = text("content")
val created = datetime("created").index() val created = datetime("created").index()
val uri = varchar("uri", 10).uniqueIndex()
} }
/* /*