Epic Saga: Uploading Images from PhoneGap/Cordova to Azure Storage using Mobile Services

- or -

If You Want to Create a Cordova App and All You Have is a Windows Phone Device…Good Luck

I set out on a simple journey…only a three-hour tour (a three-hour tour). My plan was to create a PhoneGap/Cordova version of the popular tutorial Upload images to Azure Storage by using Azure Mobile Services using HTML and JavaScript. (Right now we only have this topic for Windows apps, because at this point only Windows has an Azure Storage services client.)

Armed with just my trusty Windows Phone 8.1 device (Lumia 920), PhoneGap/Cordova tools, I thought this would be a fairly easy task. After all, these were all the tool I needed to demonstrate push notifications from a PhoneGap app. Instead, this project took a series of weird turns due to multiple blocked paths, which I will recount in dramatic narrative form in the time-honored format of 7 serialized blog posts.

Chapter 1: Wherein I try to use the FileTransfer object to upload images to Azure Bob storage

The original Mobile Services tutorial uploads images by using this simple process:

  1. The client captures the images from the camera.
  2. The client uploads a new TodoItem to the mobile service.
  3. The service requests an SAS for the upload from the Blob Storage service.
  4. The SAS is returned (along with the URL of the image) as a property on the item.
  5. The client uses the SAS to upload the image, as a raw binary-encoded image/jpeg to Azure.
  6. The image URL is used to display the image on the device, via data binding/automatic download.

This approach is simple, and it works well when using the .NET client library for Azure Storage services. (You might argue that it would be better to return the SAS in a header instead of “dirtying-up” the data object, but that’s for a later discussion.)

Without an HTML/JavaScript library that I could use to access the Blob storage service, I was planning to use REST to upload the image. Note that Azure requires an upload using an SAS to be a PUT request. To make this work in Cordova, I installed the following plugins:

  • Capture: enables you to capture images, audio, or video using the cameras on the device.
  • FileTransfer: helps you to more easily upload files to a remote server.
  • Console: you pretty much need this when trying to debug your app without setting alerts everywhere.

After getting my mobile service configured to handle image uploads (following these instructions), I replaced the add-item.submit event handling function with the following:

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

        // Set the core properties of the new item.
        var newItem = { text: itemText, complete: false };
        var capturedFile;

        // Do the capture before we do the insert.
        // Launch device camera application to capture a single image. 
        navigator.device.capture.captureImage(function (mediaFiles) {
            if (mediaFiles) {
                // Set a reference to the captured file.
                capturedFile = mediaFiles[0];

                // Set the properties we need on the inserted item.
                newItem.containerName = "todoitemimages";
                newItem.resourceName = capturedFile.name;
            }
            // Do the insert to get the SAS query string from Blob storage.
            todoItemTable.insert(newItem).then(function (item) {
                if (item.sasQueryString !== undefined) {
                    console.debug("Query string: " + item.sasQueryString);

                    // Build the request URI with the SAS, 
 // which gives us permissions to upload.
                    var uriWithAccess = item.imageUri + "?" + item.sasQueryString;

                    // Set the upload options.
                    var options = new FileUploadOptions();
                    options.httpMethod = "PUT";
                    options.mimeType = "image/jpeg";
                    options.fileName = capturedFile.name;
                    options.chunkedMode = false;                            
                    options.headers = {
                        // We need to set this header.
                        'x-ms-blob-type': 'BlockBlob'
                    };

                    // Create the new FileTransfer and upload the image file.
                    var ft = new FileTransfer();
                    ft.upload(capturedFile.fullPath, uriWithAccess, function (result) {
                        if (result.responseCode === 201)
                        {
                            alert("Upload complete (" + result.bytesSent + " bytes).");
                        }
                        else {
                            alert("Upload failed!");
                        }                                
                    }, handleError, options);
                }
            }, handleError).then(refreshTodoItems, handleError);
        }, function (error) {
            alert(error);
        });
    }
    textbox.val('').focus();
    evt.preventDefault();
});

This was great, and way easier than I expected using REST, I thought—until I tried to download my JPEG images from Azure. They were all not recognized as images, and Fiddler showed me why:

 HTTP/1.1 200 OK
Content-Length: 2307942
Content-Type: 

multipart/form-data;

  boundary=----------------------------8d1bf89243a7394
Last-Modified: Mon, 27 Oct 2014 06:10:39 GMT
ETag: 0x8D1BFC3DD92DA4A
Server: Windows-Azure-Blob/1.0 Microsoft-HTTPAPI/2.0
x-ms-request-id: ab624139-0001-0033-7532-4fea34000000
x-ms-version: 2009-09-19
x-ms-lease-status: unlocked
x-ms-blob-type: BlockBlob
Date: Mon, 27 Oct 2014 06:11:48 GMT

------------------------------8d1bf89243a7394
Content-Disposition: form-data; name="null"; filename="WP_20141026_004.jpg"
Content-Type: image/jpeg

�����Exif��MM�*����
�����������

My precious binary image data was being uploaded as part of a multipart/form-data blob, and not just raw binary that I would get when using the .NET Azure storage client. Drat. After a little digging into the source code, I can see that this is hard-coded in the plugin:

 webRequest = (HttpWebRequest)WebRequest.Create(serverUri); 
webRequest.ContentType = "multipart/form-data; boundary=" + Boundary; 
webRequest.Method = uploadOptions.Method; 

Since one of my requirements was to be able to download the image/jpeg-encoded binary files directly from Blob storage, and I didn’t feel like hacking the FileTransfer plugin, I needed to find another way to get the binary data into Azure blob storage. I turned, in great hope, to using an XMLHttpRequest to upload my image to Azure.

Next time… Chapter 2: Wherein I try to use an XMLHttpRequest from my Windows Phone.

 

1.