Scriptorama.nl

Header image showing a keyboard, mouse, laptop and books on design patterns

Gears 0.4: HttpRequest met Progress Events

Een van de meest populaire artikelen op Scriptorama is, nog steeds, het artikel over het tonen van een progressbar bij het uploaden van een bestand. In dat artikel gaat dat met een aparte PHP extensie en moet het Javascript deel iedere zoveel tijd aan een PHP script vragen hoe ver z'n upload is. Niet het meest efficiente, zeker niet als je wat meer gebruikers hebt die wat uploaden.

De mensen bij het W3C zijn ook eindelijk een beetje wakker geworden wat betreft het uploaden van bestanden en hebben een nieuwe draft geschreven: Progress Events. Hiermee vuurt de browser bij bijvoorbeeld uploads, iedere zoveel tijd een event af met daarin gegevens over hoe ver de upload gevorderd is.

Op dit moment ondersteunt van de browsers alleen Firefox 3.1 alpha 2 "Shiretoko" een implementatie van Progress events. Maar, niet getreurd, er is ook nog de browser plugin Gears die in versie 0.4 Progress Events implementeert en die is beschikbaar voor alle recente browsers! Laten we de upload progress bar nog eens opnieuw knutselen, maar dan met Gears.

Wat is Gears?

Gears, voorheen Google Gears, is een plugin voor browsers, ontstaan om de voortgang van nieuwe features in browsers te stimuleren. Zo ondersteunde Gears als eerste de mogelijkheid om volledig offline te werken (en later naar online te syncen) en ontwikkelden ze de Web Worker API, waarmee stukken Javascript code in een aparte thread gedraaid kunnen worden.

Gears bestaat voor verschillende browsers: IE6/7, Firefox 2/3 en uiteraard de nieuwe Chrome browser van Google. Ondersteuning voor Safari is ook onderweg en is momenteel in beta.

De Gears ontwikkelaars ontwikkelen nieuwe ideeën met de hoop dat browsers ze later zullen overnemen, en met succes. Zo is de Web Workers API, die z'n origin heeft in Gears, is opgenomen in HTML5 en van de Geolocation API die Gears heeft voorgesteld is inmiddels ook een W3C draft opgesteld.

Uploaden met Gears

Een andere feature van Gears is het HttpRequest object, welke de Progress Events specificatie implementeert. Met het HttpRequest object is het mogelijk om uploads gemakkelijk te regelen van uit Javascript, zonder dat er speciale extensies geinstalleerd hoeven te worden op de server. Het nadeel, is natuurlijk dat er wél een speciale extensie - namelijk Gears - geinstalleerd moet worden bij de gebruiker.

Hoewel de naam misschien anders doet vermoeden is het met HttpRequest niet mogelijk om bestanden naar een willekeurige server te sturen. Net als het XmlHttpRequest object uit je browser is Gears' HttpRequest gebonden aan de same-origin policy, oftewel het request moet naar de site gaan die hem heeft gecreeërd.

Let's get to some code. De HTML code waarmee we zullen gaan werken als volgt:

HTML:
  1. <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
  2.     "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
  3.  
  4. <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
  5.     <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
  6.     <title>Scriptorama.nl ~ Bestanden uploaden met Gears 0.4</title>
  7.     <link rel="stylesheet" href="upload.css" type="text/css" />
  8.     <script src="http://ajax.googleapis.com/ajax/libs/jquery/1.2.6/jquery.min.js"></script>
  9. </head>
  10.  
  11.     <h1>Scriptorama: Uploaden met Gears.</h1>
  12.    
  13.     <div class="gears-available">
  14.         <div id="file-info">
  15.             <label>Bestand uploaden:</label>
  16.             <span>Geen bestand geselecteerd.</span>
  17.         </div>
  18.                
  19.         <div id="progress-bar">  
  20.             <div class="progress-bar-bg"></div>  
  21.             <div class="progress-bar-text">0%</div>  
  22.         </div>
  23.        
  24.         <input type="button" id="select_file" value="Kies een bestand" />
  25.         <input type="button" id="upload_file" value="Uploaden" />      
  26.     </div>
  27.    
  28.     <div class="gears-notavailable">
  29.         <div class="gears-notavailable" style="display: none;">
  30.             <h2>Gears plugin niet gevonden</h2>
  31.             <p>Voor deze web applicatie heb je de Gears browser plugin nodig. Deze kun je hier <a href="http://gears.google.com/?action=install&message=Installeer%20gears%20om%20van%20deze%20webapplicatie%20gebruik%20te%20kunnen%20maken&return=http://localhost/~mathieu/gears/">gratis en veilig downloaden</a>. Je wordt
  32.                 na de installatie vanzelf terug gestuurd naar deze pagina.</p>
  33.         </div>
  34.     </div>
  35. </body>
  36. </html>

Wat javascript, CSS, een progress bar, 2 buttons en een aparte verstopte div met installatie instructies. Nothing fancy.

Gears initialiseren

Voordat je ook maar iets kunt beginnen zul je eerst Gears moeten initialiseren. Om de plugin zelf te initialiseren wordt aangeraden om gears_init.js (gears_init.js downloaden) te gebruiken. Dus voeg het volgende toe aan de HEAD-tag:

HTML:
  1. <script src="gears_init.js"></script>

Maar, dat is nog niet alles. Het kan natuurlijk ook zo zijn dat de gebruiker nog geen Gears hééft. Dit is eenvoudig te detecteren door te controleren of na het laden van gears_init.js, de variabelen window.google en google.gears bestaan. Vervolgens kun je bijvoorbeeld installatie instructies laten zien:

HTML:
  1. <script type="text/javascript">
  2. $(document).ready(function() {       
  3.     if (!window.google || !google.gears)
  4.     {
  5.         $("div.gears-available").hide();
  6.         $("div.gears-notavailable").show();
  7.         return;
  8.     }
  9.  
  10.     /* Initialiseer de rest van de webapplicatie */  
  11. });
  12. </script>

De Gears website heeft een speciale installer pagina waar je een eigen bericht kunt laten zien en de gebruiker na installatie terug kunt sturen naar jouw pagina. Een voorbeeld URL is:

CODE:
  1. http://gears.google.com/?action=install&message=Installeer+gears+om+van+deze+webapplicatie+gebruik+te+kunnen+maken&return=http://localhost/~mathieu/gears/

Deze link staat bij onze HTML al in de div met CSS klasse gears-notavailable.

Bestanden selecteren

Omdat we Gears gebruiken zullen we geen standaard file input element gebruiken. Gears biedt namelijk met de Desktop API ook de mogelijkheid om bestanden (ook meerdere) te selecteren aan de hand van een opgegeven filter.

Om dat te doen moeten we eerst een "Desktop" object aanmaken:

JAVASCRIPT:
  1. var desktop = google.gears.factory.create('beta.desktop');

Vervolgens kun je met de methode desktop.openFiles() een venster laten openen waarin een gebruiker de gewenste bestanden kan selecteren.

openFiles() accepteert 2 argumenten. De eerste is een callback naar een functie die iets nuttigs met de geselecteerde bestanden kan doen, terwijl je met de tweede opties kan opgeven zoals dat een gebruiker maar 1 bestand mag opgeven of alleen JPEG bestanden mag opgeven.

Uiteraard willen we al dit pas uitvoeren op het moment dat iemand op de "Selecteer bestand" knop klikt. Dus, eerst definiëren we de functie om met de geselecteerde bestanden om te gaan:

JAVASCRIPT:
  1. var fileToUpload = null;
  2.  
  3. /* Ook als de optie singleFile op staat zal het argument voor de callback een array van files zijn */
  4. function handleSelectedFile(files)
  5. {
  6.     if (files.length)
  7.     {
  8.         fileToUpload = files[0];
  9.        
  10.         /* Laat de gebruiker weten wat hij heeft geselecteerd */
  11.         $("#file-info span").text(fileToUpload.name);
  12.     }
  13. }

Vervolgens voegen we in onze $(document).ready event handler een onclick event handler toe aan de knop die met desktop.openFiles het venster opent:

JAVASCRIPT:
  1. $("#select_file").click(
  2.     function() {
  3.         var options = { singleFile: true, filter: [ 'image/jpeg', 'image/png'] };
  4.         desktop.openFiles(handleSelectedFile, options);
  5.     }
  6. );

Als iemand nu op de Selecteer bestand knop klikt en een bestand selecteert hebben we daarna het geselecteerde bestand in de globale variabele fileToUpload.

Deze variabele is een object met 2 elementen: name (de bestandsnaam) en blob wat een referentie naar de daadwerkelijke inhoud van het bestand is. Deze zullen we later gebruiken om de bestandsgrootte uit te lezen en om het bestand daadwerklijk te uploaden.

Bestanden uploaden

Nu de gebruiker een bestand geselecteerd heeft, kunnen we het bestand ook gaan uploaden. Hiervoor moeten we weer een Gears object aanmaken, namelijk een HttpRequest object:

JAVASCRIPT:
  1. var request = google.gears.factory.create('beta.httprequest');

Op het moment dat iemand op de Upload bestand knop klikt, willen we het bestand versturen naar de server en bijhouden hoe ver we zijn met het uploaden van het bestand. Hiervoor zullen we de methode HttpRequest.send() gebruiken, maar voordat dat zover is zullen we eerst het request moeten samen stellen.

Dit doen we, net als met desktop.openFiles() in een onclick handler voor de knop, binnen de $(document).ready event handler:

JAVASCRIPT:
  1. $("#upload_file").click (
  2.     function() {
  3.         if (fileToUpload)
  4.         {
  5.             $("#file-info p").text("Uploading...");
  6.        
  7.             request.open('POST', '/~mathieu/gears/upload.php');
  8.             request.setRequestHeader("Content-Disposition", "attachment; filename=\"" + fileToUpload.name + "\"");
  9.             request.onreadystatechange = uploadReadyStateChange;
  10.             request.upload.onprogress  = uploadProgressChange;
  11.    
  12.             request.send(fileToUpload.blob);
  13.         }   
  14.     }
  15. );

De eerste twee regels zetten het request klaar:

JAVASCRIPT:
  1. request.open('POST', '/~mathieu/gears/upload.php');
  2. request.setRequestHeader("Content-Disposition", "attachment; filename=\"" + fileToUpload + "\"");

Dit zorgt ervoor dat het nieuwe HTTP request als POST request naar /~mathieu/gears/upload.php wordt verzonden. De regel daaronder geeft wat extra informatie over het bestand. Je mist misschien het Content-Length element, maar, deze wordt automatisch toegevoegd door Gears zelf.

Vervolgens definiëer ik 2 event handlers. Eentje voor het OnReadyStateChange event, wat afgevuurd wordt op het moment dat het request begint, bezig en afgerond is én eentje die voor de Uploads een progress update geeft. Let op dat dat gebeurt op request.upload, een specifiek eigenschap van het HttpRequest.

JAVASCRIPT:
  1. request.onreadystatechange = uploadReadyStateChange;
  2. request.upload.onprogress  = uploadProgressChange;

Met name de laatste is natuurlijk interessant. Dit is het event dat meerdere keren wordt getriggered tijdens het uploaden van het bestand met up-to-date informatie over hoever de upload is. Deze informatie staat in een object met 3 elementen: total, loaded en lengthComputable. Deze informatie kunnen we gebruiken om onze progressbar verder te tekenen:

JAVASCRIPT:
  1. function uploadProgressChange(event)
  2. {
  3.     var percentage = Math.round( (event.loaded / event.total) * 100 );
  4.    
  5.     if (percentage>= 50) {
  6.         $("#progress-bar .progress-bar-text").addClass("progress-50-percent");
  7.     }
  8.        
  9.     $("#progress-bar .progress-bar-bg").width(percentage + "%");
  10.     $("#progress-bar .progress-bar-text").text(percentage + "%");
  11. }

Nu rest ons alleen nog de afhandeling voor het moment dat de upload afgerond is, en dit doen we met het onreadystatechange event waar we het net al over hadden. Dit is ook de plek waarop we kunnen bepalen of het request gelukt is of niet. We kunnen de gebruiker dan informeren, bijvoorbeeld door de progressbar rood te maken:

JAVASCRIPT:
  1. function uploadReadyStateChange()
  2. {   
  3.     if (request.readyState == 4)
  4.     {
  5.         if (request.status != 200)
  6.         {
  7.             $("#progress-bar .progress-bar-bg").css('backgroundColor', '#FF0000');
  8.             $("#progress-bar .progress-bar-bg").css('color', 'white');
  9.             $("#file-info p").text("Upload mislukt: " + request.responseText);
  10.         } else {
  11.             $("#progress-bar .progress-bar-text").text("Klaar!");
  12.             $("#progress-bar .progress-bar-bg").css('background-color', '#00FF00');
  13.             $("#file-info p").text("Upload succesvol: " + request.responseText);
  14.         }
  15.     }
  16. }

Uploads van Gears verwerken met PHP

En voila, we hebben het bestand geupload met een mooie progress bar. Maar, daar zijn we er natuurlijk nog niet mee. We moeten het bestand ook nog verwerken binnen bijvoorbeeld een PHP script.

Nu zul je misschien denken "ach, je gebruikt $_FILES zoals je uitlegt in een van je artikelen en klaar is klara". Jammer, maar helaas. Gears ondersteunt nog niet multi-part form data uploads (RFC 1867) en dat houdt in dat PHP niet automatisch de geuploade data voor je kan verwerken via $_FILES.

We zullen dat zelf moeten doen, maar gelukkig biedt PHP ons nog wel een helpende hand. Het geuploade bestand is uit te lezen via de speciale stream wrapper php://input:

PHP:
  1. <?php
  2.  
  3. function report_error($title, $msg)
  4. {
  5.     header("HTTP/1.1 500 $title");
  6.     echo $msg;
  7.     exit;
  8. }
  9.  
  10. define('UPLOAD_DIRECTORY', '/Users/mathieu/Sites/gears/uploads/');
  11.  
  12. if (!is_writeable(UPLOAD_DIRECTORY))
  13.     report_error("System error", "Doel directory niet schrijfbaar!");
  14.  
  15. $filename = '';
  16.  
  17. /* Workaround voor bugje in Gears 0.4: http://code.google.com/p/gears/issues/detail?id=497 */
  18. $headers  = array_change_key_case(getallheaders(), CASE_LOWER);
  19. if (!isset($headers['content-disposition']) || !preg_match('~filename="([a-z0-9_\. \-]+)"$~i', $headers['content-disposition'], $matches))
  20.     report_error('System error', 'Geen bestandsinformatie gevonden: ' . $headers['content-disposition']);
  21.  
  22. $filename = basename($matches[1]);
  23. $fp_dst = @fopen(UPLOAD_DIRECTORY . $filename, 'w');
  24.  
  25. if (!$fp_dst)
  26.     report_error("System error", "Kan doelbestand niet schrijven");
  27.    
  28. /* De data van de upload is beschikbaar via php://input. We lezen
  29. * dit uit in blokken van 2Kb en schrijven die 2Kb direct weg naar het doel
  30. * bestand in plaats van het hele bestand eerst in geheugen te laden. Wel zo
  31. * geheugen efficient. */
  32. $fp_src = fopen('php://input', 'r');
  33.  
  34. $length = 0;
  35. while (!feof($fp_src)) {
  36.     $data = fread($fp_src, 2048);
  37.     $length += strlen($data);
  38.     fwrite($fp_dst, $data);
  39. }
  40.  
  41. fclose($fp_dst);
  42. fclose($fp_src);
  43.  
  44. echo "Victory! Wrote $length bytes";
  45. ?>

Downloaden

Uiteraard heb ik alle gebruikte bestanden even voor je verzameld in een makkelijke download, zodat je alles snel en gemakkelijk kunt gebruiken.

Download Gears-Progressbar.zip »

Conclusie

Hoewel het jammer is dat je met Gears de gebruiker een aparte extensie moet laten installeren, biedt het wel een hoop nieuwe mogelijkheden en vergeet niet dat een gebruiker best wel bereid is om een kleine extensie te installeren als dat betekent dat andere dingen makkelijker en beter gaan. Vergeet ook niet dat veel van de dingen die Gears biedt inmiddels ook in browsers zelf geimplementeerd wordt.

Buiten het HttpRequest object biedt Gears nog meer, zoals de Geolocation API, de LocalServer en de WorkerPool. Nieuwe ideeën liggen ook op tafel, zoals bijvoorbeeld de NotificationAPI of de CameraAPI. Ben benieuwd welke van deze dingen het ook nog schoppen tot een browser implementatie.

Have fun!

Reageer ook!

Beste Mathieu,

Je download link doet het niet bij mij. Ik krijg een melding dat de opgevraagde post niet bestaat.

Verder een heel mooi artikel !

Thanks Arian, ik heb de download link gefixed.

leuk om te melden dat php binnenkort standaard ook uploadinfo kan teruggeven vanuit de sessie.

Ietwat voorbarig, volgens mij is vandaag alleen een RFC met een patch aangeleverd maar nog niet geaccepteerd.

Daarbij, je kunt je afvragen wat voor nut dat nog heeft op het moment dat browsers progress events ondersteunen.

Deze RFC heeft inmiddels de status Implemented gekregen (zie de RFC Wiki van php.net). Ik neem aan dat dat inhoudt dat hij is geïmplementeerd in een recente PHP 5.3 build.

En ik denk dat je daar wel wat aan kunt hebben. Via een API in de browsers is het uiteraard sneller, en minder data- en requestvretend, maar je kunt hier altijd als een zekere factor op terugvallen (er van uitgaande dat je server PHP 5.3 heeft).

Sorry, het lijkt er op dat ik mij heb vergist. Hij is inderdaad geïmplementeerd, maar voor PHP 6. Zie de Undocumented PHP Features. Daar zie je onder het kopje 'PHP 6' staan dat er session upload progress feedback is toegevoegd.

Mooi artikel. Ben er alleen nog niet helemaal uit hoe ik meerdere files tegelijk door zo'n request moet sturen, of is het een beter idee om die dan te verwerken via een for loop? (maar dat maakt het vrij ingewikkeld om nog een smooth progress bar te krijgen..)

Leave a comment
Line and paragraph breaks automatic, e-mail address never displayed, HTML allowed: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>