views:

273

answers:

2

I have a script that requires quite e few seconds of processing, up to about minute. The script resizes an array of images, sharpens them and finally zips them up for the user to download.

Now I need some sort of progress messages. I was thinking that with jQuery's .post() method the data from the callback function would progressively update, but that doesn't seem to work.

In my example I am just using a loop to simulate my script:

        $(document).ready(function() {
            $('a.loop').click(function() {
                $.post('loop.php', {foo:"bar"},
                function(data) {
                    $("div").html(data);                        
                });
                return false;
            });
        });

loop.php:

for ($i = 0; $i <= 100; $i++) {
    echo $i . "<br />";
}
echo "done";
+3  A: 

You need some kind of progress info from the server. The ajax callbacks do no progressive work, they fire just once - after the request returned successfully.

So... in PHP you would need something like this:

/* progress.php */
$batch_done = some_way_to_find_out_that_number();
$batch_size = some_way_to_find_out_that_number_too();
header('Content-type: application/json');
// TODO: format number
echo '{"progress":'. ($batch_size==0 ? '0' : $batch_done*100.0/$batch_size).'}';

For this to work your image processing script must leave some evidence of its progress of course.

And in JavaScript something like this:

$(document).ready(function() {
  $('a.loop').click(function() {
    var queryData = {foo:"bar"};
    // prepare a function that does cyclic progress checking
    var progressCheck = function() {
      $.getJSON(
        "progress.php", queryData,
        function(data) { 
          $("div.progress").css("width", data.progress+"%");
        }
      )
    };
    $.post(
      'loop.php', queryData,
      /* create the post request callback now */
      function(intvalId){
        return function(data) {
          $("div").html(data);
          clearInterval(intvalId);
        }
      }( setInterval(progressCheck, 1000) )
    );
    return false;
  });
});

This part requires some explanation:

function(intvalId){
  return function(data) {
    $("div").html(data);
    clearInterval(intvalId);
  };
}( setInterval(progressCheck, 1000) )

function(intvalId) is an anonymous function that takes one argument - an interval ID. This ID is necessary to stop an interval that has been set up via setInterval(). Luckily, the call to setInterval() returns this very ID.

The anonymous outer function returns an inner function(data), this one will be the actual callback for $.post().

We call the outer function immediately, doing two things in the process: Triggering off the interval with setInterval() and passing in its return value (the ID) as an argument. This argument value will be available to the inner function at its call time (which may be some minutes in the future). The callback for post() now can actually stop the interval.

As an exercise for you ;)

  • Modify the ajax call such that it stops the interval on request error or timeout, too. Currently, if the callback is never run (and it runs on success only!), the interval will never stop.
  • Make sure the post() cannot be triggered twice inadvertently.
Tomalak
This is a serious reply that will require some studying. Thanks Tomalak!
FFish
@FFish: Note that none of this is actually tested, it is merely to get you started. The thing I explained more thoroughly is called a closure, BTW. You will find much stuff on JavaScript closures and their importance in asynchronous JS programming around the Internet.
Tomalak
I am stuck with your line "For this to work your image processing script must leave some evidence of its progress of course."I tried with sessions with the loop example (also tried with a sleep() to mimic the processing), but the script gets executed immediately. Why do I need 2 php scripts anyway?
FFish
@FFish: You need one PHP to do the actual work of converting a dozen images or so. This will run on a thread on the server until it's done and be unresponsive during that time. You need a way of querying how far that script got in the meantime. The only way to do this is making another PHP script that responds the current progress back to you (say by comparing the number of images done with the number of images left, like indicated). So unless I got you completely wrong, two PHP files are the only way to do it.
Tomalak
Hey what about E_USER_ERROR with PHP, I have seen something like this before.. it should write at runtime no? It doesn't need to be ajax, request to another php page is fine for me. Cheers
FFish
@FFish, Frankly, I did not understand that last comment. I have a feeling that your PHP part of the problem is not even solved yet. You should not be writing any client side JS code as long as the server side code does not work reliably…
Tomalak
+2  A: 

Thanks to Tomalak I finally put something together that works. Since I am not actually writing my image files on the server when processing the batch I wrote a log file that I am consulting in the progress.php script.

I would like to know if this is the best way of doing this. I wanted to avoid writing to a file and tried with PHP's $_session but cannot seem to progressively read from it. Is this possible with $_session?

HTML:

<a class="download" href="#">request download</a>
<p class="message"></p>

JS:

$('a.download').click(function() {
    var queryData = {images : ["001.jpg", "002.jpg", "003.jpg"]};

    var progressCheck = function() {
        $.get("progress.php",
            function(data) {
                $("p.message").html(data); 
            }
        );
    };

    $.post('proccess.php', queryData,
        function(intvalId) {
            return function(data) {
                $("p.message").html(data);
                clearInterval(intvalId);
            }
        } (setInterval(progressCheck, 1000))
    );

    return false;
});

process.php:

$arr = $_POST['images'];
$arr_cnt = count($arr);
$filename = "log.txt";

$i = 1;
foreach ($arr as $val) {
    $content = "processing $val ($i/$arr_cnt)";

    $handle = fopen($filename, 'w');
    fwrite($handle, $content);
    fclose($handle);

    $i++;
    sleep(3); // to mimic image processing
}

echo "<a href='#'>download zip</a>";

progress.php:

$filename = "log.txt";
$handle = fopen($filename, "r");
$contents = fread($handle, filesize($filename));
fclose($handle);

echo $contents; 
FFish