34
votes

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?

3

3 Answers

64
votes

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();
11
votes

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);
11
votes

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");