Skip to content

Commit

Permalink
refactor: rearrange code, add CLI, gson -> jackson :)
Browse files Browse the repository at this point in the history
  • Loading branch information
zlataovce authored and powercasgamer committed Nov 11, 2023
1 parent a73cd4f commit 984b91b
Show file tree
Hide file tree
Showing 7 changed files with 144 additions and 159 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
# imagery

A WIPShareX file server
A WIP ShareX file server.
14 changes: 6 additions & 8 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
plugins {
id("org.jetbrains.kotlin.jvm") version "1.9.20"
`java-library`
id("com.github.johnrengelman.shadow") version "8.1.1"
id("net.kyori.indra") version "3.1.3"
`java-library`
application
}

Expand All @@ -15,12 +15,13 @@ repositories {
dependencies {
implementation(kotlin("stdlib"))
implementation(kotlin("reflect"))
implementation("com.google.guava:guava:32.1.3-jre")
implementation("com.google.code.gson:gson:2.10.1")
implementation("io.github.oshai:kotlin-logging-jvm:5.1.0")
implementation("org.jetbrains.kotlinx:kotlinx-cli-jvm:0.3.6")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.15.3")
implementation("org.slf4j:slf4j-simple:2.0.9")
implementation("io.javalin:javalin:6.0.0-beta.2")
implementation("com.github.ben-manes.caffeine:caffeine:3.1.8")
implementation("org.spongepowered:configurate-gson:4.2.0-SNAPSHOT")
implementation("org.spongepowered:configurate-jackson:4.2.0-SNAPSHOT")
implementation("org.spongepowered:configurate-hocon:4.2.0-SNAPSHOT")
implementation("org.spongepowered:configurate-extra-kotlin:4.2.0-SNAPSHOT")
}
Expand All @@ -43,9 +44,6 @@ tasks {
}

runShadow {
file("run").mkdirs()
workingDir = file("run")
systemProperty("terminal.jline", false)
systemProperty("terminal.ansi", true)
workingDir = file("run").also(File::mkdirs)
}
}

This file was deleted.

213 changes: 82 additions & 131 deletions app/src/main/kotlin/dev/mizule/imagery/app/App.kt
Original file line number Diff line number Diff line change
@@ -1,165 +1,116 @@
package dev.mizule.imagery.app

import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.github.benmanes.caffeine.cache.Cache
import com.github.benmanes.caffeine.cache.Caffeine
import com.google.gson.Gson
import com.google.gson.JsonObject
import dev.mizule.imagery.app.config.Config
import dev.mizule.imagery.app.model.ImageLookupResult
import dev.mizule.imagery.app.model.UploadedFile
import io.github.oshai.kotlinlogging.KotlinLogging
import io.javalin.Javalin
import io.javalin.http.ContentType
import io.javalin.http.Context
import io.javalin.http.HttpStatus
import io.javalin.json.JavalinGson
import io.javalin.json.JavalinJackson
import org.eclipse.jetty.http.MimeTypes
import org.spongepowered.configurate.BasicConfigurationNode
import org.spongepowered.configurate.CommentedConfigurationNode
import org.spongepowered.configurate.ConfigurateException
import org.spongepowered.configurate.gson.GsonConfigurationLoader
import org.spongepowered.configurate.hocon.HoconConfigurationLoader
import java.io.FileOutputStream
import java.nio.file.Files
import org.spongepowered.configurate.jackson.JacksonConfigurationLoader
import org.spongepowered.configurate.kotlin.objectMapperFactory
import java.nio.file.Path
import java.time.Duration
import kotlin.io.path.Path
import kotlin.io.path.exists

class App {
private val gson = Gson()
private lateinit var config: Config
private lateinit var configNode: CommentedConfigurationNode
private lateinit var configLoader: HoconConfigurationLoader
private lateinit var dataConfig: BasicConfigurationNode
private lateinit var dataLoader: GsonConfigurationLoader
private val javalin: Javalin by lazy {
Javalin.create {
it.jsonMapper(JavalinGson(gson))
it.router.ignoreTrailingSlashes = true
import java.util.concurrent.TimeUnit
import kotlin.io.path.*

private val logger = KotlinLogging.logger {}

class App(private val config: Config) {
private val storageDir = Path(config.storagePath)
private val dataLoader = JacksonConfigurationLoader.builder()
.path(Path(config.indexPath))
.defaultOptions { options ->
options.serializers { builder ->
builder.registerAnnotatedObjects(objectMapperFactory())
}
}
}
private val path = Path(".")
val storage: Path = path.resolve("storage")
val cache = Caffeine.newBuilder()
.expireAfterWrite(Duration.ofMinutes(15))
.expireAfterAccess(Duration.ofMinutes(10))
.build<String, Pair<UploadedFile, Path>>()

fun load() {
initializeStorage()
initializeConfigs()

javalin.unsafeConfig().contextResolver.ip = { ctx ->
.build()

private val dataNode = dataLoader.load()
private val cache: Cache<String, FileCacheEntry> = Caffeine.newBuilder()
.expireAfterWrite(15, TimeUnit.MINUTES)
.expireAfterAccess(10, TimeUnit.MINUTES)
.build()

private val javalin = Javalin.create {
it.jsonMapper(JavalinJackson(MAPPER))
it.showJavalinBanner = false
it.router.ignoreTrailingSlashes = true
it.contextResolver.ip = { ctx ->
ctx.header("CF-Connecting-IP") ?: ctx.req().remoteAddr
}

configureEndpoints()
}

private fun initializeStorage() {
if (!storage.exists()) {
Files.createDirectories(storage)
}

// cache?
}
init {
storageDir.createDirectories()

private fun initializeConfigs() {
this.dataLoader = GsonConfigurationLoader.builder()
.path(path.resolve("files.json"))
.build()
this.configLoader = HoconConfigurationLoader.builder()
.path(path.resolve("config.conf"))
.defaultOptions {
it.shouldCopyDefaults(true)
}
.build()

try {
this.dataConfig = dataLoader.load()
this.configNode = configLoader.load()
dataLoader.save(this.dataConfig)
configLoader.save(this.configNode)
this.config = configNode.get(Config::class.java) ?: return
} catch (e: ConfigurateException) {
throw RuntimeException(e)
javalin.beforeMatched { ctx ->
logger.info { "Received ${ctx.method()} request from: ${ctx.ip()}:${ctx.port()} for ${ctx.fullUrl()}" }
}
javalin.get("/{id}", ::serveUploadedFile)
javalin.post("/upload", ::handleFileUpload)
}

private fun configureEndpoints() {
javalin.beforeMatched { ctx ->
println(
String.format(
"Received %s request from: %s:%s for %s",
ctx.method(),
ctx.ip(),
ctx.port(),
ctx.fullUrl()
)
)
}
javalin.post("/upload") { ctx ->
handleFileUpload(ctx)
private fun handleFileUpload(ctx: Context) {
val file = ctx.uploadedFiles("file").firstOrNull()
if (file == null) {
ctx.status(HttpStatus.BAD_REQUEST)
return
}

javalin.get("{id}") { ctx ->
serveUploadedFile(ctx)
val fileName = getRandomString() + file.extension()
val filePath = storageDir.resolve(fileName)
filePath.outputStream().use {
file.content().copyTo(it)
}

val uploadedFile = UploadedFile(
fileName,
"user",
System.currentTimeMillis(),
file.filename(),
file.extension(),
MimeTypes.getDefaultMimeByExtension(file.extension())
)

dataNode.node(uploadedFile.id).set(uploadedFile)
dataLoader.save(dataNode) // TODO: probably shouldn't be done during the http request

cache.put(fileName, FileCacheEntry(uploadedFile, filePath))
ctx.json(mapOf("data" to ImageLookupResult("${config.baseUrl}/$fileName")))
}

private fun handleFileUpload(ctx: io.javalin.http.Context) {
ctx.uploadedFiles("file").first().also { file ->
val fileName = getRandomString() + file.extension()
val filePath = storage.resolve(fileName)
Files.createFile(filePath)
FileOutputStream(filePath.toFile()).use {
file.content().copyTo(it)
private fun serveUploadedFile(ctx: Context) {
cache.getIfPresent(ctx.pathParam("id"))
?.also { (uploadedFile, path) ->
ctx.result(path.inputStream())
.contentType(ContentType.getContentTypeByExtension(uploadedFile.extension) ?: ContentType.IMAGE_PNG)
}
val uploadedFile = UploadedFile(
fileName,
"user",
System.currentTimeMillis(),
file.filename(),
file.extension(),
MimeTypes.getDefaultMimeByExtension(file.extension())
)
dataConfig.node(uploadedFile.id).set(uploadedFile)
try {
dataLoader.save(this.dataConfig)
} catch (e: ConfigurateException) {
throw RuntimeException(e)
}
cache.put(fileName, Pair(uploadedFile, filePath))
ctx.json(JsonObject().apply {
add("data", JsonObject().apply {
addProperty("url", "https://i.mizule.dev/$fileName")
addProperty("delete", "https://i.mizule.dev/$fileName/del")
})
})
}
?: ctx.status(HttpStatus.NOT_FOUND)
}

private fun serveUploadedFile(ctx: io.javalin.http.Context) {
val path = cache.getIfPresent(ctx.pathParam("id"))

if (path == null) {
ctx.status(HttpStatus.NOT_FOUND)
} else {
val file = path.second.toFile()
ctx.result(file.inputStream())
.contentType(ContentType.getContentTypeByExtension(path.first.extension) ?: ContentType.IMAGE_PNG)
}
fun start() {
logger.info { "Starting HTTP server at port ${config.port}..." }
javalin.start(config.port)
}

fun enable() {
javalin.start(this.config.port)
fun stop() {
logger.info { "Shutting down..." }
}

fun disable() {
println("Shutting down...")
}
companion object {
private val MAPPER = jacksonObjectMapper()
private val ALLOWED_CHARS = ('A'..'Z') + ('a'..'z') + ('0'..'9')

private fun getRandomString(length: Int = 8): String {
val allowedChars = ('A'..'Z') + ('a'..'z') + ('0'..'9')
return (1..length)
.map { allowedChars.random() }
.joinToString("")
private fun getRandomString(length: Int = 8): String =
generateSequence(ALLOWED_CHARS::random).take(length).joinToString("")
}

data class FileCacheEntry(val file: UploadedFile, val path: Path)
}
12 changes: 9 additions & 3 deletions app/src/main/kotlin/dev/mizule/imagery/app/config/Config.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,15 @@ import org.spongepowered.configurate.objectmapping.meta.Comment

@ConfigSerializable
data class Config(
@Comment("The port to start the application on")
@Comment("The port to start the application on.")
val port: Int = 8052,

@Comment("The domain that this will be on")
val domain: String = "https://example.com"
@Comment("The base URL that this will be on, without trailing slashes.")
val baseUrl: String = "https://i.mizule.dev",

@Comment("The path to the file upload index.")
val indexPath: String = "./files.json",

@Comment("The path to the uploaded file storage directory.")
val storagePath: String = "./storage"
)
47 changes: 40 additions & 7 deletions app/src/main/kotlin/dev/mizule/imagery/app/launcher/Launcher.kt
Original file line number Diff line number Diff line change
@@ -1,15 +1,48 @@
@file:JvmName("Launcher")

package dev.mizule.imagery.app.launcher

import dev.mizule.imagery.app.App
import dev.mizule.imagery.app.config.Config
import kotlinx.cli.ArgParser
import kotlinx.cli.ArgType
import kotlinx.cli.default
import org.spongepowered.configurate.hocon.HoconConfigurationLoader
import org.spongepowered.configurate.kotlin.extensions.get
import org.spongepowered.configurate.kotlin.objectMapperFactory
import kotlin.io.path.Path
import kotlin.io.path.exists

fun main(args: Array<String>) {
val parser = ArgParser("imagery")
val path by parser.option(ArgType.String, shortName = "p", description = "The configuration file path, created if not present.")
.default("./config.conf")

parser.parse(args)

val configPath = Path(path)
val configLoader = HoconConfigurationLoader.builder()
.path(configPath)
.defaultOptions { options ->
options.shouldCopyDefaults(true)
options.serializers { builder ->
builder.registerAnnotatedObjects(objectMapperFactory())
}
}
.build()

val configNode = configLoader.load()
val config = requireNotNull(configNode.get<Config>()) {
"Could not read configuration"
}

fun main() {
val app = App()
if (!configPath.exists()) {
configNode.set(config) // update the backing node to add defaults
configLoader.save(configNode)
}

app.load()
app.enable()
val app = App(config)
Runtime.getRuntime().addShutdownHook(Thread(app::stop))

Runtime.getRuntime().addShutdownHook(Thread {
app.disable()
})
app.start()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package dev.mizule.imagery.app.model

data class ImageLookupResult(
val url: String,
val delete: String = "$url/del"
)

0 comments on commit 984b91b

Please sign in to comment.