Advanced Spring: File Upload
By: Milan Brankovic
Tags: Education, Spring, Software Development, Spring Boot

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 file

— 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 file findById — this operation will find file by requested id

— this operation will find file by requested id delete — 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 uploaded

— will accept file and model which will be used to store additional information related to the file uploaded GET /api/files/{id} — will provide additional info related to the file

— will provide additional info related to the file GET /api/files/{id}/download — will download the actual file

— will download the actual file DELETE /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.

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 a multipart/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:

Testing file upload with postman

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.