{"id":973,"date":"2011-01-16T21:25:28","date_gmt":"2011-01-16T20:25:28","guid":{"rendered":"http:\/\/blog.benny-baumann.de\/?p=973"},"modified":"2011-01-16T21:25:28","modified_gmt":"2011-01-16T20:25:28","slug":"resourcenschonendes-upload-tracking","status":"publish","type":"post","link":"https:\/\/blog.benny-baumann.de\/?p=973","title":{"rendered":"Resourcenschonendes Upload-Tracking"},"content":{"rendered":"<p>Wenn man auf einer Web-Oberfl\u00e4che Dateien hochladen m\u00f6chte, so gibt es hierf\u00fcr im wesentlichen zwei sehr verbreitete M\u00f6glichkeiten: W\u00e4hrend die erste Version gem\u00e4\u00df dem HTTP-Standard und dem application\/x-www-form-urlencoded-Encoding die Daten\u00fcbertr\u00e4gt, was jeder heutige Browser unterst\u00fctzt, so findet man an verschiedensten Stellen sogenannte Flash-Uploader, die zwar im Wesentlichen das Gleiche tun, jedoch versuchen verschiedene Funktionen nachzur\u00fcsten, die in vielen Browsern fehlen. Eine dieser Funktionen ist das Anzeigen des Upload-Fortschritts oder die Anzeige der Upload-Geschwindigkeit.<\/p>\n<p>Im Internet findet man f\u00fcr diese Funktion auch verschiedene Ans\u00e4tze, die jedoch meist darauf hinauslaufen, auf dem Server ein zus\u00e4tzliches Perl-Script zu installieren, was dann versucht aus dem Temp-Verzeichnis von PHP die Daten zusammenzukratzen. Dies ist nicht nur ineffizient, da f\u00fcr jede Fortschrittsabfrage eine vollst\u00e4ndige Perl-Instanz gestartet werden muss, sondern oft auch reichlich wacklig, wenn es um neuere Versionen von Scripten geht.<\/p>\n<p>Eine wesentlich bessere L\u00f6sung w\u00e4re hier, wenn der Server sich um das Tracken von Uploads k\u00fcmmern k\u00f6nnte und man somit keinen zus\u00e4tzlichen Speicher f\u00fcr derlei Fortschrittsabfragen verwenden muss. Zus\u00e4tzlich kann durch den Wegfall solcher externen Programme deren Ladezeit eingespart werden, wenn der Server dies bereits selbst verwaltet.<\/p>\n<p>Und genau hier setzt <a href=\"https:\/\/github.com\/drogus\/apache-upload-progress-module\">mod_upload_progress<\/a> an, der als Apache-Modul alle laufenden Upload-Vorg\u00e4nge verfolgt und deren Status abfragbar macht. Diese l\u00e4sst sich <a href=\"http:\/\/piotrsarnacki.com\/2008\/06\/18\/upload-progress-bar-with-mod_passenger-and-apache\/\">mit wenigen Schritten installieren<\/a> und zus\u00e4tzlich an die eigenen W\u00fcnsche anpassen. Aber der Reihe nach.<!--more--><\/p>\n<p>Der erste Schritt ist, sich den Code entweder via Git <\/p>\n<pre lang=\"bash\">git clone git:\/\/github.com\/drogus\/apache-upload-progress-module.git<\/pre>\n<p>herunterzuladen oder aus dem <a href=\"https:\/\/github.com\/drogus\/apache-upload-progress-module\/tarball\/master\">Download-Bereich bei Github abzuholen<\/a>. Danach m\u00fcssen die Dateien in einen leeren Ordner entpackt werden. Ab hier gibt es nun zwei M\u00f6glichkeiten:<\/p>\n<ol>\n<li>Die mitgelieferte Makefile nutzen<\/li>\n<li>Via apt-src das bestehende Debian-Source-Paket laden und dort die C-Sourcefile durch die heruntergeladene Version ersetzen<\/li>\n<\/ol>\n<p>Bei der zweiten Version wird uns von apt-src auch gleich ein weiterer Schritt abgenommen: Wir ben\u00f6tigen das Programm apxs2, welches im Paket apache2-threaded-dev bzw. apache2-prefork-dev enthalten ist. Zus\u00e4tzlich ben\u00f6tigen wir einen Apache ab Version 2.0.47, der aber mit einem ausreichend auf dem aktuellen Stand gehaltenen System gegeben sein d\u00fcrfte. Sollten diese Voraussetzungen nicht gegeben sein, sollten diese jetzt mit einer kurzen Installation der fehlenden Teile vervollst\u00e4ndigt werden.<\/p>\n<p>Als n\u00e4chstes geht es nun an das Bauen des Apache-Moduls. Je nach gew\u00e4hltem Weg ist der n\u00e4chste Schritt nun:<\/p>\n<ol>\n<li>make im Source-Verzeichnis aufzurufen<\/li>\n<li>Das Paket mit\n<pre lang=\"bash\">apt-src build -i apache-upload-progress-module<\/pre>\n<p> zu \u00fcbersetzen und zu installieren.<\/li>\n<\/ol>\n<p>Wer der Makefile nicht vertraut, kann auch selbst \u00fcber apxs2 Hand anlegen: Hierzu muss im Quelltextverzeichnis<\/p>\n<pre lang=\"bash\">apxs2 -c -i -a mod_upload_progress.c<\/pre>\n<p>aufgerufen werden, was den gleichen Effekt wie die Variante mit dem Debian Package besitzt, aber eine Reihe von Nachteilen mit sich bringt:<\/p>\n<ul>\n<li>Das Modul wird nicht von Debian verwaltet<\/li>\n<li>Man kann schwerer nachvollziehen, WAS man da installiert hat<\/li>\n<li>Im Debian-Paket ist eine Beispiel-Datei enthalten, mit der man sein Modul testen kann.<\/li>\n<\/ul>\n<p>Nach Abschluss muss nun noch der Apache neugestartet werden.<\/p>\n<pre lang=\"bash\">apache2ctl restart<\/pre>\n<p>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:<\/p>\n<pre lang=\"html4strict\" escaped=\"true\">&lt;!DOCTYPE html\r\n     PUBLIC \"-\/\/W3C\/\/DTD XHTML 1.0 Strict\/\/EN\"\r\n     \"http:\/\/www.w3.org\/TR\/xhtml1\/DTD\/xhtml1-strict.dtd\"&gt;\r\n&lt;html xmlns=\"http:\/\/www.w3.org\/1999\/xhtml\" xml:lang=\"en\" lang=\"en\"&gt;\r\n&lt;head&gt;\r\n    &lt;title&gt;mod_upload_progress test page&lt;\/title&gt;\r\n    &lt;meta http-equiv=\"Content-Type\" content=\"text\/html; charset=utf-8\"\/&gt;\r\n    &lt;script type=\"text\/javascript\"&gt;\/\/ &lt;![CDATA[\r\ninterval = null;\r\n\r\nfunction fetch(uuid) {\r\n    req = new XMLHttpRequest();\r\n    req.open(\"GET\", \"\/progress\", 1);\r\n    req.setRequestHeader(\"X-Progress-ID\", uuid);\r\n    req.onreadystatechange = function () {\r\n        if (req.readyState == 4) {\r\n            if (req.status == 200) {\r\n                \/* poor-man JSON parser *\/\r\n                document.getElementById('dump').innerHTML = req.responseText;\r\n                var upload = eval('new Object(' + req.responseText + ')');\r\n                document.getElementById('tp').innerHTML = upload.state;\r\n\r\n                \/* change the width if the inner progress-bar *\/\r\n                if (upload.state == 'done' || upload.state == 'uploading') {\r\n                    bar = document.getElementById('progressbar');\r\n                    w = 400 * upload.received \/ upload.size;\r\n                    bar.style.width = w + 'px';\r\n                }\r\n\r\n                \/* we are done, stop the interval *\/\r\n                if (upload.state == 'done') {\r\n                    bar = document.getElementById('progressbar');\r\n                    bar.style.width = '398px';\r\n                    window.clearTimeout(interval);\r\n                }\r\n            }\r\n        }\r\n    }\r\n\r\n    req.send(null);\r\n}\r\n\r\nfunction openProgressBar(event) {\r\n    \/* generate random progress-id *\/\r\n    uuid = \"\";\r\n    for (i = 0; i &lt; 32; i++) {\r\n        uuid += Math.floor(Math.random() * 16).toString(16);\r\n    }\r\n\r\n    \/* create an iframe as a form target *\/\r\n    document.getElementById(\"frameholder\").innerHTML =\r\n        '&lt;iframe id=\"uploadframe\" name=\"uploadframe\" width=\"0\" height=\"0\" ' +\r\n            'frameborder=\"0\" border=\"0\" src=\"about:blank\"&gt;&lt;\/iframe&gt;';\r\n\r\n    \/* patch the form-action tag to include the progress-id *\/\r\n    var form = document.getElementById(\"upload\");\r\n    form.action=\"\/upload.html?X-Progress-ID=\" + uuid;\r\n    form.target=\"uploadframe\";\r\n\r\n    \/* call the progress-updater every 1000ms *\/\r\n    interval = window.setInterval(\r\n        function () {\r\n            fetch(uuid);\r\n        }, 1000);\r\n\r\n    form.submit();\r\n    return false;\r\n}\r\n\/\/ ]]&gt;&lt;\/script&gt;\r\n&lt;\/head&gt;\r\n\r\n&lt;body&gt;\r\n    &lt;h1&gt;mod_upload_progress test page&lt;\/h1&gt;\r\n    &lt;div&gt;\r\n        &lt;form id=\"upload\" enctype=\"multipart\/form-data\"\r\n            action=\"\/upload.html\" method=\"post\"\r\n            onsubmit=\"return openProgressBar();\"&gt;\r\n            &lt;div&gt;\r\n                &lt;input type=\"hidden\" name=\"MAX_FILE_SIZE\" value=\"30000000\" \/&gt;\r\n                &lt;input name=\"userfile\" type=\"file\" \/&gt;\r\n                &lt;input type=\"submit\" value=\"Send File\" \/&gt;\r\n            &lt;\/div&gt;\r\n        &lt;\/form&gt;\r\n    &lt;\/div&gt;\r\n\r\n    &lt;div&gt;\r\n        &lt;div id=\"progress\" style=\"width: 400px; border: 1px solid black\"&gt;\r\n            &lt;div id=\"progressbar\"\r\n                style=\"width: 1px; background-color: black; border: 1px solid white\"&gt;\r\n                &amp;nbsp;\r\n            &lt;\/div&gt;\r\n        &lt;\/div&gt;\r\n        &lt;div id=\"tp\"&gt;(throughput)&lt;\/div&gt;\r\n        &lt;pre id=\"dump\"&gt;&lt;\/pre&gt;\r\n        &lt;div id=\"frameholder\"&gt;&lt;\/div&gt;\r\n    &lt;\/div&gt;\r\n&lt;\/body&gt;\r\n&lt;\/html&gt;<\/pre>\n<p>W\u00e4hrend 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:<\/p>\n<pre lang=\"javascript\" escaped=\"true\">\r\ninterval = null;\r\n\r\nfunction fetch(uuid) {\r\n    req = new XMLHttpRequest();\r\n    req.open(\"GET\", \"\/progress\", 1);\r\n    req.setRequestHeader(\"X-Progress-ID\", uuid);\r\n    req.onreadystatechange = function () {\r\n        if (req.readyState == 4) {\r\n            if (req.status == 200) {\r\n                \/* poor-man JSON parser *\/\r\n                document.getElementById('dump').innerHTML = req.responseText;\r\n                var upload = eval('new Object(' + req.responseText + ')');\r\n                document.getElementById('tp').innerHTML = upload.state;\r\n\r\n                \/* change the width if the inner progress-bar *\/\r\n                if (upload.state == 'done' || upload.state == 'uploading') {\r\n                    bar = document.getElementById('progressbar');\r\n                    w = 400 * upload.received \/ upload.size;\r\n                    bar.style.width = w + 'px';\r\n                }\r\n\r\n                \/* we are done, stop the interval *\/\r\n                if (upload.state == 'done') {\r\n                    bar = document.getElementById('progressbar');\r\n                    bar.style.width = '398px';\r\n                    window.clearTimeout(interval);\r\n                }\r\n            }\r\n        }\r\n    }\r\n\r\n    req.send(null);\r\n}\r\n\r\nfunction openProgressBar(event) {\r\n    \/* generate random progress-id *\/\r\n    uuid = \"\";\r\n    for (i = 0; i &lt; 32; i++) {\r\n        uuid += Math.floor(Math.random() * 16).toString(16);\r\n    }\r\n\r\n    \/* create an iframe as a form target *\/\r\n    document.getElementById(\"frameholder\").innerHTML =\r\n        '&lt;iframe id=\"uploadframe\" name=\"uploadframe\" width=\"0\" height=\"0\" ' +\r\n            'frameborder=\"0\" border=\"0\" src=\"about:blank\"&gt;&lt;\/iframe&gt;';\r\n\r\n    \/* patch the form-action tag to include the progress-id *\/\r\n    var form = document.getElementById(\"upload\");\r\n    form.action=\"\/upload.html?X-Progress-ID=\" + uuid;\r\n    form.target=\"uploadframe\";\r\n\r\n    \/* call the progress-updater every 1000ms *\/\r\n    interval = window.setInterval(\r\n        function () {\r\n            fetch(uuid);\r\n        }, 1000);\r\n\r\n    form.submit();\r\n    return false;\r\n}\r\n<\/pre>\n<p>Der JavaScript-Teil besteht aus zwei Funktionen, von denen die erste &#8211; fetch &#8211; in regelm\u00e4\u00dfigen Abst\u00e4nden aufgerufen wird, um den Fortschritt vom Server abzufragen, der daraufhin ein JSON-Objekt mit einer Reihe von Angaben liefert:<\/p>\n<ul>\n<li>state: Der Zustand des Uploads. Dieser kann sein:\n<ul>\n<li>starting: Download noch nicht beim Server bekannt, oder existiert (noch) nicht),<\/li>\n<li>uploading: Die Datei wird hochgeladen<\/li>\n<li>done: Der Upload ist abgeschlossen<\/li>\n<li>error: Beim Upload ist ein Fehler aufgetreten<\/li>\n<\/ul>\n<p>\t\tJe nach Inhalt dieses Wertes unterscheidet sich die Verf\u00fcgbarkeit einiger anderer Angaben im zur\u00fcckgelieferten Objekt.\n\t<\/li>\n<li>uuid: Die UUID des Uploads. Dazu gleich mehr.<\/li>\n<li>size: Die Gesamtgr\u00f6\u00dfe des Uploads in Bytes.<\/li>\n<li>received: Bereits auf dem Server registrierte Datenmenge<\/li>\n<li>speed: \u00dcbertragungsgeschwindigkeit in Bytes\/s.<\/li>\n<li>started_at: Unix-Timestamp zu dem der Upload zum Server gestartet wurde.<\/li>\n<li>status: Enth\u00e4lt im Fehlerfalle die Ursache als HTTP-Fehlercode.<\/li>\n<\/ul>\n<p>Okay, zur UUID: Diese ist (bisher) ein ungefilterter String beliebiger L\u00e4nge, den der Server nicht weiter auswertet. Dieser muss sowohl beim Abfragen des Status als auch beim Starten des Uploads \u00fcbergeben werden. Bei Uploads muss dazu am Ende der URL ein GET-Parameter mit dem Namen X-Progress-ID (ja, ich wei\u00df, der ist nicht RFC1739-konform) \u00fcbergeben 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.<\/p>\n<p>F\u00fcr die Abfrage des Status ruft man mit GET die Seite \/progress (oder was f\u00fcr das Upload-Tracking konfiguriert wurde) mit einem zus\u00e4tzlichen HTTP-Header X-Progress-ID auf, der den gleichen Wert, wie der f\u00fcr den Upload verwendete, enth\u00e4lt.<\/p>\n<p>Nun noch ein kurzes Wort zur Konfiguration:<\/p>\n<p>Im Apache wird das Modul wie gewohnt geladen:<\/p>\n<pre lang=\"apache\">LoadModule upload_progress_module \/usr\/lib\/apache2\/modules\/mod_upload_progress.so<\/pre>\n<p>und kann dann mit Hilfe der Direktive<\/p>\n<pre lang=\"apache\">UploadProgressSharedMemorySize 1024000<\/pre>\n<p>dazu gebracht werden, die intern verwendete Gr\u00f6\u00dfe f\u00fcr den Shared Memory zu setzen. Eine Erh\u00f6hung ist hier ggf. n\u00f6tig, sollte man einen Server haben, der EXTREM viele gleichzeitige Uploads behandeln muss. Dieser sollte aber in der Regel vollkommen ausreichen.<\/p>\n<p>Zwei weitere wichtige Direktiven erkl\u00e4ren sich am Besten mit Hilfe der zugeh\u00f6rigen Konfigurationsdatei des VHosts:<\/p>\n<pre lang=\"apache\" escaped=\"true\">    &lt;Location \/&gt;\r\n        # enable tracking uploads in \/\r\n        TrackUploads On\r\n    &lt;\/Location&gt;\r\n\r\n    &lt;Location \/progress&gt;\r\n        # enable upload progress reports in \/progress\r\n        ReportUploads On\r\n    &lt;\/Location&gt;<\/pre>\n<p>Der erste Location-Block legt hierbei fest, dass alle Uploads, die in diese Location fallen \u00fcberwacht werden sollen, w\u00e4hrend der zweite Location-Block festlegt, wo man die Informationen zu diesen Uploads abfragen kann.<\/p>\n<p>Hat man alles im Apache konfiguriert, sollte diese Variante mit Response-Zeiten nur unwesentlich langsamer als Pings besitzen und somit ein relativ fl\u00fcssiges Anzeigen des Fortschritts erm\u00f6glichen. Bei Problemen einfach bei Github nen Bug filen oder selber fixen \ud83d\ude09<\/p>\n<p class=\"wp-flattr-button\"><a href=\"https:\/\/blog.benny-baumann.de\/?flattrss_redirect&amp;id=973&amp;md5=ecb564e9300ec7d2f557a11387b867c5\" title=\"Flattr\" target=\"_blank\"><img src=\"http:\/\/blog.benny-baumann.de\/wp-content\/plugins\/flattr\/img\/flattr-badge-large.png\" srcset=\"http:\/\/blog.benny-baumann.de\/wp-content\/plugins\/flattr\/img\/flattr-badge-large.png\" alt=\"Flattr this!\"\/><\/a><\/p>","protected":false},"excerpt":{"rendered":"<p>Wenn man auf einer Web-Oberfl\u00e4che Dateien hochladen m\u00f6chte, so gibt es hierf\u00fcr im wesentlichen zwei sehr verbreitete M\u00f6glichkeiten: W\u00e4hrend die erste Version gem\u00e4\u00df dem HTTP-Standard und dem application\/x-www-form-urlencoded-Encoding die Daten\u00fcbertr\u00e4gt, was jeder heutige Browser unterst\u00fctzt, so findet man an verschiedensten Stellen sogenannte Flash-Uploader, die zwar im Wesentlichen das Gleiche tun, jedoch versuchen verschiedene Funktionen nachzur\u00fcsten, [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"ngg_post_thumbnail":0,"footnotes":""},"categories":[4],"tags":[156,10,98,69,346],"class_list":["post-973","post","type-post","status-publish","format-standard","hentry","category-server","tag-apache","tag-debian","tag-developement","tag-internet","tag-server"],"_links":{"self":[{"href":"https:\/\/blog.benny-baumann.de\/index.php?rest_route=\/wp\/v2\/posts\/973","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/blog.benny-baumann.de\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/blog.benny-baumann.de\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/blog.benny-baumann.de\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/blog.benny-baumann.de\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=973"}],"version-history":[{"count":2,"href":"https:\/\/blog.benny-baumann.de\/index.php?rest_route=\/wp\/v2\/posts\/973\/revisions"}],"predecessor-version":[{"id":975,"href":"https:\/\/blog.benny-baumann.de\/index.php?rest_route=\/wp\/v2\/posts\/973\/revisions\/975"}],"wp:attachment":[{"href":"https:\/\/blog.benny-baumann.de\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=973"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/blog.benny-baumann.de\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=973"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/blog.benny-baumann.de\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=973"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}