smart | Webentwicklung
Alles rund um HTML5, PHP, WordPress & Co.

AJAX: Dateien mit HTML5 File API in Chunks uploaden

24. Juni 2013
Stephan
AJAX: Dateien mit HTML5 File API stückweise in Chunks uploaden

Mithilfe der HTML5 File API ist es nicht nur möglich Dateien im Ganzen zum Server hochzuladen bzw. zu uploaden, sondern das geht auch stückweise. Dabei wird eine Datei in mehrere Teile, in sogenannte Chunks aufgeteilt und diese dann einzeln zum Server gesendet. Der Server selbst setzt die einzelnen Chunks dann wieder zu einer Datei zusammen.

Für kleine Dateien macht das natürlich wenig Sinn, aber für den Upload von großen Dateien ergeben sich so einige interessante Vorteile.

In diesem Artikel zeige ich euch, wie ihr per AJAX und mittels der HTML5 File API größere Dateien stückweise zum Server uploaden könnt.

Welchen Nutzen/Vorteil hat ein stückweiser Datei-Upload?

Viele Webserver sind so eingerichtet, dass der Datei-Upload auf eine bestimmte Dateigröße pro HTTP-Anfrage, wie z.B. 2MB begrenzt ist. Das bedeutet, dass wir eine 10MB Datei nicht hochladen können. Wie wäre es also, wenn wir die Datei in 5x2MB Teile/Chunks aufteilen und jeden Chunk einzeln zum Server hochladen? Nachdem alle Chunks hochgeladen sind, kann der Server die Datei dann auf Serverseite wieder zusammensetzen.

Außerdem könntet ihr den Upload einer sehr großen Datei, z.B. > 1GB, jederzeit pausieren und später an gleicher Stelle fortsetzen. Hierzu könntet ihr die HTML5 WebStoage API und/oder HTML5 IndexedDB API nutzen, um die entsprechenden Upload-Informationen clientseitig zu speichern, so dass der Upload jederzeit an der pausierten Stelle fortgesetzt werden kann.

HTML- & JavaScript-Grundgerüst

Als Grundgerüst reicht uns für unser Beispiel ein input-Feld zur Auswahl einer Datei, die stückweise zum Server gesendet werden soll:

<input id="file" type="file" />

Natürlich könnte ihr auch das multiple-Attribut angeben, so dass ihr nicht nur eine, sondern gleich mehrere Dateien hochladen könnt. In unserem Beispiel beschränken wir uns aber auf den Upload einer einzelnen Datei.

Als nächstes implementieren wir mithilfe von jQuery einen Event-Handler, so dass der Upload gestartet wird, sobalb wir eine Datei ausgewählt haben:

$(document).ready(function()
{
    $('#file').bind('change', function()
    {
        upload(this.files[0]);
    });
});

Sobald wir eine Datei auswählen, feuert unser input-Feld ein change-Event und daraufhin rufen wir die upload-Funktion auf.

Funktion für stückweisen Datei-Upload erstellen

Als nächstes implementieren wir nun die besagte upload-Funktion:

function upload(file)
{
    var chunkSize = 1024 * 1024;
    var chunkCount = {
        currentNumber: 1,
        numOfChunks: Math.ceil(file.size / chunkSize),
        numOfUploadedChunks: 0
    };

    var chunkStart = 0;
    var chunkEnd = chunkSize;

    while(chunkStart < file.size)
    {
        var currentChunk = file.slice(chunkStart, chunkEnd);

        chunkUpload(currentChunk, file.name, chunkCount);

        chunkCount.currentNumber++;
        chunkStart = chunkEnd;
        chunkEnd = chunkEnd + chunkSize;
    }
}

Einige Erklärungen zum Code:

Zeile 3
Hier legen wir die Chunkgröße fest. In unserem Beispiel sind das 1024×1024 Byte bzw. 1MB. Um die Größe der Chunks auf z.B. 5MB zu erhöhen, müsstest ihr halt nur die 1024×1024 mit 5 multiplizieren.

Zeile 4 – 8
Ein Hilfobjekt zum Speichern der aktuellen Chunknummer, der Anzahl der hochzuladenden Chunks und der Anzahl der bereits erfolgreich hochgeladenden Chunks.

Zeile 10 – 11
Festlegen, wo der erste Chunk anfängt und endet – bezieht sich auf die Bytes der Datei.

Zeile 13
Wir nutzen eine Schleife, die solange ausgeführt wird, solange wir nicht alle Chunks gesendet haben. Dieser Fall tritt ein, wenn die Dateigröße der Originaldatei kleiner chunkStart ist.

Zeile 15
Wählen wir mit einem input-Feld vom Typ file eine Datei aus und greifen mittels JavaScript auf diese zu, dann erhalten wir ein File-Objekt, welches das Blob-Interface implementiert. Letzteres stellt eine Methode namens slice zur Verfügung. Dabei können wir angeben, welchen Teil der Datei, die Methode uns zurückliefert.

Zeile 17
Aufruf der noch zu implementierenden chunkUpload-Funktion.

Zeile 20 – 21
Start und Ende für den nächsten Chunk berechnen.

Jetzt kommen wir zur chunkUpload-Funktion:

function chunkUpload(chunk, fileName, chunkCount)
{
    var xhr = new XMLHttpRequest();

    xhr.open('post', 'upload.php', true);

    xhr.onload = function()
    {
        chunkCount.numOfUploadedChunks++
		
        if(chunkCount.numOfUploadedChunks === chunkCount.numOfChunks)
        {
            console.log('File uploaded!');
        }
    };

    var formData = new FormData();
    formData.append('chunk', chunk);
    formData.append('fileName', fileName);
    formData.append('chunkNumber', chunkCount.currentNumber);
    formData.append('numOfChunks', chunkCount.numOfChunks);

    xhr.send(formData);
}

Diese Funktion dient letztlich zum eigentlichen Upload der Chunks. Dabei senden wir nicht nur den Chunk, sondern auch einige Metainformationen, die wir auf Serverseite benötigen.

Nähere Informationen zum Upload von Dateien mittels XHR2 und FormData findet ihr im Artikel „AJAX: Datei-Upload mit XHR2 und FormData“.

Serverseite: Chunks speichern und Datei wieder zusammensetzen

Damit ihr euch auch vorstellen könnt, wie ihr die Chunks nun auf Serverseite verarbeiten müsst, im Folgenden ein einfaches Beispiel für PHP (upload.php):

$uploadDir = 'downloads/';

// loops through all files (well we just send one chunk but never mind)
foreach($_FILES as $type => $file)
{
    $fileName = $file['tmp_name'];
    $originalFileName = $_POST['fileName'];
    $destination = $destination = $uploadDir . $originalFileName . $_POST['chunkNumber'];

    move_uploaded_file($fileName, $destination);

    if($_POST['chunkNumber'] === $_POST['numOfChunks'])
    {
        for($countFile = 1; $countFile <= $_POST['numOfChunks']; $countFile++)
        {
            $data = file_get_contents($uploadDir . $originalFileName . $countFile);
            file_put_contents($uploadDir . $originalFileName, $data, FILE_APPEND);            
            unlink($uploadDir . $originalFileName . $countFile);
        }
    }
}

Kurze Erklärung dazu:

Zeile 10
Speichert den aktuell empfangenen Chunk im downloads-Verzeichnis (was natürlich extistieren muss).

Zeile 12 – 20
Sobald der letzte Chunk empfangen wurde, werden alle gespeicherten Chunks ausgelesen und deren Inhalt in eine Datei mit dem Namen der Originaldatei gespeichert. Die Chunks werden nach dem Auslesen wieder gelöscht.

Das Ganze dann auch nochmal für die Ruby-Liebhaber unter uns:

require 'fileutils'

file_name = params[:file_name]
    file_path = File.join('downloads', file_name)

    File.open(params[:chunk].tempfile, 'r') do |tmp_file|
        File.open(file_path + params[:chunk_number], 'wb') do |file|
            file.write(tmp_file.read)
        end
    end
    
    if params[:chunk_number] === params[:num_of_chunks]
        content = ''

        for i in 1..params[:num_of_chunks].to_i
            File.open(file_path + i.to_s, 'rb') do |tmp_file|
            content += tmp_file.read
        end
    end

    File.open(file_path, 'wb') do |file|
        file.write(content)
    end
end

Auf eine nähere Erläuterung hierzu verzichte ich mal, aber wenn ihr Fragen habt, dann hinterlasst einfach ein Kommentar.

Problem: Asynchronität

Wenn ihr den hier vorgestellten Code selbst mal ausprobiert, werdet ihr feststellen, dass die Zusammensetzung der Datei auf Serverseite nicht immer richtig klappt. Das liegt daran, dass die Chunks asynchron gesendet werden, und eventuell der letzte Chunk der Originaldatei bereits früher vom Server empfangen wird, als ein anderer Chunk. Auf Serverseite gehen wir aber davon aus, dass wenn der letzte Chunk empfangen wurde, alle anderen Chunks auch schon empfangen wurden.

Eine mögliche Lösung zeige ich euch demnächst in einem weiteren Artikel zu diesen Thema. Anstatt nämlich die Chunks asynchron zu senden, müsst ihr sie synchron senden. Ja, richtig gehört, denn durch das synchrone Senden ist garantiert, dass der letzte Chunk auch als letztes vom Server empfangen wird.

Blockieren so viele synchrone Anfragen nicht den Webbrowser? Jup, das tun sie! Genau deswegen, beruht die Lösung darauf das synchrone Senden der Chunks in einen anderen Thread und somit in einen Hintegrundprozess auszulagern. Dafür eignet sich die HTML5 Web Worker API hervorragend.

Fazit

Die HTML5 File API ermöglicht es relativ unkompliziert eine große Datei stückweise mittels AJAX zum Server hochzuladen. Speziell beim Upload einer großen Datei, könnte der Upload somit auch pausiert bzw. falls durch Verlust der Internetverbindung abgebrochen, wieder an gleicher Stelle fortgesetzt werden.

Was haltet ihr von der Idee bzw. Möglichkeit große Dateien stückweise zum Server hochzuladen?

Kommentare  
4 Kommentare vorhanden
1 JonasB schrieb am 24. Juni 2013 um 18:07 Uhr

Sehr interessantes Thema, danke für den Artikel. Gerade bei Shared Hosting Paketen stößt man immer wieder auf das Problem, dass bei einer gewissen Upload-Größe einfach Schluss ist.

2 Stephan L. schrieb am 24. Juni 2013 um 20:53 Uhr

Hi Jonas,

jup, bei Shared Hosting hat man ja meistens selten Zugriff auf alle Servereinstellungen, wie z.B. upload_max_filesize bei PHP. Insofern ist die File API vor allem in Verbindung mit der Web Worker API echt eine nützeliche Sache.

Grüße

Stephan

3 ND schrieb am 6. November 2013 um 02:08 Uhr

Super Artikel! Gibt es schon den Nachfolger:

„Eine mögliche Lösung zeige ich euch demnächst in einem weiteren Artikel zu diesen Thema. Anstatt nämlich die Chunks asynchron zu senden, müsst ihr sie synchron senden. Ja, richtig gehört, denn durch das synchrone Senden ist garantiert, dass der letzte Chunk auch als letztes vom Server empfangen wird.“

Bin brennend interessiert 😉

4 Stephan L. schrieb am 9. November 2013 um 16:36 Uhr

Hallo,

nein, leider gibt es derzeit noch keinen weiterführenden Artikel bzgl. der Umsetzung des Datei-Uploads mithilfe der Web Worker API.

Komme zurzeit nicht viel zum Bloggen, so dass ich dir auch nicht sagen kann, wann ich dazu kommen werde.

Beste Grüße

Stephan

0 Trackbacks/Pingbacks vorhanden
Du bist herzlich eingeladen auch ein Kommentar zu hinterlassen!
Kommentar schreiben

Vielen Dank für dein Kommentar!