Windows 8 How to upload an Image using a Blob Storage SAS generated by Windows Azure Mobile Services

Updated: Please read the Background on Shared Access Signatures from this post then move to this updated post – How to upload an Image to Windows Azure Storage using Mobile Services – that will show you how to make use of the Windows Azure Storage SDK for Node.

Windows 8 How to upload an Image using Blob Storage SAS generated by Windows Azure Mobile Services

This post details the specific scenario on how to capture an image on windows 8 and upload it directly to Windows Azure Blob Storage using a Shared Access Signature (SAS) generated within Windows Azure Mobile Services. It demonstrates and alternative approach suited for larger scale implementations (i.e using a SAS) when contrasted with the following article Storing Images from Android in Windows Azure Mobile Services

Note: This topic is advanced and assumes that you have a good knowledge of the Windows Azure Blob REST API and Windows Azure Mobile Services. I would suggest you check out the following tutorials and Blob Storage REST API prior to starting
Note: that although this is specifically an image example you could upload any media/binary data to blob storage using the same approach.

Background – Shared Access Signature

What Are Shared Access Signatures?

A Shared Access Signature is a URL that grants access rights to containers, blobs, queues, and tables. By specifying a Shared Access Signature, you can grant users who have the URL access to a specific resource for a specified period of time. You can also specify what operations can be performed on a resource that’s accessed via a Shared Access Signature. In the case of Blobs operations include:

  • Reading and writing page or block blob content, block lists, properties, and metadata
  • Deleting, leasing, and creating a snapshot of a blob
  • Listing the blobs within a container

Why not just use the storage account name and key directly?

There are a few standout reasons:

  • Security – When building device applications you should not store your storage account name and key within the device app. The reason is that it makes your storage account susceptible to being misused. If someone were to reverse engineer your application take your storage account key then they would essentially have access to 100TB of cloud based storage until such a time that you realized and reset the key. The safer approach is to use a SAS as it provides a time boxed token with defined permissions to a defined resource. With policies the token can also be invalidated/revoked
  • Scale Out (and associated costs)- A common approach I see is uploading an image directly through their web tier e.g a Web API or Mobile Service unfortunate consequence of this at scale is that you are unnecessarily loading your web tier. Consider that each of your instances on your web tier has a limited network I/O. Uploading images directly through this will result in maxing out that I/O and the need to scale out (add more instances) much sooner then alternative approaches. Now consider a scenario where your application requests only a SAS from your web tier you have now moved MBs or image load off your web tier and instead replaced it with a small ~ 100 – 200 byte SAS. This essentially means a single instance now will provide much more throughput and your upload I/O now directly hits the Blob storage service

What is the general workflow for uploading a blob using a SAS?

The four basic steps required when uploading an image using the SAS approach depicted are as follows:

  1. Request a SAS from your service
  2. SAS returned from your service
  3. Upload blob (image/video/binary data) directly to Blob Storage using the SAS
  4. Storage service returns response

For this post we will focus specifically on how to write/upload a blob using a shared access signature that is generated in the mobile service insert trigger.

Creating your Mobile Service

In this post I will extend the Mobile Services quick start sample. Before proceeding to the next section create a mobile service and download the quickstart as detailed in the tutorial here

Capturing the Image|Media

Our first task is to capture the media we wish to upload. To do this follow the following steps.

  1. Add an AppBar to MainPage.xaml with a take photo button to allow us to capture the image
  2. ...
    </Grid>
    ...
    <Page.BottomAppBar>
    <AppBar>
    <Button Name="btnTakePhoto" Style="{StaticResource PhotoAppBarButtonStyle}"
    Click="OnTakePhotoClick" />
    </AppBar>
    </Page.BottomAppBar>
    ...
    </Page>
    view raw gistfile1.cs hosted with ❤ by GitHub
  3. Add the OnTakePhotoClick handler and use the CameraCaptureUI class for taking photo and video
  4. using Windows.Media.Capture;
    private async void OnTakePhotoClick(object sender, RoutedEventArgs e)
    {
    //Take photo or video
    CameraCaptureUI cameraCapture = new CameraCaptureUI();
    StorageFile media = await cameraCapture.CaptureFileAsync(CameraCaptureUIMode.PhotoOrVideo);
    }
    view raw gistfile1.cs hosted with ❤ by GitHub

Generating a Shared Access Signature (SAS) using Mobile Services server-side script

In this step we add sever-side script to generate a SAS on insert operation of the TodoItem table.

To do this perform the following steps:

  1. Navigate to your Mobile Service and select the Data Tab, then click on Todoitem
  2. Select Script, then the Insert drop down
  3. Add the following server side script to generate the SAS
  4. Note: this code assumes there is already a public container called test.
    Note:Simple example of Generating a Windows Azure blob SAS in Node created using the guidance here.

    //Simple example of Generating a Windows Azure blob SAS in Node created using the guidance at http://msdn.microsoft.com/en-us/library/windowsazure/hh508996.aspx.
    //If your environment has access to the Windows Azure SDK for Node (https://github.com/WindowsAzure/azure-sdk-for-node) then you should use that instead.
    function insert(item, user, request) {
    var accountName = '<Your Account Name>';
    var accountKey = '<Your Account Key>';
    //Note: this code assumes the container already exists in blob storage.
    // If you wish to dynamically create the container then implement guidance here - http://msdn.microsoft.com/en-us/library/windowsazure/dd179468.aspx
    var container = 'test';
    var imageName = item.ImageName;
    item.SAS = getBlobSharedAccessSignature(accountName, accountKey, container, imageName);
    request.execute();
    }
    function getBlobSharedAccessSignature(accountName, accountKey, container, fileName){
    signedExpiry = new Date();
    signedExpiry.setMinutes(signedExpiry.getMinutes() + 30);
    canonicalizedResource = util.format(canonicalizedResource, accountName, container, fileName);
    signature = getSignature(accountKey);
    var queryString = getQueryString();
    return util.format(resource, accountName, container, fileName, queryString);
    }
    function getSignature(accountKey){
    var decodedKey = new Buffer(accountKey, 'base64');
    var stringToSign = signedPermissions + "\n" + signedStart + "\n" + getISO8601NoMilliSeconds(signedExpiry) + "\n" + canonicalizedResource + "\n" + signedIdentifier + "\n" + signedVersion;
    stringToSign = stringToSign.toString('UTF8');
    return crypto.createHmac('sha256', decodedKey).update(stringToSign).digest('base64');
    }
    function getQueryString(){
    var queryString = "?";
    queryString += addEscapedIfNotNull(queryString, Constants.SIGNED_VERSION, '2012-02-12');
    queryString += addEscapedIfNotNull(queryString, Constants.SIGNED_RESOURCE, signedResource);
    queryString += addEscapedIfNotNull(queryString, Constants.SIGNED_START, getISO8601NoMilliSeconds(signedStart));
    queryString += addEscapedIfNotNull(queryString, Constants.SIGNED_EXPIRY, getISO8601NoMilliSeconds(signedExpiry));
    queryString += addEscapedIfNotNull(queryString, Constants.SIGNED_PERMISSIONS, signedPermissions);
    queryString += addEscapedIfNotNull(queryString, Constants.SIGNATURE, signature);
    queryString += addEscapedIfNotNull(queryString, Constants.SIGNED_IDENTIFIER, signedIdentifier);
    return queryString;
    }
    function addEscapedIfNotNull(queryString, name, val){
    var result = '';
    if(val)
    {
    var delimiter = (queryString.length > 1) ? '&' : '' ;
    result = util.format('%s%s=%s', delimiter, name, encodeURIComponent(val));
    }
    return result;
    }
    function getISO8601NoMilliSeconds(date){
    if(date)
    {
    var raw = date.toJSON();
    //blob service does not like milliseconds on the end of the time so strip
    return raw.substr(0, raw.lastIndexOf('.')) + 'Z';
    }
    }
    var Constants = {
    SIGNED_VERSION: 'sv',
    SIGNED_RESOURCE: 'sr',
    SIGNED_START: 'st',
    SIGNED_EXPIRY: 'se',
    SIGNED_PERMISSIONS: 'sp',
    SIGNED_IDENTIFIER: 'si',
    SIGNATURE: 'sig',
    };
    var crypto = require('crypto');
    var util = require('util');
    //http://msdn.microsoft.com/en-us/library/windowsazure/hh508996.aspx
    var resource = 'https://%s.blob.core.windows.net/%s/%s%s';
    //Version of the storage rest API
    var signedVersion = '2012-02-12';
    //signedResource. use b for blob, c for container
    var signedResource = 'b'; //
    // The signedpermission portion of the string must include the permission designations in a fixed order that is specific to each resource type. Any combination of these permissions is acceptable, but the order of permission letters must match the order in the following table.
    var signedPermissions = 'rw'; //blob perms must be in this order rwd
    // Example - Use ISO 8061 format
    var signedStart = '';
    var signedExpiry = '';
    // Eample Blob
    // URL = https://myaccount.blob.core.windows.net/music/intro.mp3
    // canonicalizedresource = "/myaccount/music/intro.mp3"
    var canonicalizedResource = '/%s/%s/%s';
    //The string-to-sign is a unique string constructed from the fields that must be verified in order to authenticate the request. The signature is an HMAC computed over the string-to-sign and key using the SHA256 algorithm, and then encoded using Base64 encoding.
    var signature = '';
    //Optional. A unique value up to 64 characters in length that correlates to an access policy specified for the container, queue, or table.
    var signedIdentifier = '';
    view raw gistfile1.js hosted with ❤ by GitHub

    Uploading the Image directly to storage using the SAS

  5. To generate the sas we must insert a todoItem. which will now return the SAS property for the image. Thus update the OnTakePhotoClick handler to insert an item.
  6. private async void OnTakePhotoClick(object sender, RoutedEventArgs e)
    {
    //Take photo or video
    CameraCaptureUI cameraCapture = new CameraCaptureUI();
    StorageFile media = await cameraCapture.CaptureFileAsync(CameraCaptureUIMode.PhotoOrVideo);
    //add todo item to trigger insert operation which returns item.SAS
    var todoItem = new TodoItem() { Text = "test image", ImageName = media.Name };
    await todoTable.InsertAsync(todoItem);
    items.Add(todoItem);
    //TODO: Upload image direct to blob storage using SAS
    }
    view raw gistfile1.cs hosted with ❤ by GitHub
  7. Update OnTakePhotoClick handler to update the image directly to blob storage using the HttpClient and the generated item.SAS
private async void OnTakePhotoClick(object sender, RoutedEventArgs e)
{
//Take photo or video
CameraCaptureUI cameraCapture = new CameraCaptureUI();
StorageFile media = await cameraCapture.CaptureFileAsync(CameraCaptureUIMode.PhotoOrVideo);
//add todo item
var todoItem = new TodoItem() { Text = "test image", ImageName = media.Name };
await todoTable.InsertAsync(todoItem);
items.Add(todoItem);
//Upload image with HttpClient to the blob service using the generated item.SAS
using (var client = new HttpClient())
{
//Get a stream of the media just captured
using (var fileStream = await media.OpenStreamForReadAsync())
{
var content = new StreamContent(fileStream);
content.Headers.Add("Content-Type", media.ContentType);
content.Headers.Add("x-ms-blob-type", "BlockBlob");
using (var uploadResponse = await client.PutAsync(new Uri(todoItem.SAS), content))
{
//TODO: any post processing
}
}
}
}
view raw gistfile1.cs hosted with ❤ by GitHub

Run the application

  1. Hit F5 on the application and right click with your mouse to show the app bar
  2. Press the Take Photo button
  3. Observe that the SAS is returned from your Mobile Service
  4. Check your storage account now has the captured virtual High Five photo/video :)

Enjoy,
Nick