BenBE's humble thoughts Thoughts the world doesn't need yet …

16.01.2011

Resourcenschonendes Upload-Tracking

Filed under: Server — Schlagwörter: , , , , — BenBE @ 21:25:28

Wenn man auf einer Web-Oberfläche Dateien hochladen möchte, so gibt es hierfür im wesentlichen zwei sehr verbreitete Möglichkeiten: Während die erste Version gemäß dem HTTP-Standard und dem application/x-www-form-urlencoded-Encoding die Datenüberträgt, was jeder heutige Browser unterstützt, so findet man an verschiedensten Stellen sogenannte Flash-Uploader, die zwar im Wesentlichen das Gleiche tun, jedoch versuchen verschiedene Funktionen nachzurüsten, die in vielen Browsern fehlen. Eine dieser Funktionen ist das Anzeigen des Upload-Fortschritts oder die Anzeige der Upload-Geschwindigkeit.

Im Internet findet man für diese Funktion auch verschiedene Ansätze, die jedoch meist darauf hinauslaufen, auf dem Server ein zusätzliches Perl-Script zu installieren, was dann versucht aus dem Temp-Verzeichnis von PHP die Daten zusammenzukratzen. Dies ist nicht nur ineffizient, da für jede Fortschrittsabfrage eine vollständige Perl-Instanz gestartet werden muss, sondern oft auch reichlich wacklig, wenn es um neuere Versionen von Scripten geht.

Eine wesentlich bessere Lösung wäre hier, wenn der Server sich um das Tracken von Uploads kümmern könnte und man somit keinen zusätzlichen Speicher für derlei Fortschrittsabfragen verwenden muss. Zusätzlich kann durch den Wegfall solcher externen Programme deren Ladezeit eingespart werden, wenn der Server dies bereits selbst verwaltet.

Und genau hier setzt mod_upload_progress an, der als Apache-Modul alle laufenden Upload-Vorgänge verfolgt und deren Status abfragbar macht. Diese lässt sich mit wenigen Schritten installieren und zusätzlich an die eigenen Wünsche anpassen. Aber der Reihe nach.

Der erste Schritt ist, sich den Code entweder via Git

git clone git://github.com/drogus/apache-upload-progress-module.git

herunterzuladen oder aus dem Download-Bereich bei Github abzuholen. Danach müssen die Dateien in einen leeren Ordner entpackt werden. Ab hier gibt es nun zwei Möglichkeiten:

  1. Die mitgelieferte Makefile nutzen
  2. Via apt-src das bestehende Debian-Source-Paket laden und dort die C-Sourcefile durch die heruntergeladene Version ersetzen

Bei der zweiten Version wird uns von apt-src auch gleich ein weiterer Schritt abgenommen: Wir benötigen das Programm apxs2, welches im Paket apache2-threaded-dev bzw. apache2-prefork-dev enthalten ist. Zusätzlich benötigen wir einen Apache ab Version 2.0.47, der aber mit einem ausreichend auf dem aktuellen Stand gehaltenen System gegeben sein dürfte. Sollten diese Voraussetzungen nicht gegeben sein, sollten diese jetzt mit einer kurzen Installation der fehlenden Teile vervollständigt werden.

Als nächstes geht es nun an das Bauen des Apache-Moduls. Je nach gewähltem Weg ist der nächste Schritt nun:

  1. make im Source-Verzeichnis aufzurufen
  2. Das Paket mit
    apt-src build -i apache-upload-progress-module

    zu übersetzen und zu installieren.

Wer der Makefile nicht vertraut, kann auch selbst über apxs2 Hand anlegen: Hierzu muss im Quelltextverzeichnis

apxs2 -c -i -a mod_upload_progress.c

aufgerufen werden, was den gleichen Effekt wie die Variante mit dem Debian Package besitzt, aber eine Reihe von Nachteilen mit sich bringt:

  • Das Modul wird nicht von Debian verwaltet
  • Man kann schwerer nachvollziehen, WAS man da installiert hat
  • Im Debian-Paket ist eine Beispiel-Datei enthalten, mit der man sein Modul testen kann.

Nach Abschluss muss nun noch der Apache neugestartet werden.

apache2ctl restart

Nachdem wir nun den langweiligen Teil abgearbeitet haben, nun zum interessanten Aspekt: Wie nutzt man das. Hierzu werfen wir einfach einmal einen kurzen Blick in den Quelltext der Demo-Seite:

<!DOCTYPE html
     PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
     "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
    <title>mod_upload_progress test page</title>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
    <script type="text/javascript">// <![CDATA[
interval = null;

function fetch(uuid) {
    req = new XMLHttpRequest();
    req.open("GET", "/progress", 1);
    req.setRequestHeader("X-Progress-ID", uuid);
    req.onreadystatechange = function () {
        if (req.readyState == 4) {
            if (req.status == 200) {
                /* poor-man JSON parser */
                document.getElementById('dump').innerHTML = req.responseText;
                var upload = eval('new Object(' + req.responseText + ')');
                document.getElementById('tp').innerHTML = upload.state;

                /* change the width if the inner progress-bar */
                if (upload.state == 'done' || upload.state == 'uploading') {
                    bar = document.getElementById('progressbar');
                    w = 400 * upload.received / upload.size;
                    bar.style.width = w + 'px';
                }

                /* we are done, stop the interval */
                if (upload.state == 'done') {
                    bar = document.getElementById('progressbar');
                    bar.style.width = '398px';
                    window.clearTimeout(interval);
                }
            }
        }
    }

    req.send(null);
}

function openProgressBar(event) {
    /* generate random progress-id */
    uuid = "";
    for (i = 0; i < 32; i++) {
        uuid += Math.floor(Math.random() * 16).toString(16);
    }

    /* create an iframe as a form target */
    document.getElementById("frameholder").innerHTML =
        '<iframe id="uploadframe" name="uploadframe" width="0" height="0" ' +
            'frameborder="0" border="0" src="about:blank"></iframe>';

    /* patch the form-action tag to include the progress-id */
    var form = document.getElementById("upload");
    form.action="/upload.html?X-Progress-ID=" + uuid;
    form.target="uploadframe";

    /* call the progress-updater every 1000ms */
    interval = window.setInterval(
        function () {
            fetch(uuid);
        }, 1000);

    form.submit();
    return false;
}
// ]]></script>
</head>

<body>
    <h1>mod_upload_progress test page</h1>
    <div>
        <form id="upload" enctype="multipart/form-data"
            action="/upload.html" method="post"
            onsubmit="return openProgressBar();">
            <div>
                <input type="hidden" name="MAX_FILE_SIZE" value="30000000" />
                <input name="userfile" type="file" />
                <input type="submit" value="Send File" />
            </div>
        </form>
    </div>

    <div>
        <div id="progress" style="width: 400px; border: 1px solid black">
            <div id="progressbar"
                style="width: 1px; background-color: black; border: 1px solid white">
                &nbsp;
            </div>
        </div>
        <div id="tp">(throughput)</div>
        <pre id="dump"></pre>
        <div id="frameholder"></div>
    </div>
</body>
</html>

Während der untere Teil hier relativ uninteressant ist (und nur das Webformular zum Upload, sowie die Progress-Bar definiert, ist der Java-Script durchaus einen Blick wert:

interval = null;

function fetch(uuid) {
    req = new XMLHttpRequest();
    req.open("GET", "/progress", 1);
    req.setRequestHeader("X-Progress-ID", uuid);
    req.onreadystatechange = function () {
        if (req.readyState == 4) {
            if (req.status == 200) {
                /* poor-man JSON parser */
                document.getElementById('dump').innerHTML = req.responseText;
                var upload = eval('new Object(' + req.responseText + ')');
                document.getElementById('tp').innerHTML = upload.state;

                /* change the width if the inner progress-bar */
                if (upload.state == 'done' || upload.state == 'uploading') {
                    bar = document.getElementById('progressbar');
                    w = 400 * upload.received / upload.size;
                    bar.style.width = w + 'px';
                }

                /* we are done, stop the interval */
                if (upload.state == 'done') {
                    bar = document.getElementById('progressbar');
                    bar.style.width = '398px';
                    window.clearTimeout(interval);
                }
            }
        }
    }

    req.send(null);
}

function openProgressBar(event) {
    /* generate random progress-id */
    uuid = "";
    for (i = 0; i < 32; i++) {
        uuid += Math.floor(Math.random() * 16).toString(16);
    }

    /* create an iframe as a form target */
    document.getElementById("frameholder").innerHTML =
        '<iframe id="uploadframe" name="uploadframe" width="0" height="0" ' +
            'frameborder="0" border="0" src="about:blank"></iframe>';

    /* patch the form-action tag to include the progress-id */
    var form = document.getElementById("upload");
    form.action="/upload.html?X-Progress-ID=" + uuid;
    form.target="uploadframe";

    /* call the progress-updater every 1000ms */
    interval = window.setInterval(
        function () {
            fetch(uuid);
        }, 1000);

    form.submit();
    return false;
}

Der JavaScript-Teil besteht aus zwei Funktionen, von denen die erste – fetch – in regelmäßigen Abständen aufgerufen wird, um den Fortschritt vom Server abzufragen, der daraufhin ein JSON-Objekt mit einer Reihe von Angaben liefert:

  • state: Der Zustand des Uploads. Dieser kann sein:
    • starting: Download noch nicht beim Server bekannt, oder existiert (noch) nicht),
    • uploading: Die Datei wird hochgeladen
    • done: Der Upload ist abgeschlossen
    • error: Beim Upload ist ein Fehler aufgetreten

    Je nach Inhalt dieses Wertes unterscheidet sich die Verfügbarkeit einiger anderer Angaben im zurückgelieferten Objekt.

  • uuid: Die UUID des Uploads. Dazu gleich mehr.
  • size: Die Gesamtgröße des Uploads in Bytes.
  • received: Bereits auf dem Server registrierte Datenmenge
  • speed: Übertragungsgeschwindigkeit in Bytes/s.
  • started_at: Unix-Timestamp zu dem der Upload zum Server gestartet wurde.
  • status: Enthält im Fehlerfalle die Ursache als HTTP-Fehlercode.

Okay, zur UUID: Diese ist (bisher) ein ungefilterter String beliebiger Länge, den der Server nicht weiter auswertet. Dieser muss sowohl beim Abfragen des Status als auch beim Starten des Uploads übergeben werden. Bei Uploads muss dazu am Ende der URL ein GET-Parameter mit dem Namen X-Progress-ID (ja, ich weiß, der ist nicht RFC1739-konform) übergeben werden. Der eigentliche Upload muss als POST-Request erfolgen: Der besagte Parameter ist hierbei Teil der Abfrage. Dies wird durch die zweite Funktion openProgressBar erledigt.

Für die Abfrage des Status ruft man mit GET die Seite /progress (oder was für das Upload-Tracking konfiguriert wurde) mit einem zusätzlichen HTTP-Header X-Progress-ID auf, der den gleichen Wert, wie der für den Upload verwendete, enthält.

Nun noch ein kurzes Wort zur Konfiguration:

Im Apache wird das Modul wie gewohnt geladen:

LoadModule upload_progress_module /usr/lib/apache2/modules/mod_upload_progress.so

und kann dann mit Hilfe der Direktive

UploadProgressSharedMemorySize 1024000

dazu gebracht werden, die intern verwendete Größe für den Shared Memory zu setzen. Eine Erhöhung ist hier ggf. nötig, sollte man einen Server haben, der EXTREM viele gleichzeitige Uploads behandeln muss. Dieser sollte aber in der Regel vollkommen ausreichen.

Zwei weitere wichtige Direktiven erklären sich am Besten mit Hilfe der zugehörigen Konfigurationsdatei des VHosts:

    <Location />
        # enable tracking uploads in /
        TrackUploads On
    </Location>

    <Location /progress>
        # enable upload progress reports in /progress
        ReportUploads On
    </Location>

Der erste Location-Block legt hierbei fest, dass alle Uploads, die in diese Location fallen überwacht werden sollen, während der zweite Location-Block festlegt, wo man die Informationen zu diesen Uploads abfragen kann.

Hat man alles im Apache konfiguriert, sollte diese Variante mit Response-Zeiten nur unwesentlich langsamer als Pings besitzen und somit ein relativ flüssiges Anzeigen des Fortschritts ermöglichen. Bei Problemen einfach bei Github nen Bug filen oder selber fixen 😉

Flattr this!

Keine Kommentare »

No comments yet.

RSS feed for comments on this post. TrackBack URL

Leave a comment

Powered by WordPress