264

I have a WebApi / MVC app for which I am developing an angular2 client (to replace MVC). I am having some troubles understanding how Angular saves a file.

The request is ok (works fine with MVC, and we can log the data received) but I can't figure out how to save the downloaded data (I am mostly following the same logic as in this post). I am sure it is stupidly simple, but so far I am simply not grasping it.

The code of the component function is below. I've tried different alternatives, the blob way should be the way to go as far as I understood, but there is no function createObjectURL in URL. I can't even find the definition of URL in window, but apparently it exists. If I use the FileSaver.js module I get the same error. So I guess this is something that changed recently or is not yet implemented. How can I trigger the file save in A2?

downloadfile(type: string){

    let thefile = {};
    this.pservice.downloadfile(this.rundata.name, type)
        .subscribe(data => thefile = new Blob([data], { type: "application/octet-stream" }), //console.log(data),
                    error => console.log("Error downloading the file."),
                    () => console.log('Completed file download.'));

    let url = window.URL.createObjectURL(thefile);
    window.open(url);
}

For the sake of completeness, the service that fetches the data is below, but the only thing it does is to issue the request and pass on the data without mapping if it succeeds:

downloadfile(runname: string, type: string){
   return this.authHttp.get( this.files_api + this.title +"/"+ runname + "/?file="+ type)
            .catch(this.logAndPassOn);
}
Basil
  • 1,664
  • 13
  • 25
rll
  • 5,509
  • 3
  • 31
  • 46

32 Answers32

241

The problem is that the observable runs in another context, so when you try to create the URL var, you have an empty object and not the blob you want.

One of the many ways that exist to solve this is as follows:

this._reportService.getReport().subscribe(data => this.downloadFile(data)),//console.log(data),
                 error => console.log('Error downloading the file.'),
                 () => console.info('OK');

When the request is ready it will call the function "downloadFile" that is defined as follows:

downloadFile(data: Response) {
  const blob = new Blob([data], { type: 'text/csv' });
  const url= window.URL.createObjectURL(blob);
  window.open(url);
}

the blob has been created perfectly and so the URL var, if doesn't open the new window please check that you have already imported 'rxjs/Rx' ;

import 'rxjs/Rx' ;

I hope this can help you.

SwissCodeMen
  • 4,222
  • 8
  • 24
  • 34
Alejandro Corredor
  • 2,478
  • 1
  • 10
  • 6
  • Will it work to download zip file also ? I have application/octet-stream as response from java REST service which accepts custom headers.I want to download that as zip file. Any Idea ? – Techno Cracker Jul 25 '17 at 09:35
  • 17
    What is `this._reportService.getReport()` and what does it return? – Burjua Aug 15 '17 at 16:37
  • 5
    @Burjua the `getReport()` returns a `this.http.get(PriceConf.download.url)` – ji-ruh Sep 04 '17 at 08:55
  • @michalzfania you can implement it like this: https://stackoverflow.com/a/19328891/790635 – emp Nov 22 '17 at 08:55
  • The problem I have is that after windows downloads the file, it doesn't recognize it's format. For example there is no .csv at the end of the file, though the file content was saved correctly. – Benjamin Jun 29 '18 at 07:05
  • 7
    The issue I'm having is that the window opens and closes immediately not downloading the file – Braden Brown Jul 23 '18 at 22:29
  • 14
    How can we set file name in here ? by default it choose a numeric value as name – Saurabh Aug 21 '18 at 12:28
  • 1
    Can this be used to download GBs of files in clientside without lag? – Valentino Pereira Jan 15 '19 at 14:42
  • This doesn't work, As stated by @BradenBrown, same is happening in my system as well. – Neeraj Jain Mar 09 '19 at 07:46
  • what is the possible value of type passed to the function as a parameter. This 'type' is then passed to http.get(url,type) ? @Amr ElAdawy – Shantam Mittal May 09 '19 at 18:31
  • @ShantamMittal the `type` in the Blob object is the MIME type of the file to be saved by the browser. In this case it is `text/csv` – Amr Eladawy May 09 '19 at 18:37
  • 1
    `this.uploadFileService.downloadDocument().subscribe((data: any)=>{ console.log(data); const blob = new Blob([data['data']],{type: 'application/pdf'}); const url = window.URL.createObjectURL(blob); window.open(url); })` Service `downloadDocument() { return this.httpClient.get('getdocument/23'); }` I want to download pdf but the pdf fails to open. I am doing the same thing as in the answer @Amr ElAdawy – Shantam Mittal May 09 '19 at 18:57
  • 15
    I've used the above code for downloading a file from API response but i'm getting some error in creating the Blob part "Type response is not assignable to type Blobpart". Kindly help if anyone knows this issue – knbibin Oct 11 '19 at 06:10
  • 1
    @knbibin I changed the type from Response to any and it worked – tltjr Dec 03 '20 at 18:56
  • 2
    Don't forget `window.URL.revokeObjectURL(url);`. You call this method when you've finished using an object URL to let the browser know not to keep the reference to the file any longer. – Ralpharoo Mar 31 '21 at 01:16
110

Try this!

1 - Install dependencies for show save/open file pop-up

npm install file-saver --save
npm install -D @types/file-saver

2- Create a service with this function to recive the data

downloadFile(id): Observable<Blob> {
    let options = new RequestOptions({responseType: ResponseContentType.Blob });
    return this.http.get(this._baseUrl + '/' + id, options)
        .map(res => res.blob())
        .catch(this.handleError)
}

3- In the component parse the blob with 'file-saver'

import {saveAs as importedSaveAs} from "file-saver";

  this.myService.downloadFile(this.id).subscribe(blob => {
            importedSaveAs(blob, this.fileName);
        }
    )

This works for me!

glinda93
  • 7,659
  • 5
  • 40
  • 78
Hector Cuevas
  • 1,125
  • 1
  • 7
  • 3
  • 1
    I used step 2 in combination with the answer from @Alejandro and it worked without the need to install file-saver... – Ewert Mar 13 '18 at 11:11
  • 8
    Thank you! It works perfectly! I wonder if we can get the filename that is defined on the header of the response. Is that possible? – jfajunior Mar 26 '18 at 09:41
  • 2
    error Av5 Argument of type 'RequestOptions' is not assignable to parameter of type '{ headers?: HttpHeaders | { [header: string]: string | string[]; }; – giveJob Nov 16 '18 at 08:35
  • 2
    This one however is not suitable for big files download. – Reven Apr 03 '20 at 09:19
  • 1
    @jfajunior if you wan't the file-name from the response you need to access the response header: add `observe: 'response'` to the request-options and return `Observable>` instead of `Observable` - now you can access the response header and read file-name e.g. `res.headers.get('File-Name')` – Sepultura Dec 17 '20 at 16:33
96

If you don't need to add headers in the request, to download a file in Angular2 you can do a simple (KISS PRINCIPLE):

window.location.href='http://example.com/myuri/report?param=x';

in your component.

surfealokesea
  • 4,971
  • 4
  • 28
  • 38
  • 5
    Can someone please tell why this answer is downvoted? The topic is to download a file using angular2. If this method works to do a simple download then it should also be marked as a valid answer. – Saurabh Shetty Feb 15 '17 at 18:37
  • 10
    @SaurabhShetty, This won't help in case you want to send custom headers, what if you want to send an auth token for example? If you look into OP question you can see he uses `authHttp`! – A.Akram Apr 17 '17 at 10:50
  • 9
    I do understand the downvotes, nevertheless this answer solved my issue. – JoeriShoeby Apr 21 '17 at 13:06
  • 1
    If you let the server return the url in some context, the server could prepare the url. ex: object: MyRecord.Cover. The cover could be a url to an image in the server. When calling get(Myrecord) you let the server return the prepared url (Cover), with security token and other headers set. – Jens Alenius Oct 23 '17 at 13:26
  • 8
    It is an answer that works. Just becuase it doesnt have that doenst make it not an answer. – speciesUnknown Nov 19 '19 at 16:21
  • It's like you need to pass something through your door but this door doesn't have knob nor lock,but yes you still can move things. – Persk Dec 23 '19 at 03:51
  • 1
    @A.Akram this answers the question, not the description. I got here by googling the question (title), not the description, so this is what I was looking for, therefor this answer is valid for the question (title). You would have been right if the question would have said "... with custom headers". This community... I swear to God...Worse than LOL – bokkie Sep 30 '22 at 11:09
  • @Persk yes, because the title asks "how to pass something through a door?", not "...through a door that doesn't have a knob or lock"... – bokkie Sep 30 '22 at 11:12
  • This did not work for me, I was redirected to the home page which is the default route, maybe there is a relative path issue, the routing should have been ignored for this location, looks simple solution, maybe a small glitch – Naga Feb 16 '23 at 20:38
57

This is for folks looking how to do it using HttpClient and file-saver:

  1. Install file-saver

npm install file-saver --save

npm install @types/file-saver --save

API Service class:

export() {
    return this.http.get(this.download_endpoint, 
        {responseType: 'blob'});
}

Component:

import { saveAs } from 'file-saver';
exportPdf() {
    this.api_service.export().subscribe(data => saveAs(data, `pdf report.pdf`));
}
Frederik Struck-Schøning
  • 12,981
  • 8
  • 59
  • 68
Justin
  • 901
  • 8
  • 11
  • 1
    How to show the filesize in the browser when the download starts? I am sending the filesize as content-length in the http header. – humbleCoder Dec 09 '18 at 18:13
54

How about this?

this.http.get(targetUrl,{responseType:ResponseContentType.Blob})
        .catch((err)=>{return [do yourself]})
        .subscribe((res:Response)=>{
          var a = document.createElement("a");
          a.href = URL.createObjectURL(res.blob());
          a.download = fileName;
          // start download
          a.click();
        })

I could do with it.
no need additional package.

Frederik Struck-Schøning
  • 12,981
  • 8
  • 59
  • 68
harufumi.abe
  • 817
  • 9
  • 8
  • 4
    So simple yet it is the one that work flawlessly. It doesn't clutter the DOM, doesn't create any element. I combined this solution with some of the aboves and it works like a charm. – Chax Nov 01 '18 at 14:33
  • with **responseType: "blob"** i didn't use **res.blob()** and it works already! – Armin Torkashvand Aug 29 '20 at 10:48
  • how about the file extension? for example I hardcode `fileName='download'` but I don't know the type of the file (could be .txt, .pdf, .png etc), how would I handle appending the extension to the file name so I the downloaded file would be download.pdf or download.png etc ? – Andrei Manolache Oct 28 '22 at 06:04
42

For newer angular versions:

npm install file-saver --save
npm install @types/file-saver --save


import {saveAs} from 'file-saver';

this.http.get('endpoint/', {responseType: "blob", headers: {'Accept': 'application/pdf'}})
  .subscribe(blob => {
    saveAs(blob, 'download.pdf');
  });
AbdullahA
  • 113
  • 1
  • 8
Tobias Ernst
  • 4,214
  • 1
  • 32
  • 30
21

As mentioned by Alejandro Corredor it is a simple scope error. The subscribe is run asynchronously and the open must be placed in that context, so that the data finished loading when we trigger the download.

That said, there are two ways of doing it. As the docs recommend the service takes care of getting and mapping the data:

//On the service:
downloadfile(runname: string, type: string){
  var headers = new Headers();
  headers.append('responseType', 'arraybuffer');
  return this.authHttp.get( this.files_api + this.title +"/"+ runname + "/?file="+ type)
            .map(res => new Blob([res],{ type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' }))
            .catch(this.logAndPassOn);
}

Then, on the component we just subscribe and deal with the mapped data. There are two possibilities. The first, as suggested in the original post, but needs a small correction as noted by Alejandro:

//On the component
downloadfile(type: string){
  this.pservice.downloadfile(this.rundata.name, type)
      .subscribe(data => window.open(window.URL.createObjectURL(data)),
                  error => console.log("Error downloading the file."),
                  () => console.log('Completed file download.'));
  }

The second way would be to use FileReader. The logic is the same but we can explicitly wait for FileReader to load the data, avoiding the nesting, and solving the async problem.

//On the component using FileReader
downloadfile(type: string){
    var reader = new FileReader();
    this.pservice.downloadfile(this.rundata.name, type)
        .subscribe(res => reader.readAsDataURL(res), 
                    error => console.log("Error downloading the file."),
                    () => console.log('Completed file download.'));

    reader.onloadend = function (e) {
        window.open(reader.result, 'Excel', 'width=20,height=10,toolbar=0,menubar=0,scrollbars=no');
  }
}

Note: I am trying to download an Excel file, and even though the download is triggered (so this answers the question), the file is corrupt. See the answer to this post for avoiding the corrupt file.

Frederik Struck-Schøning
  • 12,981
  • 8
  • 59
  • 68
rll
  • 5,509
  • 3
  • 31
  • 46
  • 7
    I think the reason the file gets corrupted is because you are loading `res` into the blob and you actually want `res._body` . However `_body` is a private variable and not accessible. As of today `.blob()` and `.arrayBuffer()` on a http response object have not been implemented in Angular 2. `text()` and `json()` are the only two options but both will garble your body. Have you found a solution? – sschueller Mar 18 '16 at 14:05
  • Still not implemented as far as I can tell.. this is a huge show stopper for me, as my project requires my app to be able to download pdfs from the server. Oh no – Spock Mar 28 '16 at 21:21
  • 1
    hi @rll, i followed the above steps and i am getting subscribe as completed. Still i couldn't see the file getting downloaded. I couldn't see any error as well. Please help – AishApp May 04 '16 at 13:40
  • which of the methods are you using? have you `import 'rxjs/Rx'`? – rll May 04 '16 at 15:26
  • Can you post a link to the question regarding the corrupted file (which you mention in your note) - which I am also getting when downloading Excel. I searched but could not find it. – Bren Gunning Aug 05 '16 at 11:00
  • @BrenGunning I did not come back to this, and as such ended up not posting that question yet. See the comment above, by sschueller. – rll Aug 05 '16 at 12:32
  • @rll Thanks. I actually brought the response back as "any" thus allowing me to access "_body" once compiled to JavaScript. Still corrupts the spreadsheets that I am trying to read. Thanks for answering - I will start a new question. – Bren Gunning Aug 05 '16 at 15:22
  • @rll Did you get any success. I am using RC4 and getting corrupted file in Chrom and not working with IE. I'll appreciate for any help. – Ravinder Kumar Oct 13 '16 at 15:45
  • @RavinderKumar I have updated the answer with a link to the follow-up question by Bren Gunning. Maybe that helps. – rll Oct 14 '16 at 14:23
  • 1
    The 2 options lets me download the file, but it loads the data in the background first. What if I have a large file that has to be downloaded? – f123 Nov 04 '16 at 06:30
  • Does anyone has solution for this? I am also getting corrupt file. Thanks! – genericuser Nov 10 '16 at 00:37
  • I am also facing the same problem of getting corrupt file. Did anyone find any solution for this? I am using Angular 2.0. – monica Nov 28 '16 at 10:03
  • This feels like the most complete answer to the problem, but like others, it doesnt actually work for me. Can i also ask, what if you dont know the file type? if the file can be an image, a pdf , a doc or whatever. but all you have is the url to it – JAG Dec 15 '16 at 16:19
  • 1
    My solution is to just use `` to download a file. – user2061057 Jan 02 '17 at 14:35
  • 1
    I know this is an old answer but it's high up on search results and is the accepted answer: The line ` headers.append('responseType', 'arraybuffer');` is wrong. It's an option, not a header. Please fix it. Aaaand... The headers are created and not used. Not helpful. – Stevo Apr 21 '17 at 13:54
  • This was on an early beta release, I cannot reproduce it anymore so it stays as it is. I did this for the sake of completeness, the accepted answer is actually not this one. – rll Apr 21 '17 at 19:14
17

Download *.zip solution for angular 2.4.x: you must import ResponseContentType from '@angular/http' and change responseType to ResponseContentType.ArrayBuffer (by default it ResponseContentType.Json)

getZip(path: string, params: URLSearchParams = new URLSearchParams()): Observable<any> {
 let headers = this.setHeaders({
      'Content-Type': 'application/zip',
      'Accept': 'application/zip'
    });

 return this.http.get(`${environment.apiUrl}${path}`, { 
   headers: headers, 
   search: params, 
   responseType: ResponseContentType.ArrayBuffer //magic
 })
          .catch(this.formatErrors)
          .map((res:Response) => res['_body']);
}
Frederik Struck-Schøning
  • 12,981
  • 8
  • 59
  • 68
Alex Dzeiko
  • 866
  • 11
  • 12
12

Downloading file through ajax is always a painful process and In my view it is best to let server and browser do this work of content type negotiation.

I think its best to have

<a href="api/sample/download"></a> 

to do it. This doesn't even require any new windows opening and stuff like that.

The MVC controller as in your sample can be like the one below:

[HttpGet("[action]")]
public async Task<FileContentResult> DownloadFile()
{
    // ...
    return File(dataStream.ToArray(), "text/plain", "myblob.txt");
}
Massimiliano Kraus
  • 3,638
  • 5
  • 27
  • 47
GingerBeer
  • 888
  • 10
  • 11
  • 2
    You're right, but then how can you manage server errors within the single-page application? In case of an error, normally, a REST service returns the JSON with the error, thus resulting the application to open the JSON in another browser window, which is not what the user wants to see – Luca Apr 04 '17 at 13:01
  • 4
    If you have an access token you need to provide this doesn't work – chris31389 Sep 20 '17 at 16:15
  • This is plain and simple. But if you wanna do some authentication, then there is a possibility of having something like a one time token. So, instead of having it like this, you can have the url as: https://example.com/myuri/report?tokenid=1234-1233 And verify the token ID in the database. Of course its not a simple scenario and works in all the situations, but can be a solution in situation where, you have access to the database before returning the report as a stream.. – GingerBeer Sep 22 '17 at 09:59
  • Get the download url from the server. So the server can prepare the url with onetime security token. – Jens Alenius Oct 23 '17 at 13:23
12

I am using Angular 4 with the 4.3 httpClient object. I modified an answer I found in Js' Technical Blog which creates a link object, uses it to do the download, then destroys it.

Client:

doDownload(id: number, contentType: string) {
    return this.http
        .get(this.downloadUrl + id.toString(), { headers: new HttpHeaders().append('Content-Type', contentType), responseType: 'blob', observe: 'body' })
}

downloadFile(id: number, contentType: string, filename:string)  {

    return this.doDownload(id, contentType).subscribe(  
        res => { 
            var url = window.URL.createObjectURL(res);
            var a = document.createElement('a');
            document.body.appendChild(a);
            a.setAttribute('style', 'display: none');
            a.href = url;
            a.download = filename;
            a.click();
            window.URL.revokeObjectURL(url);
            a.remove(); // remove the element
        }, error => {
            console.log('download error:', JSON.stringify(error));
        }, () => {
            console.log('Completed file download.')
        }); 

} 

The value of this.downloadUrl has been set previously to point to the api. I am using this to download attachments, so I know the id, contentType and filename: I am using an MVC api to return the file:

 [ResponseCache(Location = ResponseCacheLocation.None, NoStore = true)]
    public FileContentResult GetAttachment(Int32 attachmentID)
    { 
        Attachment AT = filerep.GetAttachment(attachmentID);            
        if (AT != null)
        {
            return new FileContentResult(AT.FileBytes, AT.ContentType);  
        }
        else
        { 
            return null;
        } 
    } 

The attachment class looks like this:

 public class Attachment
{  
    public Int32 AttachmentID { get; set; }
    public string FileName { get; set; }
    public byte[] FileBytes { get; set; }
    public string ContentType { get; set; } 
}

The filerep repository returns the file from the database.

Hope this helps someone :)

Frederik Struck-Schøning
  • 12,981
  • 8
  • 59
  • 68
davaus
  • 1,145
  • 13
  • 16
9

It will be better if you try to call the new method inside you subscribe

this._reportService.getReport()
    .subscribe((data: any) => {
        this.downloadFile(data);
    },
        (error: any) => сonsole.log(error),
        () => console.log('Complete')
    );

Inside downloadFile(data) function we need to make block, link, href and file name

downloadFile(data: any, type: number, name: string) {
    const blob = new Blob([data], {type: 'text/csv'});
    const dataURL = window.URL.createObjectURL(blob);

    // IE doesn't allow using a blob object directly as link href
    // instead it is necessary to use msSaveOrOpenBlob
    if (window.navigator && window.navigator.msSaveOrOpenBlob) {
      window.navigator.msSaveOrOpenBlob(blob);
      return;
    }

    const link = document.createElement('a');
    link.href = dataURL;
    link.download = 'export file.csv';
    link.click();

    setTimeout(() => {

      // For Firefox it is necessary to delay revoking the ObjectURL
      window.URL.revokeObjectURL(dataURL);
      }, 100);
    }
}
Volodymyr Khmil
  • 1,078
  • 15
  • 13
8

Well, I wrote a piece of code inspired by many of the above answers that should easily work in most scenarios where the server sends a file with a content disposition header, without any third-party installations, except rxjs and angular.

First, how to call the code from your component file

this.httpclient.get(
   `${myBackend}`,
   {
      observe: 'response',
      responseType: 'blob'
   }
).pipe(first())
.subscribe(response => SaveFileResponse(response, 'Custom File Name.extension'));

As you can see, it's basically pretty much the average backend call from angular, with two changes

  1. I am observing the response instead of the body
  2. I am being explicit about the response being a blob

Once the file is fetched from the server, I am in principle, delegating the entire task of saving the file to the helper function, which I keep in a separate file, and import into whichever component I need to

export const SaveFileResponse = 
(response: HttpResponse<Blob>, 
 filename: string = null) => 
{
    //null-checks, just because :P
    if (response == null || response.body == null)
        return;

    let serverProvidesName: boolean = true;
    if (filename != null)
        serverProvidesName = false;

    //assuming the header is something like
    //content-disposition: attachment; filename=TestDownload.xlsx; filename*=UTF-8''TestDownload.xlsx
    if (serverProvidesName)
        try {
            let f: string = response.headers.get('content-disposition').split(';')[1];
            if (f.includes('filename='))
                filename = f.substring(10);
        }
        catch { }
    SaveFile(response.body, filename);
}

//Create an anchor element, attach file to it, and
//programmatically click it. 
export const SaveFile = (blobfile: Blob, filename: string = null) => {
    const a = document.createElement('a');
    a.href = window.URL.createObjectURL(blobfile);
    a.download = filename;
    a.click();
}

There, no more cryptic GUID filenames! We can use whatever name the server provides, without having to specify it explicitly in the client, or, overwrite the filename provided by the server (as in this example). Also, one can easily, if need be, change the algorithm of extracting the filename from the content-disposition to suit their needs, and everything else will stay unaffected - in case of an error during such extraction, it will just pass 'null' as the filename.

As another answer already pointed out, IE needs some special treatment, as always. But with chromium edge coming in a few months, I wouldn't worry about that while building new apps (hopefully). There is also the matter of revoking the URL, but I'm kinda not-so-sure about that, so if someone could help out with that in the comments, that would be awesome.

7

I share the solution that helped me (any improvement is greatly appreciated)

On your service 'pservice' :

getMyFileFromBackend(typeName: string): Observable<any>{
    let param = new URLSearchParams();
    param.set('type', typeName);
    // setting 'responseType: 2' tells angular that you are loading an arraybuffer
    return this.http.get(http://MYSITE/API/FILEIMPORT, {search: params, responseType: 2})
            .map(res => res.text())
            .catch((error:any) => Observable.throw(error || 'Server error'));
}

Component Part :

downloadfile(type: string){
   this.pservice.getMyFileFromBackend(typename).subscribe(
                    res => this.extractData(res),
                    (error:any) => Observable.throw(error || 'Server error')
                );
}

extractData(res: string){
    // transforme response to blob
    let myBlob: Blob = new Blob([res], {type: 'application/vnd.oasis.opendocument.spreadsheet'}); // replace the type by whatever type is your response

    var fileURL = URL.createObjectURL(myBlob);
    // Cross your fingers at this point and pray whatever you're used to pray
    window.open(fileURL);
}

On the component part, you call the service without subscribing to a response. The subscribe for a complete list of openOffice mime types see : http://www.openoffice.org/framework/documentation/mimetypes/mimetypes.html

Frederik Struck-Schøning
  • 12,981
  • 8
  • 59
  • 68
Ismail H
  • 4,226
  • 2
  • 38
  • 61
7

For those using Redux Pattern

I added in the file-saver as @Hector Cuevas named in his answer. Using Angular2 v. 2.3.1, I didn't need to add in the @types/file-saver.

The following example is to download a journal as PDF.

The journal actions

public static DOWNLOAD_JOURNALS = '[Journals] Download as PDF';
public downloadJournals(referenceId: string): Action {
 return {
   type: JournalActions.DOWNLOAD_JOURNALS,
   payload: { referenceId: referenceId }
 };
}

public static DOWNLOAD_JOURNALS_SUCCESS = '[Journals] Download as PDF Success';
public downloadJournalsSuccess(blob: Blob): Action {
 return {
   type: JournalActions.DOWNLOAD_JOURNALS_SUCCESS,
   payload: { blob: blob }
 };
}

The journal effects

@Effect() download$ = this.actions$
    .ofType(JournalActions.DOWNLOAD_JOURNALS)
    .switchMap(({payload}) =>
        this._journalApiService.downloadJournal(payload.referenceId)
        .map((blob) => this._actions.downloadJournalsSuccess(blob))
        .catch((err) => handleError(err, this._actions.downloadJournalsFail(err)))
    );

@Effect() downloadJournalSuccess$ = this.actions$
    .ofType(JournalActions.DOWNLOAD_JOURNALS_SUCCESS)
    .map(({payload}) => saveBlobAs(payload.blob, 'journal.pdf'))

The journal service

public downloadJournal(referenceId: string): Observable<any> {
    const url = `${this._config.momentumApi}/api/journals/${referenceId}/download`;
    return this._http.getBlob(url);
}

The HTTP service

public getBlob = (url: string): Observable<any> => {
    return this.request({
        method: RequestMethod.Get,
        url: url,
        responseType: ResponseContentType.Blob
    });
};

The journal reducer Though this only sets the correct states used in our application I still wanted to add it in to show the complete pattern.

case JournalActions.DOWNLOAD_JOURNALS: {
  return Object.assign({}, state, <IJournalState>{ downloading: true, hasValidationErrors: false, errors: [] });
}

case JournalActions.DOWNLOAD_JOURNALS_SUCCESS: {
  return Object.assign({}, state, <IJournalState>{ downloading: false, hasValidationErrors: false, errors: [] });
}

I hope this is helpful.

Frederik Struck-Schøning
  • 12,981
  • 8
  • 59
  • 68
Casper Nybroe
  • 1,179
  • 3
  • 23
  • 47
5

To download and show PDF files, a very similar code snipped is like below:

  private downloadFile(data: Response): void {
    let blob = new Blob([data.blob()], { type: "application/pdf" });
    let url = window.URL.createObjectURL(blob);
    window.open(url);
  }

  public showFile(fileEndpointPath: string): void {
    let reqOpt: RequestOptions = this.getAcmOptions();  //  getAcmOptions is our helper method. Change this line according to request headers you need.
    reqOpt.responseType = ResponseContentType.Blob;
    this.http
      .get(fileEndpointPath, reqOpt)
      .subscribe(
        data => this.downloadFile(data),
        error => alert("Error downloading file!"),
        () => console.log("OK!")
      );
  }
Frederik Struck-Schøning
  • 12,981
  • 8
  • 59
  • 68
Baatar
  • 221
  • 2
  • 9
5

Here's something I did in my case -

// service method
downloadFiles(vendorName, fileName) {
    return this.http.get(this.appconstants.filesDownloadUrl, { params: { vendorName: vendorName, fileName: fileName }, responseType: 'arraybuffer' }).map((res: ArrayBuffer) => { return res; })
        .catch((error: any) => _throw('Server error: ' + error));
}

// a controller function which actually downloads the file
saveData(data, fileName) {
    var a = document.createElement("a");
    document.body.appendChild(a);
    a.style = "display: none";
    let blob = new Blob([data], { type: "octet/stream" }),
        url = window.URL.createObjectURL(blob);
    a.href = url;
    a.download = fileName;
    a.click();
    window.URL.revokeObjectURL(url);
}

// a controller function to be called on requesting a download
downloadFiles() {
    this.service.downloadFiles(this.vendorName, this.fileName).subscribe(data => this.saveData(data, this.fileName), error => console.log("Error downloading the file."),
        () => console.info("OK"));
}

The solution is referenced from - here

Tushar Walzade
  • 3,737
  • 4
  • 33
  • 56
5

I found the answers so far lacking insight as well as warnings. You could and should watch for incompatibilities with IE10+ (if you care).

This is the complete example with the application part and service part after. Note that we set the observe: "response" to catch the header for the filename. Also note that the Content-Disposition header has to be set and exposed by the server, otherwise the current Angular HttpClient will not pass it on. I added a dotnet core piece of code for that below.

public exportAsExcelFile(dataId: InputData) {
    return this.http.get(this.apiUrl + `event/export/${event.id}`, {
        responseType: "blob",
        observe: "response"
    }).pipe(
        tap(response => {
            this.downloadFile(response.body, this.parseFilename(response.headers.get('Content-Disposition')));
        })
    );
}

private downloadFile(data: Blob, filename: string) {
    const blob = new Blob([data], {type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=utf-8;'});
    if (navigator.msSaveBlob) { // IE 10+
        navigator.msSaveBlob(blob, filename);
    } else {
        const link = document.createElement('a');
        if (link.download !== undefined) {
            // Browsers that support HTML5 download attribute
            const url = URL.createObjectURL(blob);
            link.setAttribute('href', url);
            link.setAttribute('download', filename);
            link.style.visibility = 'hidden';
            document.body.appendChild(link);
            link.click();
            document.body.removeChild(link);
        }
    }
}

private parseFilename(contentDisposition): string {
    if (!contentDisposition) return null;
    let matches = /filename="(.*?)"/g.exec(contentDisposition);

    return matches && matches.length > 1 ? matches[1] : null;
}

Dotnet core, with Content-Disposition & MediaType

 private object ConvertFileResponse(ExcelOutputDto excelOutput)
    {
        if (excelOutput != null)
        {
            ContentDisposition contentDisposition = new ContentDisposition
            {
                FileName = excelOutput.FileName.Contains(_excelExportService.XlsxExtension) ? excelOutput.FileName : "TeamsiteExport.xlsx",
                Inline = false
            };
            Response.Headers.Add("Access-Control-Expose-Headers", "Content-Disposition");
            Response.Headers.Add("Content-Disposition", contentDisposition.ToString());
            return File(excelOutput.ExcelSheet, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
        }
        else
        {
            throw new UserFriendlyException("The excel output was empty due to no events.");
        }
    }
David Zwart
  • 431
  • 5
  • 23
4

Update to Hector's answer using file-saver and HttpClient for step 2:

public downloadFile(file: File): Observable<Blob> {
    return this.http.get(file.fullPath, {responseType: 'blob'})
}
dmcgrandle
  • 5,934
  • 1
  • 19
  • 38
4

The following code worked for me

Made the HTML like this:

<button type="button" onclick="startDownload(someData)">Click to download!</button>

JS is as follows:

let someData = {};
someData.name = 'someName';
someData.fileurl= 'someUrl';

function startDownload(someData){
    let link = document.createElement('a');
    link.href = someData.fileurl; //data is object received as response
    link.download = someData.fileurl.substr(someData.fileurl.lastIndexOf('/') + 1);
    link.click();
}
Basil
  • 1,664
  • 13
  • 25
3

I got a solution for downloading from angular 2 without getting corrupt, using spring mvc and angular 2

1st- my return type is :-ResponseEntity from java end. Here I am sending byte[] array has return type from the controller.

2nd- to include the filesaver in your workspace-in the index page as:

<script src="https://cdnjs.cloudflare.com/ajax/libs/FileSaver.js/2014-11-29/FileSaver.min.js"></script>

3rd- at component ts write this code:

import {ResponseContentType} from '@angular.core';

let headers = new Headers({ 'Content-Type': 'application/json', 'MyApp-Application' : 'AppName', 'Accept': 'application/pdf' });
        let options = new RequestOptions({ headers: headers, responseType: ResponseContentType.Blob });
            this.http
            .post('/project/test/export',
                    somevalue,options)
              .subscribe(data => {

                  var mediaType = 'application/vnd.ms-excel';
                  let blob: Blob = data.blob();
                    window['saveAs'](blob, 'sample.xls');

                });

This will give you xls file format. If you want other formats change the mediatype and file name with right extension.

Mel
  • 5,837
  • 10
  • 37
  • 42
user2025069
  • 101
  • 2
  • 7
3

<a href="my_url" download="myfilename">Download file</a>

my_url should have the same origin, otherwise it will redirect to that location

3

I was facing this same case today, I had to download a pdf file as an attachment (the file shouldn't be rendered in the browser, but downloaded instead). To achieve that I discovered I had to get the file in an Angular Blob, and, at the same time, add a Content-Disposition header in the response.

This was the simplest I could get (Angular 7):

Inside the service:

getFile(id: String): Observable<HttpResponse<Blob>> {
  return this.http.get(`./file/${id}`, {responseType: 'blob', observe: 'response'});
}

Then, when I need to download the file in a component, I can simply:

fileService.getFile('123').subscribe((file: HttpResponse<Blob>) => window.location.href = file.url);

UPDATE:

Removed unnecessary header setting from service

  • If I use window.location.href instead of window.open Chrome treats it as multiple file downloads. – DanO Jun 13 '19 at 16:33
  • this won't work if you have auth token needed in header – garg10may Dec 12 '19 at 14:13
  • If you save file with randomized name, you can permit security for download url. By removing security from download url, improvement in the donwload speed will be achieved. – whitefang Sep 10 '21 at 14:18
3

Angular 12 + ASP.NET 5 WEB API

You can return a Blob object from the server and create an anchor tag and set the href property to an object URL created from the Blob. Now clicking on the anchor will download the file. You can set the file name as well.

downloadFile(path: string): Observable<any> {
        return this._httpClient.post(`${environment.ApiRoot}/accountVerification/downloadFile`, { path: path }, {
            observe: 'response',
            responseType: 'blob'
        });
    }

saveFile(path: string, fileName: string): void {
            this._accountApprovalsService.downloadFile(path).pipe(
                take(1)
            ).subscribe((resp) => {
                let downloadLink = document.createElement('a');
                downloadLink.href = window.URL.createObjectURL(resp.body);
                downloadLink.setAttribute('download', fileName);
                document.body.appendChild(downloadLink);
                downloadLink.click();
                downloadLink.remove();
            });
            
        }

Backend

[HttpPost]
[Authorize(Roles = "SystemAdmin, SystemUser")]
public async Task<IActionResult> DownloadFile(FilePath model)
{
    if (ModelState.IsValid)
    {
        try
        {
            var fileName = System.IO.Path.GetFileName(model.Path);
            var content = await System.IO.File.ReadAllBytesAsync(model.Path);
            new FileExtensionContentTypeProvider()
                .TryGetContentType(fileName, out string contentType);
            return File(content, contentType, fileName);
        }
        catch
        {
            return BadRequest();
        }
    }

    return BadRequest();

}
Tanvir
  • 111
  • 1
  • 3
1
 let headers = new Headers({
                'Content-Type': 'application/json',
                'MyApp-Application': 'AppName',
                'Accept': 'application/vnd.ms-excel'
            });
            let options = new RequestOptions({
                headers: headers,
                responseType: ResponseContentType.Blob
            });


this.http.post(this.urlName + '/services/exportNewUpc', localStorageValue, options)
                .subscribe(data => {
                    if (navigator.appVersion.toString().indexOf('.NET') > 0)
                    window.navigator.msSaveBlob(data.blob(), "Export_NewUPC-Items_" + this.selectedcategory + "_" + this.retailname +"_Report_"+this.myDate+".xlsx");

                    else {
                        var a = document.createElement("a");
                        a.href = URL.createObjectURL(data.blob());
                        a.download = "Export_NewUPC-Items_" + this.selectedcategory + "_" + this.retailname +"_Report_"+this.myDate+ ".xlsx";
                        a.click();
                    }
                    this.ui_loader = false;
                    this.selectedexport = 0;
                }, error => {
                    console.log(error.json());
                    this.ui_loader = false;
                    document.getElementById("exceptionerror").click();
                });
user2025069
  • 101
  • 2
  • 7
1

Simply put the url as href as below .

<a href="my_url">Download File</a>
Harun Or Rashid
  • 5,589
  • 1
  • 19
  • 21
1

You may also download a file directly from your template where you use download attribute and to [attr.href] you can provide a property value from the component. This simple solution should work on most browsers.

<a download [attr.href]="yourDownloadLink"></a>

Reference: https://www.w3schools.com/tags/att_a_download.asp

B--rian
  • 5,578
  • 10
  • 38
  • 89
Max
  • 11
  • 3
1

Create a temporary anchor tag, then click it programmatically with Javascript

async function downloadFile(fileName) {
    const url = document.getElementById("url").value

    const link = document.createElement('a');
    link.href = await toDataURL(url);
    link.setAttribute('download', fileName ? fileName : url.split('/').pop());
    link.setAttribute('target', 'blank');
    document.body.appendChild(link);
    link.click();
}


function toDataURL(url) {
    return fetch(url)
        .then((response) => {
            return response.blob();
        })
        .then((blob) => {
            return URL.createObjectURL(blob);
        });
}
<input id="url" value="https://images.pexels.com/photos/1741205/pexels-photo-1741205.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=2"/>
<button onclick="downloadFile('test')">Download</button>
James Ikubi
  • 2,552
  • 25
  • 18
1

Although the question is old, none of the answers are that viable. As far as I saw all the files are first loaded in memory and saved after that. This way we:

  1. Cause a lag, for which a custom loading must be implemented.
  2. Load the file in memory, which means for big files the browser will crash.
  3. Do not use the implemented browser download function.

The front end side is simple enough (Angular 12):

downloadFile(url: string, fileName: string): void {
   const downloadLink = document.createElement('a');
   downloadLink.download = fileName;
   downloadLink.href = url;
   downloadLink.click();
}

On the back end (.NET 6) we need to work with streams and write to the response body:

public void Get(string fileId)
{
    var fileName = fileService.GetFileName(fileId);
    var fileContentType = fileService.GetFileContentType(fileId);
    this.Response.Headers.Add(HeaderNames.ContentType, fileContentType);
    this.Response.Headers.Add(HeaderNames.ContentDisposition, $"attachment; filename=\"{fileName}\"");
    fileService.GetFile(Response.Body, fileId);
}

File content type and name can be retrieved from either the DB (if you save file info in there) or the file system. Content type is parsed from the extension.

I write to the stream like this:

public void GetFile(Stream writeStream, string fileId)
{
    var file = GetFileInfo(fileId);
    try
    {
        var fileStream = File.OpenRead(file.FullName);
        byte[] buffer = new byte[32768];
        int read;
        while ((read = fileStream.Read(buffer, 0, buffer.Length)) > 0)
        {
            writeStream.Write(buffer, 0, read);
        }
        writeStream.Flush();
    }
    catch (Exception e)
    {
        throw new CustomException($"Error occured while reading the file. Inner Exception Message: ({e.Message}) Stack Trace: ({e.StackTrace})", ErrorCode.FileReadFailure, e);
    }
}

Keep in mind I have simplified my implementation for presentation purposes, so it has not been tested.

  • I was just wondering, @Aleksandar if you have written unit test for your Angular decision. Thanks! – Anton Mitsev Jul 22 '22 at 11:25
  • 1
    @AntonMitsev no, I have not written any unit tests for the Angular side of things. However, it is pretty standart way to auto-download files - I have used it here for simplicity sake, in practice I would use something like File Saver - https://www.npmjs.com/package/file-saver. – Aleksandar Angelov Jul 25 '22 at 07:11
1

The answers I found were either not working on Angular 13.1 and/or unnecessary complex (like the accepted example) without explaining why this is necessary. It would be useful for constantly changing ecosystems like Angular to require the version number to be attached.

The mini snippet provided by user @Aleksandar Angelov bypasses the session system, so an unnecessary authorization is necessary.

Derived by his answer, I came up with the following code:

  downloadConfiguration(url: string, filename: string) {
    this.http.get(url, {responseType: 'blob'})
    .subscribe(data => {
      // console.log("data", data);
      var url = window.URL.createObjectURL(data);
      let downloadLink = document.createElement('a');
      downloadLink.href = url
      downloadLink.setAttribute('download', filename);
      downloadLink.click();
    });
  }
Jay-Pi
  • 343
  • 3
  • 13
0

If you only send the parameters to a URL, you can do it this way:

downloadfile(runname: string, type: string): string {
   return window.location.href = `${this.files_api + this.title +"/"+ runname + "/?file="+ type}`;
}

in the service that receives the parameters

Joe68
  • 11
0

This answer suggests that you cannot download files directly with AJAX, primarily for security reasons. So I'll describe what I do in this situation,

01. Add href attribute in your anchor tag inside the component.html file,
eg:-

<div>
       <a [href]="fileUrl" mat-raised-button (click)='getGenaratedLetterTemplate(element)'> GENARATE </a>
</div>

02. Do all following steps in your component.ts to bypass the security level and bring the save as popup dialog,
eg:-

import { environment } from 'environments/environment';
import { DomSanitizer } from '@angular/platform-browser';
export class ViewHrApprovalComponent implements OnInit {
private apiUrl = environment.apiUrl;
  fileUrl
 constructor(
    private sanitizer: DomSanitizer,
    private letterService: LetterService) {}
getGenaratedLetterTemplate(letter) {

    this.data.getGenaratedLetterTemplate(letter.letterId).subscribe(
      // cannot download files directly with AJAX, primarily for security reasons);
    console.log(this.apiUrl + 'getGeneratedLetter/' + letter.letterId);
    this.fileUrl = this.sanitizer.bypassSecurityTrustResourceUrl(this.apiUrl + 'getGeneratedLetter/' + letter.letterId);
  }

Note: This answer will work if you are getting an error "OK" with status code 200

INDRAJITH EKANAYAKE
  • 3,894
  • 11
  • 41
  • 63
0

If a tab opens and closes without downloading anything, i tried following with mock anchor link and it worked.

downloadFile(x: any) {
var newBlob = new Blob([x], { type: "application/octet-stream" });

    // IE doesn't allow using a blob object directly as link href
    // instead it is necessary to use msSaveOrOpenBlob
    if (window.navigator && window.navigator.msSaveOrOpenBlob) {
      window.navigator.msSaveOrOpenBlob(newBlob);
      return;
    }

    // For other browsers: 
    // Create a link pointing to the ObjectURL containing the blob.
    const data = window.URL.createObjectURL(newBlob);

    var link = document.createElement('a');
    link.href = data;
    link.download = "mapped.xlsx";
    // this is necessary as link.click() does not work on the latest firefox
    link.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true, view: window }));

    setTimeout(function () {
      // For Firefox it is necessary to delay revoking the ObjectURL
      window.URL.revokeObjectURL(data);
      link.remove();
    }, 100);  }
Damitha
  • 662
  • 5
  • 7