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

View File

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

View File

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

View File

@ -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<RawPasteRequest> { req ->
call.handleRaw(req)
}
get<TypedPasteRequest> { 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
}

View File

@ -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<String>()
val user by optional("kodeshare")
val database by optional("kode")
}
object ServerSpec : ConfigSpec() {
val port by optional(9092)
}

View File

@ -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

View File

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

View File

@ -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<Paste>? = 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()
}
/*