From: Lanius Trolling Date: Fri, 24 Feb 2023 17:15:51 +0000 (-0500) Subject: Cache cacheable resources X-Git-Url: https://gitweb.starshipfights.net/?a=commitdiff_plain;h=aded7242a29adc51fd46ed4e09523fde6139cad4;p=factbooks Cache cacheable resources --- diff --git a/build.gradle.kts b/build.gradle.kts index f18047b..6630ff9 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,4 +1,27 @@ +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 + +buildscript { + repositories { + mavenCentral() + } + + dependencies { + classpath("com.nixxcode.jvmbrotli:jvmbrotli:0.2.0") + + // why does this need to be done MANUALLY?!?! + classpath("com.nixxcode.jvmbrotli:jvmbrotli-win32-x86-amd64:0.2.0") + classpath("com.nixxcode.jvmbrotli:jvmbrotli-darwin-x86-amd64:0.2.0") + classpath("com.nixxcode.jvmbrotli:jvmbrotli-linux-x86-amd64:0.2.0") + } +} plugins { java @@ -27,6 +50,7 @@ dependencies { implementation("io.ktor:ktor-server-call-id:2.2.3") implementation("io.ktor:ktor-server-call-logging:2.2.3") + implementation("io.ktor:ktor-server-conditional-headers:2.2.3") implementation("io.ktor:ktor-server-forwarded-header:2.2.3") implementation("io.ktor:ktor-server-html-builder:2.2.3") implementation("io.ktor:ktor-server-sessions-jvm:2.2.3") @@ -68,6 +92,45 @@ tasks.withType { } } +tasks.named("processResources") { + doLast { + 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) + gzipStream.write(bytes) + gzipStream.close() + + encoderParams?.let { encParams -> + val brResult = File("${file.absolutePath}.br").outputStream() + val brStream = BrotliOutputStream(brResult, encParams) + brStream.write(bytes) + brStream.close() + } + + println("Done compressing ${file.name}") + countDownLatch.countDown() + } + } + + countDownLatch.await() + } +} + application { mainClass.set("info.mechyrdia.Factbooks") } diff --git a/src/main/kotlin/info/mechyrdia/Factbooks.kt b/src/main/kotlin/info/mechyrdia/Factbooks.kt index 82ad2fd..b915734 100644 --- a/src/main/kotlin/info/mechyrdia/Factbooks.kt +++ b/src/main/kotlin/info/mechyrdia/Factbooks.kt @@ -6,6 +6,7 @@ import info.mechyrdia.auth.* import info.mechyrdia.data.* import info.mechyrdia.lore.* import io.ktor.http.* +import io.ktor.http.content.* import io.ktor.server.application.* import io.ktor.server.engine.* import io.ktor.server.html.* @@ -14,6 +15,7 @@ import io.ktor.server.netty.* import io.ktor.server.plugins.* import io.ktor.server.plugins.callid.* import io.ktor.server.plugins.callloging.* +import io.ktor.server.plugins.conditionalheaders.* import io.ktor.server.plugins.forwardedheaders.* import io.ktor.server.plugins.statuspages.* import io.ktor.server.request.* @@ -26,8 +28,15 @@ import io.ktor.util.* import org.slf4j.event.Level import java.io.File import java.io.IOException +import java.io.InputStream import java.util.concurrent.atomic.AtomicLong +object ResourceLoader { + fun getResource(resource: String): InputStream? = javaClass.getResourceAsStream(resource) + + val SHA256AttributeKey = AttributeKey("SHA256Hash") +} + lateinit var application: Application private set @@ -64,6 +73,14 @@ fun Application.factbooks() { } } + install(ConditionalHeaders) { + version { call, _ -> + call.attributes.getOrNull(ResourceLoader.SHA256AttributeKey)?.let { hash -> + listOf(EntityTagVersion(hash)) + }.orEmpty() + } + } + install(Sessions) { cookie("USER_SESSION", SessionStorageMongoDB) { identity { Id().id } @@ -115,7 +132,49 @@ fun Application.factbooks() { // Factbooks and assets static("/static") { - resources("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) } + } } get("/lore/{path...}") {