Downloading Azure Blob files in MVC3

22,781

Solution 1

Two options really... the first is to just redirect the user to the blob directly (if the blobs are in a public container). That would look a bit like:

return Redirect(container.GetBlobReference(name).Uri.AbsoluteUri);

If the blob is in a private container, you could either use a Shared Access Signature and do redirection like the previous example, or you could read the blob in your controller action and push it down to the client as a download:

Response.AddHeader("Content-Disposition", "attachment; filename=" + name); // force download
container.GetBlobReference(name).DownloadToStream(Response.OutputStream);
return new EmptyResult();

Solution 2

I noticed that writing to the response stream from the action method messes up the HTTP headers. Some expected headers are missing and others are not set correctly.

So instead of writing to the response stream, I get the blob content as a stream and pass it to the Controller.File() method.

CloudBlockBlob blob = container.GetBlockBlobReference(blobName);
Stream blobStream = blob.OpenRead();
return File(blobStream, blob.Properties.ContentType, "FileName.txt");

Solution 3

Here's a resumable version (useful for large files or allowing seek in video or audio playback) of private blob access:

public class AzureBlobStream : ActionResult
{
    private string filename, containerName;

    public AzureBlobStream(string containerName, string filename)
    {
        this.containerName = containerName;
        this.filename = filename;
        this.contentType = contentType;
    }

    public override void ExecuteResult(ControllerContext context)
    {
        var response = context.HttpContext.Response;
        var request = context.HttpContext.Request;

        var connectionString = ConfigurationManager.ConnectionStrings["Storage"].ConnectionString;
        var account = CloudStorageAccount.Parse(connectionString);
        var client = account.CreateCloudBlobClient();
        var container = client.GetContainerReference(containerName);
        var blob = container.GetBlockBlobReference(filename);

        blob.FetchAttributes();
        var fileLength = blob.Properties.Length;
        var fileExists = fileLength > 0;
        var etag = blob.Properties.ETag;

        var responseLength = fileLength;
        var buffer = new byte[4096];
        var startIndex = 0;

        //if the "If-Match" exists and is different to etag (or is equal to any "*" with no resource) then return 412 precondition failed
        if (request.Headers["If-Match"] == "*" && !fileExists ||
            request.Headers["If-Match"] != null && request.Headers["If-Match"] != "*" && request.Headers["If-Match"] != etag)
        {
            response.StatusCode = (int)HttpStatusCode.PreconditionFailed;
            return;
        }

        if (!fileExists)
        {
            response.StatusCode = (int)HttpStatusCode.NotFound;
            return;
        }

        if (request.Headers["If-None-Match"] == etag)
        {
            response.StatusCode = (int)HttpStatusCode.NotModified;
            return;
        }

        if (request.Headers["Range"] != null && (request.Headers["If-Range"] == null || request.Headers["IF-Range"] == etag))
        {
            var match = Regex.Match(request.Headers["Range"], @"bytes=(\d*)-(\d*)");
            startIndex = Util.Parse<int>(match.Groups[1].Value);
            responseLength = (Util.Parse<int?>(match.Groups[2].Value) + 1 ?? fileLength) - startIndex;
            response.StatusCode = (int)HttpStatusCode.PartialContent;
            response.Headers["Content-Range"] = "bytes " + startIndex + "-" + (startIndex + responseLength - 1) + "/" + fileLength;
        }

        response.Headers["Accept-Ranges"] = "bytes";
        response.Headers["Content-Length"] = responseLength.ToString();
        response.Cache.SetCacheability(HttpCacheability.Public); //required for etag output
        response.Cache.SetETag(etag); //required for IE9 resumable downloads
        response.ContentType = blob.Properties.ContentType;

        blob.DownloadRangeToStream(response.OutputStream, startIndex, responseLength);
    }
}

Example:

Response.AddHeader("Content-Disposition", "attachment; filename=" + filename); // force download
return new AzureBlobStream(blobContainerName, filename);
Share:
22,781
James
Author by

James

Experienced entrepreneur with a track record of building both businesses and products. Hands on, pragmatic servant leader whom is focused on the holistic and supporting objectives. Learned in the lean startup methods , customer discovery, customer-product fit and mvp with rapid iteration. Extensive history in starting and managing both physical and software projects. Strong adherence to TDD and Agile development methodology. Most proficient in Ruby on Rails and well versed in Python, Django, Javascript, C#, APS.NET MVC, SQL, JSON, REST, XML, HTML5, CSS3 and UX/UI design

Updated on April 29, 2020

Comments

  • James
    James about 4 years

    Our ASP.NET MVC 3 application is running on Azure and using Blob as file storage. I have the upload part figured out.

    The View is going to have the File Name, which, when clicked will prompt the file download screen to appear.

    Can anyone tell me how to go about doing this?

  • James
    James almost 13 years
    It is a private blob so I used the second method you post and it worked exactly how I wanted it. Thank you very much!
  • James
    James almost 13 years
    I want to hide the file name from the user (and put my own in) do you know how to do this?
  • user94559
    user94559 almost 13 years
    Just put whatever you want in the Content-Disposition header.
  • BentOnCoding
    BentOnCoding over 12 years
    @smarx why can't this header be set on an azure blob ?
  • user94559
    user94559 about 12 years
    @Robotsushi Because the blob API doesn't support it.
  • Mischa
    Mischa about 12 years
    I tried the last option, which works great, but with big files I run into the load balancer 60 second timeout and the file is not complete. Azure just stops sending data after 1 minute. Is there any way around that? blogs.msdn.com/b/avkashchauhan/archive/2011/11/12/…
  • user94559
    user94559 about 12 years
    WA only terminates idle connections after 60 seconds. I would expect .DownloadToStream(Response.OutputStream) to be sending data more frequently than once per 60 seconds. (It should truly be streaming the file, not downloading everything first and then sending it.)
  • dlras2
    dlras2 almost 9 years
    Any idea how to force the result's Cache-Control header to be the same as the blob's?
  • Martin Dawson
    Martin Dawson about 8 years
    What is Util.Parse?
  • WalderFrey
    WalderFrey about 7 years
    Do you need to add a Response.ContentType to ensure that the downloaded file behaves nicely in the browser (e.g. Firefox will let you open the file directly)?
  • Michael
    Michael about 4 years
    @MartinDawson oops should have simplified, it comes from this stackoverflow.com/a/6474962/222748