-
-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
refactor: rearrange code, add CLI, gson -> jackson :)
- Loading branch information
1 parent
a73cd4f
commit 984b91b
Showing
7 changed files
with
144 additions
and
159 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,3 @@ | ||
# imagery | ||
|
||
A WIPShareX file server | ||
A WIP ShareX file server. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
9 changes: 0 additions & 9 deletions
9
app/src/main/java/dev/mizule/imagery/app/config/JavaConfigNotUsed.java
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
47 changes: 40 additions & 7 deletions
47
app/src/main/kotlin/dev/mizule/imagery/app/launcher/Launcher.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | ||
} |
6 changes: 6 additions & 0 deletions
6
app/src/main/kotlin/dev/mizule/imagery/app/model/ImageLookupResult.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
) |