Erstellt:25.01.2014
Aktualisiert:26.01.2014

AJAX Dateiupload und Uploadfortschritt

Ziel soll sein einen Dateiupload mit Fortschrittsanzeige zu gestalten. Es gibt dabei unterschiedliche Ansätze. Der Fortschritt einer Anfrage kann mit Javascript vom Browser oder auf dem Server mit einem Script erfasst werden. Für den Upload einer Datei gibt es auch zwei Lösungen. Zum einen kann man den Klassischen Ansatz über ein Form wählen oder die Datei mit einem XHR (Xml Http Request) übertragen. Für das empfangende Script ist es egal welche Methode eingesetzt wird.

Interessant ist die Kompatibilität zu verschiedenen Browsern. Der Internetexplorer kann erst ab Version 10 (oder 9) Dateien mit der hier beschriebenen Methode senden. Ältere Versionen beherrschen das FormData-Objekt nicht. Hier ist die Methode über ein Form die einfachste. Allerdings wird bei dieser Methode eine neue Seite geladen. Webkit basiere Browser, wie Chrome oder Safari, haben allerdings das Problem, dass während der Übermittlung eines Froms keine XHR verschickt werden können (Webkit-Bug). Am schönsten ist natürlich sowohl den Upload, als auch den Fortschritt mit XHR zu realisieren.

AJAX Dateiupload.rar

 

Upload mit XHR

Es wird natürlich ein Data-Input und ein Button zu senden und eine Anzeige für den Fortschirtt benötigt.

<!-- Ladebalken //--> <div align="left" id="ladebalken"> <div id="balken_rahmen" align="center"> <a id="zahl" class="zahl">0</a> <a class="zahl">%</a> </div> <div id="balken"></div> </div> <form enctype="multipart/form-data" name="dateiuploadForm" method="post"> <input type="file" name="datei"> <input type="button" value="hochladen" onClick="upload()"> </form>

Das Form-Element ist nicht nötig. Ich benutze es, da ich dadurch den Zugriff auf die darin enthaltenen Elemente einfacher habe. Alternativ kann man dem File-Input auch eine ID geben und über die diese darauf zugreifen.

Die Funktion upload() lädt die Datei hoch.

function upload() { datei = document.dateiuploadForm.datei.files[0]; fd = new FormData(); fd.append("datei", datei); request = new XMLHttpRequest(); request.open("POST", "/cgi-bin/upload.pl"); request.onreadystatechange = responseUpload; pingRequest = true; request.send(fd); setTimeout("sendPing()", 500); }

Mit dem FormData-Objekt können Name-Wert Paare erzeugt werden, wie sie in Forms vorkommen. Wir erzeugen ein Form-Element mit dem Namen datei und dem Inhalt der Variablen datei, also der Datei, die wir hochladen möchten. Anschließend wird ein Request erzeugt. Mit dem Event onreadystatechange können wir feststellen wann der Upload abgeschlossen ist. Mit der Funktion send() wird das erzeugte FormData-Objekt übertragen. Die Variable pingRequest ist nur ein kleiner Merker, mit dem wir feststellen können, ob der Upload noch läuft oder beendet ist. Als letztes wird noch die Funktion sendPing() mit einer Verzögerung von 500 ms aufgerufen.

Die Funktion sendPing() fordert den aktuellen Uploadfortschritt an.

function sendPing() { request = new XMLHttpRequest(); request.open("POST", "/cgi-bin/upload.pl?ping=1", true); request.onreadystatechange = responsePing; request.send(); return true; }

Hier wird wieder ein XHR erstellt und gesendet. Zum Empfangen des Fortschritts wird wieder das onreadystatechange-Event benutzt. Damit unser empfangendes Script nachher weiß ob wir gerade eine Datei hochladen möchten oder den Fortschritt haben möchten wird an die URL noch das Argument ping=1 angehängt.

Wurde der Fortschritt vom Serverscript berechnet und zurückgesendet wird die Funktion responsePing() mit dem Status 4 aufgerufen.

function responsePing() { if(this.readyState == 4) { if(pingRequest) { fortschritt = this.responseText; document.getElementById("zahl").innerHTML = Math.round(fortschritt); document.getElementById("balken").style.width = Math.round(fortschritt)+"px"; setTimeout("sendPing()", 500); } } }

Ist der ready State 4, also die Anfrage beendet, können wir die Antwort auslesen und den Fortschrittsbalken aktualisieren. Dann wird die Funktion sendPing() nach 500 ms erneut aufgerufen. Hier kommt auch wieder unser pingRequest ins Spiel. Es soll nur eine weitere Anfrage stattfinden, wenn der Upload noch läuft.

Ist der Upload abgeschlossen wird das Event responseUpload mit dem Status 4 aufgerufen.

function responseUpload(eventObject) { if(this.readyState == 4) { pingRequest = false; document.getElementById("zahl").innerHTML = 100; document.getElementById("balken").style.width = "100px"; } }

Hier wird die Variable pingRequest auf false gesetzt, damit keine weiteren Anfragen zum Fortschritt gesendet werden. An dieser Stelle könnte auch eine Meldung erfolgen, dass der Upload abgeschlossen ist oder ein hochgeladenen Bild angezeigt werden.

 

Upload mit Form

Der größte Unterschied zum Upload mit XHR ist das Form. Hier wird natürlich noch ein action angegeben. Zudem wird noch ein onSubmit-Event hinzugefügt, das die erste Anfrage für den Uploadfortschritt sendet. Der Button ist vom Typ submit und hat kein onClick-Event.

<form enctype="multipart/form-data" name="dateiform" method="post" action="/cgi/upload.pl" onSubmit="sendPing()"> <input type="file" name="datei"> <input type="submit" value="hochladen"> </form>

Die Funktion sendPing() und responsePing() bleiben fast identisch. Man muss nur die Variable pingRequest und dessen Abfrage entfernen. Bei dieser Art der Übermittlung wird die Variable nicht gebraucht, da ohnehin eine neue Seite geladen wird.

 

Datenempfang mit Perl

Zuerst möchte ich das gundsätzliche Prinzip erläutern. Die Datei wird von dem Script empfangen. Während der Übertragung wir zyklisch eine Funktion aufgerufen, die in einer Datei den Fortschritt speichert. Damit die Datei einem Upload zugeordnet werden kann wird die Session ID als Dateiname verwendet. Wird das Script jetzt erneut aufgerufen mit der Anfrage nach dem Fortschritt, wird die Session-Datei mit dem aktuelle Fortschritt ausgelesen.

Wir fangen erstmal mit den benötigten Bibliotheken an.

use CGI::Carp qw(fatalsToBrowser); # Fehler in Browser anzeigen use CGI qw/:standard/; use CGI::Cookie; # Cookie verwenden use Fcntl ':flock'; # Dateien sperren use Fcntl qw/:DEFAULT/;

Damit der Fortschritt des Uploads ermittelt werden kann brauchen wir eine Funktion, die während der Übertragung zyklisch aufgerufen wird. Dazu gibt es einen sogenannten hook, ein Event, dass während der Übertragung mehrmals ausgelöst wird.

$cgi = new CGI(\&hook, $ENV{'CONTENT_LENGTH'}) or die $!;

Wir übergeben der Funktion, die aufgerufen wird noch die Größe der zu übertragenden Daten. Zu beachten ist, dass in der Umgebungsvariablen $ENV{'CONTENT_LENGTH'} die Größe alle im Request enthaltenen Daten gespeichert ist. Aber in diesem Fall können wir das vernachlässigen.

Im folgenden Code wird die Datei empfangen und gespeichert.

binmode(STDOUT); print ("Content-type: text/html\n\n"); # Session ID aus Cookie auslesen %cgiCookie = CGI::Cookie->fetch or die print "Cookie konnte nicht gelesen werden."; $sessionID = $cgiCookie{'session'}->value or die print "Session ID konnte nicht gelesen werden."; $upload = $cgi->param('datei') or die print "Keine Datei übergeben"; # Dateiinhalt (Handle) binmode($upload); $dateiName = $cgi->param('datei'); # Dateiname # Datei speichern open ($zielDatei, ">$dateiName"); binmode($zielDatei); while($buf = readline($upload)) { print $zielDatei $buf; } # Ausgabe wenn Datei mit Form übertragen wird print "<html> <body> <p>Fertig!</p> </body> </html>"; # Session-datei löschen unlink("$sessionID.txt");

Zuerst wird die Session ID aus dem Cookie ausgelesen. An dem Punkt $upload = $cgi->param('datei') wird das Script nicht weiter ausgeführt, bis die Dtei empfangen wurde. Ist die Datei übertragen wird eine neue Datei angelegt und der Dateiinhalt in einer Schleife kopiert. Ist die Datei gespeichert wird noch ein Ausgabe gemacht, falls das Script von einem Form aufgerufen wurde. Zuletzt wird die Session-Datei gelöscht.

Im hook wird der aktuelle Fortschritt in die Session-Datei gespeichert.

sub hook { my ($dateiname, $buffer, $bytesGelesen, $content_legnth) = @_; # Session ID aus Cookie auslesen my %cgiCookie = CGI::Cookie->fetch or die $!; my $sessionID = $cgiCookie{'session'}->value or die $!; # Fortschritt in Session-Datei schreiben sysopen($sessionDatei, $sessionID.".txt", O_WRONLY | O_CREAT) or die $!; flock($sessionDatei, LOCK_EX) or die $!; binmode($sessionDatei); seek($sessionDatei, 0, 0); truncate($sessionDatei, 0); print $sessionDatei $bytesGelesen * 100 / $content_legnth; close($sessionDatei); }

Wichtig dabei ist, dass die Funktion sysopen() und nicht open() in Verbindung mit flock() verwendet wird. Unter ungünstigen Umständen kann die Funktion open() einen Dateizugriff zulassen, da die Datei nicht sofort gesperrt wird. Deswegen wird die Datei zuerst geöffnet, der Zugriff gesperrt, dann der Inhalt gelöscht und anschließend neu beschrieben. Mit dem Schließen der Datei wird auch der Lock aufgehoben. Ist die Datei gesperrt wartet die Funktion sysopen() so lange bis die Datei wirder freigegeben wird (open() funktioniert auf gleiche Art).

Zuletzt der Code für den Downloadfortschritt.

binmode(STDOUT); print ("Content-type: text/plain\n\n"); # Session ID aus Cookie auslesen %cgiCookie = CGI::Cookie->fetch or die $!; $sessionID = $cgiCookie{'session'}->value or die $!; # Downloadfortschritt aus Datei auslesen if( sysopen($sessionDatei, $sessionID.".txt", O_RDWR) ) { binmode($sessionDatei); flock($sessionDatei, LOCK_EX) or die $!; $fortschritt = readline $sessionDatei; close($sessionDatei); } else { $fortschritt = 0; } print $fortschritt;

Es wird wieder die Session ID ausgelesen und die Session-Datei geöffnet. Dabei muss die Datei von sysopen() lesend und schreibend geöffnet werden, damit die Funktion flock() funktioniert. Kann die Datei nicht geöffnet werden, wird der Fortschritt = 0 zurückgegeben. Ist die Datei gerade gesperrt ist die Bedingung trotzdem erfüllt, dann wird gewartet bis die Datei wieder freigegeben wird.