1

I am attempting to download a file from Azure Storage in the form of an CloudBlockBlob. I want to allow the user to select where to put the downloaded file, so I have written the following code to do this

[AllowAnonymous]
public async Task<ActionResult> DownloadFile(string displayName)
{
    ApplicationUser user = null;
    if (ModelState.IsValid)
    {
        user = await UserManager.FindByIdAsync(User.Identity.GetUserId());

        // Retrieve storage account and blob client.
        CloudStorageAccount storageAccount = CloudStorageAccount.Parse(
            ConfigurationManager.AppSettings["StorageConnectionString"]);
        CloudBlobClient blobClient = storageAccount.CreateCloudBlobClient();
        CloudBlobContainer container = blobClient.GetContainerReference(
            VisasysNET.Utilities.Constants.ContainerName);

        // If the container does not exist, return error.
        if (container.Exists())
        {
            foreach (IListBlobItem item in container.ListBlobs(null, false))
            {
                if (item.GetType() == typeof(CloudBlockBlob))
                {
                    CloudBlockBlob blob = (CloudBlockBlob)item;
                    if (blob.Name.CompareNoCase(displayName))
                    {
                        using (Stream stream = new MemoryStream())
                        {
                            await blob.DownloadRangeToStreamAsync(stream, null, null);
                            return File(stream, displayName);
                        }
                    }
                }
            }
            return RedirectToAction("Index", "Tools");
        }
    }
    return new HttpStatusCodeResult(HttpStatusCode.ServiceUnavailable);
}

This idea was to return the FileStreamResult and allow the browser to provide the "Save Dialog". However, when the controller posts to the view, I get an

Cannot access a closed Stream. Exception Details: System.ObjectDisposedException: Cannot access a closed Stream.

Now the problem is clear, however, how to circumvent it is not. I want to ensure the MemoryStream is released and disposed as it is likely to hold data up to ~10M. How can I dispose the MemoryStream but allow the FileStreamResult to be passed back and used by the browser?

Thanks for your time.

b2zw2a
  • 2,663
  • 15
  • 15
MoonKnight
  • 23,214
  • 40
  • 145
  • 277

1 Answers1

1

Simply don't dispose it. Have look at FileStreamResult implementation:

 protected override void WriteFile(HttpResponseBase response) {
            // grab chunks of data and write to the output stream
            Stream outputStream = response.OutputStream;
            using (FileStream) {
                byte[] buffer = new byte[_bufferSize];

                while (true) {
                    int bytesRead = FileStream.Read(buffer, 0, _bufferSize);
                    if (bytesRead == 0) {
                        // no more data
                        break;
                    }

                    outputStream.Write(buffer, 0, bytesRead);
                }
            }
        }

FileStreamResult will dispose your stream after writing it to response.OutputStream;

If you don't have to proxy your downloads through the website (security) you can generate Shared Access Signature and redirect your user straight to the storage. See here

Community
  • 1
  • 1
b2zw2a
  • 2,663
  • 15
  • 15
  • This solves the exception, so thanks. But I now have the problem that once the `FileStreamResult` is returned the browser is not launching the save file as dialog. How can I get this to occur? I got this method from http://stackoverflow.com/a/19026034/626442. – MoonKnight Nov 26 '14 at 14:20