#HOWTO: Up- and download files with React and Spring Boot

Today I want to share a simple approach for up- and downloading files with JavaScript (ES6), React and a Spring Boot backend. This example can be used for any common content type like jpg, pdf, txt, html, png etc. and is not limited to a specific content type. I’ll use the latest version of React (16.4.2 at the time of writing) and Spring Boot (2.0.4). I won’t use any additional npm module for up- and downloading files at the client side and just rely on the Fetch API and plain JavaScript. The backend will store the files in a H2 in-memory database and will randomly return a file.

The final application will look like the following:

Implementing the frontend

For a quick setup, I used Facebook’s create-react-app CLI to generate a basic React project.  The following package.json is created per default:

{
  "name": "frontend",
  "version": "0.1.0",
  "private": true,
  "dependencies": {
    "react": "^16.4.2",
    "react-dom": "^16.4.2",
    "react-scripts": "1.1.5"
  },
  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test --env=jsdom",
    "eject": "react-scripts eject"
  }
}

All the code will reside in the default App.js file. The component has the following state:

state = {
    file: '',
    error: '',
    msg: ''
}

The file attribute is used for storing the uploaded file, the error attribute for displaying possible error messages and msg is used for displaying information messages.

The upload related JSX looks like the following:

<div className="App-intro">
  <h3>Upload a file</h3>
  <h4 style={{color: 'red'}}>{this.state.error}</h4>
  <h4 style={{color: 'green'}}>{this.state.msg}</h4>
  <input onChange={this.onFileChange} type="file"></input>
  <button onClick={this.uploadFile}>Upload</button>   
 </div>

Any change for the input field will result in setting the uploaded file in the component’s state:

onFileChange = (event) => {
  this.setState({
    file: event.target.files[0]
  });
}

For uploading a file, I’ll check if the user uploaded a file and if the file size exceeds 2MB as our backend will only allow files with a maximum size of 2MB. If one of the two validations fail, I’ll set the error attribute and leave the upload process.

uploadFile = (event) => {
    event.preventDefault();
    this.setState({error: '', msg: ''});

    if(!this.state.file) {
      this.setState({error: 'Please upload a file.'})
      return;
    }

    if(this.state.file.size >= 2000000) {
      this.setState({error: 'File size exceeds limit of 2MB.'})
      return;
    }

    let data = new FormData();
    data.append('file', this.state.file);
    data.append('name', this.state.file.name);

    fetch('http://localhost:8080/api/files', {
      method: 'POST',
      body: data
    }).then(response => {
      this.setState({error: '', msg: 'Sucessfully uploaded file'});
    }).catch(err => {
      this.setState({error: err});
    });

}

For a file with less than 2MB of size, I’ll wrap the file in a FromData object, so that the request content type will be multipart/form-data. This is required as otherwise, our backend won’t be able to deserialize the request correctly. For sending the request I’ll use the Fetch API and display a message in case of a successful upload.

Downloading a random file just requires a button and a function:

<div className="App-intro">
 <h3>Download a random file</h3>
 <button onClick={this.downloadRandomImage}>Download</button>
</div>

The downloadRandomImage() function looks like the following:

downloadRandomImage = () => {
  fetch('http://localhost:8080/api/files')
    .then(response => {
      const filename =  response.headers.get('Content-Disposition').split('filename=')[1];
      response.blob().then(blob => {
        let url = window.URL.createObjectURL(blob);
        let a = document.createElement('a');
        a.href = url;
        a.download = filename;
        a.click();
    });
 });
}

The response from the backend contains the byte array representation of the random file. To read this byte array in full, I make use of the blob() function of the response. This function returns a promise that resolves with a Blob. To extract the correct filename, I access the response header Content-Disposition and store the filename in a local variable. As the browsers currently don’t support a standard way of downloading files from an AJAX request, I create an object URL for the incoming Blob and force the browser to download the image with a hidden <a> HTML element.

That’s everything for the frontend. You can find the full App.js code on GitHub.

 Implementing the backend

The backend is a simple Spring Boot application which contains the dependencies for H2, JPA, and Web. 

The JPA entity for storing the uploaded files looks like the following:

@Entity
public class FileEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String fileName;

    private String contentType;

    @Lob
    private byte[] data;
    
    // constructors, getters & setters

}

To upload and download files, I provide one URL: /api/files which is accessible via HTTP GET for downloading a random file and HTTP POST for uploading a file. To overcome any CORS related issues and to access the Content-Disposition header in the frontend application, the REST controller needs the following annotations:

@RestController
@RequestMapping("/api/files")
@CrossOrigin(value = {"*"}, exposedHeaders = {"Content-Disposition"})
public class FileBoundary {

   // code ...
}

To limit the maximum size of the uploaded file, I added the following configuration to the application.properties file.

spring.h2.console.enabled=true
spring.servlet.multipart.max-file-size=2MB
spring.servlet.multipart.max-request-size=10MB

Uploading a file is straightforward as I receive an object of the type MultipartFile as @RequestParam("file") and therefore have access to the filename, the size etc.:

@PostMapping
public ResponseEntity<Void> uploadNewFile(@NotNull @RequestParam("file") MultipartFile multipartFile) throws IOException {

  FileEntity fileEntity = new FileEntity(multipartFile.getOriginalFilename(),
    multipartFile.getContentType(),
    multipartFile.getBytes());

  fileEntityRepository.save(fileEntity);

  URI location = ServletUriComponentsBuilder.fromCurrentRequest().build().toUri();
  return ResponseEntity.created(location).build();
}

For downloading a file, I added some logic to select a random file from the database. The endpoint will return a ResponseEntity of type byte[]. The most important part here is setting the correct HttpHeaders for the browser. As I store the content type of the file in the database, I can add the right Content-Type header of the response. In addition, I set the Content-Disposition header to inform the browser about the attached file and its name.

@GetMapping
public ResponseEntity<byte[]> getRandomFile() {

  long amountOfFiles = fileEntityRepository.count();
  Long randomPrimaryKey;

  if (amountOfFiles == 0) {
     return ResponseEntity.ok(new byte[0]);
   } else if (amountOfFiles == 1) {
     randomPrimaryKey = 1L;
   } else {
     randomPrimaryKey = ThreadLocalRandom.current().nextLong(1, amountOfFiles + 1);
   }

   FileEntity fileEntity = fileEntityRepository.findById(randomPrimaryKey).get();

   HttpHeaders header = new HttpHeaders();
   header.setContentType(MediaType.valueOf(fileEntity.getContentType()));
   header.setContentLength(fileEntity.getData().length);
   header.set("Content-Disposition", "attachment; filename=" + fileEntity.getFileName());

   return new ResponseEntity<>(fileEntity.getData(), header, HttpStatus.OK);
}

The code logic for retrieving a random file is just optional, as I used this to show you that this code works for every common content type. Storing the files in a database might also not be the best solution for production.

You can find the code on GitHub. I created a docker-compose.yml file for a convenient deployment on your machine. Detailed instructions can be found in the README.md file in the repository.

Keep up-/downloading files,

Phil.

2 Comments

  1. Anonymous October 17, 2018 at 11:54 pm

    When I try to access Content-Disposition in react, I get null.

    1. rieckpil October 22, 2018 at 11:18 am

      which React/Spring versions are you using? Have you looked at my open GitHub repository for the full codebase?

Leave a comment

Your email address will not be published. Required fields are marked *