§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 allows mixing of standard form data with file attachments. Please note: the HTTP method for the form has to be POST (not GET).
Start by writing an HTML form:
@form(action = routes.Application.upload, 'enctype -> "multipart/form-data") {
<input type="file" name="picture">
<p>
<input type="submit">
</p>
}
Now let’s define the upload
action:
public Result upload() {
MultipartFormData<File> body = request().body().asMultipartFormData();
FilePart<File> picture = body.getFile("picture");
if (picture != null) {
String fileName = picture.getFilename();
String contentType = picture.getContentType();
File file = picture.getFile();
return ok("File uploaded");
} else {
flash("error", "Missing file");
return badRequest();
}
}
§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.fromFile(file));
Http.RequestBuilder request = Helpers.fakeRequest().uri(routes.MyController.upload().url())
.method("POST")
.header(Http.HeaderNames.CONTENT_TYPE, "multipart/form-data")
.bodyMultipart(
Collections.singletonList(part),
play.libs.Files.singletonTemporaryFileCreator(),
app.getWrappedApplication().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() {
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().maxDiskBuffer(), 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 Sink<ByteString, CompletionStage<IOResult>> sink = FileIO.toFile(file);
return Accumulator.fromSink(
sink.mapMaterializedValue(completionStage ->
completionStage.thenApplyAsync(results ->
new Http.MultipartFormData.FilePart<>(partname,
filename,
contentType,
file))
));
};
}
/**
* Generates a temp file directly without going through TemporaryFile.
*/
private File generateTempFile() {
try {
final EnumSet<PosixFilePermission> attrs = EnumSet.of(OWNER_READ, OWNER_WRITE);
final FileAttribute<?> attr = PosixFilePermissions.asFileAttribute(attrs);
final Path path = Files.createTempFile("multipartBody", "tempFile", attr);
return path.toFile();
} catch (IOException e) {
throw new IllegalStateException(e);
}
}
}
Here, akka.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.