There and Back Again

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

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: http://svn.bluga.net/HTML_AJAX/UploadProgressMeter/trunk/

Or use websvn to get a tarball: http://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

249 thoughts on “AJAX File upload Progress

  1. Joshua Eichorn Post author

    Patrick:
    Either you have an extension loaded that is messing things up or you’ve hit a bug somewhere. If you have any error messages please email them to the HTML_AJAX mailing list: http://lists.bluga.net/mailman/listinfo/html_ajax-devel

    If you can get a dump of the http messages that are being sent and returned that would be helpful as well.

    Besides that im not sure how to get it fixed, I’m guessing were into debugging browser weirdness and that can be a hard thing to do over email.

  2. Jaimz

    hey, if someone has compiled php5 (any version) with this patch and it works, would you please email me jaimz(at)gi-tools.com i can’t seem to compile php at all. thanks in advance

  3. Waldo

    Firefox – “client error: Call in progress”

    over and over while uploading

    file uploading is

    /usr/share/dict/web2 (about 2.5megs)

    I’ll file a bug if it lets me w/o registering for anything.

  4. Pingback: PHP & AJAX: Upload Progress Meter · Style Grind

  5. Pingback: AJAX道 » AJAX upload progress meter for PHP

  6. bongobongo

    Hi.

    Does not work in Firefox 1.5 on XP.
    Does not work in IE 6 on XP.

    Both browsers just show 100% uploaded instantly after hitting the
    Upload File button.

    So…. for now: Would be nice if this was tested a bit more before offering to online users. Anyway, best luck with an updated version …

  7. Strawp

    Interesting hack. The demo doesn’t actually work though, does it?

    I’m testing with a 75 meg file on a 10 meg connection and it jumps from 0 to 100% straight away which is useless. Who cares about smaller files or files over 250k? It’s exactly the large files that I’m trying out that it should work with, but it doesn’t.

  8. Pingback: Ajax学习笔记 » Blog Archive » AJAX File upload Progress

  9. Joshua Eichorn Post author

    Update the demo isn’t broke, the max upload limit just got set smaller. And were not detecting the error properly.

    The max file upload on the server is 8meg, im not going to change that since I really don’t want the server hammered more then it needs.

    The new version contains a readme on how to setup php to work with larger files. http://websvn.bluga.net/wsvn/HTML_AJAX/UploadProgressMeter/trunk/LargeFileReadme.txt?op=file&rev=0&sc=0

    Also note that detecting file uploading errors is hard in PHP since they don’t happen until the upload is done, even if the error is the file is too large.

  10. Pingback: El utilitario » Gifs animados de cargando - loading

  11. Pingback: nxmxbbd

  12. bonjomatic

    Hello,
    Is there any example for this involving multiple-file uploads?

    Nice script by the way !

    Rgds,
    bonjo

  13. Jason

    Hi Joshua,

    Is there any way to have an overall form with the file upload field, then, when the user hits Upload, collect the file, run the whole progress meter shebang, and then redirect to another page with the $_POST array being used so that I can actually process the file and do various other things (add entries to a db, etc)? The reason I’d prefer it as $_POST is to avoid variable injection by user…

    Thanks!

    -jB

  14. Joshua Eichorn Post author

    Bonjomatic:
    I don’t have a multiple file example, but it does work with multiple, but there is only one progress meter for the entire form no matter how many uploads your doing.

    Jason:
    I’m not sure what you mean here. The script that is targeted in the iframe gets the normal form post. You have _POST with all the info there.

    Take a look at the new version, it will make that a bit more clear.

  15. Jason

    What I meant was, is there any way to have the overall page redirect somewhere. In your case, have demo.php refresh with new information, rather than the IFRAME; the reason for that is, if I just click Upload on the demo.php, it uploads the page, but overall stays on the same page – this might be confusing to a user, so I want to bring him somewhere completely different on the site.

  16. Joshua Eichorn Post author

    Jason: you do this by adding onload javascript in the html rendered by the iframe. Look at the final status code on the new version, you could enhance that to also run your own javascript on the parent page when the download is complete.

    You can’t submit a form two 2 places so all the processing needs to take place in the iframe target, with javascript from its onload doing anything else that is needed.

  17. Yusuke

    Hi there, nice work on this one.
    I suppose there is still no hope to have the patch installed directly in official PHP versions? And if not, how do we install this patch on our servers? do you have a walkthrough?

  18. Joshua Eichorn Post author

    Yusuke:
    No the patch hasn’t been added upstream. I’m not sure why the PHP dev’s refuse to add a feature like this.

    As for applying the patch the download at http://pdoru.from.ro/upload-progress-meter/ includes instructions. If you need more then that, i’m guessing you’ll need more of a walkthrough then I have time to provide. Also note that you at least get status feedback in the new version even without the patch.

  19. Wylie

    While I see and believe the file uploads — I am not able to find it or where in the code (demo.php?) I would insert the standard upload logic.

    e.g.

    ‘;
    if (move_uploaded_file($_FILES[‘userfile’][‘tmp_name’], $uploadfile)) {
    echo “File is valid, and was successfully uploaded.\n”;
    } else {
    echo “Possible file upload attack!\n”;
    }

    echo ‘Here is some more debugging info:’;
    print_r($_FILES);

    print “”;

    ?>

  20. Wylie

    Sorry to have posted twice; feel free to delete. I am using the SVN trunk, but it is not obvious to me. “clearly marked” where? thanks. this is one of the best small php/upload/ajax pieces I’ve seen yet.

  21. Joshua Eichorn Post author

    demo.php line 9

    if ($fileWidget->uploadComplete()) {
    // output javascript to the iframe to send a final status to the main window
    // this will catch error conditions
    echo $fileWidget->finalStatus();

    // move the file(s) where they need to go

    exit;
    }

    This code gets run in the iframe so you won’t see your output unless you unhide the iframe

  22. Wylie

    I will be more explicit, actually. I understand that upload_progress_meter_get_info is part of pdoru’s work @ http://pdoru.from.ro/. The information is conflicting:

    “Hopefully the patch will be integrated into PHP and in the future you will not have to rebuild php to make this work. In the mean time download this patch.”

    However, there is comment that since PHP v4.3.7 and PHP5.0.0RC3 it is no longer needed. I suspect this means only the SAPI patch? And the rest of the patches and new build are necessary?

  23. Vik

    Josh:
    would you mind posting example for multiple file uploads? one progress bar would do …

    Thanks.

  24. Pingback: Smarking

  25. fabs

    Hi Josh,

    I tried to install the extension, but I have a compilation error. I have just send an email to pdoru.

    But I have another pb. When I add the following lines in the httpd.conf and restar t httpd :

    php_value upload_progress_tracking.store_method “file”
    php_value upload_progress_tracking.file 1
    php_value upload_progress_tracking.file.filename_template “/tmp/uploadbar/upl_%s.txt”

    but when I try the demo, the values are not initialize. What can I do ?

  26. Joshua Eichorn Post author

    Fabs:
    Try the new version it still gives some feedback even when the extension isn’t working. Also if the extension was never installed setting update_progress_tracking values won’t matter

  27. fabs

    Hi Josh,

    Yes I’m working with the new version. Now my installation is ok, but when i upload a file (250ko) the bar jumps 0 to 100. This is ok because it is a small file. But when i search the file on my server, i can’t find it in the temporary folder upload. I don’t know what to do ?

    Thanks a lot

  28. fabs

    Excuse me forget my last post, it runs now for the temporary files.
    Thanks a lot for your great blog !

  29. fabs

    another question, I put in the demo my move_uploaded_file fonction after : // move the file(s) where they need to go
    But nothing append.
    I don’t understand why.

    (Sorry for the flood)

  30. Serj Tiutiun

    I searched the whole internet for a trick to upload a file without reloading windows content. AJAX is helpless.
    That attribute TARGET in the FORM tag do the damned trick 🙂
    Thank you VERY much.

  31. Michiel

    Hi Joshua,

    could you give me en example of the solution you provided eariler?
    i quote:

    Jason: you do this by adding onload javascript in the html rendered by the iframe. Look at the final status code on the new version, you could enhance that to also run your own javascript on the parent page when the download is complete.

    You can’t submit a form two 2 places so all the processing needs to take place in the iframe target, with javascript from its onload doing anything else that is needed.

    where to put the onload? and can i use: onload=” form.submit();” ?

    thnx already!

  32. Serj Tiutiun

    How to upload FILE without reloading browser window:
    ——————————————————-

    Filename

    ———————————–
    Thus your form is being submitted to the window IFRAME, and your browser window remains intact.
    Also you can track when IFRAME window will reload, and then you can read and parse answer from there.
    If any questions appear, you can contact me via:
    tiutiun AT moldova DOT cc

  33. Serj Tiutiun

    Update to previous comment.
    My HTML code was wipped out I don’t know why
    —————-
    form action=”?” method=”post” style=”margin: 0px; display: inline;” name=”add_new_file” enctype=”multipart/form-data” target=”myframe”

    iframe frameborder=”0″ height=”0″ width=”0″ scrolling=”no” id=”myframe” name=”myframe”

    /iframe
    —————–

  34. Joshua Eichorn Post author

    Charlie:

    I’m not sure how you get too 6 scripts my count is:

    1 script to produce the page the form is on
    1 script to accept the form upload
    1 script to accept AJAX calls and return upload status info

    In theory you could combine it all into 1 php script but its still 3 seperate functions.

  35. Graham

    Hi Joshua,

    Love your script, works great so long as you don’t try to have more than one upload going at a time. I tried two concurrent downloads(using the demo) and the progress stopped on both. once one of the uploads had finished, then the other began showing the progress that was remaining on it.

    I am looking at using this in a commerical situation where multiple upload windows are required.

    We would be prepared to pay for your time to make the script multiple windows / tabs capable. help ???