Documentation

You are viewing the documentation for the 3.0.3 release. The latest stable release series is 3.0.x.

§Handling file upload

§Uploading files in a form using multipart/form-data

The standard way to upload files in a web application is to use a form with a special multipart/form-data encoding, which lets you mix standard form data with file attachment data.

Note: The HTTP method used to submit the form must be POST (not GET).

Start by writing an HTML form:

@helper.form(action = routes.HomeController.upload(), Symbol("enctype") -> "multipart/form-data") {
    
    <input type="file" name="picture">
    
    <p>
        <input type="submit">
    </p>
    
}

Add a CSRF token to the form, unless you have the CSRF filter disabled. The CSRF filter checks the multi-part form in the order the fields are listed, so put the CSRF token before the file input field. This improves efficiency and avoids a token-not-found error if the file size exceeds play.filters.csrf.body.bufferSize.

Now define the upload action using a multipartFormData body parser:

def upload: Action[MultipartFormData[Files.TemporaryFile]] = Action(parse.multipartFormData) { request =>
  request.body
    .file("picture")
    .map { picture =>
      // only get the last part of the filename
      // otherwise someone can send a path like ../../home/foo/bar.txt to write to other files on the system
      val filename    = Paths.get(picture.filename).getFileName
      val fileSize    = picture.fileSize
      val contentType = picture.contentType

      picture.ref.copyTo(Paths.get(s"/tmp/picture/$filename"), replace = true)
      Ok("File uploaded")
    }
    .getOrElse {
      Redirect(routes.HomeController.index()).flashing("error" -> "Missing file")
    }
}

The ref attribute gives you a reference to a TemporaryFile. This is the default way the multipartFormData parser handles file uploads.

Note: As always, you can also use the anyContent body parser and retrieve it as request.body.asMultipartFormData.

At last, add a POST router

POST  /          democontrollers.HomeController.upload()

Note: An empty file will be treated just like no file was uploaded at all. The same applies if the filename header of a multipart/form-data file upload part is empty - even when the file itself would not empty.

§Direct file upload

Another way to send files to the server is to use Ajax to upload files asynchronously from a form. In this case, the request body will not be encoded as multipart/form-data, but will just contain the plain file contents.

In this case we can just use a body parser to store the request body content in a file. For this example, let’s use the temporaryFile body parser:

def upload: Action[Files.TemporaryFile] = Action(parse.temporaryFile) { request =>
  request.body.moveTo(Paths.get("/tmp/picture/uploaded"), replace = true)
  Ok("File uploaded")
}

§Writing your own body parser

If you want to handle the file upload directly without buffering it in a temporary file, you can just write your own BodyParser. In this case, you will receive chunks of data that you are free to push anywhere you want.

If you want to use multipart/form-data encoding, you can still use the default multipartFormData parser by providing a FilePartHandler[A] and using a different Sink to accumulate data. For example, you can use a FilePartHandler[File] rather than a TemporaryFile by specifying an Accumulator(fileSink):

type FilePartHandler[A] = FileInfo => Accumulator[ByteString, FilePart[A]]

def handleFilePartAsFile: FilePartHandler[File] = {
  case FileInfo(partName, filename, contentType, dispositionType) =>
    val perms       = java.util.EnumSet.of(OWNER_READ, OWNER_WRITE)
    val attr        = PosixFilePermissions.asFileAttribute(perms)
    val path        = JFiles.createTempFile("multipartBody", "tempFile", attr)
    val file        = path.toFile
    val fileSink    = FileIO.toPath(path)
    val accumulator = Accumulator(fileSink)
    accumulator.map {
      case IOResult(count, status) =>
        FilePart(partName, filename, contentType, file, count, dispositionType)
    }(ec)
}

def uploadCustom: Action[MultipartFormData[File]] = Action(parse.multipartFormData(handleFilePartAsFile)) {
  request =>
    val fileOption = request.body.file("name").map {
      case FilePart(key, filename, contentType, file, fileSize, dispositionType, _) =>
        file.toPath
    }

    Ok(s"File uploaded: $fileOption")
}

§Cleaning up temporary files

Uploading files uses a TemporaryFile API which relies on storing files in a temporary filesystem, accessible through the ref attribute. All TemporaryFile references come from a TemporaryFileCreator trait, and the implementation can be swapped out as necessary, and there’s now an atomicMoveWithFallback method that uses StandardCopyOption.ATOMIC_MOVE if available.

Uploading files is an inherently dangerous operation, because unbounded file upload can cause the filesystem to fill up – as such, the idea behind TemporaryFile is that it’s only in scope at completion and should be moved out of the temporary file system as soon as possible. Any temporary files that are not moved are deleted.

However, under certain conditions, garbage collection does not occur in a timely fashion. As such, there’s also a play.api.libs.Files.TemporaryFileReaper that can be enabled to delete temporary files on a scheduled basis using the Pekko scheduler, distinct from the garbage collection method.

The reaper is disabled by default, and is enabled through configuration of application.conf:

play.temporaryFile {
  reaper {
    enabled = true
    initialDelay = "5 minutes"
    interval = "30 seconds"
    olderThan = "30 minutes"
  }
}

The above configuration will delete files that are more than 30 minutes old, using the “olderThan” property. It will start the reaper five minutes after the application starts, and will check the filesystem every 30 seconds thereafter. The reaper is not aware of any existing file uploads, so protracted file uploads may run into the reaper if the system is not carefully configured.

Next: Accessing an SQL database