Angular-Java-Spring BootNo Comments

Angular 9 + Spring Boot REST Api – File Upload with Progress Bar

We’ll build a frontend on Angular 9.x to perform file upload with progress bar and also perform file download from server with backend built on Spring Boot.

git Download the code from GitHub

Here’s what the end result will look like:-

Single File Upload:-



Multiple File Upload:-



File Download:-



Now lets get started building the application.

The Backend:-


Folder Structure:-



Setup application.properties as per your needs:-

# Enable multipart uploads
spring.servlet.multipart.enabled=true
# Threshold after which files are written to disk.
spring.servlet.multipart.file-size-threshold=2KB
# Max file size.
spring.servlet.multipart.max-file-size=200MB
# Max Request Size
spring.servlet.multipart.max-request-size=215MB
# All files uploaded through the REST API will be stored in this directory
file.upload-dir=G:/Your/download/directory/path


Now lets create FileUploadDownloadService.java which will perform the upload and download operation.

package io.javaninja.ajeet.springfileupload.service;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.env.Environment;
import org.springframework.core.io.Resource;
import org.springframework.core.io.UrlResource;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import org.springframework.web.multipart.MultipartFile;

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.nio.file.StandardCopyOption;
import java.util.List;
import java.util.stream.Collectors;

@Service
public class FileUploadDownloadService {

    @Autowired
    private Environment env;

    public String uploadFile(MultipartFile file) {
        // Normalize file name
        String fileName = StringUtils.cleanPath(file.getOriginalFilename());
        try {
            Path fileStorageLocation = Paths.get(env.getProperty("file.upload-dir"))
                    .toAbsolutePath().normalize();
            Path targetLocation = fileStorageLocation.resolve(fileName);
            Files.copy(file.getInputStream(), targetLocation, 
                            StandardCopyOption.REPLACE_EXISTING);
        } catch (Exception ex) {
            System.out.println("Exception:" + ex);
        }
        return fileName;
    }

    public List<String> getFiles() throws IOException {

        return Files.walk(Paths.get(env.getProperty("file.upload-dir")))
                .filter(Files::isRegularFile)
                .map(file -> file.getFileName().toString())
                .collect(Collectors.toList());
    }

    public Resource loadFileAsResource(String fileName) throws MalformedURLException {
        Path fileStorageLocation = Paths.get(env.getProperty("file.upload-dir"))
                .toAbsolutePath().normalize();
        Path filePath = fileStorageLocation.resolve(fileName).normalize();
        Resource resource = new UrlResource(filePath.toUri());
        if (resource.exists()) {
            return resource;
        }
        return null;
    }
}


UploadFileResponse will be the response sent from backend to angular frontend after file upload is complete.

package io.javaninja.ajeet.springfileupload.model;

public class UploadFileResponse {
    // You may add other file properties like size, file type, etc.
    private String fileName;

    public UploadFileResponse(String fileName) {
        this.fileName = fileName;
    }

    public String getFileName() {
        return fileName;
    }

    public void setFileName(String fileName) {
        this.fileName = fileName;
    }
}


Now lets focus on FileUploadDownloadController.java which will expose REST endpoints to the frontend. Make sure that you put CrossOrigin annotation with the path as the url on which you frontend runs. In my case, angular frontend is running on http://localhost:4200, so have used the same or else the request from frontend will not reach the backend server as CORS requests are blocked by default.

package io.javaninja.ajeet.springfileupload.controller;

import io.javaninja.ajeet.springfileupload.model.UploadFileResponse;
import io.javaninja.ajeet.springfileupload.service.FileUploadDownloadService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.Resource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.net.MalformedURLException;
import java.util.List;

@CrossOrigin(origins = "http://localhost:4200")
@RestController
public class FileUploadDownloadController {

    private static final Logger logger = LoggerFactory.getLogger(FileUploadDownloadController.class);

    @Autowired
    private FileUploadDownloadService fileUploadDownloadService;

    @PostMapping(value = "/uploadFile", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
    public UploadFileResponse uploadFile(@RequestParam("file") MultipartFile file) {
        String fileName = fileUploadDownloadService.uploadFile(file);

        return new UploadFileResponse(fileName);
    }

    // Displays the list of uploaded files.
    @GetMapping("/getFiles")
    public List<String> getFiles() throws IOException {
        return fileUploadDownloadService.getFiles();
    }

    // Downloads a file using filename.
    @GetMapping("/downloadFile/{fileName}")
    public ResponseEntity<Resource> downloadFile(@PathVariable String fileName, HttpServletRequest request) throws MalformedURLException {
        Resource resource = fileUploadDownloadService.loadFileAsResource(fileName);
        // Try to determine file's content type
        String contentType = null;
        try {
            contentType = request.getServletContext().getMimeType(resource.getFile().getAbsolutePath());
        } catch (IOException ex) {
            logger.info("Could not determine file type.");
        }
        // Fallback to the default content type if type could not be determined
        if (contentType == null) {
            contentType = "application/octet-stream";
        }
        return ResponseEntity.ok()
                .contentType(MediaType.parseMediaType(contentType))
                .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + resource.getFilename() + "\"")
                .body(resource);
    }

}


Now let’s build the frontend with Angular. The frontend is built with Angular CLI with Angular v9.x.


The Frontend:-


Folder Structure:-



Lets make the frontend form to uplaod files. Material Design is used for UI. Material Design doesn’t support input type=”file”, so a workaround is used to add a traditional input type=”file” and hiding it, then triggering the file choosing window with (click) event.


<div class="container" fxLayout="row" fxLayout.xs="column" fxLayoutWrap fxLayoutGap="0.5%" fxLayoutAlign="center">
  
<div fxFlex="25%">
    Progress: {{loaded}}%
    <mat-progress-bar mode="determinate" value="{{loaded}}"></mat-progress-bar>
    <mat-card>
      <mat-card-header>
        <mat-card-title>Single File Upload</mat-card-title>
        <mat-card-subtitle>
          <input #fileInput type="file" (change)="selectFile($event)" style="display:none;" ngModel/>
          <mat-form-field class="example-full-width" (click)="fileInput.click()">
            <input matInput placeholder="Choose File" value="{{fileInput.value.substr(fileInput.value.lastIndexOf('\\')+1)}}">
            <mat-icon matSuffix>folder_open</mat-icon>
          </mat-form-field>
          &nbsp;
          <button mat-raised-button color="primary" (click)="upload()">Upload</button>
        </mat-card-subtitle>
      </mat-card-header>
    </mat-card>
  </div>

</div>


Bonus: Upload multiple files and show the progress bar for each file seamlessly.


<div class="container" fxLayout="row" fxLayout.xs="column" fxLayoutWrap fxLayoutGap="0.5%" fxLayoutAlign="center">
  
<div fxFlex="25%">
    <mat-card>
      <mat-card-header>
        <mat-card-title>Multiple File Upload</mat-card-title>
        <mat-card-subtitle>
          <span *ngIf="selectedFiles !== undefined">{{selectedFiles.length}} file(s) selected</span>
        </mat-card-subtitle>
        <mat-card-subtitle>
          <input #fileInput type="file" (change)="selectFile($event)" style="display:none;" ngModel multiple/>
          <mat-form-field class="example-full-width" (click)="fileInput.click()">
            <input matInput placeholder="Choose Multiple Files" value="{{fileInput.value.substr(fileInput.value.lastIndexOf('\\')+1)}}">
            <mat-icon matSuffix>folder_open</mat-icon>
          </mat-form-field>
          &nbsp;
          <button mat-raised-button color="primary" (click)="upload()">Upload</button>
        </mat-card-subtitle>
      </mat-card-header>
    </mat-card>
  </div>

  
<div fxFlex="25%">
    <mat-card>
      <mat-card-header>
        <mat-card-title *ngIf="!showProgress">No Upload in Progress!</mat-card-title>
        <mat-card-title *ngIf="showProgress">Files</mat-card-title>
        <mat-card-subtitle *ngFor="let uploadedFile of uploadedFiles">
          <mat-icon>insert_drive_file</mat-icon>
          {{uploadedFile.name}} - {{uploadedFile.progress}}%
          <mat-progress-bar mode="determinate" value="{{uploadedFile.progress}}"></mat-progress-bar>
        </mat-card-subtitle>
      </mat-card-header>
    </mat-card>
  </div>

</div>


Let’s check the component which handles the single file upload. We are using tap a pipe operator to find out how much file has been uploaded without interfering with the stream.

import {Component, OnInit} from '@angular/core';
import {HttpClient, HttpEventType, HttpResponse} from '@angular/common/http';
import {tap} from 'rxjs/operators';
import {FileService} from '../file.service';
import {MatSnackBar} from '@angular/material/snack-bar';

@Component({
  selector: 'app-single-file-upload',
  templateUrl: './single-file-upload.component.html',
  styleUrls: ['./single-file-upload.component.css'],
})

export class SingleFileUploadComponent implements OnInit {
  loaded = 0;
  selectedFiles: FileList;
  currentFileUpload: File;

  ngOnInit(): void {
  }

  constructor(private http: HttpClient, private fileService: FileService, private snackBar: MatSnackBar) {
  }

  // Selected file is stored into selectedFiles.
  selectFile(event) {
    this.selectedFiles = event.target.files;
  }

  // Uploads the file to backend server.
  upload() {
    this.currentFileUpload = this.selectedFiles.item(0);
    this.fileService.uploadSingleFile(this.currentFileUpload)
      .pipe(tap(event => {
        if (event.type === HttpEventType.UploadProgress) {
          this.loaded = Math.round(100 * event.loaded / event.total);
        }
      })).subscribe(event => {
      if (event instanceof HttpResponse) {
        this.snackBar.open('File uploaded successfully!', 'Close', {
          duration: 3000
        });
        this.fileService.fetchFileNames();
      }
    });
  }
}


Bonus: Handling multiple file uploads. The only difference in this component is to manage an array of FileDetails module. Add files in the array and update their progress. So that the array can be accessed from the frontend to show progress bars of all the files.

import {Component, OnInit} from '@angular/core';
import {tap} from 'rxjs/operators';
import {HttpClient, HttpEventType, HttpResponse} from '@angular/common/http';
import {FileService} from '../file.service';
import {MatSnackBar} from '@angular/material/snack-bar';
import {FileDetails} from '../file.model';

@Component({
  selector: 'app-multi-file-upload',
  templateUrl: './multi-file-upload.component.html',
  styleUrls: ['./multi-file-upload.component.css']
})
export class MultiFileUploadComponent implements OnInit {
  loaded = 0;
  selectedFiles: FileList;
  uploadedFiles: FileDetails[] = [];
  showProgress = false;

  constructor(private http: HttpClient, private fileService: FileService, private snackBar: MatSnackBar) {
  }

  ngOnInit(): void {
  }

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

  upload() {
    this.showProgress = true;
    this.uploadedFiles = [];
    Array.from(this.selectedFiles).forEach(file => {
      const fileDetails = new FileDetails();
      fileDetails.name = file.name;
      this.uploadedFiles.push(fileDetails);
      this.fileService.uploadSingleFile(file)
        .pipe(tap(event => {
          if (event.type === HttpEventType.UploadProgress) {
            this.loaded = Math.round(100 * event.loaded / event.total);
            fileDetails.progress = this.loaded;
          }
        })).subscribe(event => {
        if (event instanceof HttpResponse) {
          if (this.selectedFiles.item(this.selectedFiles.length - 1) === file) {
            // Invokes fetchFileNames() when last file in the list is uploaded.
            this.fileService.fetchFileNames();
          }
        }
      });
    });
  }
}


Now lets create a service file.service.ts which will connect with the backend. Keep in mind reportProgress and observe is very important as this will send the events which are needed by the tap operator in the component.

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

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

  constructor(private http: HttpClient) {
  }

  uploadSingleFile(file: File): Observable<HttpEvent<{}>> {
    const formData: FormData = new FormData();
    formData.append('file', file);
    console.log(formData);
    return this.http.post<any>(
      'http://localhost:8080/uploadFile',
      formData,
      {
        reportProgress: true,
        observe: 'events'
      });
  }

  // Fetches the names of files to be displayed in the downloads list.
  fetchFileNames() {
    return this.http
      .get<string[]>('http://localhost:8080/getFiles');
  }
}


You can download other supporting files like app.module.ts, app.component.html, etc., from the github link below.

git Download the code from GitHub

Keep Learning! Stay Sharp!