Spring Boot upload file to filesystem

January 04, 2021 No comments Spring Boot Upload Filesystem

1. Introduction

In this article, we will show how to upload a file with Spring Boot to a folder located in the filesystem. We will use Spring MultipartFile interface to handle multi-part requests to our Rest API.

2. Application Rest uploading API

Spring Boot application server will provide API with the following endpoints:

URL Method Action
/files GET Get list of uploaded files
/files POST Upload a single file
/files DELETE Delete all uploaded files
/files/{filename} GET Download specific file

Files will be uploaded into the specific static folder which will be configured in the application.properties.

3. Project structure

The following presents a Maven project structure of the upload file application:

├── pom.xml
├── src
│   ├── main
│   │   ├── java
│   │   │   └── com
│   │   │       └── frontbackend
│   │   │           └── springboot
│   │   │               ├── Application.java
│   │   │               ├── controller
│   │   │               │   └── FilesController.java
│   │   │               ├── exceptions
│   │   │               │   └── RestExceptionHandler.java
│   │   │               ├── model
│   │   │               │   ├── FileData.java
│   │   │               │   └── UploadResponseMessage.java
│   │   │               └── service
│   │   │                   └── FileService.java
│   │   └── resources
│   │       └── application.properties

In this structure the following elements can be distinguished:

  • pom.xml is a Maven configuration file with all necessary dependencies,
  • Application - the main Spring Boot class that starts the application server,
  • FilesController - class that handle HTTP requests,
  • FileService - service class responsible for saving uploaded file in the filesystem and retrieving uploaded files,
  • RestExceptionHandler - handles MaxUploadSizeExceededException when processing file (could be extended with other exceptions as well),
  • FileData - contains information about the uploaded file like name, size, location,
  • application.properties - Spring Boot application properties with a path for uploaded files.

4. Setup Spring Boot application

To create a Spring Boot project from scratch we could use Initializer or other development tools available in Eclipse or IntelliJ.

The pom.xml has the following structure:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.frontbackend.springboot</groupId>
    <artifactId>upload-file-into-filesystem</artifactId>
    <version>0.0.1-SNAPSHOT</version>

    <!-- Inherit defaults from Spring Boot -->
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.5.RELEASE</version>
    </parent>

    <!-- Add typical dependencies for a web application -->
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
    </dependencies>

    <!-- Package as an executable jar -->
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

We added spring-boot-starter-web dependency and used spring-boot-maven-plugin plugin to create a jar executable file in /target folder.

5. Project classes

5.1. Create FileService for managing files

The FileService will be responsible for saving and downloading files from the path provided in application.properties. We used a method annotated with @PostConstruct that will create an empty directory (if it does not exist already) at the start of the Spring Boot server.

package com.frontbackend.springboot.service;

import java.io.IOException;
import java.net.MalformedURLException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;

import javax.annotation.PostConstruct;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.Resource;
import org.springframework.core.io.UrlResource;
import org.springframework.stereotype.Service;
import org.springframework.util.FileSystemUtils;
import org.springframework.web.multipart.MultipartFile;

@Service
public class FileService {

    @Value("${upload.path}")
    private String uploadPath;

    @PostConstruct
    public void init() {
        try {
            Files.createDirectories(Paths.get(uploadPath));
        } catch (IOException e) {
            throw new RuntimeException("Could not create upload folder!");
        }
    }

    public void save(MultipartFile file) {
        try {
            Path root = Paths.get(uploadPath);
            if (!Files.exists(root)) {
                init();
            }
            Files.copy(file.getInputStream(), root.resolve(file.getOriginalFilename()));
        } catch (Exception e) {
            throw new RuntimeException("Could not store the file. Error: " + e.getMessage());
        }
    }

    public Resource load(String filename) {
        try {
            Path file = Paths.get(uploadPath)
                             .resolve(filename);
            Resource resource = new UrlResource(file.toUri());

            if (resource.exists() || resource.isReadable()) {
                return resource;
            } else {
                throw new RuntimeException("Could not read the file!");
            }
        } catch (MalformedURLException e) {
            throw new RuntimeException("Error: " + e.getMessage());
        }
    }

    public void deleteAll() {
        FileSystemUtils.deleteRecursively(Paths.get(uploadPath)
                                               .toFile());
    }

    public List<Path> loadAll() {
        try {
            Path root = Paths.get(uploadPath);
            if (Files.exists(root)) {
                return Files.walk(root, 1)
                            .filter(path -> !path.equals(root))
                            .collect(Collectors.toList());
            }

            return Collections.emptyList();
        } catch (IOException e) {
            throw new RuntimeException("Could not list the files!");
        }
    }
}

5.2. Create Model classes: FileData and UploadResponseMessage

The model layer will contain:

  • FileData - object with field like filename, url(to download file) and size,
  • UploadResponseMessage - will be used to return information about how uploading process ran.
package com.frontbackend.springboot.model;

public class FileData {

    private String filename;
    private String url;
    private Long size;

    public String getFilename() {
        return filename;
    }

    public void setFilename(String filename) {
        this.filename = filename;
    }

    public String getUrl() {
        return url;
    }

    public void setUrl(String url) {
        this.url = url;
    }

    public Long getSize() {
        return size;
    }

    public void setSize(Long size) {
        this.size = size;
    }
}

The UploadResponseMessage class will be used in FilesController and RestExceptionHandler.

package com.frontbackend.springboot.model;

public class UploadResponseMessage {

    private final String responseMessage;

    public UploadResponseMessage(String responseMessage) {
        this.responseMessage = responseMessage;
    }

    public String getResponseMessage() {
        return responseMessage;
    }
}

5.3. Create FilesController main Rest controller for handing uploading and downloading files

In the controller package, we created the FilesController class that will be responsible for handing all POST, GET, and DELETE requests to /files endpoint.

package com.frontbackend.springboot.controller;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.stream.Collectors;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.Resource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder;

import com.frontbackend.springboot.model.FileData;
import com.frontbackend.springboot.model.UploadResponseMessage;
import com.frontbackend.springboot.service.FileService;

@RestController
@RequestMapping("files")
public class FilesController {

    private final FileService fileService;

    @Autowired
    public FilesController(FileService fileService) {
        this.fileService = fileService;
    }

    @PostMapping
    public ResponseEntity<UploadResponseMessage> uploadFile(@RequestParam("file") MultipartFile file) {
        try {
            fileService.save(file);

            return ResponseEntity.status(HttpStatus.OK)
                                 .body(new UploadResponseMessage("Uploaded the file successfully: " + file.getOriginalFilename()));
        } catch (Exception e) {
            return ResponseEntity.status(HttpStatus.EXPECTATION_FAILED)
                                 .body(new UploadResponseMessage("Could not upload the file: " + file.getOriginalFilename() + "!"));
        }
    }

    @GetMapping
    public ResponseEntity<List<FileData>> getListFiles() {
        List<FileData> fileInfos = fileService.loadAll()
                                              .stream()
                                              .map(this::pathToFileData)
                                              .collect(Collectors.toList());

        return ResponseEntity.status(HttpStatus.OK)
                             .body(fileInfos);
    }

    @DeleteMapping
    public void delete() {
        fileService.deleteAll();
    }

    private FileData pathToFileData(Path path) {
        FileData fileData = new FileData();
        String filename = path.getFileName()
                              .toString();
        fileData.setFilename(filename);
        fileData.setUrl(MvcUriComponentsBuilder.fromMethodName(FilesController.class, "getFile", filename)
                                               .build()
                                               .toString());
        try {
            fileData.setSize(Files.size(path));
        } catch (IOException e) {
            e.printStackTrace();
            throw new RuntimeException("Error: " + e.getMessage());
        }

        return fileData;
    }

    @GetMapping("{filename:.+}")
    @ResponseBody
    public ResponseEntity<Resource> getFile(@PathVariable String filename) {
        Resource file = fileService.load(filename);
        return ResponseEntity.ok()
                             .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + file.getFilename() + "\"")
                             .body(file);
    }
}

This class has some interesting annotations:

  • @RestController - annotation is used to define a Rest controller,
  • @GetMapping, @PostMapping and @DeleteMapping - annotation is for handing HTTP GET, POST and DELETE requests with specific class methods:
HTTP Method Endpoint Method
POST /files uploadFile()
GET /files getListFiles()
GET /files/{filename:.+} getFile()
DELETE /files delete()

5.4. Handle upload file exceptions

The class annotated with @ControllerAdvice is responsible for handling specific exceptions that may occur during uploading/downloading files. RestExceptionHandler class beside the special annotation should also extend RestExceptionHandler. To handle the exception when uploading too large files we need to handle MaxUploadSizeExceededException like in the following:

package com.frontbackend.springboot.exceptions;

import com.frontbackend.springboot.model.UploadResponseMessage;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.multipart.MaxUploadSizeExceededException;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;

@ControllerAdvice
public class RestExceptionHandler extends ResponseEntityExceptionHandler {

    @ExceptionHandler(MaxUploadSizeExceededException.class)
    public ResponseEntity<UploadResponseMessage> handleMaxSizeException(MaxUploadSizeExceededException exc) {
        return ResponseEntity.status(HttpStatus.EXPECTATION_FAILED)
                             .body(new UploadResponseMessage("Unable to upload. File is too large!"));
    }
}

Note that we could also handle other exceptions that may occur during processing requests.

5.5. Create application.properties

To define the maximum file size that could be uploaded we need to add the following entries in the application.properties:

upload.path=/uploads

spring.servlet.multipart.max-file-size=700KB
spring.servlet.multipart.max-request-size=700KB

spring.servlet.multipart.max-file-size - this is the maximum file size for each request, spring.servlet.multipart.max-request-size - the maximum request size for a multipart/form-data.

Additionally, we added the upload.path custom parameter to define our root folder where all the files will be uploaded. This parameter is used in the FileService class.

6. Test Spring Boot upload file application

Find upload-file-into-filesystem-0.0.1-SNAPSHOT.jar in the target folder and Start Spring Boot application by running java -jar upload-file-into-filesystem-0.0.1-SNAPSHOT.jar.

You should see a message similar to this: Started Application in 1.625 seconds (JVM running for 1.98). That means the server started successfully.

To test our uploading/downloading API we used Postman.

6.1. Uploding file

Upload file

Uploading files returns information about the wrapped in UploadResponseMessage object.

6.2. Get file list

Get file list

6.3. Delete all files

Delete files

Deleting files will just return HTTP 200 status when successful.

6.4. Empty list after deleting all files

Get empty list

When the upload folder is empty we will get an empty collection on GET request.

7. Conclusion

In this tutorial, we presented how to create a simple Spring Boot application for uploading and downloading files to/from a static folder located somewhere in the filesystem. The application provides a Rest API without any front-end, that's why we tested how it works using Postman tool.

As usual code used in this tutorial is available on our GitHub repository.

{{ message }}

{{ 'Comments are closed.' | trans }}