AJAX file upload progress

Example of how to use the newest version is available in another post.

Update

There is a new version of this code that follows the same approach but uses HTML_AJAX instead of JPspan.
You can view the new demo, and view the code in websvn.

Also note that the server isn’t setup to accept files larger then 8meg, so anything bigger will fail.

Also I’m looking for someone to help me improve the error handling, if your interested in getting involved and making some regular releases of the new code let me know.

Original Post below

A couple days ago I found an interesting ruby on rails project. It uses AJAX to update a progress bar as the file uploads. The trick is a patch to rails for getting upload status and doing the upload in an iframe so that the main page is still active.

So to replicate this I just had to find a patch that provides upload status in PHP and then implment my little iframe upload widget.

I found the PHP with a little work from google: Upload Progress Meter

First you need to install the patch and the extension, the included instructions are easy to follow. The only problem I found is that: upload_progress_meter.store_method = “file” had to be set in my php.ini before thing would work.

I also ran into a JPSpan problem, if your having network problems the status call might take longer then 1 second, and you’ll get Call in Progres error alert. This can be fixed with the current version of JPSpan but i’d like to see some api added to help. The proxied objects need some type of inProgress call to make this an easy fix.

One item of note is that the extension only provides information in a way to provide 1 progress bar per form no matter how many files. The javascript code is setup so multiple forms could be on a page at once and both uploading, but this hasn’t been tested.

Here is the demo you’ve been waiting for, for most connections a 250k file will be enough to see something besides connecting and complete.

Also if anyone has the time and skills to review the php patch and see what it would take to get integrated, please let me know. I haven’t heard from its author so I don’t know why its not integrated but it just seems crazy to have a 3K patch that is this useful only available to those who are willing to patch.

Code walk through

So the basic flow happens like this.

Display a page with a form:
this page has a hidden iframe, a hidden progress div, and some extra javascript code

Select the file to upload and submit the form:
The form has a target of the hidden iframe so even though the throbber starts the main page view won’t be getting new content when the upload is done.

The form onclick handler fires a setup function:
The function finds the progress div and shows it, it also registers a function to update the status ever second.

The update function fires every second:
This functions checks a counter to see if there are any more divs to update, if the counter is 0 it stops the update function from firing again
The function creates a remote proxy object to the php class UploadProgressMeterStatus if it hasn’t already been created
The function calls the get_status method on the proxy object with a list of all the progress divs and UPLOAD_IDENTIFIER’s that we need status for.
The function exits

The get_status method on the php class is called
The method calls upload_progress_meter_get_info() for each passed in identifier, the information is formated to a percent and a message which is returned

The callback function for get_status is called when the PHP class returns data
The callback updates the progress div
If were at 100% the we decrement our progress div counter and remove us from the list of divs to be updated

The iframe can also load a page once the file upload is done, it doesn’t currently do anything.

Example Code

This is really the only interestin php code in the project, and its not that interesting. When you have the Upload Progress Meter extension installedany form that is doing a file upload and has a hidden var called UPLOAD_IDENTIFIER can be tracked. The identifier has to be passed into the upload_progress_meter_get_info function so you have to keep track of it on the javascript side. Here we just pass those ids in, do a bunch of formating on the resulting array, and return the results. Note the returned array isn’t documented anywhere so this code and the code in the example php code provided with the extension is the best place to start if you want to do something else with it.

/**

* Get the status of all uploads passed in

*/

function get_status($ids) {

$ret = array();

foreach($ids as $id => $upId) {

$ret[$id] = new stdClass();

$tmp = upload_progress_meter_get_info($upId);

if (!is_array($tmp)) {

$ret[$id]->message = «Complete»;

$ret[$id]->percent = «100»;

break;

}

if ($tmp['bytes_total'] < 1) {

$percent = 100;

}

else {

$percent = round($tmp['bytes_uploaded'] / $tmp['bytes_total'] * 100, 2);

}

if ($percent == 100)

{

$ret[$id]->message = «Complete»; }

$eta = sprintf(«%02d:%02d», $tmp['est_sec'] / 60, $tmp['est_sec'] % 60 );

$speed = $this->_formatBytes($tmp['speed_average']);

$current = $this->_formatBytes($tmp['bytes_uploaded']);

$total = $this->_formatBytes($tmp['bytes_total']);

$ret[$id]->message = «$eta left (at $speed/sec) $current/$total($percent%)»;

$ret[$id]->percent = $percent;

}

return $ret;

}

On the javascript side there is a bit more code, but none of its all that complex. This code is used to show the progress bar and give it some initial values. The main things to notice are that were add an update method to the div, this is a nice trick since it allows for runtime extension of objects in the DOM, and it will make updating things nice and easy in the other functions. Were also adding a getFirstDivByClass method to the div, I’m doing this so I don’t have to have so many divs to track, the classes only have to be unique inside the progress bar for this to work and thats much easier to achieve.

/**

* Shows a progress bar and sets it to 0

*/

function UploadProgressMeter_EnableProgress(progress_id) {

var progress = document.getElementById(progress_id);

progress.style.display = 'block'; progress.percent = 0;

progress.message = «Connecting»;

progress.update = function() { this.getFirstDivByClass('bar').style.width = this.percent+'%'; this.getFirstDivByClass('message').innerHTML = this. message; }

progress.getFirstDivByClass = function(className) {

var nodes = this.getElementsByTagName('div');

for(var i = 0; i < nodes.length; i++) {

if (nodes[i].className == className) {

return nodes[i];

}

}

}

progress.update();

}

The code below calls the remote proxy and creates the callback function to handle the results. The is one area where improvements could be made. First there should be a check if there is currently a call in progress. Then it might also be smart to call the server less (especially on large files) and just generate the stats from the current download rate. This would add some complexity but would allow the progress bar to update smoothly and would allow the server calls to get down to once every 5 or 10 seconds.

If you don't do a lot of javscript programming its worth nothing the use of for(var prop in result) and delete UploadProgressMeter_active[prop];

for(var prop in result) is how you loop through the properties on an object, this allow you to use them as associative arrays (just watch for methods on the objects since you'll loop through them too).

delete UploadProgressMeter_active[prop] is the equivalent of unset($array['key']);

/**

* Update the progress bars of all the current bars

*/

function UploadProgressMeter_Update() {

if (UploadProgressMeter_count == 0) {

clearInterval(UploadProgressMeter_intervalId);

UploadProgressMeter_intervalId = false;

return;

}

if (UploadProgressMeter_remote == false) {

var callback = {

get_status: function(result) {

for(var prop in result) {

if (prop != «toString») {

document.getElementById(prop).percent = result[prop].percent;

document.getElementById(prop).message = result[prop].message;

document.getElementById(prop).update();

if (document.getElementById(prop).percent == 100) {

UploadProgressMeter_count--;

delete UploadProgressMeter_active[prop];

}

}

}

}

}

UploadProgressMeter_remote = new uploadprogressmeterstatus(callback);

}

UploadProgressMeter_remote.get_status(UploadProgressMeter_active);

}

Code List
  • Tarball
  • Main widget php code
  • Widget Javascript code
  • Demo html/php page
  • Demo JPSpan server page
Updates

You may have noticed that the demo has stopped working a couple times. This was related to 2 things and there things you might want t think about if your going to use the patch. First it writes tmp files and fails silently if the directly no longer exists (darn /tmp cleaning scripts). Second its on my php5 server which has some users who are pushing the envelope which required me to upgrade to the php 5.1 beta and I forgot to repatch. Repatching wasn’t a big deal though I did have to move where a function was declared to get the extension to compile in gcc 4 (I upgraded to fedora core 4 as well).

Anyhow the demo is working and it should stay working as long as I remember to repatch with each upgrade.

New Version

This code has been updated to work with HTML_AJAX and to handle error conditions better. I haven’t done a formal release but you can grab it from svn.

Check it out from: svn.bluga.net/HTML_AJAX/UploadProgressMeter/trunk/

Or use websvn to get a tarball: websvn.bluga.net/wsvn/HTML_AJAX/UploadProgressMeter/trunk/?rev=0&sc=0

Also if your interested in helping out with the Upload Progress Meter let me know.

0 comments