Friday, August 8, 2008

AJAX File Upload Progress Bars (with jQuery)

I recently added progress bars (actually, a percentage instead of a bar, but it was the same to implement) to Ringlight uploads and downloads. I was suprised to find that the available server-side libraries for dealing with file uploads seemed to be inadequate for adding this functionality to my website.

The basic technique for adding progress bars is relatively simple. With jQuery:

  1. Install the Ajax File Upload extension.
  2. Install the periodic execution extension.
  3. Register some Ajax event callbacks to reveal the progress bar on the page when the upload starts and also to catch errors.
  4. Call the $.ajaxFileUpload function with the URL of the upload handler script, the id of the file input element, and the callback function to handle output from the upload handler.
  5. Have the upload handler return a Json object with an id for the upload.
  6. Call your progress bar update function with the periodic execution model: $.periodic(updateProgressBar);
  7. The updateProgressBar function should fetch the download status from a server-side script, supplying the upload id and a callback function: $.getJSON("fetchProgress", {id: id}, function(data) {/* update progress bar with data.percentDownloaded*/});
  8. The fetchProgress script should return upload progress information in a Json object. I return percentDownloaded, but you can include anything you'd like, such as upload rate. You should also provide error information here, such as if the upload failed.
  9. The callback function for fetchProgress should update the page to reflect updated progress. For instance, updating a percentage to completion could be as simple as $("#percent").empty().append(data.percentDownloaded);
This was all very simple to implement and jQuery made it possible in very few lines of code. The difficulty was in providing a percentDownloaded value. The difficulty comes from the fact that it is common for browsers to not include the Content-Length field for uploaded files. The file upload handling libraries generally solve this problem by either 1) not providing a content length or 2) loading the whole file into memory (or disk, in some cases) and then finding the length of it. Either way, not very useful for a progress bar! This total failure to handle streaming files is a common problem in libraries and if you could avoid it in the libraries you implement then the world would be most appreciative.

In the meantime, there are a number of action items that require your attention. First, calculate the file length by taking the HTTP request Content-Length field and subtracting the size of everything which is not the file in order to yield the file length. I did this with the following shoddy algorithm:
  1. Extract the MIME boundary from the Content-Disposition field.
  2. Subtract the size of the MIME boundary twice (there is a boundary on both sides of the file).
  3. Subtract 2 because the second boundary has a trailing "--".
  4. Subtract 4 because each boundary has a trailing two-character newline (\r\n).
  5. Subtract 8 because my numbers were always off by exactly 8. I'm not sure where this additional 8 is coming from.
As I said, this algorithm is shoddy, a kludge not fit for use in production. However, it works for now! It needs extensive testing and tweaking on a variety of browsers. The next steps:
  1. Improve algorithm so that it's robust enough to work with most browsers
  2. Submit a patch to python's FieldStorage class to support this algorithm
  3. Submit a bug report to Mozilla requesting that they supply content length in file uploads
For now, my upload progress bars are working, so I'm happy.