javascripthtmlangulartypescriptblob

how to create a downloadable link which loads the file from a backend server only when clicked in Angular


I have a situation where I have a list of items each represent a file. All these files can be fetched from a backend as blobs. I want to have links in the frontend when user clicks should be able to download the file for him/her.
The solution I have pre downloads the file and creates a SafeResourceUrl.

But, my problem is since I now have to show a list of links pre downloading all of them initially is a waste. I need to make it such that when I click the link the network call will go and fetch the file and then the download popup should be opened.

here is the code I have for predownloading

component

import { Component, OnInit } from '@angular/core';
import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser';
import { AppService } from './app.service';
import { HttpResponse } from '@angular/common/http';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
})
export class AppComponent implements OnInit {
  url?: SafeResourceUrl;
  stream: ArrayBuffer | null = null;
  fileName: string = 'test.pdf';

  constructor(
    private appService: AppService,
    private sanitizer: DomSanitizer
  ) {}

  ngOnInit(): void {
    this.appService.fetchPdf().subscribe((response: HttpResponse<Blob>) => {
      const blob = response.body!;
      blob.arrayBuffer().then((res) => {
        this.stream = res;
      });
      this.createLink(blob);
    });
  }

  async createLink(blob: Blob): Promise<void> {
    const buffer = await blob.arrayBuffer();
    if (buffer.byteLength) {
      const uint = new Uint8Array(buffer);
      let bin = '';
      for (let i = 0; i < uint.byteLength; i++) {
        bin += String.fromCharCode(uint[i]);
      }
      const base64 = window.btoa(bin);
      const url = `data:${blob.type};base64,${base64}`;
      this.url = this.sanitizer.bypassSecurityTrustResourceUrl(url);
    }
  }
}

service

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

@Injectable({
  providedIn: 'root',
})
export class AppService {
  private pdfUrl =
    'https://s28.q4cdn.com/392171258/files/doc_downloads/test.pdf';

  constructor(private http: HttpClient) {}

  fetchPdf(): Observable<HttpResponse<Blob>> {
    return this.http.get(this.pdfUrl, {
      observe: 'response',
      responseType: 'blob',
    });
  }
}

html

<h1>download test</h1>
<div *ngIf="url">
  <a [href]="url" [download]="fileName">{{ fileName }}</a>
</div>

here is demo


Solution

  • For this case the most convenient thing would be to bring you a list of the files IDs that you are going to use from the backend and when the user clicks on the download button a call is made to the backend to bring the link of the element and generate the file. I leave an example here below

    service

    const backendURL = "YOUR_BACKEND_URL"
       
    getListFromBackend() {
      let uri = `${backendURL}/getArchiveList`;
      return this.http.get(uri);
    }
    
    fetchPdf(): Observable<HttpResponse<Blob>> {
        let uri = `${backendURL}/archives/${id}`;
        return this.http.get(`${uri}`, {
          observe: 'response',
          responseType: 'blob',
        });
    }
    

    component

    itemsList = [];
    
    ngOnInit(): void {
      this.appService.getListFromBackend().subscribe((response) => {
        this.itemsList = response; // [id: 1, id: 2, id: 3]
      });
    }
    
    callItemBackend(id) {
      this.appService.fetchPdf(id).subscribe((response: HttpResponse<Blob>) => {
        const blob = response.body!;
          blob.arrayBuffer().then((res) => {
            this.stream = res;
          });
          this.createLink(blob);
      });
    }
    
    async createLink(blob: Blob): Promise<void> {
        const buffer = await blob.arrayBuffer();
        if (buffer.byteLength) {
          const uint = new Uint8Array(buffer);
          let bin = '';
          for (let i = 0; i < uint.byteLength; i++) {
            bin += String.fromCharCode(uint[i]);
          }
          const base64 = window.btoa(bin);
          const url = `data:${blob.type};base64,${base64}`;
          this.url = this.sanitizer.bypassSecurityTrustResourceUrl(url);
          const link = document.createElement('a');
          if(link) {
            link.setAttribute('target', '_blank');
            link.setAttribute('href', this.url);
            link.setAttribute('download', `NAME.pdf`);
            document.body.appendChild(link);
            link.click();
            link.remove();
          }
        }
    }
    

    html

    <div *ngFor="let item of this.itemsList">
      <a (click)="this.callItemBackend(item.id)">DOWNLOAD</a>
    </div>