Add ETag hashes of files
authorTheSaminator <TheSaminator@users.noreply.github.com>
Sat, 21 May 2022 17:18:45 +0000 (13:18 -0400)
committerTheSaminator <TheSaminator@users.noreply.github.com>
Sat, 21 May 2022 17:18:45 +0000 (13:18 -0400)
build.gradle.kts
src/jvmMain/kotlin/starshipfights/server.kt

index f211d1082e0696d3b81a33d0189929581252be22..265eda09653754f22f7d496b7710b1d331bce409 100644 (file)
@@ -1,7 +1,8 @@
 import com.nixxcode.jvmbrotli.common.BrotliLoader
 import com.nixxcode.jvmbrotli.enc.BrotliOutputStream
 import com.nixxcode.jvmbrotli.enc.Encoder
-
+import java.security.MessageDigest
+import java.util.*
 import java.util.concurrent.CountDownLatch
 import java.util.concurrent.Executors
 import java.util.zip.GZIPOutputStream
@@ -119,13 +120,21 @@ tasks.named<Copy>("jvmProcessResources") {
        
        doLast {
                val pool = Executors.newWorkStealingPool()
+               
                val encoderParams = if (BrotliLoader.isBrotliAvailable()) Encoder.Parameters().setQuality(8) else null
-               val resourceTree = fileTree(mapOf("dir" to outputs.files.asPath + "/static/", "exclude" to listOf("*.gz", "*.br")))
+               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)
index 7f096a3eaa3f79dcea2f6f0d5fdc4a9d4a9bf321..2383309f8327907b5c004a73f43e94050b187b24 100644 (file)
@@ -12,6 +12,7 @@ import io.ktor.response.*
 import io.ktor.routing.*
 import io.ktor.server.engine.*
 import io.ktor.server.netty.*
+import io.ktor.util.*
 import io.ktor.websocket.*
 import org.slf4j.event.Level
 import starshipfights.auth.AuthProvider
@@ -24,6 +25,8 @@ import java.util.concurrent.atomic.AtomicLong
 
 object ResourceLoader {
        fun getResource(resource: String): InputStream? = javaClass.getResourceAsStream(resource)
+       
+       val SHA256AttributeKey = AttributeKey<String>("SHA256Hash")
 }
 
 fun main() {
@@ -55,6 +58,14 @@ fun main() {
                        }
                }
                
+               install(ConditionalHeaders) {
+                       version { outgoingContent ->
+                               outgoingContent.getProperty(ResourceLoader.SHA256AttributeKey)?.let { hash ->
+                                       listOf(EntityTagVersion(hash))
+                               }.orEmpty()
+                       }
+               }
+               
                install(StatusPages) {
                        status(HttpStatusCode.NotFound) {
                                call.respondHtml(HttpStatusCode.NotFound, call.error404())
@@ -111,6 +122,10 @@ fun main() {
                                        val staticContentPath = call.parameters.getAll("static-content")?.joinToString("/") ?: return@get
                                        val contentPath = "/static/$staticContentPath"
                                        
+                                       val hashContentPath = "$contentPath.sha256"
+                                       val sha256Hash = ResourceLoader.getResource(hashContentPath)?.reader()?.readText()
+                                       val configureContent: OutgoingContent.() -> Unit = { setProperty(ResourceLoader.SHA256AttributeKey, sha256Hash) }
+                                       
                                        val brContentPath = "$contentPath.br"
                                        val gzContentPath = "$contentPath.gz"
                                        
@@ -125,7 +140,7 @@ fun main() {
                                                        
                                                        call.response.header(HttpHeaders.ContentEncoding, CompressedFileType.BROTLI.encoding)
                                                        
-                                                       call.respondBytes(brContent.readBytes(), contentType)
+                                                       call.respondBytes(brContent.readBytes(), contentType, configure = configureContent)
                                                        
                                                        return@get
                                                }
@@ -138,13 +153,13 @@ fun main() {
                                                        
                                                        call.response.header(HttpHeaders.ContentEncoding, CompressedFileType.GZIP.encoding)
                                                        
-                                                       call.respondBytes(gzContent.readBytes(), contentType)
+                                                       call.respondBytes(gzContent.readBytes(), contentType, configure = configureContent)
                                                        
                                                        return@get
                                                }
                                        }
                                        
-                                       ResourceLoader.getResource(contentPath)?.let { call.respondBytes(it.readBytes(), contentType) }
+                                       ResourceLoader.getResource(contentPath)?.let { call.respondBytes(it.readBytes(), contentType, configure = configureContent) }
                                }
                        }
                }