Spring Boot 2 + Angular 11 + Upload File With Progress Bar Example

March 29, 2021 No comments Spring Boot Angular upload file progress

1. Introduction

In this tutorial, we are going to learn how to create Angular 11 application for uploading files to Spring Boot server with progress bar.

2. Architecture

The architecture contains two main layers:

Spring boot upload single file

The Angular web application will be responsible for interactions with the end-user. The backend side will handle uploaded files and store them into the filesystem. The relation between the frontend and backend will be RESTful.

3. Spring Boot application

The Spring Boot application provides a single REST service that will handle multipart requests.

3.1. Project structure

The backend Maven project will have the following structure:

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

The application contains the following classes:

  • Application - Spring Boot application class that starts the server,
  • FilesController - Spring controller used for handing HTTP requests,
  • FileUploadException - File upload exception class,
  • RestExceptionHandler - class mapping exceptions into HTTP responses,
  • UploadResponseMessage - Java representation of JSON that provides information about uploading status,
  • FileService - service that saves uploaded files in the filesystem,
  • application.properties - Spring Boot application configuration file.

3.2. Maven configuration file

The backend project will be build using Maven.

The pom.xml file 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>angular11-spring-boot2-upload-file-with-progress</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.4.1</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>

3.2. Rest Controller

The FilesController class is responsible for handling uploaded files.

package com.frontbackend.springboot.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
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.RestController;
import org.springframework.web.multipart.MultipartFile;

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) {
        fileService.save(file);

        return ResponseEntity.status(HttpStatus.OK)
                             .body(new UploadResponseMessage("Uploaded the file successfully: " + file.getOriginalFilename()));
    }
}

The uploadFile(...) method is associated with a POST request to files endpoint. It uses the MultipartFile object that handles reference to the uploaded file in a temporary folder. This file will be removed at the end of the request processing.

3.3. Service class

The FileService was introduced to separate business logic from the REST controller. The service is checking if a file with the same uploaded name exists in the folder. If yes the exception is thrown:

package com.frontbackend.springboot.service;

import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

import com.frontbackend.springboot.exceptions.FileUploadException;

@Service
public class FileService {

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

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

3.4. Spring Boot application class

In the Spring Boot application class we configured CORS and upload size limits:

package com.frontbackend.springboot;

import javax.servlet.MultipartConfigElement;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.servlet.MultipartConfigFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.util.unit.DataSize;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@SpringBootApplication
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }

    @Bean
    MultipartConfigElement multipartConfigElement() {
        MultipartConfigFactory factory = new MultipartConfigFactory();
        factory.setMaxFileSize(DataSize.ofMegabytes(5));
        factory.setMaxRequestSize(DataSize.ofMegabytes(5));
        return factory.createMultipartConfig();
    }

    @Bean
    WebMvcConfigurer corsConfigurer() {
        return new WebMvcConfigurer() {
            @Override
            public void addCorsMappings(CorsRegistry registry) {
                registry.addMapping("/files")
                        .allowedOrigins("http://localhost:4200");
            }
        };
    }
}

3.5. Application configuration file

The application.properties contains the path for uploaded files:


upload.path=/path/to/upload/folder

4. Angular application

To generate the basic Angular project we will use Angular CLI:

The following command will create project angular in the current folder:

ng new angular
? Would you like to add Angular routing? No
? Which stylesheet format would you like to use? CSS

And, below commands used to create a service and component:

ng g s services/upload-file
ng g c components/upload-file

4.1. Web project structure

The Angular project contains the following files:

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

In frontend side we could distinguished:

  • app.module - for import necessary libraries and components,
  • upload-file.service - using Angular HTTP Clienct to upload files to the backend,
  • upload-files.component - the main website that contains upload form,
  • app.component - main application component,
  • index.html - index HTML website.

4.2. Upload file service

The UploadFileService will use the Angular HTTPClient library to send requests to the Spring Boot server:

import { Injectable } from '@angular/core';
import { HttpClient, HttpEvent, HttpRequest } from '@angular/common/http';
import { Observable } from 'rxjs';
import { environment } from '../../environments/environment';

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

  constructor(private http: HttpClient) {
  }

  upload(file: File): Observable<HttpEvent<any>> {
    const formData: FormData = new FormData();

    formData.append('file', file);

    const req = new HttpRequest('POST', `${environment.baseUrl}/files`, formData, {
      reportProgress: true,
      responseType: 'json'
    });

    return this.http.request(req);
  }
}

Note we set the reportProgress: true, flag to show the progress of every HTTP request call.

4.3. Environment variables

The angular.json file is a place for build instructions and configurations.

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

Every environment-specific file is represented by the fileReplacements section. If the production configuration is used Angular will replace src/environments/environment.ts with src/environments/environment.prod.ts, so the production settings will be used.

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

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

if we use ng build --configuration=production command to build the project, Angular CLI will perform the file replacement and src/environments/environment.ts is overwritten by src/environments/environment.prod.ts.

Without this special configuration attribute the regular file will be used environments/environment.ts that contains:

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

This environment file is imported in UploadFileService to get the baseUrl where we will be uploading files: ${environment.baseUrl}/files.

4.4. Upload file component

The UploadFileComponent will be responsible for interactions with user:

import { Component, OnInit } from '@angular/core';
import { UploadFileService } from '../../services/upload-file.service';
import { HttpEventType, HttpResponse } from '@angular/common/http';

@Component({
  selector: 'app-upload-file',
  templateUrl: './upload-file.component.html',
  styles: []
})
export class UploadFileComponent implements OnInit {

  selectedFiles?: FileList;
  currentFile?: File;
  progress = 0;
  message = '';
  errorMsg = '';

  constructor(private uploadService: UploadFileService) {
  }

  ngOnInit(): void {
  }

  selectFile(event: any): void {
    this.selectedFiles = event.target.files;
  }

  upload(): void {
    this.errorMsg = '';
    this.message = '';
    this.progress = 0;

    if (this.selectedFiles) {
      const file: File | null = this.selectedFiles.item(0);

      if (file) {
        this.currentFile = file;

        this.uploadService.upload(this.currentFile).subscribe(
          (event: any) => {
            if (event.type === HttpEventType.UploadProgress) {
              this.progress = Math.round(100 * event.loaded / event.total);

            } else if (event instanceof HttpResponse) {
              this.message = event.body.responseMessage;
            }
          },
          (err: any) => {
            console.log(err);

            if (err.error && err.error.responseMessage) {
              this.errorMsg = err.error.responseMessage;
            } else {
              this.errorMsg = 'Error occurred while uploading a file!';
            }

            this.currentFile = undefined;
          });
      }

      this.selectedFiles = undefined;
    }
  }
}

The selectFile is called then the user selects the file for upload.

selectFile(event: any): void {
    this.selectedFiles = event.target.files;
}

Clicking 'Upload file' will run the upload(): function that sends the file to the backend side.

The upload progress is holded in progress variable:

if (event.type === HttpEventType.UploadProgress) {
    this.progress = Math.round(100 * event.loaded / event.total);
}

The HTML component file contains:

  • input with type="file" for select files for upload,
  • button - upload file button,
  • Bootstrap progress bar for presenting upload progress status.
<div class="row">
    <div class="col-8">
        <label class="btn btn-default p-0">
            <input type="file" (change)="selectFile($event)"/>
        </label>
    </div>

    <div class="col-4 text-right">
        <button class="btn btn-success btn-sm" [disabled]="!selectedFiles" (click)="upload()">
            Upload file
        </button>
    </div>
</div>
<div class="row">
    <div class="col">
        <div *ngIf="currentFile" class="progress my-3">
            <div
                    class="progress-bar progress-bar-info progress-bar-striped"
                    role="progressbar"
                    attr.aria-valuenow="{{ progress }}"
                    aria-valuemin="0"
                    aria-valuemax="100"
                    [ngStyle]="{ width: progress + '%' }">
                {{ progress }}%
            </div>
        </div>

        <div *ngIf="message" class="alert alert-success" role="alert">{{ message }}</div>
        <div *ngIf="errorMsg" class="alert alert-danger" role="alert">{{ errorMsg }}</div>
    </div>
</div>

4.5. Application module

In Application module we import HTTP client:

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';

import { AppComponent } from './app.component';
import { UploadFileComponent } from './components/upload-file/upload-file.component';
import { HttpClientModule } from '@angular/common/http';

@NgModule({
  declarations: [
    AppComponent,
    UploadFileComponent
  ],
  imports: [
    BrowserModule,
    HttpClientModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

5. Demo

The application presents the following functionality:

Spring boot angular upload file with progress

5.1. Slow down internet connection for local testing

Normally when testing locally, uploading will be very fast. You won't even notice changing progress bar. Fortunately, there is a way to slow down this process using Chrom Developer Tools.

  1. use the F12 key to open Developer Tools in Chrome browser,
  2. navigate to the Network tab,
  3. change the Online option to Fast 3G or Slow 3G.
  4. done, that's it.

Change internet speed chrome

6. Conclusion

In this tutorial, we presented how to build an Angular application for uploading a file with the progress bar.

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

{{ message }}

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