<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="KotlinJpsPluginSettings">
- <option name="version" value="1.8.0" />
+ <option name="version" value="1.8.10" />
</component>
</project>
\ No newline at end of file
+import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
import com.nixxcode.jvmbrotli.common.BrotliLoader
import com.nixxcode.jvmbrotli.enc.BrotliOutputStream
import com.nixxcode.jvmbrotli.enc.Encoder
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
-import java.security.MessageDigest
-import java.util.*
import java.util.concurrent.CountDownLatch
import java.util.concurrent.Executors
import java.util.zip.GZIPOutputStream
plugins {
java
- kotlin("jvm") version "1.8.0"
- kotlin("plugin.serialization") version "1.8.0"
+ kotlin("jvm") version "1.8.10"
+ kotlin("plugin.serialization") version "1.8.10"
id("com.github.johnrengelman.shadow") version "7.1.2"
application
}
implementation("org.jetbrains.kotlinx:kotlinx-serialization-core-jvm:1.4.1")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json-jvm:1.4.1")
- implementation("io.ktor:ktor-server-core-jvm:2.2.4")
- implementation("io.ktor:ktor-server-netty-jvm:2.2.4")
+ implementation("io.ktor:ktor-server-core-jvm:2.3.0")
+ implementation("io.ktor:ktor-server-netty-jvm:2.3.0")
- implementation("io.ktor:ktor-server-call-id:2.2.4")
- implementation("io.ktor:ktor-server-call-logging:2.2.4")
- implementation("io.ktor:ktor-server-conditional-headers:2.2.4")
- implementation("io.ktor:ktor-server-forwarded-header:2.2.4")
- implementation("io.ktor:ktor-server-html-builder:2.2.4")
- implementation("io.ktor:ktor-server-sessions-jvm:2.2.4")
- implementation("io.ktor:ktor-server-status-pages:2.2.4")
+ implementation("io.ktor:ktor-server-call-id:2.3.0")
+ implementation("io.ktor:ktor-server-call-logging:2.3.0")
+ implementation("io.ktor:ktor-server-conditional-headers:2.3.0")
+ implementation("io.ktor:ktor-server-forwarded-header:2.3.0")
+ implementation("io.ktor:ktor-server-html-builder:2.3.0")
+ implementation("io.ktor:ktor-server-sessions-jvm:2.3.0")
+ implementation("io.ktor:ktor-server-status-pages:2.3.0")
implementation("org.jetbrains.kotlinx:kotlinx-html-jvm:0.8.0")
val pool = Executors.newWorkStealingPool()
val encoderParams = if (BrotliLoader.isBrotliAvailable()) Encoder.Parameters().setQuality(8) else null
- val base64 = Base64.getUrlEncoder()
- val hashDigest = ThreadLocal.withInitial { MessageDigest.getInstance("SHA-256") }
val resourceTree = fileTree(mapOf("dir" to outputs.files.asPath + "/static/", "exclude" to listOf("*.gz", "*.br", "*.sha256")))
val countDownLatch = CountDownLatch(resourceTree.count())
for (file in resourceTree) {
pool.execute {
val bytes = file.readBytes()
- val hashFile = File("${file.absolutePath}.sha256").bufferedWriter()
- hashFile.write(base64.encodeToString(hashDigest.get().digest(bytes)).trimEnd('='))
- hashFile.close()
val result = File("${file.absolutePath}.gz").outputStream()
val gzipStream = GZIPOutputStream(result)
}
}
+tasks.named<ShadowJar>("shadowJar") {
+ mergeServiceFiles()
+ exclude { it.name == "module-info.class" }
+}
+
application {
mainClass.set("info.mechyrdia.Factbooks")
}
import io.ktor.server.routing.*
import io.ktor.server.sessions.*
import io.ktor.server.sessions.serialization.*
-import io.ktor.server.util.*
import io.ktor.util.*
import org.slf4j.event.Level
import java.io.File
import java.io.InputStream
import java.util.concurrent.atomic.AtomicLong
-object ResourceLoader {
- fun getResource(resource: String): InputStream? = javaClass.getResourceAsStream(resource)
-
- val SHA256AttributeKey = AttributeKey<String>("SHA256Hash")
-}
-
lateinit var application: Application
private set
}
}
- install(ConditionalHeaders) {
- version { call, _ ->
- call.attributes.getOrNull(ResourceLoader.SHA256AttributeKey)?.let { hash ->
- listOf(EntityTagVersion(hash))
- }.orEmpty()
- }
- }
-
install(Sessions) {
cookie("USER_SESSION", SessionStorageMongoDB) {
identity { Id<UserSession>().id }
// Factbooks and assets
- static("/static") {
- get("{static-content...}") {
- val staticContentPath = call.parameters.getAll("static-content")?.joinToString("/") ?: return@get
- val contentPath = "/static/$staticContentPath"
-
- ResourceLoader.getResource("$contentPath.sha256")?.reader()?.readText()?.let { sha256Hash ->
- call.attributes.put(ResourceLoader.SHA256AttributeKey, sha256Hash)
- }
-
- val brContentPath = "$contentPath.br"
- val gzContentPath = "$contentPath.gz"
-
- val contentType = ContentType.fromFileExtension(contentPath.substringAfterLast('.')).firstOrNull()
-
- val acceptedEncodings = call.request.acceptEncodingItems().map { it.value }.toSet()
-
- if (CompressedFileType.BROTLI.encoding in acceptedEncodings) {
- val brContent = ResourceLoader.getResource(brContentPath)
- if (brContent != null) {
- call.attributes.put(SuppressionAttribute, true)
-
- call.response.header(HttpHeaders.ContentEncoding, CompressedFileType.BROTLI.encoding)
-
- call.respondBytes(brContent.readBytes(), contentType)
-
- return@get
- }
- }
-
- if (CompressedFileType.GZIP.encoding in acceptedEncodings) {
- val gzContent = ResourceLoader.getResource(gzContentPath)
- if (gzContent != null) {
- call.attributes.put(SuppressionAttribute, true)
-
- call.response.header(HttpHeaders.ContentEncoding, CompressedFileType.GZIP.encoding)
-
- call.respondBytes(gzContent.readBytes(), contentType)
-
- return@get
- }
- }
-
- ResourceLoader.getResource(contentPath)?.let { call.respondBytes(it.readBytes(), contentType) }
- }
+ staticResources("/static", "static", index = null) {
+ preCompressed(CompressedFileType.BROTLI, CompressedFileType.GZIP)
}
get("/lore/{path...}") {
call.respondHtml(HttpStatusCode.OK, call.loreArticlePage())
}
- static("/assets") {
- files(File(Configuration.CurrentConfiguration.assetDir))
- }
+ staticFiles("/assets", File(Configuration.CurrentConfiguration.assetDir), index = null)
// Client settings
comment.lastEdit?.let { lastEdit ->
p {
style = "font-size:0.8em"
- +"Edited ${comment.numEdits} times, last edited at "
+ val nounSuffix = if (comment.numEdits != 1) "s" else ""
+ +"Edited ${comment.numEdits} time$nounSuffix, last edited at "
dateTime(lastEdit)
}
}
// Preview themes
const themeChoices = document.getElementsByName("theme");
for (const themeChoice of themeChoices) {
- const theme = themeChoice.value;
- themeChoice.addEventListener("click", () => {
- document.documentElement.setAttribute("data-theme", theme);
+ themeChoice.addEventListener("click", e => {
+ document.documentElement.setAttribute("data-theme", e.currentTarget.value);
});
}
});
window.addEventListener("load", function () {
// Image previewing
- const thumbView = document.getElementById("thumb-view");
- const thumbViewImg = thumbView.getElementsByTagName("img")[0];
- thumbView.addEventListener("click", e => {
+ document.getElementById("thumb-view").addEventListener("click", e => {
e.preventDefault();
- thumbView.classList.remove("visible");
- thumbViewImg.src = "";
+ e.currentTarget.classList.remove("visible");
+ e.currentTarget.getElementsByTagName("img")[0].src = "";
});
const thumbs = document.querySelectorAll("a.thumb");
thumb.onclick = e => {
e.preventDefault();
+ const thumbView = document.getElementById("thumb-view");
+ const thumbViewImg = thumbView.getElementsByTagName("img")[0];
thumbViewImg.src = e.currentTarget.getAttribute("href");
thumbView.classList.add("visible");
};
const canvases = document.getElementsByTagName("canvas");
for (const canvas of canvases) {
- const modelName = canvas.getAttribute("data-model");
- if (modelName == null || modelName === "") continue;
+ const canvasModelName = canvas.getAttribute("data-model");
+ if (canvasModelName == null || canvasModelName === "") continue;
- (async () => {
+ (async (modelName) => {
const modelAsync = loadObj(modelName);
const camera = new THREE.PerspectiveCamera(69, 1, 0.01, 1000.0);
light.position.set(0, 0, 0);
render();
- })().catch(reason => {
- console.error("Error rendering model " + modelName, reason);
+ })(canvasModelName).catch(reason => {
+ console.error("Error rendering model " + canvasModelName, reason);
});
}
});
let form = document.createElement("form");
form.style.display = "none";
- form.action = anchor.href;
- form.method = method;
+ form.action = e.currentTarget.href;
+ form.method = e.currentTarget.getAttribute("data-method");
- const csrfToken = anchor.getAttribute("data-csrf-token");
+ const csrfToken = e.currentTarget.getAttribute("data-csrf-token");
if (csrfToken != null) {
let csrfInput = document.createElement("input");
csrfInput.name = "csrf-token";
display: flex;
flex-wrap: nowrap;
align-items: stretch;
- justify-content: center;
+ justify-content: start;
flex-direction: column;
margin: 0;