4

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)

EECOLOR
  • 11,184
  • 3
  • 41
  • 75
KajMagnus
  • 11,308
  • 15
  • 79
  • 127

5 Answers5

3

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.

biesior
  • 55,576
  • 10
  • 125
  • 182
  • Hi biesior and thanks for your answer. The file that is being served is a HTML file for an admin page of my web app. That file is bundled with my application, and it's not possible to hardcode a path like `/tmp/file` to that file, because its location depends on where the Play app happens to be installeld. – KajMagnus Aug 14 '12 at 05:29
  • (I updated my question to clarify that the file is bundled with my app.) – KajMagnus Aug 14 '12 at 05:33
  • Thanks to the link to `Streaming HTTP responses`. For some reason, I didn't consider that page (probably becaus I didn't have in mind to ... stream huge video files). – KajMagnus Aug 14 '12 at 05:41
  • @KajMagnus - it's good idea to preview all docs to know other tricks and possibilities. – biesior Aug 14 '12 at 10:47
  • (Yes I agree — I did that, but perhaps 6 months ago. There are probably some things that I've forgotten) – KajMagnus Aug 14 '12 at 13:59
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
Community
  • 1
  • 1
KajMagnus
  • 11,308
  • 15
  • 79
  • 127
2

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.

jazmit
  • 5,170
  • 1
  • 29
  • 36
  • That looks like a cleaner way to do this than what I did (long ago, I'd guess `sendResource` wasn't available at that time). Here's the docs: https://www.playframework.com/documentation/2.6.20/api/java/play/mvc/StatusHeader.html#sendResource-java.lang.String- – KajMagnus Mar 11 '19 at 04:52
1

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"""")
EECOLOR
  • 11,184
  • 3
  • 41
  • 75
1

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)

}
ndeverge
  • 21,378
  • 4
  • 56
  • 85