Wie die Kiwix PWA es Nutzern ermöglicht, Gigabyte an Daten aus dem Internet zur Offlinenutzung zu speichern

Geoffrey Kantaris
Geoffrey Kantaris
Stéphane Coillet-Matillon
Stéphane Coillet-Matillon

Personen, die sich um einen Laptop versammeln, der auf einem einfachen Tisch steht, links ein Plastikstuhl. Der Hintergrund sieht aus wie eine Schule in einem Entwicklungsland.

In dieser Fallstudie wird untersucht, wie die gemeinnützige Organisation Kiwix die progressive Web-App-Technologie und die File System Access API einsetzt, um es Nutzern zu ermöglichen, große Internetarchive für die Offlinenutzung herunterzuladen und zu speichern. Informationen zur technischen Implementierung des Codes für das Origin Private File System (OPFS), einer neuen Browserfunktion in der Kiwix-PWA, die die Dateiverwaltung verbessert und den Zugriff auf Archive ohne Berechtigungsanfragen ermöglicht. Der Artikel befasst sich mit den Herausforderungen und zeigt mögliche zukünftige Entwicklungen in diesem neuen Dateisystem auf.

Kiwix

Mehr als 30 Jahre nach der Geburt des Webs wartet ein Drittel der Weltbevölkerung noch immer auf einen zuverlässigen Zugang zum Internet, wie die Internationale Fernmeldeunion mitteilt. Ist das das Ende der Geschichte? Nein gar nicht. Die Nonprofit-Organisation Kiwix mit Sitz in der Schweiz hat ein Ökosystem aus Open-Source-Apps und -Inhalten entwickelt, um Menschen mit eingeschränktem oder gar keinem Internetzugang ihr Wissen zugänglich zu machen. Wenn Sie nicht problemlos auf das Internet zugreifen können, kann jemand wichtige Ressourcen für Sie herunterladen, wenn und wo eine Verbindung verfügbar ist, und sie lokal für die spätere Offlinenutzung speichern. Viele wichtige Websites wie Wikipedia, Project Gutenberg, Stack Exchange oder sogar TED Talks können jetzt in hochkomprimierte Archive, sogenannte ZIM-Dateien, konvertiert und im Kiwix-Browser gelesen werden.

ZIM-Archive verwenden die hocheffiziente Zstandard-Komprimierung (ZSTD; ältere Versionen verwendeten XZ), hauptsächlich zum Speichern von HTML, JavaScript und CSS. Bilder werden in der Regel in das komprimierte WebP-Format konvertiert. Jedes ZIM enthält außerdem eine URL und einen Titelindex. Die Komprimierung ist hier entscheidend, da der gesamte englische Wikipedia-Artikel (6,4 Millionen Artikel und Bilder) nach der Konvertierung in das ZIM-Format auf 97 GB komprimiert wird.Dies klingt erst einmal ziemlich viel, bis Sie merken, dass die Summe des menschlichen Wissens jetzt auf ein Android-Smartphone im mittleren Preissegment passt. Es werden auch viele kleinere Ressourcen angeboten, darunter themenspezifische Versionen von Wikipedia, z. B. zu Mathematik oder Medizin.

Kiwix bietet eine Reihe nativer Apps für Computer (Windows/Linux/macOS) und Mobilgeräte (iOS/Android). In dieser Fallstudie geht es jedoch um die progressive Web-App (PWA), die eine universelle und einfache Lösung für jedes Gerät mit einem modernen Browser ist.

Wir sehen uns die Herausforderungen an, die sich bei der Entwicklung einer universellen Webanwendung stellen, die einen schnellen Zugriff auf große Inhaltsarchive vollständig offline bieten muss, und einige moderne JavaScript-APIs, insbesondere die File System Access API und das Origin Private File System, die innovative und spannende Lösungen für diese Herausforderungen bieten.

Eine Web-App zur Offline-Nutzung?

Kiwix-Nutzer sind eine bunte Mischung mit vielen verschiedenen Anforderungen. Kiwix hat nur wenig oder gar keine Kontrolle über die Geräte und Betriebssysteme, über die Nutzer auf ihre Inhalte zugreifen. Einige dieser Geräte sind möglicherweise langsam oder veraltet, insbesondere in Ländern mit niedrigem Einkommen. Obwohl Kiwix versucht, so viele Anwendungsfälle wie möglich abzudecken, erkannte die Organisation auch, dass sie noch mehr Nutzer erreichen könnte, indem sie die allgemeinste Software auf jedem Gerät verwendet: den Webbrowser. Inspiriert vom Atwood's Gesetz, das besagt, dass jede Anwendung, die in JavaScript geschrieben werden kann, irgendwann in JavaScript geschrieben werden wird, arbeiten einige Kiwix-Entwickler vor etwa 10 Jahren an der Portierung der Kiwix-Software von C++ in JavaScript.

Die erste Version dieses Ports, Kiwix HTML5, war für das inzwischen eingestellte Firefox OS und für Browsererweiterungen gedacht. Im Kern war (und ist) es eine C++-Dekomprimierungs-Engine (XZ und ZSTD), die mit dem Emscripten-Compiler in die Zwischensprache ASM.js und später in Wasm oder WebAssembly kompiliert wurde. Die Browsererweiterungen, die später in Kiwix JS umbenannt wurden, werden noch immer aktiv entwickelt.

Kiwix JS Offline Browser

Rufen Sie die progressive Web-App (PWA) auf. Die Entwickler von Kiwix erkannten das Potenzial dieser Technologie und entwickelten eine spezielle PWA-Version von Kiwix JS. Außerdem fügten sie Betriebssystemintegrationen hinzu, mit denen die App nativ ähnliche Funktionen bieten kann, insbesondere in den Bereichen Offlinenutzung, Installation, Dateiverwaltung und Dateisystemzugriff.

Offline-first-PWAs sind extrem effizient und eignen sich daher perfekt für Situationen, in denen es nur eine sporadische oder teure mobile Internetverbindung gibt. Die zugrunde liegende Technologie ist die Service Worker API und die zugehörige Cache API, die von allen Apps verwendet wird, die auf Kiwix JS basieren. Mit diesen APIs können die Apps als Server fungieren, Abrufanfragen vom angezeigten Hauptdokument oder Artikel abfangen und an das (JS-)Backend weiterleiten, um eine Antwort aus dem ZIM-Archiv zu extrahieren und zu erstellen.

Speicher, Speicher überall

Angesichts der Größe von ZIM-Archiven stellen der Speicher und der Zugriff darauf, insbesondere auf Mobilgeräten, wahrscheinlich die größten Probleme für Kiwix-Entwickler dar. Viele Kiwix-Endnutzer laden Inhalte in der App herunter, wenn das Internet verfügbar ist, um sie später offline zu verwenden. Andere Nutzer laden die Inhalte über einen Torrent auf einen PC herunter und übertragen sie dann auf ein Smartphone oder Tablet. Wieder andere tauschen Inhalte auf USB-Speichern oder tragbaren Festplatten in Gebieten mit unzuverlässigem oder teurem mobilen Internet aus. Alle diese Möglichkeiten, von beliebigen für Nutzer zugänglichen Orten auf Inhalte zuzugreifen, müssen von Kiwix JS und Kiwix PWA unterstützt werden.

Die File API ermöglichte es Kiwix JS, ursprünglich riesige Archive mit Hunderten von GB zu lesen (eines unserer ZIM-Archive hat 166 GB!). Diese API wird in allen Browsern unterstützt, auch in sehr alten Browsern. Sie dient daher als universeller Fallback, wenn neuere APIs nicht unterstützt werden. Das Definieren eines input-Elements in HTML ist ganz einfach. Im Fall von Kiwix sieht das so aus:

<input
  type="file"
  accept="application/octet-stream,.zim,.zimaa,.zimab,.zimac, ..."
  value="Select folder with ZIM files"
  id="archiveFilesLegacy"
  multiple
/>

Nach der Auswahl enthält das Eingabeelement die Dateiobjekte, die im Wesentlichen Metadaten sind, die auf die zugrunde liegenden Daten im Speicher verweisen. Technisch gesehen liest das objektorientierte Back-End, das in reinem clientseitigem JavaScript geschrieben ist, kleine Segmente des großen Archivs nach Bedarf. Wenn diese Segmente dekomprimiert werden müssen, leitet das Backend sie an den Wasm-Dekomprimierer weiter und ruft bei Bedarf weitere Segmente ab, bis ein vollständiges Blob dekomprimiert ist (in der Regel ein Artikel oder ein Asset). Das bedeutet, dass das große Archiv nie vollständig in den Arbeitsspeicher gelesen werden muss.

Die File API ist zwar universell, hat aber einen Nachteil, der dazu geführt hat, dass Kiwix-JS-Apps im Vergleich zu nativen Apps klobig und altmodisch wirken: Der Nutzer muss bei jedem Start der App Archive über eine Dateiauswahl auswählen oder eine Datei per Drag-and-drop in die App ziehen, da mit dieser API keine Zugriffsberechtigungen von einer Sitzung zur nächsten beibehalten werden können.

Um diese schlechte UX zu verbessern, haben die Kiwix-JS-Entwickler wie viele andere Entwickler zunächst Electron verwendet. ElectronJS ist ein erstaunliches Framework, das leistungsstarke Funktionen bietet, darunter vollständigen Zugriff auf das Dateisystem über Node APIs. Es hat jedoch einige bekannte Nachteile:

  • Sie kann nur auf Desktop-Betriebssystemen ausgeführt werden.
  • Es ist groß und umfangreich (70 MB bis 100 MB).

Die Größe der Electron-Apps ist aufgrund der Tatsache, dass jede App eine vollständige Kopie von Chromium enthält, im Vergleich zu nur 5,1 MB bei der minimierten und gebündelten PWA sehr ungünstig.

Gibt es eine Möglichkeit, wie Kiwix die Situation für Nutzer der PWA verbessern könnte?

File System Access API als Lösung

Ungefähr 2019 wurde Kiwix auf eine neue API aufmerksam, die in Chrome 78 in der Testphase war und damals Native File System API hieß. Es wurde versprochen, dass es möglich sein würde, einen Dateihandle für eine Datei oder einen Ordner abzurufen und in einer IndexedDB-Datenbank zu speichern. Entscheidend ist, dass dieses Handle zwischen App-Sitzungen beibehalten wird, sodass der Nutzer beim Neustart der App nicht noch einmal zur Auswahl der Datei oder des Ordners gezwungen wird (er muss jedoch eine kurze Berechtigungsaufforderung beantworten). Bis zur Produktionsphase wurde sie in File System Access API umbenannt und die Kernteile wurden von WHATWG als File System API (FSA) standardisiert.

Wie funktioniert der Teil der API, der den Dateisystemzugriff ermöglicht? Einige wichtige Punkte, die Sie beachten sollten:

  • Es handelt sich um eine asynchrone API (mit Ausnahme spezieller Funktionen in Web Workers).
  • Die Datei- oder Verzeichnisauswahl muss programmatisch gestartet werden, indem eine Nutzergeste erfasst wird (Klicken oder Tippen auf ein UI-Element).
  • Damit der Nutzer in einer neuen Sitzung noch einmal die Berechtigung für den Zugriff auf eine zuvor ausgewählte Datei erteilen kann, ist auch eine Nutzergeste erforderlich. Der Browser verweigert die Berechtigungsaufforderung, wenn er nicht durch eine Nutzergeste ausgelöst wird.

Der Code ist relativ einfach, abgesehen davon, dass die klobige IndexedDB API zum Speichern von Datei- und Verzeichnis-Handles verwendet werden muss. Die gute Nachricht ist, dass einige Bibliotheken einen Großteil der Arbeit übernehmen, wie z. B. browser-fs-access. Bei Kiwix JS haben wir uns entschieden, direkt mit den APIs zu arbeiten, die sehr gut dokumentiert sind.

Datei- und Verzeichnisauswahl öffnen

Das Öffnen einer Dateiauswahl sieht in etwa so aus (hier mit Promises; wenn Sie async/await Sugar bevorzugen, lesen Sie das Chrome für Entwickler-Tutorial):

return window
  .showOpenFilePicker({ multiple: false })
  .then(function (fileHandles) {
    return processFileHandle(fileHandles[0]);
  })
  .catch(function (err) {
    // This is normal if app is launching
    console.warn(
      'User cancelled, or cannot access fs without user gesture',
      err,
    );
  });

Hinweis: Der Code verarbeitet zur Vereinfachung nur die erste ausgewählte Datei und erlaubt nicht die Auswahl mehrerer Dateien. Wenn Sie mit { multiple: true } die Auswahl mehrerer Dateien zulassen möchten, verpacken Sie einfach alle Versprechen, die die einzelnen Handles verarbeiten, in eine Promise.all().then(...)-Anweisung, z. B.:

let promisesForFiles = fileHandles.map(function (fileHandle) {
    return processFileHandle(fileHandle);
});
return Promise.all(promisesForFiles).then(function (arrayOfFiles) {
    // Do something with the files array
    console.log(arrayOfFiles);
}).catch(function (err) {
    // Handle any errors that occurred during processing
    console.error('Error processing file handles!', err);
)};

Wenn Sie jedoch mehrere Dateien auswählen möchten, sollten Sie den Nutzer bitten, das Verzeichnis mit diesen Dateien auszuwählen, anstatt die einzelnen Dateien darin. Das ist vor allem dann sinnvoll, wenn Kiwix-Nutzer alle ihre ZIM-Dateien im selben Verzeichnis organisieren. Der Code zum Starten der Verzeichnisauswahl ist fast identisch mit dem oben, mit der Ausnahme, dass Sie window.showDirectoryPicker.then(function (dirHandle) { … }); verwenden.

Datei- oder Verzeichnis-Handle verarbeiten

Nachdem Sie den Handle haben, müssen Sie ihn verarbeiten. Die Funktion processFileHandle könnte also so aussehen:

function processFileHandle(fileHandle) {
  // Serialize fileHandle to indexedDB
  serializeFSHandletoIdxDB('pickedFSHandle', fileHandle, function (val) {
    console.debug('IndexedDB responded with ' + val);
  });
  return fileHandle.getFile().then(function (file) {
    // Do something with the file
    return file;
  });
}

Sie müssen die Funktion zum Speichern des Dateihandles angeben. Es gibt keine praktischen Methoden dafür, es sei denn, Sie verwenden eine Abstraktionsbibliothek. Die Implementierung dieser Funktion in Kiwix ist in der Datei cache.js zu sehen. Sie könnte jedoch erheblich vereinfacht werden, wenn sie nur zum Speichern und Abrufen eines Datei- oder Ordner-Handles verwendet wird.

Verzeichnisse verarbeiten ist etwas komplizierter, da Sie die Einträge im ausgewählten Verzeichnis mit async entries.next() durchgehen müssen, um die gewünschten Dateien oder Dateitypen zu finden. Es gibt verschiedene Möglichkeiten, dies zu tun. Im Folgenden finden Sie den Code, der in der Kiwix-PWA verwendet wird:

let iterableEntryList = dirHandle.entries();
return iterateAsyncDirEntries(iterableEntryList, []).then(function (entryList) {
  // Do something with the entry list
  return entryList;
});

/**
 * Iterates FileSystemDirectoryHandle iterator and adds entries to an array
 * @param {Iterator} entries An asynchronous iterator of entries
 * @param {Array} archives An array to which to add the entries (may be empty)
 * @return {Promise<Array>} A Promise for an array of entries in the directory
 */
function iterateAsyncDirEntries(entries, archives) {
  return entries
    .next()
    .then(function (result) {
      if (!result.done) {
        let entry = result.value[1];
        // Filter for the files you want
        if (/\.zim(\w\w)?$/i.test(entry.name)) {
          archives.push(entry);
        }
        return iterateAsyncDirEntryArray(entries, archives);
      } else {
        // We've processed all the entries
        if (!archives.length) {
          console.warn('No archives found in the picked directory!');
        }
        return archives;
      }
    })
    .catch(function (err) {
      console.error('There was an error processing the directory!', err);
    });
}

Für jeden Eintrag in der entryList müssen Sie später die Datei mit entry.getFile().then(function (file) { … }) abrufen, wenn Sie sie verwenden möchten, oder das Äquivalent mit const file = await entry.getFile() in einer async function.

Können wir noch weitergehen?

Die Anforderung, dass der Nutzer bei jedem erneuten Start der App eine Berechtigung gewähren muss, die durch eine Nutzergeste eingeleitet wird, erhöht den Aufwand beim Öffnen und erneuten Öffnen von Dateien und Ordnern etwas, ist aber immer noch viel flüssiger als die Zwangswiederauswahl einer Datei. Chromium-Entwickler arbeiten derzeit daran, den Code fertigzustellen, der dauerhafte Berechtigungen für installierte PWAs ermöglichen würde. Viele PWA-Entwickler haben dies gefordert und freuen sich auf diese Neuerung.

Aber was ist, wenn wir nicht warten müssen?! Die Entwickler von Kiwix haben vor Kurzem herausgefunden, dass es möglich ist, alle Berechtigungsanfragen zu entfernen, indem eine neue Funktion der File Access API verwendet wird, die sowohl von Chromium- als auch von Firefox-Browsern unterstützt wird (und teilweise von Safari, aber FileSystemWritableFileStream fehlt). Diese neue Funktion ist das Origin Private File System.

Vollständig nativ: das Origin Private File System

Das Origin Private File System (OPFS) ist noch eine experimentelle Funktion in der Kiwix-PWA. Das Team freut sich jedoch, Nutzer zum Ausprobieren zu animieren, da es die Lücke zwischen nativen Apps und Webanwendungen weitgehend schließt. Die wichtigsten Vorteile:

  • Auf Archive in OPFS kann ohne Berechtigungsaufforderung zugegriffen werden, auch beim Start. Nutzer können das Lesen eines Artikels und das Stöbern in einem Archiv genau dort fortsetzen, wo sie in einer vorherigen Sitzung aufgehört haben.
  • Er bietet einen stark optimierten Zugriff auf darin gespeicherte Dateien: Auf Android-Geräten konnten wir Geschwindigkeitsverbesserungen von fünf- bis zehnfach verzeichnen.

Der Standarddateizugriff unter Android mit der File API ist extrem langsam, insbesondere wenn große Archive (wie oft bei Kiwix-Nutzern) nicht im Gerätespeicher, sondern auf einer microSD-Karte gespeichert sind. Das ändert sich mit dieser neuen API. Die meisten Nutzer können zwar keine 97 GB große Datei im OPFS speichern (das den Gerätespeicher belegt, nicht den microSD-Kartenspeicher), aber es eignet sich hervorragend für kleine bis mittelgroße Archive. Sie möchten die vollständigste medizinische Enzyklopädie von WikiProject Medicine? Kein Problem, mit 1,7 GB passt es problemlos in das OPFS. (Tipp: Suchen Sie in der Mediathek in der App nach other → mdwiki_en_all_maxi.)

So funktioniert das OPFS

Das OPFS ist ein vom Browser bereitgestelltes Dateisystem, das für jede Quelle separat ist und mit dem Speicher auf App-Ebene unter Android vergleichbar ist. Dateien können aus dem für Nutzer sichtbaren Dateisystem in das OPFS importiert oder direkt dorthin heruntergeladen werden. Mit der API können auch Dateien im OPFS erstellt werden. Sobald sie sich im OPFS befinden, sind sie vom Rest des Geräts isoliert. In Chromium-basierten Desktop-Browsern ist es auch möglich, Dateien aus dem OPFS in das für den Nutzer sichtbare Dateisystem zu exportieren.

Wenn du das OPFS verwenden möchtest, musst du zuerst über navigator.storage.getDirectory() Zugriff darauf anfordern. Wenn du lieber Code mit await sehen möchtest, lies den Artikel Das Origin Private File System.

return navigator.storage
  .getDirectory()
  .then(function (handle) {
    return processDirHandle(handle);
  })
  .catch(function (err) {
    console.warn('Unable to get the OPFS directory entry', err);
  });

Das daraus resultierende Handle ist die gleiche Art von FileSystemDirectoryHandle wie die oben erwähnte window.showDirectoryPicker(). Sie können also den Code wiederverwenden, der diesen Vorgang verarbeitet. Glücklicherweise ist es nicht nötig, dies in indexedDB zu speichern – es können Sie einfach abrufen, wenn Sie es benötigen. Angenommen, Sie haben bereits einige Dateien im OPFS und möchten sie verwenden. Mit der oben beschriebenen Funktion iterateAsyncDirEntries() könnten Sie dann Folgendes tun:

return navigator.storage.getDirectory().then(function (dirHandle) {
  let entries = dirHandle.entries();
  return iterateAsyncDirEntries(entries, [])
    .then(function (archiveList) {
      return archiveList;
    })
    .catch(function (err) {
      console.error('Unable to iterate OPFS entries', err);
    });
});

Denken Sie daran, dass Sie für jeden Eintrag im Array archiveList weiterhin getFile() verwenden müssen.

Dateien in OPFS importieren

Wie gelangen Dateien überhaupt in das OPFS? Nicht so schnell! Zuerst müssen Sie den verfügbaren Speicherplatz schätzen und dafür sorgen, dass Nutzer keine 97 GB große Datei hochladen, wenn sie nicht hineinpasst.

Das geschätzte Kontingent lässt sich ganz einfach abrufen: navigator.storage.estimate().then(function (estimate) { … });. Etwas schwieriger ist es, herauszufinden, wie dies den Nutzenden angezeigt wird. In der Kiwix-App haben wir uns für ein kleines In-App-Steuerfeld entschieden, das direkt neben dem Kästchen angezeigt wird und mit dem Nutzer die OPFS ausprobieren können:

Bereich, auf dem der genutzte Speicherplatz in Prozent und der verbleibende verfügbare Speicherplatz in Gigabyte angezeigt wird

Der Bereich wird mit estimate.quota und estimate.usage ausgefüllt, z. B.:

let OPFSQuota; // Global variable, so we don't have to keep checking it
return navigator.storage.estimate().then(function (estimate) {
  const percent = ((estimate.usage / estimate.quota) * 100).toFixed(2);
  OPFSQuota = estimate.quota - estimate.usage;
  document.getElementById('OPFSQuota').innerHTML =
    '<b>OPFS storage quota:</b><br />Used:&nbsp;<b>' +
    percent +
    '%</b>; ' +
    'Remaining:&nbsp;<b>' +
    (OPFSQuota / 1024 / 1024 / 1024).toFixed(2) +
    '&nbsp;GB</b>';
});

Wie Sie sehen, gibt es auch eine Schaltfläche, mit der Nutzer dem OPFS Dateien aus dem für Nutzer sichtbaren Dateisystem hinzufügen können. Die gute Nachricht ist, dass Sie einfach die File API verwenden können, um die erforderlichen Dateiobjekte abzurufen, die importiert werden sollen. Es ist sogar wichtig, window.showOpenFilePicker() nicht zu verwenden, da diese Methode von Firefox nicht unterstützt wird, während OPFS definitiv unterstützt wird.

Die sichtbare Schaltfläche Datei(en) hinzufügen im Screenshot oben ist keine alte Dateiauswahl, click() ruft aber eine ausgeblendete alte Auswahl (<input type="file" multiple … />-Element) auf, wenn darauf geklickt oder getippt wird. Die App erfasst dann nur das change-Ereignis der Eingabe der ausgeblendeten Datei, prüft die Größe der Dateien und lehnt sie ab, wenn sie für das Kontingent zu groß sind. Wenn alles in Ordnung ist, fragen Sie die Nutzenden, ob sie sie hinzufügen möchten:

archiveFilesLegacy.addEventListener('change', function (files) {
  const filesArray = Array.from(files.target.files);
  // Abort if user didn't select any files
  if (filesArray.length === 0) return;
  // Calculate the size of the picked files
  let filesSize = 0;
  filesArray.forEach(function (file) {
    filesSize += file.size;
  });
  // Check the size of the files does not exceed the quota
  if (filesSize > OPFSQuota) {
    // Oh no, files are too big! Tell user...
    console.log('Files would exceed the OPFS quota!');
  } else {
    // Ask user if they're sure... if user said yes...
    return importOPFSEntries(filesArray)
      .then(function () {
        // Tell user we successfully imported the archives
      })
      .catch(function (err) {
        // Tell user there was an error (error catching is important!)
      });
  }
});

Dialogfeld, in dem der Nutzer gefragt wird, ob er dem ursprünglichen privaten Dateisystem eine Liste von .zim-Dateien hinzufügen möchte.

Da der Import von Archiven auf einigen Betriebssystemen wie Android nicht besonders schnell ist, zeigt Kiwix während des Imports auch ein Banner und ein kleines rotierendes Ladesymbol an. Das Team hat noch nicht herausgefunden, wie ein Fortschrittsindikator für diese Operation hinzugefügt werden kann. Wenn Sie es herausfinden, senden Sie uns bitte eine Postkarte.

Wie hat Kiwix also die Funktion importOPFSEntries() implementiert? Dazu wird die Methode fileHandle.createWriteable() verwendet, mit der jede Datei effektiv in das OPFS gestreamt werden kann. Die ganze Arbeit übernimmt der Browser. (Kiwix verwendet hier Promises aus Gründen, die mit unserer Legacy-Codebasis zusammenhängen. Es muss jedoch gesagt werden, dass in diesem Fall await eine einfachere Syntax erzeugt und der Effekt der Verhängnisvollen Pyramide vermieden wird.)

function importOPFSEntries(files) {
  // Get a handle on the OPFS directory
  return navigator.storage
    .getDirectory()
    .then(function (dir) {
      // Collect the promises for each file that we want to write
      let promises = files.map(function (file) {
        // Create the file and get a writeable handle on it
        return dir
          .getFileHandle(file.name, { create: true })
          .then(function (fileHandle) {
            // Get a writer for the file
            return fileHandle.createWritable().then(function (writer) {
              // Show a banner / spinner, then write the file
              return writer
                .write(file)
                .then(function () {
                  // Finished with this writer
                  return writer.close();
                })
                .catch(function (err) {
                  console.error('There was an error writing to the OPFS!', err);
                });
            });
          })
          .catch(function (err) {
            console.error('Unable to get file handle from OPFS!', err);
          });
      });
      // Return a promise that resolves when all the files have been written
      return Promise.all(promises);
    })
    .catch(function (err) {
      console.error('Unable to get a handle on the OPFS directory!', err);
    });
}

Dateistream direkt in das OPFS herunterladen

Eine Variante davon ist die Möglichkeit, eine Datei direkt aus dem Internet in das OPFS oder in ein beliebiges Verzeichnis zu streamen, für das Sie einen Verzeichnis-Handle haben (d. h. Verzeichnisse, die mit window.showDirectoryPicker() ausgewählt wurden). Dabei werden dieselben Prinzipien wie im Code oben verwendet, aber eine Response wird aus einer ReadableStream und einem Controller erstellt, der die aus der Remotedatei gelesenen Bytes in die Warteschlange stellt. Die resultierende Response.body wird dann über eine Pipe an den Writer der neuen Datei im OPFS übergeben.

In diesem Fall kann Kiwix die über die ReadableStream übertragenen Byte zählen und dem Nutzer so eine Fortschrittsanzeige anzeigen und ihn warnen, die App während des Downloads nicht zu beenden. Der Code ist etwas zu kompliziert, um ihn hier zu zeigen. Da unsere App jedoch eine quelloffene App ist, können Sie sich den Quellcode ansehen, wenn Sie etwas Ähnliches machen möchten. So sieht die Kiwix-Benutzeroberfläche aus. Die verschiedenen Fortschrittswerte unten sind darauf zurückzuführen, dass das Banner nur aktualisiert wird, wenn sich der Prozentsatz ändert, der Bereich Downloadfortschritt jedoch häufiger:

Kiwix-Benutzeroberfläche mit einer Leiste unten, die den Nutzer davor warnt, die App zu beenden, und den Downloadfortschritt des .zim-Archivs anzeigt.

Da der Download ziemlich lange dauern kann, können Nutzer die App während des Vorgangs frei verwenden. Das Banner wird jedoch immer angezeigt, damit Nutzer daran erinnert werden, die App erst zu schließen, wenn der Download abgeschlossen ist.

Mini-Dateimanager in eine App implementieren

An diesem Punkt erkannten die Entwickler der Kiwix-PWA, dass es nicht ausreicht, dem OPFS Dateien hinzufügen zu können. Außerdem musste die App den Nutzern die Möglichkeit bieten, Dateien, die nicht mehr benötigt werden, aus diesem Speicherbereich zu löschen und idealerweise auch alle im OPFS gesperrten Dateien in das für Nutzer sichtbare Dateisystem zu exportieren. Effektiv wurde es notwendig, ein kleines Dateiverwaltungssystem in die App zu implementieren.

Ein kurzer Hinweis auf die tolle OPFS Explorer-Erweiterung für Chrome (funktioniert auch in Edge). Es wird ein Tab in den Entwicklertools hinzugefügt, über den Sie genau sehen können, was sich im OPFS befindet, und auch schädliche oder fehlgeschlagene Dateien löschen können. Es war unverzichtbar, um zu prüfen, ob der Code funktionierte, das Verhalten von Downloads zu beobachten und unsere Entwicklungstests im Allgemeinen zu optimieren.

Der Dateiexport hängt davon ab, ob ein Datei-Handle für eine ausgewählte Datei oder ein ausgewähltes Verzeichnis abgerufen werden kann, in dem Kiwix die exportierte Datei speichert. Dies funktioniert also nur in Kontexten, in denen die Methode window.showSaveFilePicker() verwendet werden kann. Wenn Kiwix-Dateien kleiner als mehrere GB wären, könnten wir einen Blob im Arbeitsspeicher erstellen, ihm eine URL zuweisen und ihn dann in das für Nutzer sichtbare Dateisystem herunterladen. Bei so großen Archiven ist das leider nicht möglich. Wenn dies unterstützt wird, ist der Export recht einfach. Umgekehrt ist das genauso wie das Speichern einer Datei im OPFS. Rufen Sie einen Handle für die zu speichernde Datei ab, bitten Sie den Nutzer, einen Speicherort für die Datei mit window.showSaveFilePicker() auszuwählen, und verwenden Sie dann createWriteable() im saveHandle. Sie können den Code im Repository sehen.

Das Löschen von Dateien wird von allen Browsern unterstützt und kann mit einer einfachen dirHandle.removeEntry('filename') erfolgen. Im Fall von Kiwix haben wir die OPFS-Einträge wie oben beschrieben iteriert, damit wir zuerst prüfen konnten, ob die ausgewählte Datei vorhanden ist, und um um Bestätigung zu bitten. Das ist aber nicht für alle notwendig. Wie bereits erwähnt, können Sie sich unseren Code ansehen, wenn Sie daran interessiert sind.

Es wurde beschlossen, die Kiwix-Benutzeroberfläche nicht mit Schaltflächen für diese Optionen zu überladen, sondern stattdessen kleine Symbole direkt unter die Archivliste zu setzen. Wenn Sie auf eines dieser Symbole tippen, ändert sich die Farbe der Archivliste. Das ist ein visueller Hinweis für den Nutzer, was er tun wird. Der Nutzer klickt dann auf eines der Archive und der entsprechende Vorgang (Exportieren oder Löschen) wird nach Bestätigung ausgeführt.

Dialogfeld, in dem der Nutzer gefragt wird, ob er eine .zim-Datei löschen möchte.

Hier ist eine Demo mit allen oben beschriebenen Dateiverwaltungsfunktionen: Hinzufügen einer Datei zum OPFS, Herunterladen einer Datei direkt in das OPFS, Löschen einer Datei und Exportieren in das sichtbare Dateisystem.

Die Arbeit eines Entwicklers ist nie zu Ende

Das OPFS ist eine großartige Innovation für Entwickler von PWAs. Es bietet leistungsstarke Dateiverwaltungsfunktionen, die die Lücke zwischen nativen Apps und Webanwendungen deutlich schließen. Aber Entwickler sind ein unzufriedenes Völkchen – sie sind nie ganz zufrieden. Das OPFS ist fast perfekt, aber nicht ganz. Es ist toll, dass die Hauptfunktionen sowohl in Chromium- als auch in Firefox-Browsern funktionieren und sowohl auf Android-Geräten als auch auf dem Computer implementiert sind. Wir hoffen, dass alle Funktionen bald auch in Safari und iOS implementiert sind. Die folgenden Probleme sind noch nicht behoben:

  • Firefox setzt derzeit ein Limit von 10 GB für das OPFS-Kontingent fest, unabhängig davon, wie viel Speicherplatz vorhanden ist. Für die meisten PWA-Entwickler ist das zwar ausreichend, für Kiwix ist es jedoch ziemlich restriktiv. Glücklicherweise sind Chromium-Browser viel großzügiger.
  • In mobilen Browsern oder im Firefox-Desktop ist es derzeit nicht möglich, große Dateien aus dem OPFS in das für Nutzer sichtbare Dateisystem zu exportieren, da window.showSaveFilePicker() nicht implementiert ist. In diesen Browsern werden große Dateien praktisch im OPFS gespeichert. Dies verstößt gegen das Kiwix-Ethos des offenen Zugriffs auf Inhalte und der Fähigkeit, Archive unter Nutzern zu teilen, insbesondere in Bereichen mit unregelmäßiger oder teurer Internetverbindung.
  • Nutzer können nicht steuern, welchen Speicher das virtuelle OPFS-Dateisystem belegt. Das ist besonders auf Mobilgeräten problematisch, auf denen Nutzer möglicherweise viel Speicherplatz auf einer microSD-Karte, aber nur sehr wenig Speicherplatz auf dem Gerät haben.

Alles in allem sind dies kleine Kleinigkeiten bei dem ansonsten großen Schritt nach vorn für den Dateizugriff in PWAs. Das Kiwix PWA-Team ist den Chromium-Entwicklern und -Befürwortern, die erstmals die File System Access API vorgeschlagen und entworfen haben, sowie für die harte Arbeit, um einen Konsens zwischen den Browseranbietern über die Bedeutung des ursprünglichen privaten Dateisystems zu erzielen. Bei der Kiwix JS-PWA konnten damit viele der UX-Probleme behoben werden, die die App in der Vergangenheit behindert haben. Außerdem hilft uns das Tool dabei, die Barrierefreiheit von Kiwix-Inhalten für alle zu verbessern. Probieren Sie die Kiwix-PWA aus und teilen Sie den Entwicklern mit, was Sie davon halten.

Auf den folgenden Websites finden Sie nützliche Informationen zu PWA-Funktionen: