Use random strings as URIs
This commit is contained in:
parent
624320b8b1
commit
4919958d8f
@ -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)
|
|
18
src/main/kotlin/moe/kageru/kodeshare/Paste.kt
Normal file
18
src/main/kotlin/moe/kageru/kodeshare/Paste.kt
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
Loading…
Reference in New Issue
Block a user