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

210,379

Solution 1

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.

Solution 2

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.

Solution 3

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');
    })
  }
});

Solution 4

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.

Solution 5

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

Share:
210,379
kelsier
Author by

kelsier

Just another programmer ^-^ &gt;.&gt; Listen to Gojira, they are the coolest band on Planet Earth! :D

Updated on July 09, 2022

Comments

  • kelsier
    kelsier almost 2 years

    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;
    }
    
  • Bartosz Bialecki
    Bartosz Bialecki over 9 years
    Hi @Scott 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?
  • Scott
    Scott over 9 years
    @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.
  • Bartosz Bialecki
    Bartosz Bialecki over 9 years
  • Leandro Bardelli
    Leandro Bardelli over 9 years
    You could write the first method instead the "console.log("Not supported");" line :D
  • AlexandruC
    AlexandruC over 9 years
    Can you send parameters with window.open ? an array of ids maybe ?
  • Siddharth Pandey
    Siddharth Pandey over 9 years
    @Scott 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.
  • Dmitry Efimenko
    Dmitry Efimenko over 9 years
    @Scott, please see my answer/question. It is based on your answer. Any help is appreciated.
  • Dmitry Efimenko
    Dmitry Efimenko over 9 years
    My answer was removed so I created a new question specifically dedicated to the IE11 issue:
  • Scott
    Scott over 9 years
    @Yoda I have updated the answer with the fixed code for IE11. Yet another Microsoft quirk. :)
  • shadi
    shadi over 9 years
    @Scott What is the benefit of using saveBlob over 'window.open(downloadPath, '_blank', ''); '?
  • Scott
    Scott over 9 years
    @shadi 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.
  • t00f
    t00f over 9 years
    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!
  • Scott
    Scott about 9 years
    @MartinvanHaeften I'll try find a solution for Safari over the weekend
  • Alan Dunning
    Alan Dunning about 9 years
    How did you use Filesaver.js with this? How did you implement it?
  • PPB
    PPB about 9 years
    var blob = new Blob([atob(response.payload)], { "data":"attachment/csv;charset=utf-8;" }); saveAs(blob, 'filename');
  • Corey Quillen
    Corey Quillen over 8 years
    @Scott The simple download method triggers a pop-up that is blocked by default. Is there a way around this?
  • Scott
    Scott over 8 years
    @CoreyQuillen Blocked popups is a separate issue, outside the scope of the question. You can search that on SO or on Google. Good luck.
  • Jeeva J
    Jeeva J over 8 years
    I have copied this code. But I always get like file damaged. What can I do?
  • Jeeva J
    Jeeva J over 8 years
    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',
  • Scott
    Scott over 8 years
    @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.
  • Jeeva J
    Jeeva J over 8 years
    :-( sorry . I missed to see that. BTW this is working greatly. Even better than filesaver.js
  • user3517454
    user3517454 over 8 years
    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.
  • Scott
    Scott over 8 years
    @user3517454 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 luck
  • user3517454
    user3517454 over 8 years
    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.
  • Scott
    Scott over 8 years
    @user3517454 Glad you figured it out.
  • Kyle S
    Kyle S about 8 years
    @CoreyQuillen Simply change the '_blank' to '_self'. I think not popping up a new window is the better flow anyway.
  • Larry Flewwelling
    Larry Flewwelling about 8 years
    Thank you PPB, your solution worked for me except for the atob. That wasn't required for me.
  • Scott
    Scott about 8 years
    @iPhoneDev Yeah IE9 isn't very modern, you'll want 10+ caniuse.com/#search=blob
  • Robert Goldwein
    Robert Goldwein almost 8 years
    I'm probably missing something important, but why not just simply always use plain "window.location = ..."? Assuming server returns correct headers.
  • Scott
    Scott almost 8 years
    @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.
  • Scott
    Scott almost 8 years
    @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.
  • Robert Goldwein
    Robert Goldwein almost 8 years
    @Scott 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!
  • Erkin Djindjiev
    Erkin Djindjiev almost 8 years
    Thank you Scott for catching those items. I've refactored and added an explanation.
  • Robert Goldwein
    Robert Goldwein almost 8 years
    @Scott Please disregard my previous question, I typed faster than I was thinking.
  • Jeevanandan J
    Jeevanandan J over 7 years
    Blob is not supported in Safari. How to make this working safari browsers?
  • tfa
    tfa over 7 years
    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 stackoverflow.com/questions/9038625/detect-if-device-is-ios Step 2 (if ios) use this stackoverflow.com/questions/24485077/…
  • pradeep
    pradeep over 6 years
    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. Thanks
  • anatol
    anatol over 4 years