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

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 Nonprofit-Organisation Kiwix die Technologie der progressiven Webanwendung und die File System Access API nutzt, um Nutzern das Herunterladen und Speichern großer Internetarchive zur Offlinenutzung zu ermöglichen. 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 immer noch auf einen zuverlässigen Zugang zum Internet, wie die Internationale Fernmeldeunion mitteilt. Ist das das Ende der Geschichte? Nein gar nicht. Die Mitarbeiter von Kiwix, einer Nonprofit-Organisation in der Schweiz, haben ein Ökosystem aus Open-Source-Apps und ‑Inhalten entwickelt, mit dem Wissen für Menschen mit eingeschränktem oder keinem Internetzugang zugänglich gemacht werden soll. 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, z. B. Wikipedia, Project Gutenberg, Stack Exchange oder sogar TED-Vorträge, können jetzt in stark komprimierte Archive umgewandelt werden, die sogenannten ZIM-Dateien, und im Kiwix-Browser direkt 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 die gesamte Wikipedia auf Englisch (6,4 Millionen Artikel und Bilder) nach der Umwandlung in das ZIM-Format auf 97 GB komprimiert wird.Das klingt nach viel, bis man sich bewusst wird, dass die Summe des gesamten menschlichen Wissens jetzt auf ein Mittelklasse-Android-Smartphone passt. Es werden auch viele kleinere Ressourcen angeboten, darunter themenspezifische Versionen von Wikipedia, z. B. zu Mathematik oder Medizin.

Kiwix bietet eine Reihe von nativen Apps für Desktop-Computer (Windows/Linux/macOS) und Mobilgeräte (iOS/Android). In dieser Fallstudie liegt der Schwerpunkt jedoch auf der progressiven Web-App (PWA), die eine universelle und einfache Lösung für alle Geräte mit einem modernen Browser sein soll.

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 Webanwendung für die Offlinenutzung?

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. Kiwix versucht, so viele Anwendungsfälle wie möglich abzudecken, hat aber auch erkannt, dass sie noch mehr Nutzer erreichen könnte, wenn sie die universellste Software auf jedem Gerät verwenden würde: den Webbrowser. Inspiriert von Atwood's Law, das besagt, dass jede Anwendung, die in JavaScript geschrieben werden kann, irgendwann in JavaScript geschrieben wird, begannen vor etwa zehn Jahren einige Kiwix-Entwickler, die Kiwix-Software von C++ auf JavaScript umzustellen.

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 erstellten eine spezielle PWA-Version von Kiwix JS. Außerdem fügten sie Betriebssystemintegrationen hinzu, mit denen die App ähnliche Funktionen wie eine native App 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 großen Größe von ZIM-Archiven sind die Speicherung und der Zugriff darauf, insbesondere auf Mobilgeräten, wahrscheinlich die größte Herausforderung für die Kiwix-Entwickler. Viele Kiwix-Endnutzer laden Inhalte in der App herunter, wenn eine Internetverbindung 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 Backend von Kiwix, das in reinem clientseitigem JavaScript geschrieben ist, bei Bedarf kleine Ausschnitte des großen Archivs. 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 hervorragendes Framework mit leistungsstarken Funktionen, darunter der vollständige Zugriff auf das Dateisystem über Node APIs. Es hat jedoch einige bekannte Nachteile:

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

Die Größe der Electron-Apps ist aufgrund der Tatsache, dass in jeder App eine vollständige Kopie von Chromium enthalten ist, im Vergleich zu den nur 5,1 MB 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 zur Rettung

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 dieser Handle zwischen App-Sitzungen erhalten bleibt, sodass der Nutzer die Datei oder den Ordner nicht noch einmal auswählen muss, wenn er die App neu startet. Er muss jedoch eine kurze Berechtigungsanfrage 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? Wichtige Hinweise:

  • Es handelt sich um eine asynchrone API (außer bei speziellen 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 wieder die Berechtigung zum Zugriff auf eine zuvor ausgewählte Datei erteilen kann, ist ebenfalls eine Nutzergeste erforderlich. Der Browser zeigt die Berechtigungsanfrage nicht an, wenn sie nicht durch eine Nutzergeste initiiert wurde.

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 es einige Bibliotheken gibt, die einen Großteil der Arbeit für Sie erledigen, 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 der Einfachheit halber 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. 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);
    });
}

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 etwas näher darauf eingehen?

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 erneute Auswahl 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, 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 möchte Nutzer aber 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 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 hochoptimierten 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 können Dateien auch wieder aus dem OPFS in das für Nutzer sichtbare Dateisystem exportiert werden.

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

Der Handle, den du hier erhältst, ist derselbe FileSystemDirectoryHandle-Typ wie der, den du oben bei window.showDirectoryPicker() erhältst. Du kannst also den Code wiederverwenden, der dafür zuständig ist. Glücklicherweise musst du ihn nicht in indexedDB speichern, sondern kannst ihn einfach abrufen, wenn du ihn brauchst. Angenommen, Sie haben bereits einige Dateien im OPFS und möchten sie verwenden. Mit der zuvor 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 das 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 Sie diese Informationen dem Nutzer präsentieren. 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 mit dem belegten Speicherplatz in Prozent und dem verbleibenden verfügbaren Speicherplatz in Gigabyte.

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, frage den Nutzer, ob er sie 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 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 die importOPFSEntries()-Funktion implementiert? Dazu wird die Methode fileHandle.createWriteable() verwendet, mit der jede Datei effektiv in das OPFS gestreamt werden kann. Die gesamte Arbeit wird vom Browser erledigt. (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 erstellt, die aus einer ReadableStream und einem Controller besteht, 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.

In-App-Mini-Dateimanager 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. Es war also notwendig, in der App ein Mini-Dateiverwaltungssystem zu implementieren.

Ein kurzer Hinweis auf die tolle OPFS Explorer-Erweiterung für Chrome (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 schädliche oder fehlgeschlagene Dateien löschen. 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 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 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 unterstützt, ist der Export ziemlich einfach: praktisch das Umgekehrte zum Speichern einer Datei im OPFS (Handle für die zu speichernde Datei abrufen, Nutzer bitten, mit window.showSaveFilePicker() einen Speicherort auszuwählen, dann createWriteable() auf der saveHandle verwenden). Den Code finden Sie im Repository.

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 die vollständige Funktion bald auch in Safari und iOS implementiert wird. 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. Das widerspricht dem Kiwix-Grundsatz des offenen Zugangs zu Inhalten und der Möglichkeit, Archive zwischen Nutzern zu teilen, insbesondere in Gebieten mit unterbrochener 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 jedoch nur kleinere Probleme bei einem ansonsten großen Schritt nach vorn für den Dateizugriff in PWAs. Das Kiwix-PWA-Team ist den Chromium-Entwicklern und -Befürwortern sehr dankbar, die die File System Access API erstmals vorgeschlagen und entworfen haben, und für die harte Arbeit, die zum Konsens unter den Browseranbietern über die Bedeutung des Origin Private File Systems geführt hat. 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.

Hier finden Sie einige hilfreiche Ressourcen zu PWA-Funktionen: