Some ETag refactoring
This commit is contained in:
@@ -1,17 +1,15 @@
|
||||
package controllers
|
||||
|
||||
import javax.inject.*
|
||||
import play.api.*
|
||||
import play.api.mvc.*
|
||||
import authentication.{AuthenticatedAction, AuthenticatedRequest}
|
||||
import caching.{Etagable, CheckFresh}
|
||||
import _root_.controllers.authentication.{AuthenticatedAction, AuthenticatedRequest}
|
||||
import _root_.controllers.caching.{CheckFresh, DigestEtagable}
|
||||
import models.User
|
||||
import play.api.libs.json.Json
|
||||
import play.api.mvc.*
|
||||
|
||||
import java.nio.ByteBuffer
|
||||
import java.io.DataOutputStream
|
||||
import java.nio.charset.StandardCharsets.UTF_8
|
||||
import java.security.MessageDigest
|
||||
import java.util.Base64
|
||||
import javax.inject.*
|
||||
|
||||
/**
|
||||
* This controller creates an `Action` to handle HTTP requests to the
|
||||
@@ -37,21 +35,15 @@ class HomeController @Inject()(val controllerComponents: ControllerComponents,
|
||||
val user = request.user
|
||||
checkFresh(user) {
|
||||
// TODO: put the max-age somewhere into config
|
||||
Ok(Json.toJson(user)).withHeaders("Etag" -> user.etag, "Cache-Control" -> "private, max-age=15, must-revalidate")
|
||||
Ok(Json.toJson(user)).withHeaders("Cache-Control" -> "private, max-age=15, must-revalidate")
|
||||
}
|
||||
}
|
||||
|
||||
private given Etagable[models.User] with {
|
||||
private val base64 = Base64.getUrlEncoder
|
||||
|
||||
extension (a: User)
|
||||
override def etag: String = {
|
||||
val md = MessageDigest.getInstance("SHA-1")
|
||||
md.update(ByteBuffer.allocate(8).putLong(a.id.getOrElse(0)))
|
||||
md.update(a.gid.getBytes(UTF_8))
|
||||
md.update(a.username.getBytes(UTF_8))
|
||||
|
||||
base64.encodeToString(md.digest())
|
||||
}
|
||||
private given DigestEtagable[models.User] with {
|
||||
override protected def writeDigestData(a: User, s: DataOutputStream): Unit = {
|
||||
s.writeLong(a.id.getOrElse(0))
|
||||
s.write(a.gid.getBytes(UTF_8))
|
||||
s.write(a.username.getBytes(UTF_8))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,24 +3,55 @@ package controllers.caching
|
||||
import play.api.mvc.Results.NotModified
|
||||
import play.api.mvc.{Request, Result}
|
||||
|
||||
import java.io.{ByteArrayOutputStream, DataOutputStream, OutputStream}
|
||||
import java.security.{DigestOutputStream, MessageDigest}
|
||||
import java.util.Base64
|
||||
import scala.concurrent.{ExecutionContext, Future}
|
||||
import scala.util.Using
|
||||
|
||||
trait Etagable[A]:
|
||||
extension (a: A) def etag: String
|
||||
|
||||
|
||||
trait DigestEtagable[A] extends Etagable[A]:
|
||||
private val base64 = Base64.getUrlEncoder.withoutPadding
|
||||
|
||||
protected def writeDigestData(a: A, s: DataOutputStream): Unit
|
||||
|
||||
extension (a: A) {
|
||||
override def etag: String = {
|
||||
val md = MessageDigest.getInstance("SHA-1")
|
||||
Using(DataOutputStream(DigestOutputStream(OutputStream.nullOutputStream(), md))) { dos =>
|
||||
writeDigestData(a, dos)
|
||||
dos.flush()
|
||||
}
|
||||
|
||||
base64.encodeToString(md.digest())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
trait CheckFresh {
|
||||
protected def checkFreshAsync[A, E: Etagable](entity: E)(onStale: => Future[Result])(using request: Request[A], ec: ExecutionContext): Future[Result] = {
|
||||
compareEtag(request, entity).fold(onStale)(
|
||||
etag => Future.successful(NotModified.withHeaders("ETag" -> etag))
|
||||
compareEtag(request, entity).fold(
|
||||
entityEtag => onStale.map(_.withHeaders("etag" -> entityEtag)),
|
||||
requestEtag => Future.successful(notModified(requestEtag))
|
||||
)
|
||||
}
|
||||
|
||||
protected def checkFresh[A, E: Etagable](entity: E)(onStale: => Result)(using request: Request[A]): Result = {
|
||||
compareEtag(request, entity).fold(onStale)(
|
||||
etag => NotModified.withHeaders("ETag" -> etag)
|
||||
compareEtag(request, entity).fold(
|
||||
entityEtag => onStale.withHeaders("etag" -> entityEtag),
|
||||
requestEtag => notModified(requestEtag)
|
||||
)
|
||||
}
|
||||
|
||||
private def compareEtag[A, E: Etagable](request: Request[A], entity: E) = request.headers.get("If-None-Match").filter(e => e == entity.etag)
|
||||
private def compareEtag[A, E: Etagable](request: Request[A], entity: E) = {
|
||||
val entityEtag = entity.etag
|
||||
request.headers.get("If-None-Match").filter(e => e == entityEtag) match {
|
||||
case Some(etag) => Right(etag)
|
||||
case None => Left(entityEtag)
|
||||
}
|
||||
}
|
||||
private def notModified(etag: String) = NotModified.withHeaders("etag" -> etag)
|
||||
}
|
||||
Reference in New Issue
Block a user