pr0g33k

Managing Files Using Microsoft Azure Blob Storage (Part 1)

Since I'm using Microsoft Azure to host this blog, it just makes sense that I use Azure Blob Storage to store my images for this blog. I had a tough time finding information about configuring and using Azure Blob Storage with an existing MVC Web application, though. So that's what this little walkthrough is about.

Before we get to the code, you're going to want to download the Azure Storage Emulator. You can download the Azure Storage Emulator (v1.8) here

To access the Azure Storage Emulator from your Web application, add the following to your Web.config connectionStrings section:

<connectionStrings>
    <add name="StorageConnection" connectionString="UseDevelopmentStorage=true" />
</connectionStrings>
    

Once your project is deployed, you're going to need a different StorageConnection. I use a transformation in my Web.Release.config to handle this.

<connectionStrings>
    <add name="StorageConnection" connectionString="DefaultEndpointsProtocol=http;AccountName=[YOUR_ACCOUNT_NAME];AccountKey=[YOUR_ACCOUNT_KEY]" xdt:Transform="SetAttributes" xdt:Locator="Match(name)" />
</connectionStrings>
    

Next, install the Windows Azure Storage NuGet package to your MVC Web project.

That's all you should need to set up your project.

For my blog, I wanted to store images in one folder and other files in another folder. Azure Blob Storage doesn't really have a concept of "folders," though (not in the same way as the Windows file system, anyway). You can, however, set up "containers" so I'm going to create a container named "images" and a container named "files."

Now add a controller named "FileManagerController" and a view for Index().

public class FileManagerController : Controller
{
    public ActionResult Index()
    {
        return View();
    }
}
    

We'll fill in the Index.cshtml view in part 2 but for now just create an empty view.

@{
    ViewBag.Title = "File Manager";
}
    

Let's add some code to upload files. To help pass values to my views, I created the following class:

public class UploadModel
{
    public String Container { get; set; }
    public String[] Accept { get; set; }
}
    

The Container property holds the Azure Blob Storage container name. The Accept string array property contains the file extensions that the user is allowed to upload. I know that I could have used the HTML input tag's "accept" property but I wanted to limit the image types that could be uploaded as well as limit the file types to a specific set. It makes more sense for my application to handle the validation on the server.

Now let's create a controller action in the FileManagerController to begin the upload process. I use the "id" Route Parameter to pass the container name to the controller. For now, I just want to have 2 containers - images and files - so I enforce that by only accepting those two container names. I also populate the Accept array with the extensions I'll allow to be uploaded.

public ActionResult Upload(String id)
{
    switch (id.ToLower())
    {
        case "images":
            return View(new UploadModel() { Container = "images", Accept = new String[] { ".jpg", ".png", ".gif" } });
        case "files":
            return View(new UploadModel() { Container = "files", Accept = new String[] { ".zip", ".txt", ".docx", ".pdf" } });
    }

    return RedirectToAction("Index");
}
    

The Upload.cshtml looks like this:

@model RobertGaut.Pr0g33k.Web.Areas.Admin.Models.UploadModel
@{
    ViewBag.Title = "Upload";
}
@using (Html.BeginForm("UploadResults", "FileManager", FormMethod.Post, new { enctype = "multipart/form-data" }))
{ 
    <fieldset>
        <legend>Upload @String.Join(", ", Model.Accept)</legend>
        <ol>
            <li>
                <input name="input1" type="file">
            </li>
            <li>
                <input name="input2" type="file">
            </li>
            <li>
                <input name="input3" type="file">
            </li>
            <li>
                <input name="input4" type="file">
            </li>
            <li>
                <input name="input5" type="file">
            </li>
        </ol>
        @Html.HiddenFor(m => m.Container)
        @for (Int32 i = 0; i < Model.Accept.Length; i++)
        {
            @Html.HiddenFor(m => m.Accept[i])
        }
        <input type="submit" value="Upload" />
    </fieldset>
}
    

There are a few things to take note of here. First, the form posts using enctype = "multipart/form-data" to ensure that the file data is property encoded in the post. Second, I want to pass the file extensions through to the UploadResults controller action and @Html.HiddenFor(m => m.Accept) will not work the way you might expect because Accept is a String array. To post the contents of the array, you need to loop through the array and output each item in its own hidden form field.

On the receiving end of the form post we have the UploadResults controller function:

[HttpPost]
public ActionResult UploadResults(UploadModel uploadModel)
{
    CloudBlobContainer cloudBlobContainer = GetCloudBlobContainer(uploadModel.Container);
    HttpPostedFileBase httpPostedFile;
    CloudBlockBlob cloudBlockBlob;
    List<UploadResult> uploadResults = new List<UploadResult>();
    String fileName;

    foreach (String file in Request.Files)
    {
        httpPostedFile = Request.Files[file] as HttpPostedFileBase;
        fileName = String.Concat(Path.GetFileNameWithoutExtension(httpPostedFile.FileName).ToSlug().ToLower(), Path.GetExtension(httpPostedFile.FileName));

        if (httpPostedFile.ContentLength > 0 && uploadModel.Accept.Contains(Path.GetExtension(fileName)))
        {
            cloudBlockBlob = cloudBlobContainer.GetBlockBlobReference(fileName);
            cloudBlockBlob.Properties.ContentType = httpPostedFile.ContentType;
            cloudBlockBlob.UploadFromStream(httpPostedFile.InputStream);

            uploadResults.Add(new UploadResult() { FileName = fileName, ContentLength = httpPostedFile.ContentLength, Url = cloudBlockBlob.Uri.ToString() });
        }
        else if (!String.IsNullOrEmpty(fileName))
            uploadResults.Add(new UploadResult() { FileName = fileName, ContentLength = httpPostedFile.ContentLength, Message = "The file is either empty or the file type (extension) is not allowed." });
    }

    ViewBag.ResourceType = uploadModel.Container;

    return View(uploadResults);
}
    

There are a few places where we'll need to get a CloudBlobContainer so I refactored that functionality out to its own private method:

private CloudBlobContainer GetCloudBlobContainer(String container)
{
    CloudStorageAccount cloudStorageAccount;

    if (Request.IsLocal)
        cloudStorageAccount = CloudStorageAccount.DevelopmentStorageAccount;
    else
        cloudStorageAccount = CloudStorageAccount.Parse(ConfigurationManager.ConnectionStrings["StorageConnection"].ConnectionString);

    CloudBlobClient cloudBlobClient = cloudStorageAccount.CreateCloudBlobClient();
    CloudBlobContainer cloudBlobContainer = cloudBlobClient.GetContainerReference(container.ToLower());

    if (cloudBlobContainer.CreateIfNotExists())
    {
        BlobContainerPermissions permissions = cloudBlobContainer.GetPermissions();
        permissions.PublicAccess = BlobContainerPublicAccessType.Container;
        cloudBlobContainer.SetPermissions(permissions);
    }

    return cloudBlobContainer;
}
    

If the container does not yet exist, it is created when GetCloudBlobContainer is called. I also set the permissions to allow public access when the container is created.

The Azure Blob Storage is kind of picky about the characters it allows for file names. I wanted to use URL-friendly names - without a lot of URL encoding - so I used my extension method, .ToSlug(), to accomplish that in the UploadResults controller action. I have another post here about that extension method.

The UploadResults view displays a list of hyperlinks for each file that was successfully uploaded. It also displays a list for any files that were not uploaded because their file size was 0 bytes or because the file type was not allowed.

@model List<RobertGaut.Pr0g33k.Web.Areas.Admin.Models.UploadResult>
@using RobertGaut.Core.Extensions
@{
    ViewBag.Title = "Upload";
}
<p>@Html.ActionLink(String.Format("Upload {0}", ((String)ViewBag.ResourceType).ToTitleCase()), "Upload", new { id = ((String)ViewBag.ResourceType).ToTitleCase() })</p>
@if (Model.Count > 0)
{
    <text>
    @if (Model.Where(m => String.IsNullOrEmpty(m.Message)).Count() > 0)
    {
        <p>The following files were uploaded:</p>
        <ul>
            @foreach (var uploadResult in Model.Where(m => String.IsNullOrEmpty(m.Message)))
            {
                <li><a href="@uploadResult.Url" target="_blank">@uploadResult.FileName</a> [@uploadResult.ContentLength bytes]</li>
            }
        </ul>
    }
    <br />
    @if (Model.Where(m => !String.IsNullOrEmpty(m.Message)).Count() > 0)
    {
        <p>The following files were <strong>not</strong> uploaded:</p>
        <ul>
            @foreach (var uploadResult in Model.Where(m => !String.IsNullOrEmpty(m.Message)))
            {
                <li>@uploadResult.FileName [@uploadResult.ContentLength bytes]</li>
            }
        </ul>
    }
    </text>
}
    

To keep my names consistent, I use the following extension method to convert the container name to title case if it was changed:

public static String ToTitleCase(this String s)
{
    if (!String.IsNullOrEmpty(s))
    {
        String[] words = s.Split(new Char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
        StringBuilder stringBuilder = new StringBuilder();
        Char[] letters;

        foreach (String word in words)
        {
            letters = word.ToLower().ToCharArray();
            letters[0] = Char.ToUpper(letters[0]);
            stringBuilder.AppendFormat("{0} ", new String(letters));
        }

        return stringBuilder.ToString().Trim();
    }

    return s;
}
    

In part 2, we'll create a view to display the files we uploaded.

Posted on 4/25/2013 at 08:04 PM , Edited on 4/29/2013 at 12:04 AM
Tags: AzureMVCC#

Comments:

  1. Tim

    Thank you!  Great information.  
Leave a comment
  1. CAPTCHA