Binärdateien im Browser speichern

Permalink

Um einen Podcatcher vollständig offline nutzen zu können, müssen die Audiodateien heruntergeladen werden können. In meinem Fall muss also der Browser in die Lage versetzt werden größere Dateien, hauptsächlich MP3’s, abspeichern zu können. Das geht zum Beispiel mit der File System API. Diese API liegt momentan als Working Draft des W3C vor und wird bislang nur von Chromium unterstützt. Das heißt das folgende funktioniert leider nur in Browsern wie Opera und Chrome. Für mich ist das aber erst mal ausreichend, Unterstützung für Firefox oder Internet Explorer kann ich später eventuell noch ergänzen.

Die FileSystem API gewährt nicht wirklich Zugriff auf das lokale Dateisystem des Computers sondern stellt ein virtuelles Dateisystem in einer Sandbox bereit. Diese Sandbox ist soweit abgeschottet das sie auch verschiedene Webauftritte voneinander trennt.

Vorbereitungen

Da es sich bei der File System API und den zugrunde liegenden API’s noch nicht um fertige Standards handelt werden die eingeführten Objekte und Funktionen noch nicht mit ihren offiziellen Bezeichnungen bereitgestellt. Stattdessen sind diese Namen mit einem Präfix versehen. Um das zu berücksichtigen werden die folgende Zuweisungen vorgenommen um sicherzustellen das die Scripte auch noch funktionieren wenn die Präfixe aus den Browsern entfernt werden.

window.URL = window.URL || window.webkitURL;
window.requestFileSystem = window.requestFileSystem || window.webkitRequestFileSystem;
window.resolveLocalFileSystemURL = window.resolveLocalFileSystemURL || window.webkitResolveLocalFileSystemURL;
navigator.persistentStorage = navigator.persistentStorage || navigator.webkitPersistentStorage;

Speicherplatz

Der Speicherplatz den der Browser bereitstellt ist begrenzt. Daher ist es nötig eine passende Menge an Speicher anzufordern und gelegentlich auch zu überprüfen wie viel davon noch übrig ist. Jedes mal wenn eine größere Menge an Speicherplatz angefordert wird als vorher zur Verfügung stand fragt der Browser nach der Einverständnis des Benutzers.

Die folgende Methode reserviert eine bestimmte Menge an Speicherplatz. Prüft danach den freien Speicherplatz und gibt eine entsprechende Meldung aus. Außerdem wird eine Warnung ausgegeben wenn der freie Speicherbereich kleiner als 50 Megabyte groß ist:

var requestFileSystemQuota = function(quota) {
  "use strict";
  if(navigator.persistentStorage) {
    navigator.persistentStorage.requestQuota(quota, function(grantedBytes) {
      navigator.persistentStorage.queryUsageAndQuota(function (usage, quota) {
        var availableSpace = quota - usage;
        if (availableSpace <= (1024 * 1024 * 50)) {
          logHandler('You are out of space! Please allow more then ' + Math.ceil(quota / 1024 / 1024) + ' MiB of space', 'warning');
        } else {
          logHandler('There is ' + Math.floor(availableSpace / 1024 / 1024) + ' MiB of ' + Math.floor(quota / 1024 / 1024) + ' MiB memory available', 'info');
        }
      }, errorHandler);
    }, errorHandler);
  }
};

Ich wollte das Reservieren von Speicher und das Abfragen des freien Platzes eigentlich nicht so eng miteinander verbinden, allerdings gibt es Probleme bei der Nutzung von queryUsageAndQuota() wenn die Seite nicht vorher wenigstens einmal requestQuota() aufgerufen hat. Scheinbar wird der Storage vom Browser erst initialisiert wenn erstmalig ein Quota angefordert wird. Wenn ich also die Reihenfolge der Aufrufe vertausche werden Javascriptfehler ausgelöst. Andererseits ist das wiederholte Anfordern der gleichen Speichermenge unproblematisch. Daher habe ich beschlossen den Problemen aus dem Weg zu gehen und immer beide Funktionen zusammen aufzurufen.

Download

Jetzt habe ich einen Ort an dem ich meine Podcasts speichern kann, nur wie bekomme ich sie dorthin? Hier Hilft ein Ajax-Request. Dessen Response wird als Array Buffer weiterverarbeitet. Ein Array Buffer ist im Grunde genommen eine Folge von Bytes im Speicher. Also genau das was man bekommt wenn man eine MP3-Datei aus dem Internet lädt:

var downloadFile = function(episode, mimeType) {
    "use strict";
    var xhr = new XMLHttpRequest();
    xhr.open('GET', episode.mediaUrl, true);
    xhr.responseType = 'arraybuffer';
    xhr.addEventListener("error", errorHandler, false);
    xhr.onload = function() {
        if (this.status === 200) {
            saveFile(episode, xhr.response, mimeType);
        } else {
            logHandler('Error Downloading file ' + episode.mediaUrl + ': ' + this.statusText + ' (' + this.status + ')', 'error');
        }
    };
    xhr.send(null);
};

Wer sich jetzt wundert das ich nicht wie bisher jQuery benutze um mir das Leben leichter zu machen dem sei gesagt das es schlicht nicht geht. Die Ajax-Funktionen von jQuery unterstützen Array Buffer nicht.

Die obige Funktion muss natürlich noch mit einer Lösung für das Cross-Origin Resource Sharing kombiniert werden. Wie das aussehen kann habe ich ja schon bei meiner Beschreibung zum laden von Podcast-Feeds angesprochen.

Speichern…

Der Array Buffer der aus dem Ajax-Call resultiert kann in einen Blob, ein Binary Large Object, umgewandelt werde. Dieser Blob kann wiederum mittels eines Writer-Objekts in eine Datei geschrieben werden:

var saveFile = function(episode, arraybuffer, mimeType) {
    "use strict";
    var blob, parts, fileName;
    blob = new Blob([arraybuffer], {type: mimeType});
    parts = episode.mediaUrl.split('/');
    fileName = parts[parts.length - 1];
    window.requestFileSystem(fileSystemStatus, fileSystemSize, function(filesystem) {
        filesystem.root.getFile(fileName, {create: true, exclusive: false}, function(fileEntry) {
            fileEntry.createWriter(function(writer) {
                writer.onwriteend = function() {
                    episode.offlineMediaUrl = fileEntry.toURL();
                    writeEpisode(episode);
                };
                writer.onerror = errorHandler;
                writer.write(blob);
            }, errorHandler);
        }, errorHandler);
    }, errorHandler);
};

Diese Methode wirkt etwas unübersichtlich. Das liegt an der File System API da diese asynchron gestaltet ist. Das heißt, der meiste Code dient dazu eine Reihe von ineinander geschachtelten Callback-Funktionen zu definieren die nur zusammen zum gewünschten Ergebnis führen.

Zu erst wird das Dateisystem initialisiert (requestFileSystem()). Wenn das gelingt wird die gewünschte Datei geöffnet (getFile()). Sollte die Datei noch nicht existieren wird sie hierbei erstellt. Wenn die Datei vorliegt wird auf ihr ein Writer-Objekt erstellt (createWriter()). Und erst wenn auch dies geschafft ist wird die MP3-Datei in Form des Blob’s gespeichert (write()). Als Ergebnis des Speicherns wird wiederum die Callback-Funktion onwriteend() aufgerufen.

Um später die gespeicherte Datei ansprechen zu können gibt es die Möglichkeit eine URL abzurufen. Das erledigt die Funktion toURL(). Mittels dieser URL lässt sich die gespeicherte Datei genau so verwenden als läge sie im Internet. Insbesondere kann man sie so als Quelle für ein Audio-Tag verwenden.

…und löschen

Wer etwas speichert muss es auch wieder löschen können:

var deleteFile = function(episode) {
    "use strict";
    window.resolveLocalFileSystemURL(episode.offlineMediaUrl, function(fileEntry) {
        fileEntry.remove(function() {
            var url;
            url = episode.offlineMediaUrl;
            episode.offlineMediaUrl = undefined;
            writeEpisode(episode);
        }, errorHandler);
    }, errorHandler);
};

Auch hier ist der typisch asynchrone Aufbau der File System API zu erkennen. Zuerst wird mit der URL die ich beim Speichern erhalten habe und der Funktion resolveLocalFileSystemURL() ein File-Objekt erzeugt und dann dessen remove()-Methode ausgeführt.

Ausblick

Bislang kann ich sowohl meine Abonnements, die Playlist und die Konfiguration als auch die einzelnen Audio-Dateien für den Offline-Betrieb speichern. Ich kann auch halbwegs komfortabel meine Playlist abspielen und neue Podcasts abonnieren. Aber ich bekomme immer noch eine blöde Fehlermeldung wenn ich versuche meinen Podcatcher ohne Internetverbindung zu starten. Die benötigten HTML- und Javascript-Dateien sind nämlich noch immer auf das Internet angewiesen. Aber auch dafür gibt es auch eine Lösung die ich als nächstes austüftele.

Zum ansehen und ausprobieren gibt es auch wieder einen Prototypen zur File System API. Der funktioniert natürlich nur unter Chrome und anderen Browsern auf Basis von Chromium. Firefox und andere Browser bleiben wie gesagt außen vor.