§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
(notGET
).
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>
}
Now define the upload
action:
import java.nio.file.Paths;
import play.libs.Files.TemporaryFile;
import play.mvc.Controller;
import play.mvc.Http;
import play.mvc.Result;
public class HomeController extends Controller {
public Result upload(Http.Request request) {
Http.MultipartFormData<TemporaryFile> body = request.body().asMultipartFormData();
Http.MultipartFormData.FilePart<TemporaryFile> picture = body.getFile("picture");
if (picture != null) {
String fileName = picture.getFilename();
long fileSize = picture.getFileSize();
String contentType = picture.getContentType();
TemporaryFile file = picture.getRef();
file.copyTo(Paths.get("/tmp/picture/destination.jpg"), true);
return ok("File uploaded");
} else {
return badRequest().flashing("error", "Missing file");
}
}
}
The getRef()
method gives you a reference to a TemporaryFile
. This is the default way Play handles file uploads.
And finally, add a POST
route:
POST / controllers.HomeController.upload(request: Request)
Note: An empty file will be treated just like no file was uploaded at all. The same applies if the
filename
header of amultipart/form-data
file upload part is empty - even when the file itself would not empty.
§Testing the file upload
You can also write an automated JUnit test to your upload
action:
@Test
public void testFileUpload() throws IOException {
File file = getFile();
Http.MultipartFormData.Part<Source<ByteString, ?>> part =
new Http.MultipartFormData.FilePart<>(
"picture",
"file.pdf",
"application/pdf",
FileIO.fromPath(file.toPath()),
Files.size(file.toPath()));
Http.RequestBuilder request =
Helpers.fakeRequest()
.uri(routes.MyController.upload().url())
.method("POST")
.bodyRaw(
Collections.singletonList(part),
play.libs.Files.singletonTemporaryFileCreator(),
app.asScala().materializer());
Result result = Helpers.route(app, request);
String content = Helpers.contentAsString(result);
assertThat(content, CoreMatchers.equalTo("File uploaded"));
}
Basically, we are creating a Http.MultipartFormData.FilePart
that is required by RequestBuilder
method bodyMultipart
. Besides that, everything else is just like unit testing controllers.
§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.
public Result upload(Http.Request request) {
File file = request.body().asRaw().asFile();
return ok("File uploaded");
}
§Writing a custom multipart file part body parser
The multipart upload specified by MultipartFormData
takes uploaded data from the request and puts into a TemporaryFile object. It is possible to override this behavior so that Multipart.FileInfo
information is streamed to another class, using the DelegatingMultipartFormDataBodyParser
class:
public static class MultipartFormDataWithFileBodyParser
extends BodyParser.DelegatingMultipartFormDataBodyParser<File> {
@Inject
public MultipartFormDataWithFileBodyParser(
Materializer materializer,
play.api.http.HttpConfiguration config,
HttpErrorHandler errorHandler) {
super(
materializer,
config.parser().maxMemoryBuffer(), // Small buffer used for parsing the body
config.parser().maxDiskBuffer(), // Maximum allowed length of the request body
config.parser().allowEmptyFiles(),
errorHandler);
}
/** Creates a file part handler that uses a custom accumulator. */
@Override
public Function<Multipart.FileInfo, Accumulator<ByteString, FilePart<File>>>
createFilePartHandler() {
return (Multipart.FileInfo fileInfo) -> {
final String filename = fileInfo.fileName();
final String partname = fileInfo.partName();
final String contentType = fileInfo.contentType().getOrElse(null);
final File file = generateTempFile();
final String dispositionType = fileInfo.dispositionType();
final Sink<ByteString, CompletionStage<IOResult>> sink = FileIO.toPath(file.toPath());
return Accumulator.fromSink(
sink.mapMaterializedValue(
completionStage ->
completionStage.thenApplyAsync(
results ->
new Http.MultipartFormData.FilePart<>(
partname,
filename,
contentType,
file,
results.getCount(),
dispositionType))));
};
}
/** Generates a temp file directly without going through TemporaryFile. */
private File generateTempFile() {
try {
final Path path = Files.createTempFile("multipartBody", "tempFile");
return path.toFile();
} catch (IOException e) {
throw new IllegalStateException(e);
}
}
}
Here, pekko.stream.javadsl.FileIO
class is used to create a sink that sends the ByteString
from the Accumulator into a java.io.File
object, rather than a TemporaryFile object.
Using a custom file part handler also means that behavior can be injected, so a running count of uploaded bytes can be sent elsewhere in the system.
§Cleaning up temporary files
Uploading files uses a TemporaryFile
API which relies on storing files in a temporary filesystem, accessible through the getRef()
method. 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.