Skip to content

Instantly share code, notes, and snippets.

@mrubin
Last active April 16, 2018 06:55
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save mrubin/0b6bd0e26bd866681faaa683165f2ec4 to your computer and use it in GitHub Desktop.
Save mrubin/0b6bd0e26bd866681faaa683165f2ec4 to your computer and use it in GitHub Desktop.
StaticAssetsController
package controllers.staticAssets
import play.api.http.HeaderNames
import play.api.mvc.{Action, Request, Result}
import scala.concurrent.Future
case class AddNoCacheHeaders[A](action: Action[A]) extends Action[A] with HeaderNames {
def apply(request: Request[A]): Future[Result] = {
import scala.concurrent.ExecutionContext.Implicits.global
// some special files (i.e. index.html) cannot use "cache busting" hash characters in their filename as other React files do. We need to treat them specially and set no-cache headers on them, so that when we push a new build browsers get it without browser caching getting in the way
if (request.path == "/" || request.path == "/index.html" || !PhysicalResource.exists(request.path.dropWhile(_ == '/'))) {
action(request).map(_.withHeaders(
(CACHE_CONTROL -> "no-cache, no-store, must-revalidate"),
(PRAGMA -> "no-cache"),
(EXPIRES -> "0")
))
} else if (request.path == "/service-worker.js") {
action(request).map(_.withHeaders(
(CACHE_CONTROL -> "no-cache, no-store, must-revalidate"),
(PRAGMA -> "no-cache"),
(EXPIRES -> "0")
))
} else {
action(request)
}
}
override def parser = action.parser
override def executionContext = action.executionContext
}
package controllers.staticAssets
import java.util.concurrent.locks.ReentrantReadWriteLock
// A cache of what "physical" resources we know of, shared by both `FrontEndServingController` and `AddNoCacheHeaders`, an Action which wraps `FrontEndServingController`s `serve` via Action composition in order to possibly add no-cache headers to index.html
object PhysicalResource {
private val lock = new ReentrantReadWriteLock()
private var cache = Set[String]()
def exists(path: String) = {
lock.readLock().lock()
try {
cache.contains(path)
} finally {
lock.readLock().unlock()
}
}
def setExists(path: String) = {
lock.writeLock().lock()
try {
cache += path
} finally {
lock.writeLock().unlock()
}
}
}
# Technically this is not necessary because `frontEndPath("")` will fail to find an empty path in its set and will serve up index anyway. This is a slight optimization to be more explicit and just serve up index directly.
GET / controllers.staticAssets.StaticAssetsController.index
GET /$path<.*> controllers.staticAssets.StaticAssetsController.frontEndPath(path)
package controllers.staticAssets
import java.io.File
import javax.inject.Inject
import controllers.Assets
import play.Environment
import play.api.Logger
import play.api.mvc.{Action, AnyContent}
import play.mvc.Controller
class StaticAssetsController @Inject()(private val assets: Assets,
environment: Environment)
extends Controller {
private val logger = Logger(this.getClass.getName)
private val publicDirectory = "/public"
private val indexFile = "index.html"
// this is used to check for the existence of 'physical' files in dev mode
private val physicalPublicDirectory = s".${File.separator}public${File.separator}"
// this is used to check for the existence of files in a jar in prod mode
private val streamPublicDirectory = "public/"
// https://stackoverflow.com/a/38816414/1011953
private val fileExists: (String) => Boolean =
if (environment.isProd) prodFileExists else devFileExists
def index: Action[AnyContent] = AddNoCacheHeaders {
serve(indexFile)
}
def frontEndPath(path: String): Action[AnyContent] = AddNoCacheHeaders {
serve(path)
}
private def serve(path: String) = {
if (fileExists(path)) {
logger.debug(s"Serving physical resource: '$path'")
assets.at(publicDirectory, path, aggressiveCaching = true) // use "aggressive caching" because React should generate filenames with a hash for each build
} else {
// serve up the contents of index.html without rewriting the url in the browser, so that React routes can work
logger.debug(s"Serving virtual resource: '$path'")
assets.at(publicDirectory, indexFile, aggressiveCaching = false) // don't use "aggressive caching" in case we want to update our index.html sometimes (since that filename can't change)
}
}
private def devFileExists(path: String): Boolean = {
var exists = PhysicalResource.exists(path)
if (!exists) {
val file = new File(physicalPublicDirectory + path)
exists = file.exists
if (exists)
PhysicalResource.setExists(path)
}
exists
}
private def prodFileExists(path: String): Boolean = {
var exists = PhysicalResource.exists(path)
if (!exists) {
// https://stackoverflow.com/a/43756053/1011953
// https://stackoverflow.com/a/12133643/1011953
val streamPath = streamPublicDirectory + path
val stream = getClass.getClassLoader.getResourceAsStream(streamPath)
exists = stream != null
if (exists)
PhysicalResource.setExists(path)
}
exists
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment