handle etags in WebMapTileServer

This commit is contained in:
Adam 2022-05-14 12:51:27 +02:00
parent f0590b9df4
commit 7c77083c89

View file

@ -1,10 +1,16 @@
package dev.adamko.kafkatorio.processor.tileserver
import dev.adamko.kafkatorio.processor.config.ApplicationProperties
import java.security.MessageDigest
import kotlin.io.path.absolutePathString
import org.http4k.core.Filter
import org.http4k.core.HttpHandler
import org.http4k.core.MemoryBody
import org.http4k.core.Method.GET
import org.http4k.core.Request
import org.http4k.core.Response
import org.http4k.core.Status
import org.http4k.core.then
import org.http4k.filter.CachingFilters
import org.http4k.routing.ResourceLoader
import org.http4k.routing.RoutingHttpHandler
import org.http4k.routing.bind
@ -12,15 +18,114 @@ import org.http4k.routing.routes
import org.http4k.routing.static
internal class WebMapTileServer(
appProps: ApplicationProperties = ApplicationProperties()
private val appProps: ApplicationProperties = ApplicationProperties()
) {
private val tilesLoader = ResourceLoader.Directory(appProps.webmapTileDir.absolutePathString())
private val filters = listOf(
ETagChecker()(),
).reduce(Filter::then)
private val routes: RoutingHttpHandler = routes(
"/tiles" bind CachingFilters.Response.NoCache()
.then(static(ResourceLoader.Directory(appProps.webmapTileDir.absolutePathString())))
"/tiles" bind GET to
static(tilesLoader).withFilter(filters)
)
fun build(): HttpHandler {
return routes
}
inner class ETagChecker
// : CoroutineScope
{
// private val tagState: MutableStateFlow<MutableMap<String, String>> =
// MutableStateFlow(mutableMapOf())
//
// override val coroutineContext: CoroutineContext =
// CoroutineName("ETagChecker") +
// Dispatchers.Unconfined +
// KafkatorioTopology.rootJob
// init {
// launch {
// appProps.webmapTileDir
// .fileEventsFlow()
// .filterIsInstance<KWatchEvent.PathEvent>()
// .filter { it.path.isRegularFile() }
//// .filter { it.path.extension == "png" }
// .runningFold(mutableMapOf<String, String>()) { tags, event ->
//// println("[WebMapTileServer] handling file update ${event.path}")
//
// val relativePath = event.path
// .relativeTo(appProps.webmapTileDir)
// .invariantSeparatorsPathString
//
// when (event) {
// is KWatchEvent.Deleted -> tags.remove(relativePath)
// is KWatchEvent.Created,
// is KWatchEvent.Modified -> {
// val md5 = event.path.md5()
// if (md5 != null) {
// tags[relativePath] = md5
// } else {
// println("null md5 for ${event.path}")
// }
// }
// }
//
// tags
// }.cancellable()
// .collect(tagState)
// }
// }
private val Request.etagKey: String get() = uri.path.removePrefix("/tiles/")
operator fun invoke() = Filter { next ->
{ request ->
val requestedETag = request.header("If-None-Match")
var response = next(request)
val responseBytes = response.body.stream.readAllBytes()
response.body.stream.close()
val currentETag = responseBytes.md5()
response = response.body(MemoryBody(responseBytes))
when {
currentETag == null -> {
// println("[${request.etagKey}] currentETag is null (requested: $requestedETag), executing request")
response
.header("Cache-Control", "no-cache")
}
requestedETag == currentETag -> {
println("[${request.etagKey}] currentETag == requested, returning 302 $requestedETag")
Response(Status.NOT_MODIFIED)
.header("Cache-Control", "no-cache")
}
else -> {
println("[${request.etagKey}] currentETag != requested, executing request and adding currentETag:$currentETag (requested:$requestedETag)")
response
.header("ETag", currentETag)
.header("Cache-Control", "public, max-age=1, stale-while-revalidate=60, stale-if-error=300")
}
}
}
}
private fun ByteArray.md5(): String? {
return if (isEmpty()) {
null
} else {
MessageDigest.getInstance("MD5")
.digest(this)
.joinToString("") { "%02x".format(it) }
}
}
}
}