How do I programmatically download a file using the browser's native download widget in Angular?
Asked Answered
G

6

6

So, when I request my webservice for getting download a zip file, it downloads the file content secretely and all of a sudden, the file appears in the download task bar but already downloaded full (100%)

Using the following angular method:


const endpoint = "http://localhost:8080/download/zip"
this.http.get<Blop>(endpoint, {headers: httpHeaders, responseType: 'blob', reportProgress: true })

So here is how I am subscribing:

this.http.get<Blop>(endpoint, {headers: httpHeaders, responseType: 'blob', reportProgress: true }).subscribe({
  next: data => {
    console.log('blocking or not');
    const blob = new Blob([data as any], { type: 'application/zip' });
    window.location.href = URL.createObjectURL(blob);
  }
})

So I noticed my console.log(...) isn't called until the end of the download, so I suppose that the browser-ui can't detect the download until it reaches window.location.href.

How to force the download to be shown in the download task bar before the end of the transfert, and watch the download progress in the browser? I coudld not find anything related to async blop or something like that.

PS: my backend is serving a stream of data, so the backend is not the problem. When calling my api directly through the browser, we can see the download progress in the download task bar. Still, if you guys are interested, this is the snippet (spring-boot)

    @GetMapping("/download/zip")
    fun download(response: HttpServletResponse): StreamingResponseBody {
        val file = downloads.download("launcher")

        response.contentType = "application/zip"
        response.setHeader(
            "Content-Disposition",
            "attachment;filename=sample.zip"
        )
        response.setContentLengthLong(file.length())

        return StreamingResponseBody { outputStream: OutputStream ->
            var bytesRead: Int
            val buffer = ByteArray(2048)
            val inputStream: InputStream = file.inputStream()
            while (inputStream.read(buffer).also { bytesRead = it } != -1) {
                outputStream.write(buffer, 0, bytesRead)
            }
        }
    }
Gobble answered 13/12, 2021 at 1:19 Comment(0)
W
7

For download you have 2 ways:

  1. http.get + msSaveOrOpenBlob (if its defined)/createObjectURL
  • you have full control over request, process and errors. E.g. u can cancel request, add additional headers, etc.
  • download remains in your app flow, e.g. F5 will cancel it.
  1. create hidden download link and click it programatically
  • you cannot add headers, you cannot show progress/errors in simple way (there are some tricks involving additional cookies to set by BE)
  • Its strated as separate process, e.g. you can close your app tab or whatever.

There seems to be no chance of mixing these 2 approaches and in general I would say 1st one is more modern and preferable for small files, however if you are not happy with it try 2nd one (https://mcmap.net/q/63640/-how-to-trigger-a-file-download-when-clicking-an-html-button-or-javascript):

function download(url) {
  const a = document.createElement('a')
  a.href = url
  a.download = url.split('/').pop()
  document.body.appendChild(a)
  a.click()
  document.body.removeChild(a)
}
Wittenburg answered 17/12, 2021 at 9:12 Comment(5)
So.. that means it's not possible to trigger the native download (+progress bar included I mean) functionnality of the broswer if your request requires special headers right?Gobble
I feel like the broswer will handle the download + progress only if the request is a GETGobble
So as a workaround, what do you think about the following: - Since the download URL requires an oauth2 & some permissions, I can't actually use your 2nd way. But I think generating dynamic unique links would be the solution! 1. Request the download URL with oauth & special headers 2. Response returns a generated & one-time usable link for downloading the protected content 3. Angular creates the hidden link & click it and so the broswer download progress bar worksGobble
Yes, if you need some auth-headers, single-use token approach is a good option. We use this to pass download link to 3rd party API.Wittenburg
So since none have been able to proof that it is possible to trigger the browser download's progress bar with angular and requests with custom headers, I accept yours since it gives the 2 possibles ways for downloading a file. ThanksGobble
T
0

Assuming you are using Angular's HttpClient, something like this should work:

    const endpoint = "http://localhost:8080/download/zip"
    const req = new HttpRequest('GET', endpoint, { reportProgress: true, })
    this.Http.request(req).subscribe(event => {
      if (event.type === HttpEventType.DownloadProgress) {
        const percentDone = Math.round(100 * event.loaded / (event.total || 0))
        console.log(percentDone);
      } else if (event instanceof HttpResponse) {
        const blob = new Blob([event.body as any], { type: 'application/zip' });
        window.location.href = URL.createObjectURL(blob);
      }
    })
Tourbillion answered 13/12, 2021 at 5:34 Comment(9)
well, thanks for your time but I'm asking for the download to be shown in the broswer native download page. All you are doing here is logging the download progressGobble
@Gobble If I understand you correctly, you can't access the browser native download area. You can replace the console.log with your own code to update your page html with the percent.Tourbillion
Indeed, but this is not the desired behaviour. I'd like to use the native download areaGobble
@Gobble That is not possibleTourbillion
I'd suggest you just show your own progress indicator while the download is in progress, and remove once finished. You could even mimic the look of the native download aread. I'm not sure why this is the desired behaviour?Tourbillion
I mean if I wanted to create my own ui for the download progress, I would not have asked this question maybe? The question isn't that hard, how to use the navigator native download ui using angular :)Gobble
@Tourbillion is correct here, trying to customize the browsers native download functionality is not a practical approach to this problem. Suggest extending your own UI to show download progress.Bainbrudge
@NathanBeck I'm not trying to cuztomize the native download functionnality of the browser, I just try to use it. There's no reason to implement your own progress-bar if the broswer already does it.Gobble
@Gobble best of luckBainbrudge
M
0

You can simply use this package

npm i ngx-filesaver

and

constructor(private _http: Http, private _FileSaverService: FileSaverService) {
}

onSave() {
  this._http.get('yourfile.png', {
    responseType: ResponseContentType.Blob // This must be a Blob type
  }).subscribe(res => {
    this._FileSaverService.save((<any>res)._body, fileName);
  });
}

complete doc here ngx file saver and also file saver.js

Monotint answered 19/12, 2021 at 7:47 Comment(2)
Same problem here, the download progress is not shown by the browser. The file is visible once it's totally downloadedGobble
Are you sure about this? because I tasted file saver demo again and it seems working fine for me in browser native downloaderMonotint
M
0

From a technical point of view, you can't change the way browsers behave for receiving a file in the background. Instead, you can calculate the download progress by setting the option observe to events while making an HTTP request. In this way, you won't just receive the final response body of the request but also get access to intermediate HTTP events. After that, you can show your own download progress apart from the browser.

There are multiple kinds of HTTP events in Angular, all consolidated under the type HttpEvent. We also need to explicitly pass the option reportProgress in order to receive HttpProgressEvents. The HTTP request will eventually look like follows:

this.http.get(url, {
  reportProgress: true,
  observe: 'events',
  responseType: 'blob'
})

You can find a good implementation at angular file download progress.

Marcosmarcotte answered 20/12, 2021 at 13:30 Comment(0)
R
-1

Try this:

$('a#someID').attr({target: '_blank', 
                    href  : 'http://localhost/directory/file.pdf'});

It uses jQuery

Rigorism answered 20/12, 2021 at 21:29 Comment(0)
L
-1

User package https://www.npmjs.com/package/file-saver

npm i file-saver

and then in your component, import the SaveAs method

import { saveAs } from 'file-saver';


saveAs(url, name);
Lithotrity answered 21/12, 2021 at 11:51 Comment(1)
again, this approach won't trigger the native download chrome widget (with progress bar etc..)Gobble

© 2022 - 2024 — McMap. All rights reserved.