48

I have been playing with the Azure Blob Storage service to save/recover files in a context of a web page to be hosted in Azure Web Pages.

During the learning process I have come with two solutions; the first basically uses DownloadToStream which does the same but with a FileStream. In this case I have to write the file in the server prior to return it to the user.

public static Stream GetFileContent(string fileName, HttpContextBase context)
{
      CloudBlobContainer container = GetBlobContainer();    
      CloudBlockBlob blockBlob = container.GetBlockBlobReference(fileName);                                       
      Stream fileStream = new FileStream(
          context.Server.MapPath("~/App_Data/files/" + fileName), FileMode.Create);   
      blockBlob.DownloadToStream(fileStream);
      fileStream.Close();    
      return File.OpenRead(context.Server.MapPath("~/App_Data/files/" + fileName));
}

public ActionResult Download(string fileName)
{
    byte[] fileContent = MyFileContext.GetFileContent(fileName);
    return File(fileContent, "application/zip", fileName);        
}

On the other hand I used the DownloadToByteArray function with writes the content of the Blob in an array of bytes initialized with the size of the Blob file.

public static byte[] GetFileContent(string fileName)
{
    CloudBlobContainer container = GetBlobContainer();           
    CloudBlockBlob blockBlob = container.GetBlockBlobReference(fileName);
    blockBlob.FetchAttributes();
    long fileByteLength = blockBlob.Properties.Length;
    byte[] fileContent = new byte[fileByteLength];
    for (int i = 0; i < fileByteLength; i++)
    {
        fileContent[i] = 0x20;
    }
    blockBlob.DownloadToByteArray(fileContent,0);
    return fileContent;
}

public ActionResult Download(string fileName)
{   
   byte[] fileContent = MyFileContext.GetFileStream(fileName);
   return File(fileContent, "application/zip", fileName);
}

When I look at both options I see the first needs to create a file in the server's disk whereas the second stores the data from the Blob in a byte array consuming memory. In my particular case I am going to handle file sizes of ~150 MB.

Given the circumstances (environment, file sizes...) which approach do you think is best?

JohnnyHK
  • 305,182
  • 66
  • 621
  • 471
Julen
  • 1,574
  • 3
  • 22
  • 38
  • Is your objective always downloading the files on user's computer? Or in other words, do you want to process this data before streaming it to user's browser or will it be OK for you to directly download the file on user's computer from Azure storage? – Gaurav Mantri Jun 19 '14 at 17:20
  • 1
    Just to not lose sight of something: `DownloadToStream` requires a `Stream` but `FileStream` is not the only stream type around. – CyberDude Jun 19 '14 at 19:19
  • @GauravMantri yes, the idea is just to donwload the file without performing any operation with it. – Julen Jun 20 '14 at 08:16
  • @CyberDude thanks, just thought that if I was handling files FileStream should be appropriate, or there is a better alternative? – Julen Jun 20 '14 at 08:17
  • 2
    See this similar question: http://stackoverflow.com/questions/6752000/downloading-azure-blob-files-in-mvc3 A solution is given where the file is directly sent to the Response output stream without an intermediate saving to disk. Alternatively, if the blobs are public and your business logic allows it, you can always just give out a direct http link to the blob. – CyberDude Jun 20 '14 at 12:44
  • 1
    @Julen what is the reasoning behind `fileContent[i] = 0x20;` ? – Hawkzey Sep 04 '19 at 14:35
  • It's 2022, I've found some code in my codebase that also uses the magic `fileContent[i] = 0x20;` and have no clue why... Apparently it resolved some issue with some PNG encoding? – ldam Jan 06 '22 at 15:14
  • Not sure if it is really needed, but that code is just setting all the elements in that newly allocated byte array to `space` [ASCII 32 = 0x20 in Hex]. – Anand Sowmithiran May 31 '22 at 13:39

3 Answers3

33

Instead of streaming the blob through your server, you could download it directly from the blob storage. My answer is built on top of Steve's response here: Downloading Azure Blob files in MVC3. For downloading a blob directly from the storage, you would utilize Shared Access Signature (SAS). Recently Azure Storage has introduced an enhancement, which allows you to specify Content-Disposition header in SAS. See this modified code.

    public static string GetDownloadLink(string fileName)
    {
        CloudBlobContainer container = GetBlobContainer();
        CloudBlockBlob blockBlob = container.GetBlockBlobReference(fileName);
        //Create an ad-hoc Shared Access Policy with read permissions which will expire in 12 hours
        SharedAccessBlobPolicy policy = new SharedAccessBlobPolicy()
        {
            Permissions = SharedAccessBlobPermissions.Read,
            SharedAccessExpiryTime = DateTime.UtcNow.AddHours(12),
        };
        //Set content-disposition header for force download
        SharedAccessBlobHeaders headers = new SharedAccessBlobHeaders()
        {
            ContentDisposition = string.Format("attachment;filename=\"{0}\"", fileName),
        };
        var sasToken = blockBlob.GetSharedAccessSignature(policy, headers);
        return blockBlob.Uri.AbsoluteUri + sasToken;
    }

    public ActionResult Download(string fileName)
    {
        var sasUrl = GetDownloadLink(fileName);
        //Redirect to SAS URL ... file will now be downloaded directly from blob storage.
        Redirect(sasUrl);
    }
Community
  • 1
  • 1
Gaurav Mantri
  • 128,066
  • 12
  • 206
  • 241
  • Thanks! It works nice! One question tough, should be considered as a potential security hole to open the permissions? In my solution I am setting a 30 second window. – Julen Jun 23 '14 at 08:29
  • 3
    Shared Expiry Time in my example was just for demonstration purpose only. You should set it based on the time you think your user would need to download the file. If you think, your users can download the file in 30 seconds, you can set it to that value. You may want to read this blog post for SAS best practices: http://blogs.msdn.com/b/windowsazurestorage/archive/2012/06/12/introducing-table-sas-shared-access-signature-queue-sas-and-update-to-blob-sas.aspx. HTH. – Gaurav Mantri Jun 23 '14 at 08:34
  • 1
    It's important to note that the SAS token timeout indicates when it is no longer possible to start a download. As long as the download starts before the SAS token expiration, the download can take minutes longer than that and it will download just fine. – Peter J Jan 19 '17 at 21:57
  • @PeterJ is correct. A timeout of 1-2 seconds should do the trick, but please take note that, should your server time get out of sync, you could run into problems. To prevent this make sure you synchronise your server system clock with an atomic world clock service. – CShark Jul 21 '20 at 16:48
  • This approach is not suitable for setting image src inside the html document as this would create a new download URI every time and the browser may not be able to cache it – zafar Jul 30 '21 at 02:26
21

The benefit of Stream is that you can deal with bits piece-by-piece as they are downloaded rather than building up a big byte[] and then operating on the full thing. Your use of Stream isn't really getting the benefits since you are writing to a file and then reading that full file into memory. A good use of the stream API would be to pipe the download stream directly to the request's response stream as shown in the answer here Downloading Azure Blob files in MVC3

Community
  • 1
  • 1
Robert Levy
  • 28,747
  • 6
  • 62
  • 94
18

If you are planning to use the DownloadToBytesArray (async or not), you will have to fetch blob attributes first to get an initial size of byte array.

And if you will be using DownloadToStream you will not have to do that. That's one saved HTTP call to the blob storage and if I am not mistaken, FetchAttributes() is executed as HTTP HEAD request and that will count as a normal transaction (it will cost you some money in other words).

Zygimantas
  • 8,547
  • 7
  • 42
  • 54