34

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?

David Makogon
  • 69,407
  • 21
  • 141
  • 189
James
  • 1,158
  • 4
  • 13
  • 23

3 Answers3

64

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();
user94559
  • 59,196
  • 6
  • 103
  • 103
  • 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 Jul 20 '11 at 19:29
  • I want to hide the file name from the user (and put my own in) do you know how to do this? – James Jul 20 '11 at 19:30
  • 2
    Just put whatever you want in the Content-Disposition header. – user94559 Jul 20 '11 at 19:32
  • @smarx why can't this header be set on an azure blob ? – BentOnCoding Jan 27 '12 at 03:16
  • @Robotsushi Because the blob API doesn't support it. – user94559 Feb 27 '12 at 17:37
  • 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? http://blogs.msdn.com/b/avkashchauhan/archive/2011/11/12/windows-azure-load-balancer-timeout-details.aspx – Mischa Mar 07 '12 at 06:09
  • 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.) – user94559 Mar 08 '12 at 00:56
  • 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)? – WalderFrey Mar 25 '17 at 08:28
11

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);
Michael
  • 11,571
  • 4
  • 63
  • 61
11

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");
Bassem
  • 2,736
  • 31
  • 13