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 ist ein Plastikstuhl zu sehen. Der Hintergrund sieht aus wie eine Schule in einem Entwicklungsland.

In dieser Fallstudie wird untersucht, wie Kiwix, eine Nonprofit-Organisation, Progressive Web App-Technologie und die File System Access API verwendet, um Nutzern das Herunterladen und Speichern großer Internetarchive für die Offline-Nutzung zu ermöglichen. Hier erfahren Sie mehr über die technische Implementierung des Codes für das Origin Private File System (OPFS), eine neue Browserfunktion in der Kiwix-PWA, die die Dateiverwaltung verbessert und einen besseren Zugriff auf Archive ohne Berechtigungsaufforderungen ermöglicht. Im Artikel werden Herausforderungen und potenzielle zukünftige Entwicklungen in diesem neuen Dateisystem behandelt.

Informationen zu Kiwix

Mehr als 30 Jahre nach der Geburt des Internets wartet laut der International Telecommunication Union immer noch ein Drittel der Weltbevölkerung auf einen zuverlässigen Internetzugang. Ist das das Ende der Geschichte? Nein gar nicht. Die Leute bei Kiwix, einer in der Schweiz ansässigen Nonprofit-Organisation, haben ein Ökosystem aus Open-Source-Apps und Inhalten entwickelt, das darauf abzielt, Menschen mit eingeschränktem oder keinem Internetzugang Wissen zugänglich zu machen. Die Idee dahinter ist, dass jemand, der Sie ist, wichtige Ressourcen herunterladen kann, wenn und wo eine Verbindung verfügbar ist, und sie lokal für die spätere Offlineverwendung speichern kann, wenn Sie keinen einfachen Zugriff auf das Internet haben. Viele wichtige Websites, z. B. Wikipedia, Project Gutenberg, Stack Exchange oder sogar TED-Vorträge, können jetzt in hochkomprimierte Archive, sogenannte ZIM-Dateien, konvertiert und vom Kiwix-Browser im Handumdrehen gelesen werden.

ZIM-Archive verwenden die hocheffiziente Zstandard-Komprimierung (ZSTD) (ältere Versionen verwendeten XZ), hauptsächlich zum Speichern von HTML, JavaScript und CSS, während Bilder normalerweise in das komprimierte WebP-Format konvertiert werden. Jede ZIM-Datei enthält auch einen URL- und einen Titelindex. Die Komprimierung ist hier von entscheidender Bedeutung, da die gesamte englischsprachige Wikipedia (6,4 Millionen Artikel plus Bilder) nach der Konvertierung in das ZIM-Format auf 97 GB komprimiert wird.Das klingt nach viel, bis man feststellt, dass die Summe des gesamten menschlichen Wissens jetzt auf ein Android-Smartphone der Mittelklasse passt. Es werden auch viele kleinere Ressourcen angeboten, darunter thematische Versionen von Wikipedia, z. B. zu Mathematik oder Medizin.

Kiwix bietet eine Reihe von nativen Apps für Desktop- (Windows/Linux/macOS) und mobile Geräte (iOS/Android). In dieser Fallstudie geht es jedoch um die Progressive Web-App (PWA), die eine universelle und einfache Lösung für alle Geräte mit einem modernen Browser sein soll.

Wir werden uns die Herausforderungen ansehen, die bei der Entwicklung einer universellen Web-App entstehen, die schnellen Zugriff auf große Inhaltsarchive bietet, und zwar vollständig offline. Außerdem werden wir uns einige moderne JavaScript-APIs ansehen, 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 für die Offlinenutzung?

Kiwix-Nutzer sind eine bunte Mischung mit vielen unterschiedlichen Anforderungen. Kiwix hat wenig oder gar keinen Einfluss auf die Geräte und Betriebssysteme, auf denen sie auf ihre Inhalte zugreifen. Einige dieser Geräte sind möglicherweise langsam oder veraltet, insbesondere in einkommensschwachen Regionen der Welt. Kiwix versucht, so viele Anwendungsfälle wie möglich abzudecken. Die Organisation erkannte jedoch, dass sie noch mehr Nutzer erreichen könnte, wenn sie die universellste Software auf jedem Gerät verwendet: den Webbrowser. Inspiriert von Atwoods Law, das besagt, dass jede Anwendung, die in JavaScript geschrieben werden kann, irgendwann in JavaScript geschrieben wird, haben einige Kiwix-Entwickler vor etwa 10 Jahren damit begonnen, die Kiwix-Software von C++ zu JavaScript zu portieren.

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

Kiwix JS Offline Browser

Geben Sie die Progressive Web-App (PWA) ein. Um das Potenzial dieser Technologie zu nutzen, entwickelten die Kiwix-Entwickler eine spezielle PWA-Version von Kiwix JS und fügten Betriebssystemintegrationen hinzu, die es der App ermöglichen, native Funktionen zu bieten, insbesondere in den Bereichen Offline-Nutzung, Installation, Dateiverarbeitung und Dateisystemzugriff.

Offline-First-PWAs sind extrem schlank und eignen sich daher perfekt für Situationen, in denen es nur eine zeitweise oder teure mobile Internetverbindung gibt. Die Technologie dahinter ist die Service Worker API und die zugehörige Cache API, die von allen auf Kiwix JS basierenden Apps verwendet wird. Mit diesen APIs können die Apps als Server fungieren, Abrufanfragen aus dem 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 ist die Speicherung und der Zugriff darauf, insbesondere auf Mobilgeräten, wahrscheinlich die größte Herausforderung für Kiwix-Entwickler. Viele Kiwix-Endnutzer laden Inhalte in der App herunter, wenn eine Internetverbindung besteht, um sie später offline zu verwenden. Andere Nutzer laden Inhalte auf einem PC über einen Torrent herunter und übertragen sie dann auf ein Mobilgerät oder Tablet. Einige tauschen Inhalte auf USB-Sticks oder tragbaren Festplatten in Gebieten mit lückenhaftem oder teurem mobilen Internet aus. Alle diese Möglichkeiten, auf Inhalte von beliebigen, für Nutzer zugänglichen Orten zuzugreifen, müssen von Kiwix JS und Kiwix PWA unterstützt werden.

Die File API hat es Kiwix JS ursprünglich ermöglicht, riesige Archive mit Hunderten von Gigabyte zu lesen (eines unserer ZIM-Archive ist 166 GB groß!), selbst auf Geräten mit wenig Arbeitsspeicher. Diese API wird in jedem Browser unterstützt, auch in sehr alten Browsern. Sie dient als universeller Fallback, wenn neuere APIs nicht unterstützt werden. Dazu müssen Sie nur ein input-Element in HTML definieren, im Fall von Kiwix:

<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 File-Objekte, die im Wesentlichen Metadaten sind, die auf die zugrunde liegenden Daten im Speicher verweisen. Das objektorientierte Backend von Kiwix, das in reinem clientseitigem JavaScript geschrieben ist, liest bei Bedarf kleine Ausschnitte des großen Archivs. Wenn diese Slices dekomprimiert werden müssen, übergibt das Backend sie an den Wasm-Dekompressor und ruft bei Bedarf weitere Slices 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 führte, dass Kiwix JS-Apps im Vergleich zu nativen Apps umständlich und altmodisch wirkten: Der Nutzer musste Archive über eine Dateiauswahl auswählen oder eine Datei per Drag-and-drop in die App ziehen, jedes Mal, wenn die App gestartet wurde. Mit dieser API ist es nämlich nicht möglich, Zugriffsberechtigungen von einer Sitzung zur nächsten beizubehalten.

Um diese schlechte Nutzerfreundlichkeit zu vermeiden, entschieden sich die Entwickler von Kiwix JS, wie viele andere Entwickler, zunächst für Electron. ElectronJS ist ein leistungsstarkes Framework, das unter anderem vollen Zugriff auf das Dateisystem über Node-APIs bietet. Er hat jedoch einige bekannte Nachteile:

  • Es kann nur auf Desktop-Betriebssystemen ausgeführt werden.
  • Sie ist groß und schwer (70–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 für die minimierte und gebündelte PWA sehr ungünstig.

Gibt es eine Möglichkeit, die Situation für Nutzer der PWA zu verbessern?

Die File System Access API

Um 2019 wurde Kiwix auf eine neue API aufmerksam, die in Chrome 78 als Ursprungstest durchgeführt wurde und damals Native File System API hieß. Es wurde versprochen, dass ein Dateihandle für eine Datei oder einen Ordner abgerufen und in einer IndexedDB-Datenbank gespeichert werden kann. Wichtig ist, dass dieser Handle zwischen App-Sitzungen beibehalten wird. Der Nutzer muss die Datei oder den Ordner also nicht noch einmal auswählen, wenn er die App neu startet. Er muss jedoch eine kurze Berechtigungsaufforderung beantworten. Als sie in die Produktion ging, wurde sie in File System Access API umbenannt und die Kernbestandteile wurden vom WHATWG als File System API (FSA) standardisiert.

Wie funktioniert der Teil der API für den Dateisystemzugriff? Wichtige Hinweise:

  • Es handelt sich um eine asynchrone API (mit Ausnahme von spezialisierten Funktionen in Webworkern).
  • Die Dateiauswahl oder Verzeichnisauswahl muss programmatisch durch Erfassen einer Nutzeraktion (Klicken oder Tippen auf ein UI-Element) gestartet werden.
  • Damit der Nutzer in einer neuen Sitzung noch einmal die Berechtigung zum Zugriff auf eine zuvor ausgewählte Datei erteilen kann, ist ebenfalls eine Nutzergeste erforderlich. Der Browser weigert sich sogar, die Berechtigungsaufforderung anzuzeigen, wenn sie nicht durch eine Nutzergeste ausgelöst wird.

Der Code ist relativ einfach, abgesehen davon, dass die umständliche IndexedDB API zum Speichern von Datei- und Verzeichnishandles verwendet werden muss. Zum Glück gibt es einige Bibliotheken, die Ihnen viel Arbeit abnehmen, z. B. browser-fs-access. Bei Kiwix JS haben wir uns entschieden, direkt mit den APIs zu arbeiten, die sehr gut dokumentiert sind.

Dateiauswahl und Verzeichnisauswahl öffnen

Das Öffnen einer Dateiauswahl sieht in etwa so aus (hier mit Promises, aber wenn Sie async/await bevorzugen, sehen Sie sich das Chrome for Developers-Tutorial an):

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,
    );
  });

Der Einfachheit halber wird in diesem Code nur die erste ausgewählte Datei verarbeitet. Außerdem wird verhindert, dass mehr als eine Datei ausgewählt wird. Wenn Sie die Auswahl mehrerer Dateien mit { multiple: true } zulassen möchten, umschließen Sie einfach alle Promises, die die einzelnen Handles verarbeiten, mit einer 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);
)};

Es ist jedoch besser, den Nutzer zu bitten, das Verzeichnis mit diesen Dateien auszuwählen, anstatt die einzelnen Dateien darin. Das gilt insbesondere, da Kiwix-Nutzer in der Regel alle ihre ZIM-Dateien im selben Verzeichnis organisieren. Der Code zum Starten der Verzeichnisauswahl ist fast derselbe wie oben, nur dass Sie window.showDirectoryPicker.then(function (dirHandle) { … }); verwenden.

Verarbeiten des Datei- oder Verzeichnishandles

Sobald Sie das Handle haben, müssen Sie es verarbeiten. Die Funktion processFileHandle könnte 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 selbst bereitstellen. Es gibt keine Convenience-Methoden dafür, es sei denn, Sie verwenden eine Abstraktionsbibliothek. Die Implementierung von 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.

Verarbeitungsverzeichnisse sind etwas komplizierter, da Sie die Einträge im ausgewählten Verzeichnis mit dem asynchronen entries.next() durchlaufen müssen, um die gewünschten Dateien oder Dateitypen zu finden. Dafür gibt es verschiedene Möglichkeiten. Hier ist der 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);
    });
}

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

Können wir noch weiter gehen?

Die Anforderung, dass der Nutzer bei nachfolgenden Starts der App die Berechtigung erteilen muss, die mit einer Nutzeraktion initiiert wird, führt zu einer geringfügigen Erschwernis beim (Wieder-)Öffnen von Dateien und Ordnern. Das ist aber immer noch viel flüssiger, als wenn eine Datei neu ausgewählt werden muss. Chromium-Entwickler schließen derzeit den Code ab, der dauerhafte Berechtigungen für installierte PWAs ermöglicht. Viele PWA-Entwickler haben sich das gewünscht und freuen sich darauf.

Aber was, wenn wir nicht warten müssen? Die Entwickler von Kiwix haben vor Kurzem herausgefunden, dass es möglich ist, alle Berechtigungsaufforderungen zu eliminieren, indem sie eine neue Funktion der File Access API verwenden, die sowohl von Chromium- als auch von Firefox-Browsern unterstützt wird (und teilweise von Safari, aber immer noch 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 in der Kiwix-PWA noch eine experimentelle Funktion, aber das Team möchte Nutzer*innen dazu ermutigen, es auszuprobieren, da es die Lücke zwischen nativen Apps und Web-Apps weitgehend schließt. Die wichtigsten Vorteile:

  • Auf Archive im OPFS kann ohne Berechtigungsaufforderungen zugegriffen werden, auch beim Start. Nutzer können einen Artikel weiterlesen und ein Archiv durchsuchen, ohne dass sie sich erneut anmelden müssen.
  • Es bietet hochgradig optimierten Zugriff auf darin gespeicherte Dateien: Unter Android ist die Geschwindigkeit fünf- bis zehnmal höher.

Der Standarddateizugriff in Android über die File API ist sehr langsam, insbesondere (wie es bei Kiwix-Nutzern oft der Fall ist), wenn große Archive auf einer microSD-Karte anstatt im Gerätespeicher gespeichert sind. Das ändert sich mit dieser neuen API. Die meisten Nutzer können keine 97 GB große Datei im OPFS speichern, da dafür Gerätespeicher und nicht der Speicher der microSD-Karte verwendet wird. Das OPFS eignet sich jedoch hervorragend zum Speichern kleiner bis mittelgroßer Archive. Sie möchten die umfassendste medizinische Enzyklopädie von WikiProject Medicine? Kein Problem, mit 1,7 GB passt es problemlos in das OPFS. Tipp: Suchen Sie in der In-App-Bibliothek nach other → mdwiki_en_all_maxi.

Funktionsweise des OPFS

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

Wenn Sie das OPFS verwenden möchten, müssen Sie zuerst mit navigator.storage.getDirectory() Zugriff darauf anfordern. Wenn Sie lieber Code mit await sehen möchten, lesen Sie The 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);
  });

Der Handle, den Sie dadurch erhalten, ist derselbe Typ von FileSystemDirectoryHandle, den Sie von window.showDirectoryPicker() oben erhalten. Das bedeutet, dass Sie den Code, der ihn verarbeitet, wiederverwenden können. Glücklicherweise müssen Sie ihn nicht in indexedDB speichern, sondern können ihn einfach abrufen, wenn Sie ihn benötigen. Angenommen, Sie haben bereits einige Dateien im OPFS und möchten sie verwenden. Mit der zuvor gezeigten Funktion iterateAsyncDirEntries() können Sie 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, mit dem Sie aus dem archiveList-Array arbeiten möchten, weiterhin getFile() verwenden müssen.

Dateien in das OPFS importieren

Wie gelangen Dateien also ü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 passt.

So erhalten Sie das geschätzte Kontingent: navigator.storage.estimate().then(function (estimate) { … });. Etwas schwieriger ist es, herauszufinden, wie diese Informationen dem Nutzer angezeigt werden sollen. In der Kiwix-App haben wir uns für ein kleines In-App-Feld direkt neben dem Kästchen entschieden, mit dem Nutzer das OPFS ausprobieren können:

Das Steuerfeld zeigt den verwendeten Speicherplatz in Prozent und den verbleibenden verfügbaren Speicherplatz in Gigabyte.

Der Bereich wird mit estimate.quota und estimate.usage gefü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 Dateien aus dem für Nutzer sichtbaren Dateisystem zum OPFS hinzufügen können. Die gute Nachricht ist, dass Sie einfach die File API verwenden können, um das benötigte File-Objekt (oder die benötigten File-Objekte) abzurufen, das bzw. die importiert werden soll(en). Tatsächlich ist es wichtig, window.showOpenFilePicker() nicht zu verwenden, da diese Methode von Firefox nicht unterstützt wird. Das OPFS wird jedoch definitiv unterstützt.

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

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 privaten Dateisystem des Ursprungs eine Liste von .zim-Dateien hinzufügen möchte.

Da das Importieren von Archiven auf einigen Betriebssystemen, z. B. Android, nicht sehr schnell geht, 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 diesen Vorgang hinzugefügt werden kann. Wenn Sie eine Lösung finden, schreiben Sie uns bitte eine Postkarte.

Wie hat Kiwix die importOPFSEntries()-Funktion implementiert? Dazu wird die Methode fileHandle.createWriteable() verwendet, mit der jede Datei in das OPFS gestreamt werden kann. Die ganze Arbeit wird vom Browser erledigt. (Kiwix verwendet hier Promises aus Gründen, die mit unserem Legacy-Code zusammenhängen. Es muss jedoch gesagt werden, dass in diesem Fall await eine einfachere Syntax erzeugt und den „Pyramide des Todes“-Effekt vermeidet.)

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 aus dem Internet direkt in das OPFS oder in ein beliebiges Verzeichnis zu streamen, für das Sie ein Verzeichnishandle haben (d. h. Verzeichnisse, die mit window.showDirectoryPicker() ausgewählt wurden). Dabei werden dieselben Prinzipien wie im obigen Code verwendet, aber ein Response wird aus einem ReadableStream und einem Controller erstellt, der die aus der Remotedatei gelesenen Byte in die Warteschlange stellt. Das resultierende Response.body wird dann übergeben an den Writer der neuen Datei im OPFS.

In diesem Fall kann Kiwix die über die ReadableStream übertragenen Bytes zählen und dem Nutzer eine Fortschrittsanzeige zur Verfügung stellen. Außerdem kann der Nutzer gewarnt werden, die App während des Downloads nicht zu schließen. Der Code ist etwas zu kompliziert, um ihn hier zu zeigen. Da unsere App jedoch eine FOSS-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, das Feld Download-Fortschritt jedoch regelmäßig aktualisiert wird:

Kiwix-Benutzeroberfläche mit einer Leiste unten, in der der Nutzer gewarnt wird, die App nicht zu schließen, und der Downloadfortschritt des .zim-Archivs angezeigt wird.

Da das Herunterladen eine Weile 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 der App implementieren

An diesem Punkt stellten die Entwickler der Kiwix-PWA fest, dass es nicht ausreicht, Dateien zum OPFS hinzuzufügen. Die App musste den Nutzern auch die Möglichkeit bieten, Dateien, die sie nicht mehr benötigen, aus diesem Speicherbereich zu löschen und idealerweise auch alle im OPFS gesperrten Dateien in das für den Nutzer sichtbare Dateisystem zu exportieren. Es war also erforderlich, ein kleines Dateiverwaltungssystem in die App zu implementieren.

An dieser Stelle möchten wir kurz auf die tolle OPFS Explorer-Erweiterung für Chrome hinweisen (sie funktioniert auch in Edge). Dadurch wird in den Entwicklertools ein Tab hinzugefügt, auf dem Sie genau sehen können, was sich im OPFS befindet, und auch fehlerhafte oder fehlgeschlagene Dateien löschen können. Es war von unschätzbarem Wert, um zu prüfen, ob Code funktioniert, das Verhalten von Downloads zu überwachen und unsere Entwicklungsexperimente zu bereinigen.

Die Datei export hängt davon ab, ob ein Dateihandle für eine ausgewählte Datei oder ein ausgewähltes Verzeichnis abgerufen werden kann, in dem Kiwix die exportierte Datei speichern soll. Daher funktioniert dies nur in Kontexten, in denen die Methode window.showSaveFilePicker() verwendet werden kann. Wenn Kiwix-Dateien kleiner als mehrere Gigabyte wären, könnten wir einen Blob im Arbeitsspeicher erstellen, ihm eine URL zuweisen und ihn dann in das für den Nutzer sichtbare Dateisystem herunterladen. Das ist bei so großen Archiven leider nicht möglich. Wenn der Export unterstützt wird, ist er recht einfach: Er funktioniert im Grunde genau wie das Speichern einer Datei im OPFS, nur umgekehrt. Sie rufen ein Handle für die zu speichernde Datei ab, bitten den Nutzer, mit window.showSaveFilePicker() einen Speicherort auszuwählen, und verwenden dann createWriteable() für das saveHandle. Den Code finden Sie im Repository.

Das Löschen von Dateien wird von allen Browsern unterstützt und kann mit einem einfachen dirHandle.removeEntry('filename') erreicht werden. Im Fall von Kiwix haben wir die OPFS-Einträge wie oben beschrieben durchlaufen, damit wir zuerst prüfen konnten, ob die ausgewählte Datei vorhanden ist, und dann um Bestätigung bitten konnten. Das ist aber möglicherweise nicht für alle erforderlich. Auch hier können Sie sich unseren Code ansehen, wenn Sie möchten.

Es wurde entschieden, die Kiwix-Benutzeroberfläche nicht mit Schaltflächen für diese Optionen zu überladen, sondern stattdessen kleine Symbole direkt unter der Archivliste zu platzieren. Wenn der Nutzer auf eines dieser Symbole tippt, ändert sich die Farbe der Archivliste, um ihm visuell zu verdeutlichen, was er tun wird. Anschließend klickt oder tippt er auf eines der Archive und die entsprechende Aktion (Exportieren oder Löschen) wird nach der Bestätigung ausgeführt.

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

Abschließend sehen Sie hier einen Screencast, in dem alle oben beschriebenen Dateiverwaltungsfunktionen demonstriert werden: Hinzufügen einer Datei zum OPFS, direktes Herunterladen einer Datei in das OPFS, Löschen einer Datei und Exportieren in das für den Nutzer sichtbare Dateisystem.

Die Arbeit eines Entwicklers ist nie abgeschlossen

Das OPFS ist eine großartige Innovation für Entwickler von PWAs und bietet leistungsstarke Funktionen zur Dateiverwaltung, die dazu beitragen, die Lücke zwischen nativen Apps und Web-Apps zu 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 wichtigsten Funktionen sowohl in Chromium- als auch in Firefox-Browsern funktionieren und dass sie sowohl auf Android als auch auf dem Computer implementiert sind. Wir hoffen, dass der vollständige Funktionsumfang bald auch in Safari und iOS implementiert wird. Die folgenden Probleme bestehen weiterhin:

  • Firefox begrenzt das OPFS-Kontingent derzeit auf 10 GB, unabhängig davon, wie viel zugrunde liegender Speicherplatz vorhanden ist. Für die meisten PWA-Autoren ist das ausreichend, für Kiwix ist es jedoch sehr restriktiv. Glücklicherweise sind Chromium-Browser viel großzügiger.
  • Derzeit ist es nicht möglich, große Dateien aus dem OPFS in das für Nutzer sichtbare Dateisystem in mobilen Browsern oder in der Desktopversion von Firefox zu exportieren, da window.showSaveFilePicker() nicht implementiert ist. In diesen Browsern sind große Dateien effektiv im OPFS eingeschlossen. Dies widerspricht dem Kiwix-Ethos des offenen Zugangs zu Inhalten und der Möglichkeit, Archive zwischen Nutzern zu teilen, insbesondere in Gebieten mit zeitweiser oder teurer Internetverbindung.
  • Nutzer haben keine Möglichkeit, zu steuern, welcher Speicherplatz vom virtuellen OPFS-Dateisystem belegt wird. Das ist besonders auf Mobilgeräten problematisch, da Nutzer dort möglicherweise viel Speicherplatz auf einer microSD-Karte, aber nur sehr wenig Speicherplatz auf dem Gerätespeicher haben.

Insgesamt sind das aber nur kleine Kritikpunkte an einem ansonsten großen Schritt nach vorn für den Dateizugriff in PWAs. Das Kiwix PWA-Team ist den Chromium-Entwicklern und -Befürwortern, die die File System Access API vorgeschlagen und entwickelt haben, sehr dankbar. Außerdem möchten wir uns für die harte Arbeit bedanken, die nötig war, um die Browseranbieter von der Bedeutung des Origin Private File System zu überzeugen. Für Kiwix JS PWA wurden viele der UX-Probleme behoben, die die App in der Vergangenheit beeinträchtigt haben. Außerdem hilft uns die App 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.

Hier finden Sie einige nützliche Ressourcen zu PWA-Funktionen: