Epic Saga Final Chapter: Success!! or How to Upload Images to Azure From a Cordova App

This post is the eights post in the series: Uploading Images from PhoneGap/Cordova to Azure Storage using Mobile Services

As promised, I am finally able to share with you all, loyal readers, the glorious, successful conclusion to my epic saga, wherein I am actually able to upload images from my Cordova/PhoneGap app to the Azure Blob Service (at least from an Android device—I don’t currently have an iPhone for testing but I am getting help with that).

Before I dive straight into the code for my solution, I also want to briefly mention that in addition to success with the new Samsung Galaxy Tab 4, I was also able to get my project running on the new Visual Studio Emulator for Android. Great job there VS guys! The basic process that I used for uploading images from the TodoList sample app was:

  1. Capture the image.
  2. Insert a new item to Mobile Services.
  3. Generate the SAS in Mobile Services.
  4. Use the returned SAS to upload to the Blob Service.
  5. Display the image in the UI.

This process follows the other Mobile Services upload tutorials, where an item insert is followed by the upload, is pretty much inline with the classic media-link-entry/media-resource model prescribed by AtomPub and OData. The complete Visual Studio 2013 project for this Apache Cordova app is now published to the Mobile Services sample repository.

Capture the image

Getting an image successfully taken was the biggest blocker in my quest, because the emulators (with the exception of the new VS Android emulator) just don’t work well enough to support this basic functionality. Note that to keep my UI simple, I try to take a picture when the Add button is clicked to upload a new item. When the user cancels the capture or when camera capture just isn’t supported, I insert the item without a blob upload.

The following, which replaces the original submit event handler for the Add button, takes the picture, uses info from the captured image to set some properties on the item, then calls insertNewItemWithUpload() :

 // Handle insert--this replaces the existing handler.
$('#add-item').submit(function (evt) {
    var textbox = $('#new-item-text'),
        itemText = textbox.val();
    if (itemText !== '') {

        var newItem = { text: itemText, complete: false };
        // Do the capture before we do the insert. If user cancels, just continue.
        // Launch device camera application to capture a single image. 
        navigator.device.capture.captureImage(function (mediaFiles) {
            if (mediaFiles) {
                // Set a reference to the captured file.
                var capturedFile = mediaFiles[0];

                // Set the properties we need on the inserted item, using the device UUID
 // to avoid collisions on the server with images from other devices.
                newItem.containerName = "todoitemimages";
                newItem.resourceName = device.uuid.concat("-", capturedFile.name);
                // Insert the item and upload the blob.
                insertNewItemWithUpload(newItem, capturedFile);
            }

        }, function () {
            // Insert the item but not the blob.
            insertNewItemWithUpload(newItem, null);
        }, { limit: 1 });
    }
    textbox.val('').focus();
    evt.preventDefault();
});

Insert the new item

The following insertNewItemWithUpload() function sends the new TodoItem object to Mobile Services:

 // Insert a new item, then also upload a captured image if we have one.
var insertNewItemWithUpload = function (newItem, capturedFile) {
    // Do the insert so that we can get the SAS query string from Blob storage.
    todoItemTable.insert(newItem).then(function (item) {
        // If we have obtained an SAS, then upload the image to Blob storage.
        if (item.sasQueryString !== undefined) {

            insertedItem = item;
            readImage(capturedFile);
        }
    }, handleError).then(refreshTodoItems, handleError);
}

When the item being inserted has a containerName field, the following insert script in the mobile service (basically the same as the tutorials) generates an SAS, is used to upload the image to Azure:

 var azure = require('azure');
var qs = require('querystring');
var appSettings = require('mobileservice-config').appSettings;

function insert(item, user, request) {
  // Get storage account settings from app settings. 
  var accountName = appSettings.STORAGE_ACCOUNT_NAME;
  var accountKey = appSettings.STORAGE_ACCOUNT_ACCESS_KEY;
   
  var host = accountName + '.blob.core.windows.net';

  if ((typeof item.containerName !== "undefined") && (
  item.containerName !== null)) {
      // Set the BLOB store container name on the item, which must be lowercase.
      item.containerName = item.containerName.toLowerCase();

      // If it does not already exist, create the container 
      // with public read access for blobs. 
      var blobService = azure.createBlobService(accountName, accountKey, host);
      blobService.createContainerIfNotExists(item.containerName, {
          publicAccessLevel: 'blob'
      }, function(error) {
          if (!error) {

              // Provide write access to the container for the next 5 mins. 
              var sharedAccessPolicy = {
                  AccessPolicy: {
                      Permissions: azure.Constants.BlobConstants.SharedAccessPermissions.WRITE,
                      Expiry: new Date(new Date().getTime() + 5 * 60 * 1000)
                  }
              };

              // Generate the upload URL with SAS for the new image.
              /*var sasQueryUrl = 
 blobService.generateSharedAccessSignature(item.containerName, 
 item.resourceName, sharedAccessPolicy);*/
               
              var sasQueryUrl = 
              blobService.generateSharedAccessSignature(item.containerName, 
              '', sharedAccessPolicy);

              // Set the query string.
              item.sasQueryString = qs.stringify(sasQueryUrl.queryString);

              // Set the full path on the new new item, 
              // which is used for data binding on the client. 
              item.imageUri = sasQueryUrl.baseUrl + sasQueryUrl.path + '/' 
                  + item.resourceName;          

          } else {
              console.error(error);
          }
          request.execute();
      });
  } else {
      request.execute();
  }
}

Note that the shared secret that I need to access the Blob service is maintained securely by my mobile service as an app setting, which I set in the portal as shown in the tutorials.

image

This access key is used to generate a SAS that I send to the client to enable upload. To prevent unauthorized access, the lifespan of the SAS is set to 5 minutes.

Read the image file and upload to Azure

With the SAS provided by the mobile service, it is pretty easy to upload the JPEG file using a PUT request, with the SAS as a query string. It turns out the best way to do this is to use a good ol’ XMLHttpRequest. First, I needed to get the locally stored image, which gets saved by the camera capture operation. The following code uses the capture image metadata to get the local file and read it into an array buffer:

 // This function is called to get the newly captured image
// file and read it into an array buffer. 
functionreadImage(capturedFile) {
     // Get the URL of the image on the local device.     
     var localFileSytemUrl = capturedFile.fullPath;
     if (device.platform == 'iOS') {
         // We need the file:/ prefix on an iOS device.
         localFileSytemUrl = "file://" + localFileSytemUrl;
     }
     window.resolveLocalFileSystemURL(localFileSytemUrl, function (fileEntry) {
        fileEntry.file(function (file) {
            // We need a FileReader to read the captured file.
            var reader = new FileReader();
            reader.onloadend = readCompleted;
            reader.onerror = fail;

            // Read the captured file into a byte array.
            // This function is not currently supported on Windows Phone.
            reader.readAsArrayBuffer(file);
        }, fail);
    });
}

When the read operation completes, the binary array data is sent as the body of the XMLHttpRequest:

 // This function gets called when the reader is done loading the image
// and it is sent via an XMLHttpRequest to the Azure Storage Blob service.
var readCompleted = function (evt) {
    if (evt.target.readyState == FileReader.DONE) {

        // The binary data is the result.
        var requestData = evt.target.result;

        // Build the request URI with the SAS, which gives us permissions to upload.
        var uriWithAccess = insertedItem.imageUri + "?" + insertedItem.sasQueryString;
        var xhr = new XMLHttpRequest();
        xhr.onerror = fail;
        xhr.onloadend = uploadCompleted;
        xhr.open("PUT", uriWithAccess);
        xhr.setRequestHeader('x-ms-blob-type', 'BlockBlob');
        xhr.setRequestHeader('x-ms-blob-content-type', 'image/jpeg');
        xhr.send(requestData);
    }
}

Note that the x-ms-blob-type header is required, and I set it to BlockBlob, since I was uploading a block and not a page blob. I found the info about REST access to the Blob service here. When the HTTP request returns a success code, in this case 201 (Created), I refresh the page to ensure that the new blob gets displayed.

A note on large blobs

Please be aware that Azure Storage limits the size of block blobs to 64MB. When your file is larger than 64MB, you must upload it as a set of blocks. For more information, see the Azure Storage REST API documentation.

Display uploaded images in the UI

With the difficult uploading part of the sample complete, the final thing to do was to make sure that the images were being displayed with their items. I first had to tweak the CSS to remove the limit on height of items in the list, then I updated the

 function refreshTodoItems() {
    $('#summary').html("Loading...");
    var query = todoItemTable;//.where({ complete: false });

    // Execute the query and then generate the array list.
    query.read().then(function (todoItems) {
        var listItems = $.map(todoItems, function (item) {
            var listItem = $('<li>')
                .attr('data-todoitem-id', item.id)
                .append($('<button class="item-delete">Delete</button>'))
                .append($('<input type="checkbox" class="item-complete">').prop('checked', item.complete))
                .append($('<div>').append($('<input class="item-text">').val(item.text)));

            // Only add the image if the URL exists.
            if (item.imageUri)
            {                      
                listItem.append($('<img>').attr('src', item.imageUri));
            }
            return listItem;
        });

        $('#todo-items').empty().append(listItems).toggle(listItems.length > 0);
        $('#summary').html('<strong>' + todoItems.length + '</strong> item(s)');

        var width = $('#todo-items').width();

        $('#todo-items img').css({
            'max-width': width, 'height': 'auto'
        });

    }, handleError);
}

Note that I only add the image if a URL exists on the item (to avoid the broken image icon). I also dynamically resize the image based on the window width (to make this easier, I disabled the landscape orientation). At this point, the app runs, I can upload JPEG binary files to Azure, then download them and display them in the UI, as seen below:

image

The denouement

Well, this brings my epic saga to a close—unlike Tolkien, there is but a single ending to this harrowing tale. After many arduous weeks, and the procurement of a new Android device, I was finally able to upload my precious images to Azure from my Cordova app.

The complete Visual Studio 2013 project for this Apache Cordova app is now published to the Mobile Services sample repository.

 

Thanks for your kind attention,

 

Glenn Gailey