Add syntax highlighting and config

This commit is contained in:
kageru 2019-09-29 08:17:31 +02:00
parent 00111efb27
commit 624320b8b1
10 changed files with 112 additions and 22 deletions

3
.gitignore vendored

@ -1,3 +1,4 @@
.gradle/ *gradle*
build/ build/
kodeshare.log kodeshare.log
.idea/

@ -2,6 +2,7 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins { plugins {
kotlin("jvm") version "1.3.50" kotlin("jvm") version "1.3.50"
id("com.github.johnrengelman.shadow") version "5.1.0" apply true
application application
} }
@ -30,6 +31,7 @@ dependencies {
implementation("org.jetbrains.exposed:exposed:0.17.3") implementation("org.jetbrains.exposed:exposed:0.17.3")
implementation("org.jetbrains:kotlin-css-jvm:1.0.0-pre.83-kotlin-1.3.50") implementation("org.jetbrains:kotlin-css-jvm:1.0.0-pre.83-kotlin-1.3.50")
implementation("org.mariadb.jdbc:mariadb-java-client:2.4.4") implementation("org.mariadb.jdbc:mariadb-java-client:2.4.4")
implementation("com.uchuhimo:konf-core:0.20.0")
} }
tasks.withType<KotlinCompile> { tasks.withType<KotlinCompile> {

1
kodeshare.properties Normal file

@ -0,0 +1 @@
database.password=12345

@ -8,6 +8,7 @@ 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
@KtorExperimentalLocationsAPI @KtorExperimentalLocationsAPI
@ExperimentalStdlibApi @ExperimentalStdlibApi
@ -21,4 +22,4 @@ fun main() {
}.start(wait = true) }.start(wait = true)
} }
data class Paste(val content: String, val html: String?) data class Paste(val content: String, val created: DateTime)

@ -5,29 +5,33 @@ 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.respondRedirect
import io.ktor.response.respondText import io.ktor.response.respondText
import io.ktor.routing.Routing import io.ktor.routing.Routing
import io.ktor.routing.get import io.ktor.routing.get
import io.ktor.routing.post import io.ktor.routing.post
import moe.kageru.kodeshare.pages.Css import moe.kageru.kodeshare.pages.Css
import moe.kageru.kodeshare.pages.Homepage import moe.kageru.kodeshare.pages.Homepage
import moe.kageru.kodeshare.pages.PastePage
import moe.kageru.kodeshare.persistence.PasteDao import moe.kageru.kodeshare.persistence.PasteDao
import org.joda.time.DateTime
@KtorExperimentalLocationsAPI @KtorExperimentalLocationsAPI
@ExperimentalStdlibApi @ExperimentalStdlibApi
object Routes { object Routes {
fun Routing.createRoutes() { fun Routing.createRoutes() {
get("/") { get("/") {
call.respond(Homepage.content) call.respond(HttpStatusCode.OK, Homepage.content)
} }
get("/style.css") { get("/style.css") {
call.respondText(Css.default, ContentType.Text.CSS) call.respondText(Css.default, ContentType.Text.CSS)
@ -38,6 +42,9 @@ object Routes {
get<PasteRequest> { req -> get<PasteRequest> { req ->
call.handleGet(req) call.handleGet(req)
} }
get<RawPasteRequest> { req ->
call.handleRaw(req)
}
} }
@ExperimentalStdlibApi @ExperimentalStdlibApi
@ -48,13 +55,13 @@ object Routes {
val content = part.streamProvider().use { it.readBytes().decodeToString() } val content = part.streamProvider().use { it.readBytes().decodeToString() }
respondToUpload(content)?.let { id -> respondToUpload(content)?.let { id ->
Log.info("Saving new file paste with ID $id") Log.info("Saving new file paste with ID $id")
} ?: Log.info("Invalid upload: $content") }
} }
is PartData.FormItem -> { is PartData.FormItem -> {
val content = part.value val content = part.value
respondToUpload(content)?.let { id -> respondToUpload(content)?.let { id ->
Log.info("Saving new text paste with ID $id") Log.info("Saving new text paste with ID $id")
} ?: Log.info("Invalid upload: $content") }
} }
is PartData.BinaryItem -> { is PartData.BinaryItem -> {
Log.warn("Received binary item from upload form. This shouldn’t happen.") Log.warn("Received binary item from upload form. This shouldn’t happen.")
@ -65,10 +72,16 @@ object Routes {
private suspend fun ApplicationCall.respondToUpload(content: String): Long? { private suspend fun ApplicationCall.respondToUpload(content: String): Long? {
content.ifBlank { content.ifBlank {
respondText("Empty pastes are not allowed", ContentType.Text.Any, HttpStatusCode.BadRequest) Log.info("Rejecting blank paste")
respond(HttpStatusCode.BadRequest, "Empty pastes are not allowed")
return null return null
} }
val id = PasteDao.insert(Paste(content, null)).id if (content.length > 1 * 1024 * 1024) {
Log.info("Rejecting paste over 1MB")
respond(HttpStatusCode.BadRequest, "Pastes are limited to 1MB each")
return null
}
val id = PasteDao.insert(Paste(content, DateTime.now())).id
// 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.
@ -81,11 +94,26 @@ object Routes {
val id = req.id val id = req.id
Log.info("Retrieving paste $id") Log.info("Retrieving paste $id")
PasteDao.select(id)?.data?.let { paste -> PasteDao.select(id)?.data?.let { paste ->
respondText(paste.html ?: paste.content, ContentType.Text.Plain) respond(
HttpStatusCode.OK,
PastePage.build(paste.content)
)
} ?: respond(HttpStatusCode.NotFound, "nothing found for id $id")
}
private suspend fun ApplicationCall.handleRaw(req: RawPasteRequest) {
val id = req.id
Log.info("Retrieving raw paste $id")
PasteDao.select(id)?.data?.let { paste ->
respondText(paste.content, ContentType.Text.Plain)
} ?: respond(HttpStatusCode.NotFound, "nothing found for id $id") } ?: respond(HttpStatusCode.NotFound, "nothing found for id $id")
} }
} }
@KtorExperimentalLocationsAPI
@Location("/r/{id}")
data class RawPasteRequest(val id: Long)
@KtorExperimentalLocationsAPI @KtorExperimentalLocationsAPI
@Location("/{id}") @Location("/{id}")
data class PasteRequest(val id: Long) data class PasteRequest(val id: Long)

@ -0,0 +1,15 @@
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")
.from.env()
object DatabaseSpec : ConfigSpec() {
val port by optional(3306)
val password by required<String>()
val user by optional("kodeshare")
val database by optional("kode")
}

@ -5,21 +5,22 @@ import kotlinx.css.properties.ms
import kotlinx.css.properties.transition import kotlinx.css.properties.transition
object Css { object Css {
private val accent1 = Color("#cd7400") private val accent1 = Color("#e6db74")
private val accent2 = Color("#ed7a00") private val accent2 = Color("#a6e22e")
private val fontcolor = Color.lightGrey private val fontcolor = Color.lightGrey
private val bgcolor = Color("#23241f")
val default = CSSBuilder().apply { val default = CSSBuilder().apply {
body { body {
fontFamily = "Hack, Fira Code, Noto Mono, monospace" fontFamily = "Hack, Fira Code, Noto Mono, monospace"
fontSize = 13.pt fontSize = 13.pt
textAlign = TextAlign.center textAlign = TextAlign.center
margin = "auto" margin = "auto"
backgroundColor = Color.black backgroundColor = bgcolor
color = fontcolor color = fontcolor
} }
textarea { textarea {
fontFamily = "Hack, Fira Code, Noto Mono, monospace" fontFamily = "Hack, Fira Code, Noto Mono, monospace"
backgroundColor = Color.black backgroundColor = bgcolor
color = Color.white color = Color.white
fontSize = 13.pt fontSize = 13.pt
borderColor = accent1 borderColor = accent1
@ -30,6 +31,14 @@ object Css {
minWidth = 70.pct minWidth = 70.pct
maxWidth = 100.pct maxWidth = 100.pct
} }
// this doesn’t inherit the style from anything else for some reason
rule(".hljs") {
width = 100.pct
height = 100.pct
textAlign = TextAlign.left
fontFamily = "Hack, Fira Code, Noto Mono, monospace"
fontSize = 13.pt
}
rule("input[type=\"submit\"]") { rule("input[type=\"submit\"]") {
backgroundColor = accent1 backgroundColor = accent1
borderColor = accent1 borderColor = accent1
@ -44,7 +53,8 @@ object Css {
} }
rule("input[type=\"submit\"]:hover") { rule("input[type=\"submit\"]:hover") {
backgroundColor = Color.transparent backgroundColor = Color.transparent
color = accent1 borderColor = accent2
color = accent2
} }
rule("textarea:focus") { rule("textarea:focus") {
borderColor = accent2 borderColor = accent2

@ -8,6 +8,9 @@ object Homepage {
val content = HtmlContent(HttpStatusCode.OK) { val content = HtmlContent(HttpStatusCode.OK) {
head { head {
link(rel = "stylesheet", href = "/style.css", type = "text/css") link(rel = "stylesheet", href = "/style.css", type = "text/css")
unsafe {
+"<style>*{transition-duration: 400ms;}</style>"
}
} }
body { body {
h1 { +"kodeshare - yet another paste service" } h1 { +"kodeshare - yet another paste service" }

@ -0,0 +1,27 @@
package moe.kageru.kodeshare.pages
import io.ktor.html.HtmlContent
import io.ktor.http.HttpStatusCode
import kotlinx.html.*
object PastePage {
fun build(content: String) = HtmlContent(HttpStatusCode.OK) {
head {
link(rel = "stylesheet", href = "/style.css", type = "text/css")
link(
rel = "stylesheet",
href = "https://p.kageru.moe/static/hljs.css",
type = "text/css"
)
script(src = "https://p.kageru.moe/static/hl.js") {}
unsafe { +"<script>hljs.initHighlightingOnLoad();</script>" }
}
body {
pre {
code {
+content
}
}
}
}
}

@ -1,6 +1,8 @@
package moe.kageru.kodeshare.persistence package moe.kageru.kodeshare.persistence
import moe.kageru.kodeshare.Paste import moe.kageru.kodeshare.Paste
import moe.kageru.kodeshare.config.DatabaseSpec
import moe.kageru.kodeshare.config.config
import org.jetbrains.exposed.sql.* import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.transactions.transaction import org.jetbrains.exposed.sql.transactions.transaction
import org.mariadb.jdbc.MariaDbDataSource import org.mariadb.jdbc.MariaDbDataSource
@ -13,20 +15,20 @@ object PasteDao {
fun insert(paste: Paste): Transient<Paste> = transaction { fun insert(paste: Paste): Transient<Paste> = transaction {
val id = PasteTable.insert { val id = PasteTable.insert {
it[content] = paste.content it[content] = paste.content
it[html] = paste.html it[created] = paste.created
}[PasteTable.id] }[PasteTable.id]
Transient(id, paste) Transient(id, paste)
} }
init { init {
val source = MariaDbDataSource().apply { val source = MariaDbDataSource().apply {
userName = "kodeshare" userName = config[DatabaseSpec.user]
setPassword("12345") setPassword(config[DatabaseSpec.password])
databaseName = "kode" databaseName = config[DatabaseSpec.database]
port = config[DatabaseSpec.port]
} }
Database.connect(source) Database.connect(source)
transaction { transaction {
SchemaUtils.drop(PasteTable)
SchemaUtils.create(PasteTable) SchemaUtils.create(PasteTable)
} }
} }
@ -36,14 +38,14 @@ private fun ResultRow.toPaste() = Transient(
get(PasteTable.id), get(PasteTable.id),
Paste( Paste(
content = get(PasteTable.content), content = get(PasteTable.content),
html = get(PasteTable.html) created = get(PasteTable.created)
) )
) )
private object PasteTable : Table() { private object PasteTable : Table() {
val id = long("id").primaryKey().autoIncrement() val id = long("id").primaryKey().autoIncrement().index()
val content = text("content") val content = text("content")
val html = text("html").nullable() val created = datetime("created").index()
} }
/* /*