blexin

IT Development
CONSULTING and
training


Blog

Evolve your company

How to upload and download files with an Angular front-end and an Asp.Net Core back-end

Uploading and Downloading files with Angular and Asp.Net Core

Wednesday, January 23, 2019

A frequently required activity, in the projects I work on, is the management of the upload and download of files in Angular. There are different ways to develop these functionalities: the best approach often depends on the available API.

Suppose you have some API written in Asp.Net Core, particularly a controller with three actions:

  • Upload, to receive a file a save it in the folder./wwwroot/upload;
  • Download, to recover a file from the folder./wwwroot/upload;
  • Files, to obtain the list of files present in ./wwwroot/upload.

A possible implementation of the controller may be the following:

namespace BackEnd.Controllers
{
   [Route("api")]
   [ApiController]
   public class UploadDownloadController: ControllerBase
   {
       private IHostingEnvironment _hostingEnvironment;

       public UploadDownloadController(IHostingEnvironment environment) {
           _hostingEnvironment = environment;
       }

       [HttpPost]
       [Route("upload")]
       public async Task<iactionresult> Upload(IFormFile file)
       {
           var uploads = Path.Combine(_hostingEnvironment.WebRootPath, "uploads");
           if(!Directory.Exists(uploads))
           {
               Directory.CreateDirectory(uploads);
           }
           if (file.Length > 0) {
               var filePath = Path.Combine(uploads, file.FileName);
               using (var fileStream = new FileStream(filePath, FileMode.Create)) {
                   await file.CopyToAsync(fileStream);
               }
           }
           return Ok();
       }

       [HttpGet]
       [Route("download")]
       public async Task<iactionresult> Download([FromQuery] string file) {
           var uploads = Path.Combine(_hostingEnvironment.WebRootPath, "uploads");
           var filePath = Path.Combine(uploads, file);
           if (!System.IO.File.Exists(filePath))
               return NotFound();

           var memory = new MemoryStream();
           using (var stream = new FileStream(filePath, FileMode.Open))
           {
               await stream.CopyToAsync(memory);
           }
           memory.Position = 0;

           return File(memory, GetContentType(filePath), file);
       }

       [HttpGet]
       [Route("files")]
       public IActionResult Files() {
           var result =  new List<string>();

           var uploads = Path.Combine(_hostingEnvironment.WebRootPath, "uploads");
           if(Directory.Exists(uploads))
           {  
               var provider = _hostingEnvironment.ContentRootFileProvider;
               foreach (string fileName in Directory.GetFiles(uploads))
               {
                   var fileInfo = provider.GetFileInfo(fileName);
                   result.Add(fileInfo.Name);
               }
           }
           return Ok(result);
       } 


       private string GetContentType(string path)
       {
           var provider = new FileExtensionContentTypeProvider();
           string contentType;
           if(!provider.TryGetContentType(path, out contentType))
           {
               contentType = "application/octet-stream";
           }
           return contentType;
       }
   }
}

As you can see, there’s nothing particularly complex. Focus your attention to the FileExtensionContentTypeProvider from .Net Core: it allows you to obtain the content type from file extension.

At this point, we can create an Angular project with the CLI, with which we want to upload files, display and download them. Moreover, we will show the progress during download and upload, using one of the Angular HttpClient functionalities.

We will create a specific component for each operation, Upload and Download, and we will use it from a FileManager component, that shows the list of downloaded files.

The three components share a service, in which we implement the HTTP calls to our API:

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

@Injectable()
export class UploadDownloadService {
  private baseApiUrl: string;
  private apiDownloadUrl: string;
  private apiUploadUrl: string;
  private apiFileUrl: string;

  constructor(private httpClient: HttpClient) {
    this.baseApiUrl = 'http://localhost:5001/api/';
    this.apiDownloadUrl = this.baseApiUrl + 'download';
    this.apiUploadUrl = this.baseApiUrl + 'upload';
    this.apiFileUrl = this.baseApiUrl + 'files';
  }

  public downloadFile(file: string): Observable<HttpEvent<Blob>> {
    return this.httpClient.request(new HttpRequest(
      'GET',
      `${this.apiDownloadUrl}?file=${file}`,
      null,
      {
        reportProgress: true,
        responseType: 'blob'
      }));
  }

  public uploadFile(file: Blob): Observable<HttpEvent<void>> {
    const formData = new FormData();
    formData.append('file', file);

    return this.httpClient.request(new HttpRequest(
      'POST',
      this.apiUploadUrl,
      formData,
      {
        reportProgress: true
      }));
  }

  public getFiles(): Observable<string[]> {
    return this.httpClient.get<string[]>(this.apiFileUrl);
  }
  

Compared to the classical use of HttpClient, that you can display in the method getFiles(), in dowloadFile() and uploadFile() we use the request() method, that permits us to specify a HttpRequest with all its options, among them the option reportProgress set on true. This option enables us to receive updates on the exchange data status between client and server. How? We can see that in our Upload component:

import { Component, Output, EventEmitter, Input, ViewChild, ElementRef } from '@angular/core';
import { UploadDownloadService } from 'src/app/services/upload-download.service';
import { HttpEventType } from '@angular/common/http';
import { ProgressStatus, ProgressStatusEnum } from 'src/app/models/progress-status.model';

@Component({
  selector: 'app-upload',
  templateUrl: 'upload.component.html'
})

export class UploadComponent {
  @Input() public disabled: boolean;
  @Output() public uploadStatus: EventEmitter;
  @ViewChild('inputFile') inputFile: ElementRef;

  constructor(private service: UploadDownloadService) {
    this.uploadStatus = new EventEmitter<ProgressStatus>();
  }

  public upload(event) {
    if (event.target.files && event.target.files.length > 0) {
      const file = event.target.files[0];
      this.uploadStatus.emit({status: ProgressStatusEnum.START});
      this.service.uploadFile(file).subscribe(
        data => {
          if (data) {
            switch (data.type) {
              case HttpEventType.UploadProgress:
                this.uploadStatus.emit( {status: ProgressStatusEnum.IN_PROGRESS, percentage: Math.round((data.loaded / data.total) * 100)});
                break;
              case HttpEventType.Response:
                this.inputFile.nativeElement.value = '';
                this.uploadStatus.emit( {status: ProgressStatusEnum.COMPLETE});
                break;
            }
          }
        },
        error => {
          this.inputFile.nativeElement.value = '';
          this.uploadStatus.emit({status: ProgressStatusEnum.ERROR});
        }
      );
    }
  }
}

As you can see, the subscription to the observable returned from the HttpClient gives us an HttpEvent type object, which property type can acquire one of these five values:

  • Sent, when the request has been sent 
  • UploadProgress, when the upload is in progress;
  • ResponseHeader, when status code of headers has been received 
  • DownloadProgress, when the download is in progress;
  • Response, when the response has been received;
  • User, when a custom event is raised from an interceptor or the backend.

If you want to level out events between upload and download and abstract us from the http library, we add our enumeration and our interface to the project:

export interface ProgressStatus {
  status: ProgressStatusEnum;
  percentage?: number;
}

export enum ProgressStatusEnum {
  START, COMPLETE, IN_PROGRESS, ERROR
}

In the component you can see the use: we simply emit the START as the operation starts, IN_PROGRESS in correspondence of UploadProgress, COMPLETE in correspondence of Respose and ERROR in event of an error

The template has a hidden input file and a button “UPLOAD”. Clicking on the button, the input selection window will open, allowing the user to select a file. On the input change, we recall the component upload method.

<div class="container-upload">
    <button [disabled]="disabled" [ngClass]="{'disabled': disabled}" class="button upload" (click)="inputFile.click()">
        UPLOAD
    </button>
    <input name="file" id="file"(change)="upload($event)" type="file" #inputFile hidden>
</div>

As the operation ends, both in case of success and of error, you need to clean out selection input, or it would not be possible to make the upload of the same file consecutively. To do that, you can use both ReactiveForm and associate it a FormControl to the input, and with ngModel, cleaning out the bound property.

If you don’t want to involve the form management for so little and accepting to sully the component logic, you can use the decorator ViewChild to obtain a reference to the DOM element, by which reset the value from the nativeElement.

We can make a similar procedure for the download, but we should insert a further requisite: we want to download the file, update the progress and supply the downloaded file to a user with no more interaction.

The markup of the Download component is very simple:

<button
 [disabled]="disabled"
 class="button download"
 [ngClass]="{'disabled': disabled}"
 (click)="download()">download</button>

The component logic is very similar to that of the upload, but, as we know, once the download is terminated, we want to supply the file to user immediately.

import { Component, Input, Output, EventEmitter } from '@angular/core';
import { HttpEventType } from '@angular/common/http';
import { UploadDownloadService } from 'src/app/services/upload-download.service';
import { ProgressStatus, ProgressStatusEnum } from 'src/app/models/progress-status.model';

@Component({
  selector: 'app-download',
  templateUrl: 'download.component.html'
})

export class DownloadComponent {
  @Input() public disabled: boolean;
  @Input() public fileName: string;
  @Output() public downloadStatus: EventEmitter;

  constructor(private service: UploadDownloadService) {
    this.downloadStatus = new EventEmitter();
  }

  public download() {
    this.downloadStatus.emit( {status: ProgressStatusEnum.START});
    this.service.downloadFile(this.fileName).subscribe(
      data => {
        switch (data.type) {
          case HttpEventType.DownloadProgress:
            this.downloadStatus.emit( {status: ProgressStatusEnum.IN_PROGRESS, percentage: Math.round((data.loaded / data.total) * 100)});
            break;
          case HttpEventType.Response:
            this.downloadStatus.emit( {status: ProgressStatusEnum.COMPLETE});
            const downloadedFile = new Blob([data.body], { type: data.body.type });
            const a = document.createElement('a');
            a.setAttribute('style', 'display:none;');
            document.body.appendChild(a);
            a.download = this.fileName;
            a.href = URL.createObjectURL(downloadedFile);
            a.target = '_blank';
            a.click();
            document.body.removeChild(a);
            break;
        }
      },
      error => {
        this.downloadStatus.emit( {status: ProgressStatusEnum.ERROR});
      }
    );
  }
}

To do that, we need to “get the hands dirty” and manipulate the DOM from the component. That’s because the fastest way to download a file with no other interaction of the user, is to create quick an anchor element and hook to it a URL object, created from the downloaded blob. We can then set element download's properties with the name of the file and then, clicking the element from code, we obtain a direct download also for files that a browser can open, as PDF. After that, we can delete the created anchor.

Ugly, maybe. The better thing to do would probably be to move the whole manipulation part in a guideline, but, for the purposes of our reasoning, it would not make the much difference: I leave it to you as exercise!

We use both component from the FileManager component and there you have it. HTML will be as follow:

<app-upload [disabled]="showProgress" (uploadStatus)="uploadStatus($event)"></app-upload>
<h2>File List</h2>
<p *ngIf="showProgress"> progress <strong>{{percentage}}%</strong></p>
<hr>
<div class="container">
    <ul>
        <li *ngFor="let file of files">
            <a>
                {{file}}
                <app-download [disabled]="showProgress" [fileName]="file" (downloadStatus)="downloadStatus($event)"></app-download>
            </a>
        </li>
    </ul>
</div>

The component logic will limit itself to recover the list of available files and to show the upload and download status, based on events received from sons components:

import { Component, OnInit } from '@angular/core';
import { UploadDownloadService } from 'src/app/services/upload-download.service';
import { ProgressStatusEnum, ProgressStatus } from 'src/app/models/progress-status.model';

@Component({
  selector: 'app-filemanager',
  templateUrl: './file-manager.component.html'
})
export class FileManagerComponent implements OnInit {

  public files: string[];
  public fileInDownload: string;
  public percentage: number;
  public showProgress: boolean;
  public showDownloadError: boolean;
  public showUploadError: boolean;

  constructor(private service: UploadDownloadService) { }

  ngOnInit() {
    this.getFiles();
  }

  private getFiles() {
    this.service.getFiles().subscribe(
      data => {
        this.files = data;
      }
    );
  }

  public downloadStatus(event: ProgressStatus) {
    switch (event.status) {
      case ProgressStatusEnum.START:
        this.showDownloadError = false;
        break;
      case ProgressStatusEnum.IN_PROGRESS:
        this.showProgress = true;
        this.percentage = event.percentage;
        break;
      case ProgressStatusEnum.COMPLETE:
        this.showProgress = false;
        break;
      case ProgressStatusEnum.ERROR:
        this.showProgress = false;
        this.showDownloadError = true;
        break;
    }
  }

  public uploadStatus(event: ProgressStatus) {
    switch (event.status) {
      case ProgressStatusEnum.START:
        this.showUploadError = false;
        break;
      case ProgressStatusEnum.IN_PROGRESS:
        this.showProgress = true;
        this.percentage = event.percentage;
        break;
      case ProgressStatusEnum.COMPLETE:
        this.showProgress = false;
        this.getFiles();
        break;
      case ProgressStatusEnum.ERROR:
        this.showProgress = false;
        this.showUploadError = true;
        break;
    }
  }
}

The final result is the following:

You can download source code from my GitHub at below address:

https://github.com/AARNOLD87/down-up-load-with-angular

See you next!

Author

SUBSCRIBE TO NEWSLETTER

Services

Evolve your company