Monday, May 2, 2016

AngularJS : Download Files by Sending a HTTP Request to Web API

As you might already know you can’t directly use AngularJS/JQuery/JavaScript to download a file by sending a HTTP request. That’s because usually the file downloads are triggered when a user click on a link and link’s href is targeted to the file location.

But imagine that your server expects a HTTP request (of course an authenticated request), and he will be serving you the file as a octet-stream or as an Attachment in the response.

To understand the scenario properly, consider the following Web API action.
[HttpGet]
[Route("values/download")]
public HttpResponseMessage Download(string name)
{
    try
    {
        string fileName = string.Empty;
        if (name.Equals("pdf", StringComparison.InvariantCultureIgnoreCase))
        {
            fileName = "SamplePdf.pdf";
        }
        else if (name.Equals("zip", StringComparison.InvariantCultureIgnoreCase))
        {
            fileName = "SampleZip.zip";
        }
 
        if (!string.IsNullOrEmpty(fileName))
        {
            string filePath = HttpContext.Current.Server.MapPath("~/App_Data/") + fileName;
 
            using (MemoryStream ms = new MemoryStream())
            {
                using (FileStream file = new FileStream(filePath, FileMode.Open, FileAccess.Read))
                {
                    byte[] bytes = new byte[file.Length];
                    file.Read(bytes, 0, (int)file.Length);
                    ms.Write(bytes, 0, (int)file.Length);
 
                    HttpResponseMessage httpResponseMessage = new HttpResponseMessage();
                    httpResponseMessage.Content = new ByteArrayContent(bytes.ToArray());
                    httpResponseMessage.Content.Headers.Add("x-filename", fileName);
                    httpResponseMessage.Content.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream");
                    httpResponseMessage.Content.Headers.ContentDisposition = new ContentDispositionHeaderValue("attachment");
                    httpResponseMessage.Content.Headers.ContentDisposition.FileName = fileName;
                    httpResponseMessage.StatusCode = HttpStatusCode.OK;
                    return httpResponseMessage;
                }
            }
        }
        return this.Request.CreateResponse(HttpStatusCode.NotFound, "File not found.");
    }
    catch (Exception ex)
    {
        return this.Request.CreateResponse(HttpStatusCode.InternalServerError, ex);
    }
}
The action is expecting a URL parameter and based on the parameter it will be serving you either a pdf file or a zip file. Now to download a file by calling this endpoint, I believe what the most people would think is, following would work (including me).
$scope.downloadFile = function (name) {
   $http({
       method: 'GET',
       url: 'api/values/download',
       params: { name: name },
   }).success(function (data, status, headers) {
      
   }).error(function (data) {
       
   });
};
Now if you call the downloadFile method to send the HTTP request from client side (for instance on the ng-click event of a anchor tag), it will not download the file. The reason is because JavaScript can’t access file system to save the files.

So here is a workaround (in Stack Overflow by answered by Scott).
$scope.downloadFile = function (name) {
    $http({
        method: 'GET',
        url: 'api/values/download',
        params: { name: name },
        responseType: 'arraybuffer'
    }).success(function (data, status, headers) {
        headers = headers();
 
        var filename = headers['x-filename'];
        var contentType = headers['content-type'];
 
        var linkElement = document.createElement('a');
        try {
            var blob = new Blob([data], { type: contentType });
            var url = window.URL.createObjectURL(blob);
 
            linkElement.setAttribute('href', url);
            linkElement.setAttribute("download", filename);
 
            var clickEvent = new MouseEvent("click", {
                "view": window,
                "bubbles": true,
                "cancelable": false
            });
            linkElement.dispatchEvent(clickEvent);
        } catch (ex) {
            console.log(ex);
        }
    }).error(function (data) {
        console.log(data);
    });
};
So this is the output.
image
Download
image
Download
I have uploaded the sample to my OneDrive, so you can download and play around.

Happy Coding.

Regards,
Jaliya