Spring Boot 2 + Angular 11 + Download File

February 14, 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 from the Spring Boot server.

2. Architecture

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

Angular spring boot download file architecture

In the presented solution we have a simple division into the frontend and backend side. Angular based frontend will be responsible for requesting the backend and downloading files. 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 used for presenting a list of files and downloading them.

3.1. Project structure

The structure of the backend application 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 objects:

  • Application - main Spring Boot class used 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,
  • FileService - Service class resopnsible for managing files,
  • application.properties - Spring Boot configuration file.

3.2. Spring Controller class

The REST controller will handle the following actions:

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 - mark class as a REST controller,
  • @RequestMapping - provide base url for all endpoints used in this controller,
  • @CrossOrigin - allows cross domain requests.

The endpoint used for downloading files sets two headers:

  • Content-Type - provide the content type of the file (Files.probeContentType(path) is used to determine the type of the file),
  • Content-Disposition - this header indicating that the file should be downloaded (the 'Save as' dialog should be opened).

The following fragment of code is used to download a file:

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

3.3. Service class

The service class is used to manage files on the filesystem. This class was introduced to separate logic from the REST controller.

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 and simply return the byte array.

To present a file list 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 interesting information about the file to the frontend.

The FileData class contains:

  • 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 entry with a path to the folder in which we will search files. This property is used in FileService class.

files.path=/home/marcinw/own/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. 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.3. 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.4. 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 }}