Udostępnij za pośrednictwem


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

Comments

  • Anonymous
    December 15, 2014
    Well done. Thanks for this. I was in the middle of trying to figure out the same. I'm glad you were able to carve a path first!

  • Anonymous
    December 16, 2014
    @Bon--glad I could help.

  • Anonymous
    December 23, 2014
    Thanks so much for this! This is the last piece of the puzzle for my app: )!

  • Anonymous
    April 05, 2015
    Nice example!  I wish it also deleted the image from blob storage.

  • Anonymous
    April 06, 2015
    @Brian Yes, adding the ability delete items and the related blob from the Azure storage service would be a nice enhancement. I opened the following issue to track your suggestion: github.com/.../40 This would not be too hard to make happen, and I put some extra info about how to do this in that GitHub issue.

  • Anonymous
    April 07, 2015
    The comment has been removed

  • Anonymous
    April 23, 2015
    Hi, thanks for the article, I think that many people will serve very helpful. Know if they've made any changes as generated the SAS for writing Blobs; because my project was working, but now it shows me the following error when using the SAS: "Error: Server failed to authenticate the request. Make sure the value of Authorization header is formed correctly including the signature." Excuse my English, I speak Spanish.

  • Anonymous
    April 23, 2015
    @Peter: I don't know of any changes to the SAS behavior, but I haven't tried this in a few months. I'll take a look in the next few days. In the meantime, are you seeing any errors on the service-side when requesting the SAS from the Blob service?

  • Anonymous
    April 24, 2015
    @Glenn No, when getting the SAS; but if  to using SAS for PUT This says to make a change -> msdn.microsoft.com/.../dd179428.aspx "Shared Key for Blob, Queue, and File Services. Use the Shared Key authentication scheme to make requests against the Blob, Queue, and File services. Shared Key authentication in version 2009-09-19 and later supports an augmented signature string for enhanced security and requires that you update your service to authenticate using this augmented signature." I get SAS so API in Azure Mobile Service: jsfiddle.net/.../u8rfszf3 So I make the PUT BLOB Error PUT BLOB: jsfiddle.net/.../06d1pjp0

  • Anonymous
    May 04, 2015
    Hi @Glenn I found the solution; It is strange because it did not happen before. blogs.msdn.com/.../http-403-server-failed-to-authenticate-the-request-when-using-shared-access-signatures.aspx

  • Anonymous
    December 12, 2015
    Having a hard time getting the upload to work with this sample. My project is slightly different. I capture the image without issue. The resulting image is used in an <img /> element. Below that is a button to be tapped/clicked to insert the item and upload the image. My data model has a Caption field and ImageLink field. The ImageLink field should hold the URL to the image after it is stored in the BLOB Storage. The SAS gets generated, but the file is never uploaded and the ImageLink contains the URL for the container appended with the full path of the local file (eg https://<storage container>/file:///android..... I don't know where I went wrong. Any advice?

  • Anonymous
    December 12, 2015
    By the way, I forgot to mention this in my previous post: I have a working version written in C# using the same mobile service. I just don't know how to convert my C# code to JavaScript.