Advanced Spring: File Upload
Uploading and downloading files are common tasks for any web application. Spring offers support for these kinds of actions by providing MultipartFile
interface. Let's go over building a simple application which will enable the user to upload/download a file.
The application will be built with Spring boot and will leverage the following libraries:
- lombok
- H2 in-memory database
- Swagger
File entity
The file will be stored in database (you can also choose to store file on disk as well). As we wish to send some additional information (who triggered upload, additional description, etc.) when uploading the file, we need to have these as properties in the table. The File table would look like this:
@Entity
@Table(name = "file")
public class File {
@Id
@GeneratedValue(generator = "uuid")
@GenericGenerator(name = "uuid", strategy = "uuid2")
@Column(name = "id")
private String id;
@NotBlank
@Column(name = "username", nullable = false)
private String username;
@Column(name = "description")
private String description;
@NotBlank
@Column(name = "name", nullable = false)
private String name;
@NotBlank
@Column(name = "type", nullable = false)
private String type;
@JsonIgnore
@Column(name = "data", nullable = false)
@Lob
private byte[] data;
@Convert(converter = Jsr310JpaConverters.LocalDateTimeConverter.class)
@Column(name = "created_at")
private LocalDateTime createdAt = LocalDateTime.now();
}
We don't want to send content to the user when the user is requesting to see additional information related to this file, and that is why we annotated data
property with @JsonIgnore
.
File service
File service will expose following operations:
create
— this operation will create a row in the table with file name provided either in dto or (if the name is blank) by using file name of the uploaded filefindById
— this operation will find file by requested iddelete
— this operation will delete file by id
File resource (API endpoints)
Now as we have set the ground, we can deal with exposing endpoints for managing file upload/download. The application will provide the following operations:
POST /api/files
— will accept file and model which will be used to store additional information related to the file uploadedGET /api/files/{id}
— will provide additional info related to the fileGET /api/files/{id}/download
— will download the actual fileDELETE /api/files/{id}
— will delete the file and additional information
To store uploaded file we can use MultipartFile
variable. The MultipartFile
interface provides access to details about the uploaded file, including file name, file type, etc. We can retrieve this variable from the request parameter inside our controller’s method. Alongside the file we will send the model itself which will contain additional information:
@PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
@ApiOperation(value = "Saves File instance.", tags = {"File"})
@ApiResponses({
@ApiResponse(code = 201, message = "A File was successfully created.")
})
public ResponseEntity create(@RequestPart(name = "model") @Valid final FileCreationDto model, @RequestPart(name = "file") MultipartFile file) {
try {
model.setData(file.getBytes());
} catch (IOException e) {
return ResponseEntity.badRequest().build();
}
final File fileCreated = fileService.create(model, file.getOriginalFilename());
return ResponseEntity.status(HttpStatus.CREATED).body(fileCreated);
}
Tuning File Upload Limits
When configuring file uploads, it is often useful to set limits on the size of files. With Spring Boot, we can tune its auto-configured MultipartConfigElement
with some property settings. Let's set the following properties:
spring.servlet.multipart.max-file-size=2MB
spring.servlet.multipart.max-request-size=2MB
The multipart settings are constrained as follows:
spring.http.multipart.max-file-size
is set to 2MB, meaning total file size cannot exceed 2MB.spring.http.multipart.max-request-size
is set to 2MB, meaning total request size for amultipart/form-data
cannot exceed 2MB.
Testing
As we want to be confident that our app is working properly we need to write unit test to check upload functionality. First thing we need to do is to setup a file and we will use MockMultipartFile
for it. The next thing is to setup a model which will be passed alongside. Once we have this set in place we can write the logic which will check if uploading is working correctly:
@Test
public void create_withName_OK() throws Exception {
final FileCreationDto dto = FileCreationDto.builder()
.name("test.png")
.username("user@email.com")
.build();
final MockMultipartFile model = getFileModel(dto);
final MockMultipartFile file = createDummyFile();
final MvcResult result = mvc.perform(
MockMvcRequestBuilders.multipart("/api/files")
.file(model)
.file(file))
.andDo(print())
.andExpect(status().isCreated())
.andReturn();
final File parsed = jsonMapper.readValue(result.getResponse().getContentAsByteArray(), new TypeReference<>() {
});
Assertions.assertNotNull(parsed);
Assertions.assertNull(parsed.getData());
Assertions.assertEquals("test", parsed.getName());
Assertions.assertEquals("png", parsed.getType());
Assertions.assertNotNull(parsed.getId());
final Optional<File> fileOpt = fileRepository.findById(parsed.getId());
Assertions.assertTrue(fileOpt.isPresent());
Assertions.assertNotNull(fileOpt.get().getData());
}
And that's it, we now have the test in place, but how to test with the application running? We can use Postman like following:
Pay attention that model's content type is set to application/json
. Without it you won't be able to upload this file.
TL;DR
Jump straight to the source code.