File upload download
Angular ASP.NET Core Asynchronous C# HTML HTTP SPA Typescript Visual Code Visual Studio Web API

Downloading Content in Angular Applications

Welcome to today’s post.

In today’s post I will show how to download content within an Angular application from a Web API.

In previous posts I showed how to upload files from an Angular web application into a backend datastore, and implement a File Uploader through a .NET Core Web API.

The type (extension) of files we uploaded was not a concern to us. 

We developed a Web API in .NET Core to provide the necessary methods to allow the uploading of files to our backend data store. We also provided basic validations to check the size of uploaded files and the validity of the filename.

To be able to retrieve files from the Web API we will need to execute two tasks:

  1. Retrieve the metadata of the uploaded file.
  2. Use the metadata of uploaded file to retrieve the content of the uploaded file.

First, task 1 is just an API method that retrieves the file metadata.

To speed up retrieval, we will ensure that the metadata does not contain the file content.

The API is just one query to retrieve records from our upload table.

The API controller method is shown below:

[Route("DownloadFiles")]
[HttpGet]
public async Task<List<FileDownloadView>> DownloadFiles()
{
    var files = await _fileUtilityService.DownloadFiles();
    return files.ToList();
}

The file download method within our file service utility class is shown below:

public async Task<IEnumerable<FileDownloadView>> DownloadFiles()
{
    this._errorMessage = "";
    IEnumerable<FileDownloadView> downloadFiles = null;

    try
    {
        DateTime recentUpdate = DateTime.Now.Subtract(new TimeSpan(1, 0, 0));
        downloadFiles = _db.UploadedFiles
            .Where(f => f.UploadDate >= recentUpdate)
            .ToList()
            .Select(f =>
                new FileDownloadView
                {
                    Id = f.Id,
                    FileName = f.FileName,
                    FileType = Path.GetExtension(f.FileName),
                    FileSize = f.FileContent.Length
                });
        this._errorMessage = "OK";
    }
    catch (Exception ex)
    {
        this._errorMessage = "ERROR";
    }
    return downloadFiles;
}

On the Angular user interface, retrieving file meta data in Angular is quite straightforward.

We just use a File Manager API service component as shown:

@Injectable() 
export class FileManagerApiService {
    private baseAPI: string = environment.baseApiUrl;

    constructor(private http: HttpClient) {}

    uploadFiles(files: FormData): Observable<FileUploadResponse> {
        return this.http.post<FileUploadResponse>(this.baseAPI + '/UploadFiles/', files); 
    }

    deleteFile(request: FileDeleteRequest): Observable<FileDeleteResponse> {
        return this.http.post<FileDeleteResponse>(this.baseAPI + '/DeleteFile/', request); 
    }

    downloadFiles(): Observable<FileDownloadView[]> {
        return this.http.get<FileDownloadView[]>(this.baseAPI + '/DownloadFiles'); 
    }
}

Within our Angular download viewer component, we then subscribe to the service method:

startDownload()
{
    console.log("starting download of files..");    
    this.fileManagerApi.downloadFiles().subscribe(res => 
        {
            this.downloadFiles = this.fileViewList(res);
            console.log("number downloaded files = " + res.length);
            console.log("finished  download of files..");
        })
}

We have some helper functions to determine the file type and update the file type within the file download view array:

getFileType(fileName: string)
{
    return fileName.match(/\.([^\.]+)$/)[1];
}

fileViewList(files: FileDownloadView[])
{
    files.map(f => 
        {
            f.fileType = this.getFileType(f.fileName);
        });
    return files;
}

The HTML template to present the file meta data is as shown:

<mat-list *ngFor="let file of downloadFiles">
<div style="display: flex; flex-wrap: wrap; border-style: solid; border-width: 1px;">
    <div class="grid-row" style="flex: 0 10%;">{{file.id}}</div>
        <div class="grid-row" style="flex: 0 40%;"> 
  		    {{file.fileName}}
        </div>
        <div class="grid-row" style="flex: 0 15%;"> 
       	    {{file.fileSize}}
        </div>
      	<div class="grid-row" style="flex: 0 10%;">
   		    {{file.fileType}}
  		</div>
        <div class="grid-row" style="flex: 0 10%;" 
  		    class="clickLink" 
   			(click)="deleteFile(file.id)">
   			<a>DELETE</a>
   		</div>	
		<div class="grid-row" style="flex: 0 15%;" 
   			class="clickLink">
      		<a href={{getDownloadApiURL(file.id)}}>DOWNLOAD</a>
   		</div>
      </div> 
</mat-list>

Retrieving file content is from our grid link, which is a URL link to our API, the following helper function defines the URL:

getDownloadApiURL(id)
{
    return this.baseApiUrl + '/DownloadFileWithName?id=' + id; 
}

Retrieving the contents of a record is achieved by its record identifier with the call made to the API method:

[Route("DownloadFileWithName")]
[HttpGet]
public async Task<IActionResult> DownloadFileWithName(int id)
{
    var stream = await _fileUtilityService.DownloadFile(id);
    if (stream == null)
    {
        return NotFound();
    }
    string fileName = await _fileUtilityService.GetUploadedFileName(id);
    if (fileName.Length == 0)
        fileName = "file";
    return new FileContentResult(stream.ToArray(), "application/octet-stream")
    {
        FileDownloadName = fileName
    };
}

Note that I have included an additional return key in the content result, the file name. Without the file name we would be downloading a file with the name “application/octet-stream” without an extension.

We also check if a record with the specified id exists. I fit does not, then no result is returned as a response.

public async Task<MemoryStream> DownloadFile(int id)
{
    this._errorMessage = "";

    try
    {
        var selectedFile = _db.UploadedFiles
            .Where(f => f.Id == id)
            .SingleOrDefault();

        if (selectedFile == null)
            return null;

        this._errorMessage = "OK";

        return new MemoryStream(selectedFile.FileContent);
    }
    catch (Exception ex)
    {
        this._errorMessage = "ERROR";
        return null;
    }
}

The structure used to define the file metadata is shown:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace File.Uploader.API.Models
{
    public class FileDownloadView
    {
        public int Id { get; set; }
        public string FileName { get; set; }
        public int FileSize { get; set; }
        public string FileType { get; set; }
    }
}

The above method downloads the content from the API directly into a HTML element with a URL embedded within the HTML <a href> element. If we require the API link to the API URL to be concealed from the HTML then another alternative solution is available: We can download the file contents from the Web API through our Angular file download service component, then call this service API method from a method within our viewer component, which will then automate the download.

We will use a different method to download the file. Instead of returning a FileContentResult object, we will return a custom JSON structure with the file name, file size and file content.

The API download method is shown below:

[Route("DownloadFileWithName")]
[HttpGet]
public async Task<IActionResult> DownloadFileWithName(int id)
{
    var stream = await _fileUtilityService.DownloadFile(id);
    if (stream == null)
    {
       	return NotFound();
    }
    string fileName = await _fileUtilityService.GetUploadedFileName(id);
    if (fileName.Length == 0)
       	fileName = "file";
    return Ok(
        new FileDownloadResponse()
        {
       	    Id = id,
            FileName = fileName,
            FileSize = stream.Length,
            FileContent = stream.ToArray()
        });
}

We add a new API HTTP REST call in our Angular API File Manager service. 

With the observe HTTP option we have three choices:

  • body
  • events
  • response

If we want to consume the entire response and include headers, then use the ‘response’ observe option.

When the response is of type ‘text’ or ‘blob’ they require an observe option to be ‘response’ as the content headers are required in the response. If we want the response in include JSON data without the response headers, then we use ‘body’ and JSON response type. See the Angular reference for a detailed explanation.

We ensure the HTTP GET response is of type JSON in our API REST call:

downloadFileWithName(id): Observable<any> {
    return this.http.get<any>(this.baseAPI + '/DownloadFileWithName2?id=' + id, 
    { 
        observe: 'body', 
        responseType: 'json' } ); 
    }

Our HTML link to download the file references our new download method within the file viewer component:

<div class="grid-row" style="flex: 0 10%;" 
   class="clickLink" 
   (click)="downloadFile(file.id)">
   <a href="javascript: void(0)">DOWNLOAD</a>
</div>

Our download method takes the file content, which is in base 64, and converts it to a byte array.

The byte array is then injected as a blob within an object URL, which is then embedded within a hyperlink. The download is then actioned by clicking in the hyperlink. 

From our inspector, the response will be like the content shown:

{
    id: 173, 
    fileName: "test-file-2.txt", 
    fileSize: 91,
    fileContent: "dGVzdCBmaWxlIDENCnRlc3QgZmlsZSAyDQp0ZXN0IGZpbGUgMw0KdGVzdCBmaWxlIDQNCnRlc3QgZmlsZSA1DQp0ZXN0IGZpbGUgNg0KdGVzdCBmaWxlIDcNCg=="
}

The download method is shown:

downloadFile(id)
{
    this.fileManagerApi.downloadFileWithName(id).subscribe(
        res =>
        {
            var byteCharacters = atob(res.fileContent);
            var byteNumbers = new Array(byteCharacters.length);
            for (var i = 0; i < byteCharacters.length; i++) {
                byteNumbers[i] = byteCharacters.charCodeAt(i);
        }
        var byteArray = new Uint8Array(byteNumbers); 

        let filename = res.fileName;  
        let binaryData = [];
        binaryData.push(byteArray);
        let downloadLink = document.createElement('a');
        downloadLink.href = window.URL.createObjectURL(
   		new Blob(binaryData, { type: 'blob' }));
        downloadLink.setAttribute('download', filename);
        document.body.appendChild(downloadLink);
        downloadLink.click();
      }
    )
}

Our file viewer will function almost identically manner to the previous implementation, the only difference being that we can conceal the URL from user view.

As you have seen, the downloading of files can be quite a complex task. What I did here was to structure it in a logical way, with an API download, provide an interface for the user to select uploaded files for download with display a status of each download provided.

That’s all from this post.

I hope this post has been useful and informative.

Social media & sharing icons powered by UltimatelySocial