Almost every server application sooner or later requires some form of processing of user uploaded files. While files are small and in small quantities you can get away with saving them to the server’s RAM first. Problem starts when they become bigger than your RAM size. This may result in crashes and outages. The answer to this problem is writing your files directly to your long term storage.

For the purpose of this article saving files to the local storage of a machine hosting server is enough, but the presented solution may be easily modified to save files to the cloud.

PlayFramework comes with a great library called Akka. Akka Streams is a sub module of Akka that helps in processing of data streams – such as uploaded file. Code presented below will save streams of bytes – the file that’s being uploaded – directly to drive, without saving it as a whole file to RAM first.

Sink

In order to save files directly to drive we will utilize Akka Streams. First thing we will need is Sink.

val basePath = "/path/to/files/dir"
def localFileSink(filename: String): Sink[ByteString, Future[java.io.File]] = {
    val timestamp: Long = System.currentTimeMillis
    new java.io.File(s"$basePath/$timestamp/").mkdirs() // Creates directory with timestamp as name
    val path = s"$basePath/$timestamp/$filename" // Path string to file
    val file = new java.io.File(path) // Creates new file

    FileIO.toPath(file.toPath).mapMaterializedValue { t => // Creates Sink from file
        Future.successful(file)
    }
}

Method presented above will create and return a Sink object. Sink will save all bytes to a file with a given name. This will allow us to persist the original file name. To prevent name clashing with other uploaded files it puts the uploaded file in a directory with timestamp as its name.

Accumulator

Next step is to create a BodyParser that will accumulate all file bytes send in request into previously created Sink.

 def multipartFormDataToLocalFile: BodyParser[MultipartFormData[java.io.File]] = parse.using { request =>
    controllerComponents.parsers.multipartFormData { // Retrieves file data from request
        case FileInfo(partName, filename, contentType, dispositionType) =>
        val sink = fileService.localFileSink(filename).mapMaterializedValue(_.map { result => // Creates file sink for uploaded file
            FilePart(partName, filename, contentType, result)
        })

        Accumulator(sink) // Creates Accumulator which will transform Sing into Future object.
    }
}

 

Body parser

Next step is to create an endpoint that will handle all file uploads. To achieve this we will create Action that uses previously created BodyParser.

 def upload = Action(multipartFormDataToLocalFile).async { implicit request =>
    request.body.file("file") match { // Check body parser returned any file
        case Some(filePart) => // User uploaded some file
        fileService.doStuff(filePart).map { result =>
            Ok(Json.toJson(result))
        }
        case None =>
        Future.successful(NotFound) // No file was found in request.
    }
}

To access this endpoint we need to add it to the routes file with POST method.

 POST /file/upload controllers.FileController.upload 

 

Form

The last step is to create a multipart HTML form.

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

 

For more information on Akka Streams check out Akka Documentation