Uploading Directory to Azure Blob Storage

I was recently working on a ClickOnce application, that was part of a large business software solution hosted on Azure. I decided to host ClickOnce on Azure as well.  Azure blob storage does not really support directory structure, it just supports single level of containers, then all other files (content) goes into those containers via key/value pairs.  ClickOnce structure is more complex of course.  Luckily, Azure blob storage supports forward slash (“/”) in blob names.  You see where I am going now with it?  We just just come up with clever blob names, and as far as ClickOnce installer is concerned, it is still pulling up file from path such as

http://xxxx.blob.core.windows.net/test/2/2.txt

Where xxxx is my account name on Azure and test is the container name.  Even through the rest looks like folder structure, it is really a single blob with the name of “2/2.txt”.  On another note, there are many Azure browser that are aware of the forward slash and will on the fly convert the blobs to look like directory structure.  One example would be https://www.myazurestorage.com/

Now, with this in mind I can write a class that can upload a folder to blob storage.

using Microsoft.WindowsAzure;
using Microsoft.WindowsAzure.StorageClient;
using System;
using System.Collections.Generic;
using System.Configuration;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace AzureBloblUploader
{
    /// <summary>
    /// Blob storage manager class
    /// </summary>
    public class BlobManager
    {
        private readonly CloudStorageAccount _account;
        private readonly CloudBlobClient _blobClient;

        /// <summary>
        /// Initializes a new instance of the <see cref="BlobManager" /> class.
        /// </summary>
        /// <param name="connectionStringName">Name of the connection string in app.config or web.config file.</param>
        public BlobManager(string connectionStringName)
        {
            _account = CloudStorageAccount.Parse(ConfigurationManager.ConnectionStrings[connectionStringName].ConnectionString);

            _blobClient = _account.CreateCloudBlobClient();
            _blobClient.RetryPolicy = RetryPolicies.Retry(4, TimeSpan.Zero);
        }

        /// <summary>
        /// Updates or created a blob in Azure blobl storage
        /// </summary>
        /// <param name="containerName">Name of the container.</param>
        /// <param name="blobName">Name of the blob.</param>
        /// <param name="content">The content of the blob.</param>
        /// <returns></returns>
        public bool PutBlob(string containerName, string blobName, byte[] content)
        {
            return ExecuteWithExceptionHandlingAndReturnValue(
                    () =>
                    {
                        CloudBlobContainer container = _blobClient.GetContainerReference(containerName);
                        CloudBlob blob = container.GetBlobReference(blobName);
                        blob.UploadByteArray(content);
                    });
        }

        /// <summary>
        /// Creates the container in Azure blobl storage
        /// </summary>
        /// <param name="containerName">Name of the container.</param>
        /// <returns>True if contianer was created successfully</returns>
        public bool CreateContainer(string containerName)
        {
            return ExecuteWithExceptionHandlingAndReturnValue(
                    () =>
                    {
                        CloudBlobContainer container = _blobClient.GetContainerReference(containerName);
                        container.Create();
                    });
        }

        /// <summary>
        /// Checks if a container exist.
        /// </summary>
        /// <param name="containerName">Name of the container.</param>
        /// <returns>True if conainer exists</returns>
        public bool DoesContainerExist(string containerName)
        {
            bool returnValue = false;
            ExecuteWithExceptionHandling(
                    () =>
                    {
                        IEnumerable<CloudBlobContainer> containers = _blobClient.ListContainers();
                        returnValue = containers.Any(one => one.Name == containerName);
                    });
            return returnValue;
        }

        /// <summary>
        /// Uploads the directory to blobl storage
        /// </summary>
        /// <param name="sourceDirectory">The source directory name.</param>
        /// <param name="containerName">Name of the container to upload to.</param>
        /// <returns>True if successfully uploaded</returns>
        public bool UploadDirectory(string sourceDirectory, string containerName)
        {
            return UploadDirectory(sourceDirectory, containerName, string.Empty);
        }

        private bool UploadDirectory(string sourceDirectory, string containerName, string prefixAzureFolderName)
        {
            return ExecuteWithExceptionHandlingAndReturnValue(
                () =>
                {
                    if (!DoesContainerExist(containerName))
                    {
                        CreateContainer(containerName);
                    }
                    var folder = new DirectoryInfo(sourceDirectory);
                    var files = folder.GetFiles();
                    foreach (var fileInfo in files)
                    {
                        string blobName = fileInfo.Name;
                        if (!string.IsNullOrEmpty(prefixAzureFolderName))
                        {
                            blobName = prefixAzureFolderName + "/" + blobName;
                        }
                        PutBlob(containerName, blobName, File.ReadAllBytes(fileInfo.FullName));
                    }
                    var subFolders = folder.GetDirectories();
                    foreach (var directoryInfo in subFolders)
                    {
                        var prefix = directoryInfo.Name;
                        if (!string.IsNullOrEmpty(prefixAzureFolderName))
                        {
                            prefix = prefixAzureFolderName + "/" + prefix;
                        }
                        UploadDirectory(directoryInfo.FullName, containerName, prefix);
                    }
                });
        }

        private void ExecuteWithExceptionHandling(Action action)
        {
            try
            {
                action();
            }
            catch (StorageClientException ex)
            {
                if ((int)ex.StatusCode != 409)
                {
                    throw;
                }
            }
        }

        private bool ExecuteWithExceptionHandlingAndReturnValue(Action action)
        {
            try
            {
                action();
                return true;
            }
            catch (StorageClientException ex)
            {
                if ((int)ex.StatusCode == 409)
                {
                    return false;
                }
                throw;
            }
        }
    }
}

 

As you can see above, the main method I am using is UploadDirectory.  I can call it in the following way:

            var manager = new BlobManager("CloudStorage");
            var success = manager.UploadDirectory(@"C:Temp", "test");
            Console.WriteLine(success);
            Console.ReadKey();

Very easy now, and I can even create a utility and upload entire ClickOnce folder as part of the build process. To have uniform handling I used ExecuteXXX method inside my blob manager.  I often use this technique in my coding, actually.  The last thing is to add connection string to app.config:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <startup>
    <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5" />
  </startup>
  <connectionStrings>
    <add name="CloudStorage" connectionString="DefaultEndpointsProtocol=http;AccountName=XXXXX;AccountKey=XXXXXXXXXXXXXXXXXXXXXXXX" />
  </connectionStrings>
</configuration>

And that is all there is to it, folks. You can download entire solution here.

Thanks.

5 Comments

Leave a Reply

Your email address will not be published. Required fields are marked *