Add web upload

This commit is contained in:
kageru 2019-09-22 18:26:28 +02:00
parent e80c97e450
commit 00111efb27
Signed by: kageru
GPG Key ID: 8282A2BEA4ADA3D2
7 changed files with 154 additions and 31 deletions

@ -17,13 +17,18 @@ val ktorVersion = "1.2.4"
repositories { repositories {
mavenCentral() mavenCentral()
jcenter() jcenter()
maven { url = uri("https://dl.bintray.com/kotlin/ktor") }
maven { url = uri("https://dl.bintray.com/kotlin/kotlin-js-wrappers") }
} }
dependencies { dependencies {
implementation(kotlin("stdlib-jdk8")) implementation(kotlin("stdlib-jdk8"))
implementation("io.ktor:ktor-server-netty:$ktorVersion") implementation("io.ktor:ktor-server-netty:$ktorVersion")
implementation("io.ktor:ktor-locations:$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.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") implementation("org.mariadb.jdbc:mariadb-java-client:2.4.4")
} }

@ -2,12 +2,14 @@ package moe.kageru.kodeshare
import io.ktor.application.install import io.ktor.application.install
import io.ktor.features.DefaultHeaders import io.ktor.features.DefaultHeaders
import io.ktor.locations.KtorExperimentalLocationsAPI
import io.ktor.locations.Locations import io.ktor.locations.Locations
import io.ktor.routing.routing 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
@KtorExperimentalLocationsAPI
@ExperimentalStdlibApi @ExperimentalStdlibApi
fun main() { fun main() {
embeddedServer(Netty, 9092) { embeddedServer(Netty, 9092) {

@ -20,6 +20,10 @@ object Log {
fun info(message: String) { fun info(message: String) {
log.info(message) log.info(message)
} }
fun warn(message: String) {
log.warning(message)
}
} }
private class LogFormatter : Formatter() { private class LogFormatter : Formatter() {

@ -3,63 +3,89 @@ package moe.kageru.kodeshare
import io.ktor.application.ApplicationCall import io.ktor.application.ApplicationCall
import io.ktor.application.call import io.ktor.application.call
import io.ktor.http.ContentType import io.ktor.http.ContentType
import io.ktor.http.HttpHeaders
import io.ktor.http.HttpStatusCode import io.ktor.http.HttpStatusCode
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.streamProvider import io.ktor.http.content.streamProvider
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.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.Homepage
import moe.kageru.kodeshare.persistence.PasteDao import moe.kageru.kodeshare.persistence.PasteDao
@KtorExperimentalLocationsAPI
@ExperimentalStdlibApi @ExperimentalStdlibApi
object Routes { object Routes {
fun Routing.createRoutes() { fun Routing.createRoutes() {
get("/") { get("/") {
call.respondText("Hello, world!", ContentType.Text.Html) call.respond(Homepage.content)
}
get("/style.css") {
call.respondText(Css.default, ContentType.Text.CSS)
} }
post("/") { post("/") {
save(call) call.handlePost()
} }
get<PasteRequest> { req -> get<PasteRequest> { req ->
respondGet(call, req) call.handleGet(req)
} }
} }
}
@ExperimentalStdlibApi @ExperimentalStdlibApi
suspend fun save(call: ApplicationCall) { private suspend fun ApplicationCall.handlePost() {
call.receiveMultipart().forEachPart { part -> receiveMultipart().forEachPart { part ->
when(part) { when (part) {
is PartData.FileItem -> { is PartData.FileItem -> {
Log.info("new file") val content = part.streamProvider().use { it.readBytes().decodeToString() }
val content = part.streamProvider().use { it.readAllBytes().decodeToString() } respondToUpload(content)?.let { id ->
val id = PasteDao.insert(Paste(content = content, html = null)).id Log.info("Saving new file paste with ID $id")
call.respond(HttpStatusCode.Created, "$id") } ?: Log.info("Invalid upload: $content")
Log.info("Saving new paste with ID $id")
} }
is PartData.FormItem -> { is PartData.FormItem -> {
Log.info("form item") 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 -> { is PartData.BinaryItem -> {
Log.info("binary item") Log.warn("Received binary item from upload form. This shouldn’t happen.")
}
} }
} }
} }
}
suspend fun respondGet(call: ApplicationCall, req: PasteRequest) { 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 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 ->
call.respondText(paste.html ?: paste.content, ContentType.Text.Html) respondText(paste.html ?: paste.content, ContentType.Text.Plain)
} ?: call.respond(HttpStatusCode.NotFound, "nothing found for id $id") } ?: respond(HttpStatusCode.NotFound, "nothing found for id $id")
}
} }
@KtorExperimentalLocationsAPI
@Location("/{id}") @Location("/{id}")
data class PasteRequest(val id: Long) data class PasteRequest(val id: Long)

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

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

@ -20,7 +20,7 @@ object PasteDao {
init { init {
val source = MariaDbDataSource().apply { val source = MariaDbDataSource().apply {
userName = "kodepaste" userName = "kodeshare"
setPassword("12345") setPassword("12345")
databaseName = "kode" databaseName = "kode"
} }