Add syntax highlighting and config
This commit is contained in:
parent
00111efb27
commit
624320b8b1
3
.gitignore
vendored
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
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)
|
||||||
|
15
src/main/kotlin/moe/kageru/kodeshare/config/Config.kt
Normal file
15
src/main/kotlin/moe/kageru/kodeshare/config/Config.kt
Normal file
@ -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" }
|
||||||
|
27
src/main/kotlin/moe/kageru/kodeshare/pages/PastePage.kt
Normal file
27
src/main/kotlin/moe/kageru/kodeshare/pages/PastePage.kt
Normal file
@ -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()
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
Loading…
Reference in New Issue
Block a user