4

My issue is similar to this one but it was an old post without much context or detail, so here's what I got:

When my component calls the download file method, which in turn calls the download file service, the service makes the HTTP GET request from my API which streams a blob back to service.

Component:

    this.svc.getDownloadFile(fileID, fileName).subscribe(res => {
      console.log(res);
    });
  }

Service:

  getDownloadFile(fileId: number, fileName: string): Observable<any> {
    return this.http.get(this.downloadFileUrl + "/" + fileId, {
      reportProgress: true,
      observe: "events",
      headers: this.getHeaders(),
      responseType: "blob"
    });
  }

The problem is that because it is an API call, the blob streams through the XHR and I can see it downloading and its progress in the Network tab in Chrome. The problem I am having is getting some sort of browser download notification that you would usually expect when downloading a file in Chrome (or any browser), this only happens after the entire response is complete.

For example I have a 50MB file in the database. The user clicks a link to download the said file from Angular app, the component calls the service which in turn does an API call which gets the file from the DB and streams it as a blob response. Angular takes this response, waits until it has downloaded all 50MB then shows the download dialog.

Download notification only shows up after its 100% complete

Network Tab shows download in progress

The API is returning the Content Disposition header along with content length, content type , filename etc. I have googled all over and searched high and low for something to mimic this, I kind of understand that the browser doesn't automatically download it as usual because the request is being piped through Angular and the browser doesn't automatically just assume that all blob XHR should be saved.

I tried using: https://www.npmjs.com/package/streamsaver but this package actually only handles files that are already stored, not ones being sent from API as blobs.

Jacob R
  • 327
  • 2
  • 11

2 Answers2

4

So for people that have the same problem here is what I did:

Keep in mind this is for my work setup, we have a DB that stores encrypted blobs, an API that serves the blobs via filestream to our angular apps.

If you have an non-encrypted API then you can just do something like mentioned above by @Pace with window.open(urlToFile) or window.location.href(urlToFile).

Because my API requires authorization via Bearer token in the header, what I did is created an endpoint that checks the token along with the file ID that is being requested. The API then creates a GUID (or unique id of sorts) and stores the GUID and file ID together in a list , which is then stored in a MemoryCache along with an expiration time.

After this is complete it returns an Ok response with the GUID created. I am no security expert but I believe this may give it security because of the following reasons, while I am sure this isn't a perfect solution:

  • the user has to be authenticated to request a GUID
  • the GUID is tied to a certain file ID
  • the GUID has an expiration time of something short (like 1 min)
  • the GUID is deleted after one use
  • the time between the API sending the guid back and the Angular app using the GUID to call back the API are such a short time span, I don't see how anyone could possibly jack it in between that timeframe AND figure out how to use it

The GUID returns to the Angular app which then after the GUID comes back calls window.location.href and sets it to another endpoint passing the GUID and the File ID.

This endpoint is configured to not require authentication, and will only allow the download of files who are on the list and have the correct GUID passed with them.

For example this is my service that gets called:

getDownloadFile(fileId: number): Observable<any> {
    return this.http
      .get(this.downloadFileUrl + "/" + fileId, {
        headers: this.getHeaders() //Attaches bearer token
      })
      .pipe(
        tap(res => { //res = the GUID being returned from API
          window.location.href =
            this.downloadFileUrl + "/temp/" + fileId + "/" + res;
        })
      );
  }

As you can see it calls the authenticated endpoint, gets back a GUID and immediately sets the current browser window's url to the temp endpoint using the GUID and file ID. Note because this is a file stream or blob coming back with the Content Disposition set correctly that it does NOT navigate away from the Angular app, it just starts the download through the download manager.

This is the only way I could find that would allow the browser to handle downloads as normal from a streaming API endpoint.

Most smaller files it doesn't really matter because they download so quickly via API that you can't tell the download did or didnt start at the beginning because it is usually done in the blink of an eye, but for large files if you only do an API call via XHR then the user will have no indication the file is downloading until the request is finished and then BAM! all of a sudden you have a 150MB file just appear in your downloads.. This is also a nice method, because even if you leave the Angular app it continues to download, unlike regular API XHR subscriptions that will end with the app. I also found out how to tap an XHR request that has a blob and track progress but you would have to create your own download manager within you Angular app and not use the browser default which most users expect.

Jacob R
  • 327
  • 2
  • 11
0

If the API is returning a Content-Disposition header then you could just use window.open(url) and it should download as normal.

If you must stream it in to Angular because you want to manipulate the bytes or something like that then you can subscribe to the HttpClient's progress events to report your own progress (see here).

If you then want to save the file to the user's disk you can use the file-saver package.

Pace
  • 41,875
  • 13
  • 113
  • 156
  • A couple questions about this: 1) my API is authenticated, so I cant just window.open a file url. 2) I don't want the user to see a window popup when they click download. – Jacob R Apr 22 '19 at 15:15
  • If it's a file to download it doesn't actually open up a new window. You can also do `window.location = ...` In either case it will just start a download. If you are doing authentication with cookies then these approaches will work fine. However, if you are doing basic authentication or header based authentication then these approaches will not work directly. [This question](https://stackoverflow.com/questions/24501358/how-to-set-a-header-for-a-http-get-request-and-trigger-file-download) discusses approaches for downloading a file when header authentication is required. – Pace Apr 22 '19 at 17:41