From 00111efb27d50f0882ac1e50599770f0063eaded Mon Sep 17 00:00:00 2001 From: kageru Date: Sun, 22 Sep 2019 18:26:28 +0200 Subject: [PATCH] Add web upload --- build.gradle.kts | 7 +- .../kotlin/moe/kageru/kodeshare/Kodeshare.kt | 2 + src/main/kotlin/moe/kageru/kodeshare/Log.kt | 4 + .../kotlin/moe/kageru/kodeshare/Routes.kt | 84 ++++++++++++------- .../kotlin/moe/kageru/kodeshare/pages/Css.kt | 56 +++++++++++++ .../moe/kageru/kodeshare/pages/Homepage.kt | 30 +++++++ .../kageru/kodeshare/persistence/PasteDao.kt | 2 +- 7 files changed, 154 insertions(+), 31 deletions(-) create mode 100644 src/main/kotlin/moe/kageru/kodeshare/pages/Css.kt create mode 100644 src/main/kotlin/moe/kageru/kodeshare/pages/Homepage.kt diff --git a/build.gradle.kts b/build.gradle.kts index 0548c93..15abfd8 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -17,16 +17,21 @@ val ktorVersion = "1.2.4" repositories { mavenCentral() jcenter() + maven { url = uri("https://dl.bintray.com/kotlin/ktor") } + maven { url = uri("https://dl.bintray.com/kotlin/kotlin-js-wrappers") } } dependencies { implementation(kotlin("stdlib-jdk8")) implementation("io.ktor:ktor-server-netty:$ktorVersion") implementation("io.ktor:ktor-locations:$ktorVersion") + implementation("io.ktor:ktor-server-core:$ktorVersion") + implementation("io.ktor:ktor-html-builder:$ktorVersion") 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.mariadb.jdbc:mariadb-java-client:2.4.4") } tasks.withType { kotlinOptions.jvmTarget = "1.8" -} \ No newline at end of file +} diff --git a/src/main/kotlin/moe/kageru/kodeshare/Kodeshare.kt b/src/main/kotlin/moe/kageru/kodeshare/Kodeshare.kt index 25d90dd..0b7fe21 100644 --- a/src/main/kotlin/moe/kageru/kodeshare/Kodeshare.kt +++ b/src/main/kotlin/moe/kageru/kodeshare/Kodeshare.kt @@ -2,12 +2,14 @@ package moe.kageru.kodeshare import io.ktor.application.install import io.ktor.features.DefaultHeaders +import io.ktor.locations.KtorExperimentalLocationsAPI import io.ktor.locations.Locations import io.ktor.routing.routing import io.ktor.server.engine.embeddedServer import io.ktor.server.netty.Netty import moe.kageru.kodeshare.Routes.createRoutes +@KtorExperimentalLocationsAPI @ExperimentalStdlibApi fun main() { embeddedServer(Netty, 9092) { diff --git a/src/main/kotlin/moe/kageru/kodeshare/Log.kt b/src/main/kotlin/moe/kageru/kodeshare/Log.kt index 60176dd..530b732 100644 --- a/src/main/kotlin/moe/kageru/kodeshare/Log.kt +++ b/src/main/kotlin/moe/kageru/kodeshare/Log.kt @@ -20,6 +20,10 @@ object Log { fun info(message: String) { log.info(message) } + + fun warn(message: String) { + log.warning(message) + } } private class LogFormatter : Formatter() { diff --git a/src/main/kotlin/moe/kageru/kodeshare/Routes.kt b/src/main/kotlin/moe/kageru/kodeshare/Routes.kt index 8b4d903..7476fc3 100644 --- a/src/main/kotlin/moe/kageru/kodeshare/Routes.kt +++ b/src/main/kotlin/moe/kageru/kodeshare/Routes.kt @@ -3,63 +3,89 @@ package moe.kageru.kodeshare import io.ktor.application.ApplicationCall 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.PartData import io.ktor.http.content.forEachPart 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.receiveMultipart import io.ktor.response.respond +import io.ktor.response.respondRedirect import io.ktor.response.respondText import io.ktor.routing.Routing import io.ktor.routing.get import io.ktor.routing.post +import moe.kageru.kodeshare.pages.Css +import moe.kageru.kodeshare.pages.Homepage import moe.kageru.kodeshare.persistence.PasteDao +@KtorExperimentalLocationsAPI @ExperimentalStdlibApi object Routes { fun Routing.createRoutes() { get("/") { - call.respondText("Hello, world!", ContentType.Text.Html) + call.respond(Homepage.content) + } + get("/style.css") { + call.respondText(Css.default, ContentType.Text.CSS) } post("/") { - save(call) + call.handlePost() } get { req -> - respondGet(call, req) + call.handleGet(req) } } -} -@ExperimentalStdlibApi -suspend fun save(call: ApplicationCall) { - call.receiveMultipart().forEachPart { part -> - when(part) { - is PartData.FileItem -> { - Log.info("new file") - val content = part.streamProvider().use { it.readAllBytes().decodeToString() } - val id = PasteDao.insert(Paste(content = content, html = null)).id - call.respond(HttpStatusCode.Created, "$id") - Log.info("Saving new paste with ID $id") - } - is PartData.FormItem -> { - Log.info("form item") - } - is PartData.BinaryItem -> { - Log.info("binary item") + @ExperimentalStdlibApi + private suspend fun ApplicationCall.handlePost() { + receiveMultipart().forEachPart { part -> + 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") + } ?: Log.info("Invalid upload: $content") + } + is PartData.FormItem -> { + val content = part.value + respondToUpload(content)?.let { id -> + Log.info("Saving new text paste with ID $id") + } ?: Log.info("Invalid upload: $content") + } + is PartData.BinaryItem -> { + Log.warn("Received binary item from upload form. This shouldn’t happen.") + } } } } + + private suspend fun ApplicationCall.respondToUpload(content: String): Long? { + content.ifBlank { + respondText("Empty pastes are not allowed", ContentType.Text.Any, HttpStatusCode.BadRequest) + return null + } + val id = PasteDao.insert(Paste(content, null)).id + // 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 + } + + private suspend fun ApplicationCall.handleGet(req: PasteRequest) { + val id = req.id + Log.info("Retrieving paste $id") + PasteDao.select(id)?.data?.let { paste -> + respondText(paste.html ?: paste.content, ContentType.Text.Plain) + } ?: respond(HttpStatusCode.NotFound, "nothing found for id $id") + } } -suspend fun respondGet(call: ApplicationCall, req: PasteRequest) { - val id = req.id - Log.info("Retrieving paste $id") - PasteDao.select(id)?.data?.let { paste -> - call.respondText(paste.html ?: paste.content, ContentType.Text.Html) - } ?: call.respond(HttpStatusCode.NotFound, "nothing found for id $id") -} - +@KtorExperimentalLocationsAPI @Location("/{id}") -data class PasteRequest(val id: Long) \ No newline at end of file +data class PasteRequest(val id: Long) diff --git a/src/main/kotlin/moe/kageru/kodeshare/pages/Css.kt b/src/main/kotlin/moe/kageru/kodeshare/pages/Css.kt new file mode 100644 index 0000000..ac1a18b --- /dev/null +++ b/src/main/kotlin/moe/kageru/kodeshare/pages/Css.kt @@ -0,0 +1,56 @@ +package moe.kageru.kodeshare.pages + +import kotlinx.css.* +import kotlinx.css.properties.ms +import kotlinx.css.properties.transition + +object Css { + private val accent1 = Color("#cd7400") + private val accent2 = Color("#ed7a00") + private val fontcolor = Color.lightGrey + val default = CSSBuilder().apply { + body { + fontFamily = "Hack, Fira Code, Noto Mono, monospace" + fontSize = 13.pt + textAlign = TextAlign.center + margin = "auto" + backgroundColor = Color.black + color = fontcolor + } + textarea { + fontFamily = "Hack, Fira Code, Noto Mono, monospace" + backgroundColor = Color.black + color = Color.white + fontSize = 13.pt + borderColor = accent1 + borderWidth = 3.px + borderRadius = 8.px + borderStyle = BorderStyle.solid + padding = "5px" + minWidth = 70.pct + maxWidth = 100.pct + } + rule("input[type=\"submit\"]") { + backgroundColor = accent1 + borderColor = accent1 + borderWidth = 2.px + borderRadius = 5.px + borderStyle = BorderStyle.solid + color = Color.black + fontWeight = FontWeight.w600 + padding = "5px 15px" + cursor = Cursor.pointer + transition(duration = 500.ms) + } + rule("input[type=\"submit\"]:hover") { + backgroundColor = Color.transparent + color = accent1 + } + rule("textarea:focus") { + borderColor = accent2 + } + rule("::selection") { + color = accent1 + } + }.toString() +} \ No newline at end of file diff --git a/src/main/kotlin/moe/kageru/kodeshare/pages/Homepage.kt b/src/main/kotlin/moe/kageru/kodeshare/pages/Homepage.kt new file mode 100644 index 0000000..343bbc6 --- /dev/null +++ b/src/main/kotlin/moe/kageru/kodeshare/pages/Homepage.kt @@ -0,0 +1,30 @@ +package moe.kageru.kodeshare.pages + +import io.ktor.html.HtmlContent +import io.ktor.http.HttpStatusCode +import kotlinx.html.* + +object Homepage { + val content = HtmlContent(HttpStatusCode.OK) { + head { + link(rel = "stylesheet", href = "/style.css", type = "text/css") + } + body { + h1 { +"kodeshare - yet another paste service" } + form("/", encType = FormEncType.multipartFormData, method = FormMethod.post) { + acceptCharset = "utf-8" + p { + label { +"Enter or paste your text here " } + } + textArea { + name = "input" + rows = "20" + cols = "100" + } + p { + submitInput { value = "Upload" } + } + } + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/moe/kageru/kodeshare/persistence/PasteDao.kt b/src/main/kotlin/moe/kageru/kodeshare/persistence/PasteDao.kt index 4993ab1..332d89b 100644 --- a/src/main/kotlin/moe/kageru/kodeshare/persistence/PasteDao.kt +++ b/src/main/kotlin/moe/kageru/kodeshare/persistence/PasteDao.kt @@ -20,7 +20,7 @@ object PasteDao { init { val source = MariaDbDataSource().apply { - userName = "kodepaste" + userName = "kodeshare" setPassword("12345") databaseName = "kode" }