Sammi Sinno

Development - Off The Beaten Path

A development blog that explores everything and puts an emphasis on lesser known community driven projects.

Home

Simplifing Access to Azure Blob Storage

Monday, October 5, 2020

Coming from a world of content management systems, it was a bit difficult to wrap my head around Azure Blob Storage at first. Instead of just accessing the file system, I now have to setup a connection to an external service that requires a key and address, specify which container I want to use and set access permissions on that container (Blob, Container or Off). Blobs in azure storage are organized into containers and in the container you can then further organize your files into cloud blob directories. Not knowing how to work with containers and cloud blob directories could lead you down a path to a very unorganized storage account.

When getting started we will want to install the appropriate Nuget packages:

dotnet add package Microsoft.Azure.Storage.Blob

And you can optionally install the data movement library:

dotnet add package Microsoft.Azure.Storage.DataMovement

Next, lets take a look at how to establish a connection and setup a container.

        public async Task<CloudBlobContainer> GetContainer(string containerName)
        {
            var client = GetClient();
            var container = client.GetContainerReference(containerName);

            await container.CreateIfNotExistsAsync();

            // set access level to "blob", which means user can access the blob 
            // but not look through the whole container
            // this means the user must have a URL to the blob to access it
            BlobContainerPermissions permissions = new BlobContainerPermissions();
            permissions.PublicAccess = BlobContainerPublicAccessType.Blob;
            await container.SetPermissionsAsync(permissions);

            return container;
        }

        private CloudBlobClient GetClient()
        {
            return CloudStorageAccount.Parse(_storageOptions.StorageConnStr).CreateCloudBlobClient();
        }

The first thing we want to do is create a cloud blob client, which can easily be done by calling CloudStorageAccount.Parse and passing in the connection string (the connection string can be found in the Azure UI where you access your storage account under Access Keys).

The next thing we do is define which container we want to use. How you name your containers and organize your content is up to you, if you want you can place everything in the same container as well. Lastly, we have to specify what level of permissions we want to set on the container, I typically go with blob which allows read-only access for anonymous users.

Now that we have a connection and a container, lets look at how to add a folder:

        public async Task<string> AddFolder(string path, string newFolder)
        {
            var container = await GetContainer();
            CloudBlobDirectory directory = container.GetDirectoryReference($"{path}{newFolder}");

            string tempFile = Path.GetTempFileName();

            Random random = new Random();
            var uploadedBytes = new byte[0];
            random.NextBytes(uploadedBytes);

            File.WriteAllBytes(tempFile, uploadedBytes);

            string tempFileName = Path.GetFileName(tempFile);
            await AddFile($"{path}{newFolder}/{tempFileName}", "application/pdf", Path.GetFileNameWithoutExtension(tempFileName),
                uploadedBytes);

            return $"{path}{newFolder}/";
        }

Adding a folder is pretty simple if you are adding a file along with it, but in this case we need to add a temporary file in there for the time being. The reason for this is because you can't actually add a folder in azure blob storage without there being a files in there as well. Now that I have a folder as a way of organizing my content, I can do things like listing all files in a folder, deleting a folder, getting child folders and so on.

Setting up all of these operations can be relatively time consuming, but there is a library out there that has all of these operations built in, and that is FileManager.Azure.

Wiring up the server is very straightforward:

 services.AddScoped<IFileManagerService, FileManagerService>();  
 services.AddOptions();
 services.Configure<StorageOptions>(Configuration);

There are two properties you'll have to configure for the StorageOptions options object, first and most importantly is the StorageConnStr property and the other is a boolean property called TakeSnapshots. If TakeSnapshots is set to true then it will take a snapshot of a file whenever it is either replaced or deleted. The second one is the StorageConnStr which will be used for API access to the storage account.

Lets take a look at the methods exposed in the FileManagerService:

  	public class FileManagerService : IFileManagerService
    {
        /// <summary>
        /// Addes a folder with a temp file
        /// </summary>
        /// <param name="path"></param>
        /// <param name="newFolder"></param>
        /// <returns></returns>
        public async Task<string> AddFolder(string path, string newFolder);

        /// <summary>
        /// Gets a file for the given path, will return null if it doesn't exit
        /// </summary>
        /// <param name="path"></param>
        /// <returns></returns>
        public async Task<BlobDto> GetFile(string path);

        /// <summary>
        /// Gets the root folder for the current user
        /// </summary>
        /// <returns></returns>
        public BlobDto GetRootFolder();

        /// <summary>
        /// Deletes a file or folder based on the path specified
        /// </summary>
        /// <param name="path"></param>
        /// <returns></returns>
        public async Task<List<BlobDto>> DeleteFile(string path);

        /// <summary>
        /// Adds a file for the given content type and name
        /// </summary>
        /// <param name="path"></param>
        /// <param name="contentType"></param>
        /// <param name="name"></param>
        /// <param name="file"></param>
        /// <returns></returns>
        public async Task<BlobDto> AddFile(string path, string contentType, string name, byte[] file);

        /// <summary>
        /// Gets all of the files in a folder
        /// </summary>
        /// <param name="path"></param>
        /// <returns></returns>
        public async Task<IEnumerable<BlobDto>> GetFolderFiles(string path);

        /// <summary>
        /// Gets a folder for the given path
        /// </summary>
        /// <param name="path"></param>
        /// <returns></returns>
        public async Task<BlobDto> GetFolder(string path);

        /// <summary>
        /// Gets all of the child folders from a given path
        /// </summary>
        /// <param name="path"></param>
        /// <returns></returns>
        public async Task<IEnumerable<BlobDto>> GetChildFolders(string path);

        /// <summary>
        /// indicates if the current blob is a folder
        /// </summary>
        /// <param name="item"></param>
        /// <returns></returns>
        public bool IsFolder(BlobDto item)
        {
            return item.BlobType == BlobType.Folder;
        }

        /// <summary>
        /// Indicates if the current blog is a file
        /// </summary>
        /// <param name="item"></param>
        /// <returns></returns>
        public bool IsFile(BlobDto item)
        {
            return item.BlobType == BlobType.File;
        }

        /// <summary>
        /// Renames the given folder
        /// </summary>
        /// <param name="folder"></param>
        /// <param name="newName"></param>
        /// <returns></returns>
        public async Task<BlobDto> RenameFolder(BlobDto folder, string newName);

        /// <summary>
        /// Renames the given file
        /// </summary>
        /// <param name="file"></param>
        /// <param name="newName"></param>
        /// <returns></returns>
        public async Task<BlobDto> RenameFile(BlobDto file, string newName);

        /// <summary>
        /// Replaces a files content and takes a snapshot if take snapshots is enabled
        /// </summary>
        /// <param name="file"></param>
        /// <param name="postedFile"></param>
        /// <returns></returns>
        public async Task<BlobDto> ReplaceFile(BlobDto file, Stream postedFile);

        /// <summary>
        /// Gets a files bytes from azure storage
        /// </summary>
        /// <param name="path"></param>
        /// <returns></returns>
        public async Task<byte[]> GetFileBytes(string path);

        /// <summary>
        /// Moves a folder with its contents to the path given
        /// </summary>
        /// <param name="folder"></param>
        /// <param name="path"></param>
        /// <returns></returns>
        public async Task<BlobDto> MoveFolder(BlobDto folder, string path);

        /// <summary>
        /// Updates the given files path to the new path specified, path should have leading and trailing forward slashes (ex /temp/)
        /// </summary>
        /// <param name="file"></param>
        /// <param name="path"></param>
        /// <returns></returns>
        public async Task<BlobDto> MoveFile(BlobDto file, string path);

        /// <summary>
        /// Gets a summary of the current users number of files, space used and size limit
        /// </summary>
        /// <returns></returns>
        public async Task<SummaryInfo> GetSummaryInfo();

        /// <summary>
        /// Indicates if the file exists for any given path
        /// </summary>
        /// <param name="path"></param>
        /// <returns></returns>
        public async Task<bool> FileExists(string path);

Now if I want to add a file I can easily do it by using the FileManagerService:

string tempFile = CreateTempFile();
var uploadedBytes = File.ReadAllBytes(tempFile);

string name = Path.GetFileNameWithoutExtension(tempFile);
string path = $"/temp/{name}.pdf";

await _fileManagerService.AddFile(path, "application/pdf", name, uploadedBytes);

All of the operations work with a POCO object called BlobDto:

   public class BlobDto
    {
        public string Name { get; set; }
        public string ContentType { get; set; }
        public string StoragePath { get; set; }
        public DateTimeOffset? DateCreated { get; set; }
        public DateTimeOffset? DateModified { get; set; }
        public long FileSize { get; set; }
        public BlobType BlobType { get; set; }
        public string Path { get; set; }
    }

This is a simplified class that is mapped from the BlobProperties when working with the Azure Storage API. A few things to note is that the path is the relative path to the blob in the container, and the StoragePath is the full URI, normally used when trying to access the blob in the browser.

Whether you use FileManager.Azure or not, it might be useful just to take a look at the service and see how things are done, or even take a look at the unit tests to see how to create unit tests for azure storage functionality (hint: you'll have to install the azure storage emulator).