Serving static /public/ file from Play 2 Scala con

2020-07-17 14:00发布

问题:

What is the preferred method to serve a static file from a Play Framework 2 Scala controller?

The file is bundled with my application, so it's not possible to hardcode a filesystem absolute /path/to/the/file, because its location depends on where the Play app happens to be installeld.

The file is placed in the public/ dir, but not in app/assets/, because I don't want Play to compile it.

(The reason I don't simply add a route to that file, is that one needs to login before accessing that file, otherwise it's of no use.)

Here is what I've done so far, but this breaks on my production server.

object Application ...

  def viewAdminPage = Action ... {
    ... authorization ...
    val adminPageFile = Play.getFile("/public/admin/index.html")
    Ok.sendFile(adminPageFile, inline = true)
  }

And in my routes file, I have this line:

GET /-/admin/ controllers.Application.viewAdminPage

The problem is that on my production server, this error happens:
FileNotFoundException: app1/public/admin/index.html

Is there some other method, rather than Play.getFile and OK.sendFile, to specify which file to serve? That never breaks in production?

(My app is installed in /some-dir/app1/ and I start it from /some-dir/ (without app1/) — perhaps everything would work if I instead started the app from /some-dir/app1/. But I'd like to know how one "should" do, to serve a static file from inside a controller? So that everything always works also on the production servers, regardless of from where I happen to start the application)

回答1:

Play has a built-in method for this:

  Ok.sendResource("public/admin/file.html", classLoader)

You can obtain a classloader from an injected Environment with environment.classLoader or from this.getClass.getClassLoader.



回答2:

Check Streaming HTTP responses doc

def index = Action {
  Ok.sendFile(
    content = new java.io.File("/tmp/fileToServe.pdf"),
    fileName = _ => "termsOfService.pdf"
  )
}

You can add some random string to the fileName (individual for each logged user) to avoid sharing download link between authenticated and non-authinticated users and also make advanced download stats.



回答3:

I did this: (but see the Update below!)

val fileUrl: java.net.URL = this.getClass().getResource("/public/admin/file.html")
val file = new java.io.File(adminPageUrl.toURI())
Ok.sendFile(file, inline = true)

(this is the controller, which is (and must be) located in the same package as the file that's being served.)

Here is a related question: open resource with relative path in java

Update

Accessing the file via an URI causes an error: IllegalArgumentException: URI is not hierarchical, if the file is then located inside a JAR, which is the case if you run Play like so: play stage and then target/start.

So instead I read the file as a stream, converted it to a String, and sent that string as HTML:

val adminPageFileString: String = {
  // In prod builds, the file is embedded in a JAR, and accessing it via
  // an URI causes an IllegalArgumentException: "URI is not hierarchical".
  // So use a stream instead.
  val adminPageStream: java.io.InputStream =
    this.getClass().getResourceAsStream("/public/admin/index.html")
  io.Source.fromInputStream(adminPageStream).mkString("")
}

...

return Ok(adminPageFileString) as HTML


回答4:

The manual approach for this is the following:

val url = Play.resource(file)
url.map { url =>
  val stream = url.openStream()
  val length = stream.available 
  val resourceData = Enumerator.fromStream(stream)

  val headers = Map(
        CONTENT_LENGTH -> length.toString,
        CONTENT_TYPE -> MimeTypes.forFileName(file).getOrElse(BINARY),
        CONTENT_DISPOSITION -> s"""attachment; filename="$name"""")

  SimpleResult(
    header = ResponseHeader(OK, headers), 
    body = resourceData)

The equivalent using the assets controller is this:

val name = "someName.ext"

val response = Assets.at("/public", name)(request)
response
  .withHeaders(CONTENT_DISPOSITION -> s"""attachment; filename="$name"""")


回答5:

Another variant, without using a String, but by streaming the file content:

def myStaticRessource() = Action { implicit request =>

  val contentStream = this.getClass.getResourceAsStream("/public/content.html")

  Ok.chunked(Enumerator.fromStream(contentStream)).as(HTML)

}