Dateien Speichern mit IndexedDB

Permalink

Bislang funktioniert eine wichtige Komponenten meines Podcatchers nur mit Browsern die auf Google Chrome basieren. Der Download von Audio-Dateien und damit die Offline-Verfügbarkeit der Episoden basierte einzig auf der File System API. Diese Technik wird aber nur von Chrome unterstützt. Auf der Suche nach einer alternative bin ich auf die Indexed Database API gestoßen. Hiermit ist es möglich Javascriptobjekte im Browser abzuspeichern. Theoretisch also auch BLOB’s.

Erste Probleme

Erste Recherchen versprachen eine funktionierende Lösung. Es stellte sich aber schnell heraus das dies erst mal nur für Firefox und Internet Explorer gilt. Chrome und Opera unterstützen zwar grundsätzlich die Indexed DB können aber BLOBs nicht serialisieren und daher auch nicht speichern – schade. Wenn man stattdessen Array Buffer anstelle von BLOB’s nimmt hat man andere Probleme die ich in einem jüngeren Artikel darlege.

Datenbank

Die API ist eine asynchrone. Daraus folgt eine exzessive Nutzung von Callbacks. Beim einfachen Öffnen einer Datenbank kann man bereits vier Callback-Funktionen angeben:

request = window.indexedDB.open('PodcatcherDB', 1.0);
request.onupgradeneeded = updateIndexedDB;
request.onblocked = handleBlockedDB;
request.onsuccess = manipulateDB;
request.onerror = handleError;

Hier wird eine Datenbank namens PodcatcherDB geöffnet (wenn sie nicht existiert wird sie auch gleich angelegt). Über die Callback-Funktionen kann dann entsprechend damit gearbeitet werden wie im folgenden noch gezeigt wird.

Datenbankschema

Um Daten zu strukturieren definiert man sogenannte “Object Stores”. Ich stelle mir so einen Store als Regal vor: Auf jedes Regalbrett stellt man eine Kiste (Objekt), auf jeder Kiste ist ein Etikett (Schlüssel) und am Regal hängt noch ein Inhaltsverzeichnis (Index).

Zusätzlich hat jede Datenbank noch eine Versionsnummer. Nur wen sich diese Versionsnummer ändert sind Änderungen am Schema, bestehend aus Stores und Indexen möglich. Jedes mal wenn beim öffnen einer Datenbank eine neue Versionsnummer angegeben wird löst das Event onupgradeneeded aus und ermöglicht es so die gewünschten Änderungen am Schema vorzunehmen:

function updateIndexedDB(event) {
  "use strict";
  logHandler("Database Update from Version " + event.oldVersion + " to Version " + event.newVersion, 'info');
  var db;
  db = this.result;
  if (!db.objectStoreNames.contains('Files')) {
    db.createObjectStore('Files', {});
  }
}

Das Beispiel gibt als erstes mal die alte und neue Versionsnummer aus. Die eigentliche Anpassung des Schemas erfolgt danach. Die Datenbank (db) wird überprüft ob der Store Files bereits existiert. Wenn das nicht der Fall ist wird ein solcher angelegt. Dabei werden keine besonderen Einstellungen gewählt. Im zweiten Parameter der Methode createObjectStore() könnte man alternative ein paar Besonderheiten angeben. Eine Möglichkeit wäre es zum Beispiel einen automatischen Schlüssel für jeden Eintrag des Stores generieren zu lassen. Ich vergebe hierfür aber lieber manuell die URL der Datei die ich speichern möchte.

Speichern, lesen, löschen

Die Methode um eine Audiodatei aus dem Internet herunterzuladen ist die Selbe die ich schon im Zusammenhang mit der File System API in einem früheren Artikel erläutert habe. Daher gehe ich hier nicht noch einmal darauf ein.

Das speichern und lesen eines BLOB’s ist eigentlich nicht weiter schwer. Nachdem man die Datenbank geöffnet hat kann man sich eine Transaktion anlegen, das ist im Grunde eine Reservierung für die gewünschten Object Stores. Diese Transaktion bietet dann Zugriff auf die Stores und diese wiederum, stellen Methoden bereit für das schreiben, löschen und lesen.

function saveFile(episode, blob, onWriteCallback) {
   "use strict";
   var request;
   request = window.indexedDB.open('HTML5PodcatcherPrototyp', 5.0);
   request.onupgradeneeded = updateIndexedDB;
   request.onsuccess = function () {
      var db, transaction, store, request;
      db = this.result;
      transaction = db.transaction(['Files'], 'readwrite');
      store = transaction.objectStore('Files');
      request = store.put(blob, episode.mediaUrl);
      request.onsuccess = function() {
         episode.isFileSavedOffline = true;
         writeEpisode(episode);
         if (onWriteCallback && typeof onWriteCallback === 'function') {
            onWriteCallback(episode);
         }
      };
      request.onerror = function (event) { … };
   };
   request.onerror = function () { … };
}

Diese Methode zeigt das generelle Vorgehen am Beispiel eines Speichervorganges. In einer Transaktion wird der Store Files zum schreiben reserviert. Danach wird der Store aus der Transaktion ausgelesen und dessen Methode put() aufgerufen. Die Parameter von put() sind das zu speichernde Objekt und der Schlüssel unter dem es später wiedergefunden werden kann. Das Ergebnis dieser Methode wird wieder in einer Callbackfunktion behandelt.

Für das lesen und löschen von gespeicherten Objekten gilt grundsätzlich das gleiche Vorgehen. Hierbei werden lediglich zwei andere Methoden aufgerufen: get() zum lesen eines Eintrags und delete() zum löschen. Beim lesen aus der Datenbank steht das Resultat in der variablen target.result des Parameters der Callbackfunktion bereit. Ein Beispiel dazu gibt es im nächsten Abschnitt.

Um Sicherheit zu gewährleisten Fragen sowohl Internet Explorer als auch Firefox übrigens ab 10 Megabyte beim Benutzer nach ob das zulässig sein soll. Wenn man dem zustimmt steht dem Spaß nichts mehr im Wege.

Verlinken

Da ich nun eine Möglichkeit gefunden habe Dateien auch unter Firefox und Internet Explorer zu speichern bleibt nur noch eine Frage zu beantworten: Wie kann ich sie verlinken, wie darauf referenzieren um sie abzuspielen?

Dafür benutze ich eine Methode aus der File API. Die Methode createObjectURL() aus dem Namespace URL erzeugt zu einem BLOB eine URL die ganz normal genutzt werden kann. Dabei ist allerdings zu beachten das die Daten des BLOB’s die ganze Zeit im Arbeitsspeicher des Rechners vorgehalten werden müssen. Es ist also darauf zu achten die erzeugte URL beizeiten wieder freizugeben. Dies geschieht mittels der Methode revokeObjectURL().

function openFile(episode, onReadCallback) {
   "use strict";
    if (episode.isFileSavedOffline) {
       var request;
       request = window.indexedDB.open('HTML5PodcatcherPrototyp', 5.0);
       request.onupgradeneeded = updateIndexedDB;
       request.onblocked = function() { logHandler("Database blocked", 'debug'); };
       request.onsuccess = function () {
          var db, transaction, store, cursorRequest;
          db = this.result;
          transaction = db.transaction(['Files'], 'readonly');
          store = transaction.objectStore('Files');
          request = store.get(episode.mediaUrl);
          request.onsuccess = function(event) {
             var objectUrl, blob;
             blob = event.target.result;
             objectUrl = window.URL.createObjectURL(blob);
             episode.offlineMediaUrl = objectUrl;
             if (onReadCallback && typeof onReadCallback === 'function') {
                onReadCallback(episode);
             }
          };
          request.onerror = function (event) { … };
       };
       request.onerror = function () { … };
    }
}

Diese Methode zeigt das Zusammenspiel aus IndexdDB und createObjectURL() beim öffnen einer Datei.

Ausblick

Mittels der IndexedDB ist es nun auch möglich den Podcatcher unter Firefox und Internet Explorer zu nutzen. Leider muss ich aber weiterhin auch die File System API für Chrome und Opera vorsehen da beide keine BLOB’s in Indexed Databases abspeichern können. Trotzdem bin ich mittlerweile schon recht zufrieden. Die Usability könnte noch besser sein und ich möchte noch mehr Audioformate unterstützen aber das ist ja auch kein Hexenwerk.

Für alle die das obige Ausprobieren möchten habe ich auch wieder einen reduzierten Prototypen bereitgestellt. Denkt aber daran den passenden Browser zu verwenden.