Download file from an ASP.NET Web API method using AngularJS
Asked Answered
L

8

132

In my Angular JS project, I've an <a> anchor tag, which when clicked makes an HTTP GET request to a WebAPI method that returns a file.

Now, I want the file to be downloaded to the user once the request is successful. How do I do that?

The anchor tag:

<a href="#" ng-click="getthefile()">Download img</a>

AngularJS:

$scope.getthefile = function () {        
    $http({
        method: 'GET',
        cache: false,
        url: $scope.appPath + 'CourseRegConfirm/getfile',            
        headers: {
            'Content-Type': 'application/json; charset=utf-8'
        }
    }).success(function (data, status) {
        console.log(data); // Displays text data if the file is a text file, binary if it's an image            
        // What should I write here to download the file I receive from the WebAPI method?
    }).error(function (data, status) {
        // ...
    });
}

My WebAPI method:

[Authorize]
[Route("getfile")]
public HttpResponseMessage GetTestFile()
{
    HttpResponseMessage result = null;
    var localFilePath = HttpContext.Current.Server.MapPath("~/timetable.jpg");

    if (!File.Exists(localFilePath))
    {
        result = Request.CreateResponse(HttpStatusCode.Gone);
    }
    else
    {
        // Serve the file to the client
        result = Request.CreateResponse(HttpStatusCode.OK);
        result.Content = new StreamContent(new FileStream(localFilePath, FileMode.Open, FileAccess.Read));
        result.Content.Headers.ContentDisposition = new System.Net.Http.Headers.ContentDispositionHeaderValue("attachment");
        result.Content.Headers.ContentDisposition.FileName = "SampleImg";                
    }

    return result;
}
Legend answered 6/6, 2014 at 10:49 Comment(6)
What would be the filetype? image only ?Freemon
@RashminJaviya Could be .jpg, .doc, .xlsx, .docx, .txt or .pdf.Legend
Which .Net framework you are using?Freemon
@RashminJaviya .net 4.5Legend
File is not supported in webapi controller. This is supported only by mvc controller.Clairvoyant
@Clairvoyant you should use File of System.IO.File not from controllerLorna
D
245

Support for downloading binary files in using ajax is not great, it is very much still under development as working drafts.

#Simple download method:

You can have the browser download the requested file simply by using the code below, and this is supported in all browsers, and will obviously trigger the WebApi request just the same.

$scope.downloadFile = function(downloadPath) { 
    window.open(downloadPath, '_blank', '');  
}

#Ajax binary download method:

Using ajax to download the binary file can be done in some browsers and below is an implementation that will work in the latest flavours of Chrome, Internet Explorer, FireFox and Safari.

It uses an arraybuffer response type, which is then converted into a JavaScript blob, which is then either presented to save using the saveBlob method - though this is only currently present in Internet Explorer - or turned into a blob data URL which is opened by the browser, triggering the download dialog if the mime type is supported for viewing in the browser.

###Internet Explorer 11 Support (Fixed) Note: Internet Explorer 11 did not like using the msSaveBlob function if it had been aliased - perhaps a security feature, but more likely a flaw, So using var saveBlob = navigator.msSaveBlob || navigator.webkitSaveBlob ... etc. to determine the available saveBlob support caused an exception; hence why the code below now tests for navigator.msSaveBlob separately. Thanks? Microsoft

// Based on an implementation here: web.student.tuwien.ac.at/~e0427417/jsdownload.html
$scope.downloadFile = function(httpPath) {
    // Use an arraybuffer
    $http.get(httpPath, { responseType: 'arraybuffer' })
    .success( function(data, status, headers) {

        var octetStreamMime = 'application/octet-stream';
        var success = false;

        // Get the headers
        headers = headers();

        // Get the filename from the x-filename header or default to "download.bin"
        var filename = headers['x-filename'] || 'download.bin';

        // Determine the content type from the header or default to "application/octet-stream"
        var contentType = headers['content-type'] || octetStreamMime;

        try
        {
            // Try using msSaveBlob if supported
            console.log("Trying saveBlob method ...");
            var blob = new Blob([data], { type: contentType });
            if(navigator.msSaveBlob)
                navigator.msSaveBlob(blob, filename);
            else {
                // Try using other saveBlob implementations, if available
                var saveBlob = navigator.webkitSaveBlob || navigator.mozSaveBlob || navigator.saveBlob;
                if(saveBlob === undefined) throw "Not supported";
                saveBlob(blob, filename);
            }
            console.log("saveBlob succeeded");
            success = true;
        } catch(ex)
        {
            console.log("saveBlob method failed with the following exception:");
            console.log(ex);
        }

        if(!success)
        {
            // Get the blob url creator
            var urlCreator = window.URL || window.webkitURL || window.mozURL || window.msURL;
            if(urlCreator)
            {
                // Try to use a download link
                var link = document.createElement('a');
                if('download' in link)
                {
                    // Try to simulate a click
                    try
                    {
                        // Prepare a blob URL
                        console.log("Trying download link method with simulated click ...");
                        var blob = new Blob([data], { type: contentType });
                        var url = urlCreator.createObjectURL(blob);
                        link.setAttribute('href', url);

                        // Set the download attribute (Supported in Chrome 14+ / Firefox 20+)
                        link.setAttribute("download", filename);

                        // Simulate clicking the download link
                        var event = document.createEvent('MouseEvents');
                        event.initMouseEvent('click', true, true, window, 1, 0, 0, 0, 0, false, false, false, false, 0, null);
                        link.dispatchEvent(event);
                        console.log("Download link method with simulated click succeeded");
                        success = true;

                    } catch(ex) {
                        console.log("Download link method with simulated click failed with the following exception:");
                        console.log(ex);
                    }
                }

                if(!success)
                {
                    // Fallback to window.location method
                    try
                    {
                        // Prepare a blob URL
                        // Use application/octet-stream when using window.location to force download
                        console.log("Trying download link method with window.location ...");
                        var blob = new Blob([data], { type: octetStreamMime });
                        var url = urlCreator.createObjectURL(blob);
                        window.location = url;
                        console.log("Download link method with window.location succeeded");
                        success = true;
                    } catch(ex) {
                        console.log("Download link method with window.location failed with the following exception:");
                        console.log(ex);
                    }
                }

            }
        }

        if(!success)
        {
            // Fallback to window.open method
            console.log("No methods worked for saving the arraybuffer, using last resort window.open");
            window.open(httpPath, '_blank', '');
        }
    })
    .error(function(data, status) {
        console.log("Request failed with status: " + status);

        // Optionally write the error out to scope
        $scope.errorDetails = "Request failed with status: " + status;
    });
};

##Usage:

var downloadPath = "/files/instructions.pdf";
$scope.downloadFile(downloadPath);

###Notes:

You should modify your WebApi method to return the following headers:

  • I have used the x-filename header to send the filename. This is a custom header for convenience, you could however extract the filename from the content-disposition header using regular expressions.

  • You should set the content-type mime header for your response too, so the browser knows the data format.

I hope this helps.

Deacon answered 9/6, 2014 at 21:7 Comment(32)
Hi @Deacon I used your method and it works but the browser save the file as type html not pdf. I set content-type to application/pdf and when I check in developer tools in chrome the type of response is set to application/pdf but when I save the file it is shown as html, it works, when I open it the file is opened as pdf but in the browser and have icon default for my browser. Do you know what could I do wrong?Decurrent
@BartoszBialecki Sounds like a very specific issue with your environment. Please create an SO question, so you can provide more information, such as browser version, the filename you set on the download. A copy of the code you are using, details of the returned header etc.Deacon
I created SO question: #25490507Decurrent
You could write the first method instead the "console.log("Not supported");" line :DSahara
Can you send parameters with window.open ? an array of ids maybe ?Disc
@Deacon I still get invalid calling object on saveBlob(blob, filename) in IE11 which gets resolved if I use .bind(navigator) as context. However, doing this gives me error in Chrome that cannot read property bind of undefined. Please help.Bighead
@Scott, please see my answer/question. It is based on your answer. Any help is appreciated.Paean
My answer was removed so I created a new question specifically dedicated to the IE11 issue:Paean
@Yoda I have updated the answer with the fixed code for IE11. Yet another Microsoft quirk. :)Deacon
@Deacon What is the benefit of using saveBlob over 'window.open(downloadPath, '_blank', ''); '?Barmecide
@Barmecide The benefit of the blob method is you can have the file download without user interaction, then prompt to save when it has finished downloading. I ultimately depends on the nature of your app. Most people won't need that functionality and the window.open method is fine. Of course if you also want to store to the JavaScript filesystem then the blob method is also useful.Deacon
I am facing the same problem with Yosemite Safari. @KrzysztofKowalski, did you find any workaround on this ? I get the following error Failed to load resource: Frame load interrupted. Thanks to for sharing this guys!Dissimilitude
@MartinvanHaeften I'll try find a solution for Safari over the weekendDeacon
@Deacon The simple download method triggers a pop-up that is blocked by default. Is there a way around this?Lavernlaverna
@CoreyQuillen Blocked popups is a separate issue, outside the scope of the question. You can search that on SO or on Google. Good luck.Deacon
I have copied this code. But I always get like file damaged. What can I do?Pentosan
Thanks for such wonderful solution. While request we should include response type as array buffer. Please include in your answer. I've wasted 3 to 4 hours for this. ex. responseType:'arraybuffer',Pentosan
@JeevaJsb Thanks for the praise, however it does detail that you should use an arrayBuffer, and the code does use it. See line 4 of the code $http.get(httpPath, { responseType: 'arraybuffer' }) so I am not sure why you feel this was missing from the solution.Deacon
:-( sorry . I missed to see that. BTW this is working greatly. Even better than filesaver.jsPentosan
When I try to download a Microsoft executable via this method, I get back a blob size that is approximately 1.5 times the actual file size. The file that gets downloaded has the incorrect size of the blob. Any thoughts on why this might be happening? Based on looking at fiddler, the size of the response is correct, but converting the content to a blob is increasing it somehow.Pellerin
@Pellerin You'll need to check: - Remember that size on disk can be more than the content-length in the request, because the OS keeps meta data about the file too. - Ensure that you server is not doing post processing on your request, which modifies the data response. - If you have a remote connection, try over HTTPS just to be sure the response isn't maliciously being intercepted. - If your implementation matches that of the answer it should work, so it's likely a server or implementation issue, so open a question with your specifics, i.e. your server details and browser details. Good luckDeacon
Finally figured out the problem... I had changed the server code from a post to get, but I hadn't changed the parameters for $http.get. So the response type was never being set as arraybuffer since it was being passed in as the third argument and not the second.Pellerin
@Pellerin Glad you figured it out.Deacon
@CoreyQuillen Simply change the '_blank' to '_self'. I think not popping up a new window is the better flow anyway.Trilemma
@iPhoneDev Yeah IE9 isn't very modern, you'll want 10+ caniuse.com/#search=blobDeacon
I'm probably missing something important, but why not just simply always use plain "window.location = ..."? Assuming server returns correct headers.Emaciation
@RobertGoldwein You can do that, but the assumption is that if you are using an angularjs application you want the user to remain in the application, where the state and ability to use the functionality after the download starts is maintained. If you navigate directly to the download there is no guarantee the application will remain active, as the browser may not handle the download the way we expect. Imagine if the server 500s or 404s the request. The user is now out of the Angular app. The simplest suggestion of opening the link in a new window using window.open is suggested.Deacon
@RobertGoldwein so yes there are simple techniques to starting the download, but the more complex, yet modern technique of using the arraybuffer, gives us more control. By all means it depends on your aim. It depends how Angular and dynamic you want to be.Deacon
@Deacon Agh, yes, you're right, window.open(). I guess this is also the reason why one would prefer using <form> over window.open(). Really bad thing is that using this simple way I can't know e.g. how the request ended up, but what if I want to download e.g. 1.5 GB? In that case "blob approach" would fail - or not? Thanks!Emaciation
@Deacon Please disregard my previous question, I typed faster than I was thinking.Emaciation
Blob is not supported in Safari. How to make this working safari browsers?Westphalia
I used your code. It is working fine, but how to get the response from the server. data variable is not having response .I need to catch the response and need to do some operations.How to do that. ThanksTerrigenous
K
10

C# WebApi PDF download all working with Angular JS Authentication

Web Api Controller

[HttpGet]
    [Authorize]
    [Route("OpenFile/{QRFileId}")]
    public HttpResponseMessage OpenFile(int QRFileId)
    {
        QRFileRepository _repo = new QRFileRepository();
        var QRFile = _repo.GetQRFileById(QRFileId);
        if (QRFile == null)
            return new HttpResponseMessage(HttpStatusCode.BadRequest);
        string path = ConfigurationManager.AppSettings["QRFolder"] + + QRFile.QRId + @"\" + QRFile.FileName;
        if (!File.Exists(path))
            return new HttpResponseMessage(HttpStatusCode.BadRequest);

        HttpResponseMessage response = new HttpResponseMessage(HttpStatusCode.OK);
        //response.Content = new StreamContent(new FileStream(localFilePath, FileMode.Open, FileAccess.Read));
        Byte[] bytes = File.ReadAllBytes(path);
        //String file = Convert.ToBase64String(bytes);
        response.Content = new ByteArrayContent(bytes);
        response.Content.Headers.ContentDisposition = new ContentDispositionHeaderValue("attachment");
        response.Content.Headers.ContentType = new MediaTypeHeaderValue("application/pdf");
        response.Content.Headers.ContentDisposition.FileName = QRFile.FileName;

        return response;
    }

Angular JS Service

this.getPDF = function (apiUrl) {
            var headers = {};
            headers.Authorization = 'Bearer ' + sessionStorage.tokenKey;
            var deferred = $q.defer();
            $http.get(
                hostApiUrl + apiUrl,
                {
                    responseType: 'arraybuffer',
                    headers: headers
                })
            .success(function (result, status, headers) {
                deferred.resolve(result);;
            })
             .error(function (data, status) {
                 console.log("Request failed with status: " + status);
             });
            return deferred.promise;
        }

        this.getPDF2 = function (apiUrl) {
            var promise = $http({
                method: 'GET',
                url: hostApiUrl + apiUrl,
                headers: { 'Authorization': 'Bearer ' + sessionStorage.tokenKey },
                responseType: 'arraybuffer'
            });
            promise.success(function (data) {
                return data;
            }).error(function (data, status) {
                console.log("Request failed with status: " + status);
            });
            return promise;
        }

Either one will do

Angular JS Controller calling the service

vm.open3 = function () {
        var downloadedData = crudService.getPDF('ClientQRDetails/openfile/29');
        downloadedData.then(function (result) {
            var file = new Blob([result], { type: 'application/pdf;base64' });
            var fileURL = window.URL.createObjectURL(file);
            var seconds = new Date().getTime() / 1000;
            var fileName = "cert" + parseInt(seconds) + ".pdf";
            var a = document.createElement("a");
            document.body.appendChild(a);
            a.style = "display: none";
            a.href = fileURL;
            a.download = fileName;
            a.click();
        });
    };

And last the HTML page

<a class="btn btn-primary" ng-click="vm.open3()">FILE Http with crud service (3 getPDF)</a>

This will be refactored just sharing the code now hope it helps someone as it took me a while to get this working.

Kyla answered 4/11, 2016 at 14:56 Comment(2)
Above code works on all systems except ios so use these steps if you need this to work on ios Step 1 check if ios https://mcmap.net/q/48374/-detect-if-device-is-ios Step 2 (if ios) use this #24485577Kyla
NOTE: developer.mozilla.org/en-US/docs/Web/API/URL/…Schreibe
C
6

For me the Web API was Rails and client side Angular used with Restangular and FileSaver.js

Web API

module Api
  module V1
    class DownloadsController < BaseController

      def show
        @download = Download.find(params[:id])
        send_data @download.blob_data
      end
    end
  end
end

HTML

 <a ng-click="download('foo')">download presentation</a>

Angular controller

 $scope.download = function(type) {
    return Download.get(type);
  };

Angular Service

'use strict';

app.service('Download', function Download(Restangular) {

  this.get = function(id) {
    return Restangular.one('api/v1/downloads', id).withHttpConfig({responseType: 'arraybuffer'}).get().then(function(data){
      console.log(data)
      var blob = new Blob([data], {
        type: "application/pdf"
      });
      //saveAs provided by FileSaver.js
      saveAs(blob, id + '.pdf');
    })
  }
});
Crispation answered 20/11, 2014 at 3:23 Comment(1)
How did you use Filesaver.js with this? How did you implement it?Ass
N
2

We also had to develop a solution which would even work with APIs requiring authentication (see this article)

Using AngularJS in a nutshell here is how we did it:

Step 1: Create a dedicated directive

// jQuery needed, uses Bootstrap classes, adjust the path of templateUrl
app.directive('pdfDownload', function() {
return {
    restrict: 'E',
    templateUrl: '/path/to/pdfDownload.tpl.html',
    scope: true,
    link: function(scope, element, attr) {
        var anchor = element.children()[0];

        // When the download starts, disable the link
        scope.$on('download-start', function() {
            $(anchor).attr('disabled', 'disabled');
        });

        // When the download finishes, attach the data to the link. Enable the link and change its appearance.
        scope.$on('downloaded', function(event, data) {
            $(anchor).attr({
                href: 'data:application/pdf;base64,' + data,
                download: attr.filename
            })
                .removeAttr('disabled')
                .text('Save')
                .removeClass('btn-primary')
                .addClass('btn-success');

            // Also overwrite the download pdf function to do nothing.
            scope.downloadPdf = function() {
            };
        });
    },
    controller: ['$scope', '$attrs', '$http', function($scope, $attrs, $http) {
        $scope.downloadPdf = function() {
            $scope.$emit('download-start');
            $http.get($attrs.url).then(function(response) {
                $scope.$emit('downloaded', response.data);
            });
        };
    }] 
});

Step 2: Create a template

<a href="" class="btn btn-primary" ng-click="downloadPdf()">Download</a>

Step 3: Use it

<pdf-download url="/some/path/to/a.pdf" filename="my-awesome-pdf"></pdf-download>

This will render a blue button. When clicked, a PDF will be downloaded (Caution: the backend has to deliver the PDF in Base64 encoding!) and put into the href. The button turns green and switches the text to Save. The user can click again and will be presented with a standard download file dialog for the file my-awesome.pdf.

Nemeth answered 25/9, 2014 at 7:43 Comment(0)
T
1

Send your file as a base64 string.

 var element = angular.element('<a/>');
                         element.attr({
                             href: 'data:attachment/csv;charset=utf-8,' + encodeURI(atob(response.payload)),
                             target: '_blank',
                             download: fname
                         })[0].click();

If attr method not working in Firefox You can also use javaScript setAttribute method

Theophany answered 23/1, 2015 at 5:24 Comment(2)
var blob = new Blob([atob(response.payload)], { "data":"attachment/csv;charset=utf-8;" }); saveAs(blob, 'filename');Theophany
Thank you PPB, your solution worked for me except for the atob. That wasn't required for me.Protest
F
0

You could implement a showfile function which takes in parameters of the data returned from the WEBApi, and a filename for the file you are trying to download. What I did was create a separate browser service identifies the user's browser and then handles the rendering of the file based on the browser. For instance if the target browser is chrome on an ipad, you have to use javascripts FileReader object.

FileService.showFile = function (data, fileName) {
    var blob = new Blob([data], { type: 'application/pdf' });

    if (BrowserService.isIE()) {
        window.navigator.msSaveOrOpenBlob(blob, fileName);
    }
    else if (BrowserService.isChromeIos()) {
        loadFileBlobFileReader(window, blob, fileName);
    }
    else if (BrowserService.isIOS() || BrowserService.isAndroid()) {
        var url = URL.createObjectURL(blob);
        window.location.href = url;
        window.document.title = fileName;
    } else {
        var url = URL.createObjectURL(blob);
        loadReportBrowser(url, window,fileName);
    }
}


function loadFileBrowser(url, window, fileName) {
    var iframe = window.document.createElement('iframe');
    iframe.src = url
    iframe.width = '100%';
    iframe.height = '100%';
    iframe.style.border = 'none';
    window.document.title = fileName;
    window.document.body.appendChild(iframe)
    window.document.body.style.margin = 0;
}

function loadFileBlobFileReader(window, blob,fileName) {
    var reader = new FileReader();
    reader.onload = function (e) {
        var bdata = btoa(reader.result);
        var datauri = 'data:application/pdf;base64,' + bdata;
        window.location.href = datauri;
        window.document.title = fileName;
    }
    reader.readAsBinaryString(blob);
}
Fetishism answered 26/5, 2016 at 11:8 Comment(1)
Thank you Scott for catching those items. I've refactored and added an explanation.Fetishism
E
0

I have gone through array of solutions and this is what I found to have worked great for me.

In my case I needed to send a post request with some credentials. Small overhead was to add jquery inside the script. But was worth it.

var printPDF = function () {
        //prevent double sending
        var sendz = {};
        sendz.action = "Print";
        sendz.url = "api/Print";
        jQuery('<form action="' + sendz.url + '" method="POST">' +
            '<input type="hidden" name="action" value="Print" />'+
            '<input type="hidden" name="userID" value="'+$scope.user.userID+'" />'+
            '<input type="hidden" name="ApiKey" value="' + $scope.user.ApiKey+'" />'+
            '</form>').appendTo('body').submit().remove();

    }
Egotism answered 15/2, 2019 at 19:11 Comment(0)
W
-1

In your component i.e angular js code:

function getthefile (){
window.location.href='http://localhost:1036/CourseRegConfirm/getfile';
};
Ween answered 5/6, 2018 at 9:33 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.