Spring Boot 2 + Angular 11 + Download File

April 24, 2021 No comments Spring Boot Angular Download File

1. Introduction

In this tutorial, we are going to learn how to create a simple Angular application for downloading files using the Spring Boot on the backend side.

2. Architecture

Let's take a look at the architecture of our sample application.

Angular spring boot download file architecture

In the presented solution we have a simple division into the frontend and backend layer. Angular based frontend will be responsible for requesting the backend and downloading files in browser. The communication will be handled using HTTP. In order to integrate both sides of the communication, we need an HTTP client on the frontend side and a REST controller on the backend.

3. Spring Boot application

The Spring Boot application provides an HTTP API with endpoints used for listing files and downloading them.

3.1. Project structure

The structure of the backend project will be as follows:

├── java
│   └── com
│       └── frontbackend
│           └── springboot
│               ├── Application.java
│               ├── controller
│               │   └── FileController.java
│               ├── model
│               │   └── FileData.java
│               └── service
│                   └── FileService.java
└── resources
    └── application.properties

Here we can distinguished several classes:

  • Application - main Spring Boot class responsible for starting web container,
  • FileController - Spring Rest Controller class used for handing HTTP requests,
  • FileData - simple POJO object used for presenting a list of files (this object will be converted into JSON),
  • FileService - Service class resopnsible for managing files,
  • application.properties - Spring Boot configuration file.

3.2. Spring Controller class

The Spring REST controller will handle the following HTTP methods:

URL HTTP Method Action
/api/files GET Get list files
/api/files/{filename} GET Download a file

The Spring Rest Controller class has the following structure:

package com.frontbackend.springboot.controller;

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

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.Resource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;

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

@CrossOrigin(origins = "http://localhost:4200")
@RestController
@RequestMapping("api/files")
public class FileController {

    private final FileService fileService;

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

    @GetMapping
    public List<FileData> list() {
        return fileService.list();
    }

    @GetMapping("{filename:.+}")
    @ResponseBody
    public ResponseEntity<Resource> downloadFile(@PathVariable String filename) throws IOException {
        Resource file = fileService.download(filename);
        Path path = file.getFile()
                        .toPath();

        return ResponseEntity.ok()
                             .header(HttpHeaders.CONTENT_TYPE, Files.probeContentType(path))
                             .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + file.getFilename() + "\"")
                             .body(file);
    }
}

The controller class is annotated with:

  • @RestController - this annotation marks class as a REST controller,
  • @RequestMapping - provide base url for all endpoints mapped in this controller class,
  • @CrossOrigin - allows cross-domain requests

In order to download a file from the browser without any issue we need to set two headers:

  • Content-Type - this header holds the type of the file (Files.probeContentType(path) method was used to determine the file type),
  • Content-Disposition - this header indicates that the file should be downloaded in the browser right away at the moment when the user clicks the link (the 'Save as' dialog will be opened on the browser).

The following fragment of code is used to set up headers:

ResponseEntity.ok()
              .header(HttpHeaders.CONTENT_TYPE, Files.probeContentType(path))
              .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + file.getFilename() + "\"")
              .body(file);

3.3. Service class

The FileService class is responsible for managing files on the filesystem. It is always a good practice to separate business logic from the REST controller. That's why that class was introduced.

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 org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.Resource;
import org.springframework.core.io.UrlResource;
import org.springframework.stereotype.Service;

import com.frontbackend.springboot.model.FileData;

@Service
public class FileService {

    @Value("${files.path}")
    private String filesPath;

    public Resource download(String filename) {
        try {
            Path file = Paths.get(filesPath)
                             .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 List<FileData> list() {
        try {
            Path root = Paths.get(filesPath);

            if (Files.exists(root)) {
                return Files.walk(root, 1)
                            .filter(path -> !path.equals(root))
                            .filter(path -> path.toFile()
                                                .isFile())
                            .collect(Collectors.toList())
                            .stream()
                            .map(this::pathToFileData)
                            .collect(Collectors.toList());
            }

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

    private FileData pathToFileData(Path path) {
        FileData fileData = new FileData();
        String filename = path.getFileName()
                              .toString();
        fileData.setFilename(filename);

        try {
            fileData.setContentType(Files.probeContentType(path));
            fileData.setSize(Files.size(path));
        } catch (IOException e) {
            throw new RuntimeException("Error: " + e.getMessage());
        }

        return fileData;
    }
}

In this implementation, we used UrlResource to return the content of the file from the filesystem, but you can use any other method to read a file in Java and simply return the byte array.

For presenting a list of files we used one of the method to list files in directory - Files.walk(...).

3.4. Basic data model object

The POJO object is used to return data related to the file such as:

  • filename - the name of the file,
  • size - file size,
  • contentType - file content type.
package com.frontbackend.springboot.model;

public class FileData {

    private String contentType;
    private String filename;
    private Long size;

    public String getContentType() {
        return contentType;
    }

    public void setContentType(String contentType) {
        this.contentType = contentType;
    }

    public String getFilename() {
        return filename;
    }

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

    public Long getSize() {
        return size;
    }

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

3.5. Server configuration file

The application.properties contains a single property with a path to the folder where we will search files. This property is used in FileService class.

files.path=/home/path/to/uploads

4. Angular application

The angular application was created using ng commands:

In order to create a basic Angular project structure we used:

> ng new angular --minimal

To create a file component:

> ng g c components/files

To create a service:

> ng g s services/download

4.1. Web project structure

The generated Angular web project has the following structure:

├── app
│   ├── app.component.html
│   ├── app.component.ts
│   ├── app.module.ts
│   ├── app-routing.module.ts
│   ├── components
│   │   └── files
│   │       ├── files.component.html
│   │       └── files.component.ts
│   ├── model
│   │   └── file-data.ts
│   └── services
│       └── download.service.ts
├── assets
├── environments
│   ├── environment.prod.ts
│   └── environment.ts
├── favicon.ico
├── index.html
├── main.ts
├── polyfills.ts
└── styles.css

4.2. Environment variables

All environment-related configuration is located in the angular.json file:

"production": {
              "fileReplacements": [
                {
                  "replace": "src/environments/environment.ts",
                  "with": "src/environments/environment.prod.ts"
                }
              ],

Each environment-specific file is represented by the fileReplacements section.

For example if we use ng build --configuration=production command to build or run the project Angular CLI will replace the environment file src/environments/environment.ts with src/environments/environment.prod.ts.

In our case the environment.prod.ts file looks like this:

export const environment = {
  production: true,
  baseUrl: 'https://production.url'
};

Without --configuration=production parameter the regular file will be used environments/environment.ts that contains:

export const environment = {
  production: false,
  baseUrl: 'http://localhost:8080'
};

This environment file is used in DownloadService to get the baseUrl: ${environment.baseUrl}/files.

4.3. Angular files component

The main responsibility of FilesComponent is presenting the list of files and downloading a selected file using the file-saver library.

import { Component, OnInit } from '@angular/core';
import { DownloadService } from '../../services/download.service';
import { FileData } from '../../model/file-data';
import { saveAs } from 'file-saver';

@Component({
  selector: 'app-files',
  templateUrl: './files.component.html'
})
export class FilesComponent implements OnInit {

  fileList?: FileData[];

  constructor(private downloadService: DownloadService) {
  }

  ngOnInit(): void {
    this.getFileList();
  }

  getFileList(): void {
    this.downloadService.list().subscribe(result => {
      this.fileList = result;
    });
  }

  downloadFile(fileData: FileData): void {
    this.downloadService
      .download(fileData.filename)
      .subscribe(blob => saveAs(blob, fileData.filename));
  }
}

The HTML attached with this component has the following structure:

<div class="list-group">
    <a *ngFor="let fileData of fileList" href="#" class="list-group-item list-group-item-action" (click)="downloadFile(fileData)">{{fileData.filename}}</a>
</div>

4.4. Angular download service

The DownloadService is used Angular HttpClient to communicate with the Spring Boot application server. The service retrieves a list of files and downloads the content of the selected file.

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import { environment } from '../../environments/environment';
import { FileData } from '../model/file-data';

@Injectable({
  providedIn: 'root'
})
export class DownloadService {

  constructor(private http: HttpClient) {
  }

  download(file: string | undefined): Observable<Blob> {
    return this.http.get(`${environment.baseUrl}/files/${file}`, {
      responseType: 'blob'
    });
  }

  list(): Observable<FileData[]> {
    return this.http.get<FileData[]>(`${environment.baseUrl}/files`);
  }
}

4.5. File data model

The FileData object is a Typescript representation of the Java FileData class:

export class FileData {
  filename?: string;
  contentType?: string;
  size?: number;
}

Note we just used a filename from this object, but you could easily use other fields, maybe this is a good exercise?

5. Demo

The application shows the following functionality:

Angular spring boot download file

6. Conclusion

In this tutorial, we presented an Angular application with a Spring Boot server for downloading files from the filesystem.

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

{{ message }}

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