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

Menschen stehen um einen Laptop auf einem einfachen Tisch. Auf der linken Seite steht ein Plastikstuhl. Der Hintergrund sieht aus wie eine Schule in einem Entwicklungsland.

In dieser Fallstudie wird untersucht, wie Kiwix, eine gemeinnützige Organisation, die progressive Web-App-Technologie und die File System Access API einsetzt, damit Nutzer große Internetarchive für die Offline-Nutzung herunterladen und speichern können. Hier erfahren Sie mehr über die technische Implementierung des Codes für das Origin Private File System (OPFS), eine neue Browserfunktion innerhalb der Kiwix-PWA, die die Dateiverwaltung verbessert und den Zugriff auf Archive ohne Berechtigungsaufforderung ermöglicht. Darin geht es um Herausforderungen und potenzielle zukünftige Entwicklungen in diesem neuen Dateisystem.

Über Kiwix

Mehr als 30 Jahre nach der Einführung des Webs wartet nach Angaben der Internationalen Telekommunikationsunion noch immer ein Drittel der Weltbevölkerung auf einen zuverlässigen Zugang zum Internet. Endet die Geschichte? Nein gar nicht. Die in der Schweiz ansässige Nonprofit-Organisation Kiwix hat ein Ökosystem von Open-Source-Apps und -Inhalten entwickelt, die Menschen mit eingeschränktem oder gar keinem Internetzugang Wissen zugänglich machen sollen. Der Grundgedanke ist: Wenn Sie nicht problemlos auf das Internet zugreifen können, kann jemand wichtige Ressourcen für Sie herunterladen – unabhängig davon, wo und wann eine Internetverbindung verfügbar ist – und diese für die spätere Offlinenutzung lokal speichern. Viele wichtige Websites wie Wikipedia, Project Gutenberg, Stack Exchange oder sogar TED Talks können jetzt in hochkomprimierte Archive, sogenannten ZIM-Dateien, konvertiert und im Kiwix-Browser gelesen werden.

ZIM-Archive nutzen die hocheffiziente Komprimierung Zstandard (ZSTD). Ältere Versionen verwendeten XZ. Sie dienten hauptsächlich zum Speichern von HTML, JavaScript und CSS. Bilder werden normalerweise 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, was nach einer Menge klingt, bis Sie feststellen, dass die Gesamtmenge des menschlichen Wissens jetzt auf ein Android-Smartphone der mittleren Preisklasse passt. Es werden auch viele kleinere Ressourcen angeboten, darunter themenbezogene Versionen von Wikipedia, wie Mathematik, Medizin usw.

Kiwix bietet eine Reihe nativer Apps für die Nutzung auf Computern (Windows/Linux/macOS) und Mobilgeräten (iOS/Android). In dieser Fallstudie liegt der Schwerpunkt jedoch auf der Progressive Web App (PWA), die eine universelle und einfache Lösung für jedes Gerät mit einem modernen Browser darstellt.

Wir gehen auf die Herausforderungen ein, die bei der Entwicklung einer universellen Web-App entstehen, die einen schnellen und vollständig offline schnellen Zugriff auf große Inhaltsarchiven bieten muss. Außerdem sehen wir uns einige moderne JavaScript APIs an, 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 vielfältige Gruppe mit vielen unterschiedlichen Anforderungen und Kiwix hat nur wenig oder keine Kontrolle über die Geräte und Betriebssysteme, mit denen sie auf ihre Inhalte zugreifen. Einige dieser Geräte sind möglicherweise langsam oder veraltet, insbesondere in einkommensschwachen Regionen der Welt. Kiwix versucht zwar, so viele Anwendungsfälle wie möglich abzudecken, aber das Unternehmen erkannte auch, dass es mit der universellsten Software auf einem beliebigen Gerät noch mehr Nutzer erreichen kann: den Webbrowser. In Anlehnung an das Atwood-Gesetz, das besagt, dass jede in JavaScript geschriebene Anwendung letztendlich in JavaScript geschrieben wird, haben einige Kiwix-Entwickler vor etwa 10 Jahren sich damit befasst, die Kiwix-Software von C++ in JavaScript zu übertragen.

Die erste Version dieses Ports, Kiwix HTML5, wurde für das mittlerweile ausgediente Firefox und für Browsererweiterungen verwendet. Im Kern war (und ist) eine C++-Dekomprimierungs-Engine (XZ und ZSTD), die mit dem Emscripten-Compiler in die JavaScript-Zwischensprache ASM.js und später Wasm oder WebAssembly kompiliert wurde. Später umbenannte sie in Kiwix JS. Die Browsererweiterungen werden noch aktiv weiterentwickelt.

Kiwix JS-Offline-Browser

Geben Sie die Progressive Web-App (PWA) ein. Die Kiwix-Entwickler nutzten das Potenzial dieser Technologie und erstellten eine dedizierte PWA-Version von Kiwix JS. Außerdem erstellten sie Betriebssystemintegrationen, mit denen die App native Funktionen anbieten kann, insbesondere in den Bereichen Offlinenutzung, Installation, Dateiverarbeitung und Dateisystemzugriff.

Offline-First-PWAs sind extrem schlank und eignen sich daher perfekt für Situationen, in denen zeitweise oder teures mobiles Internet verfügbar ist. Die Technologie dahinter ist die Service Worker API und die zugehörige Cache API, die von allen auf Kiwix JS basierenden Anwendungen verwendet werden. Mit diesen APIs können die Anwendungen als Server agieren, Abrufanfragen vom angezeigten Hauptdokument oder -artikel abfangen und an das JS-Back-End weiterleiten, um eine Antwort aus dem ZIM-Archiv zu extrahieren und zu erstellen.

Sicherer Speicher – überall

Angesichts der Größe der ZIM-Archive, des Speicherplatzes und des Zugriffs darauf, insbesondere auf Mobilgeräten, sind Kiwix-Entwickler wahrscheinlich die größten Probleme. Viele Kiwix-Endnutzer laden Inhalte zur späteren Offlinenutzung in die App herunter, wenn eine Internetverbindung verfügbar ist. Andere Nutzer laden Inhalte mit einem Torrent auf einen PC herunter und übertragen sie dann auf ein Mobilgerät oder Tablet. Andere Nutzer tauschen Inhalte auf USB-Sticks oder tragbaren Festplatten in Bereichen mit defektem oder teurem mobilen Internet aus. Alle diese Möglichkeiten, auf Inhalte von beliebigen, für Nutzer zugänglichen Standorten zuzugreifen, müssen von Kiwix JS und Kiwix PWA unterstützt werden.

Anfänglich konnte Kiwix JS riesige Archive von Hunderten GB (eines unserer ZIM-Archive mit 166 GB!) selbst auf Geräten mit wenig Arbeitsspeicher lesen, nämlich die File API. Diese API wird universell in jedem Browser unterstützt, auch in sehr alten Browsern, und fungiert daher als universelles Fallback, wenn neuere APIs nicht unterstützt werden. Es ist so einfach wie das Definieren eines input-Elements in HTML in Kiwixs Fall:

<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, bei denen es sich im Wesentlichen um Metadaten handelt, die auf die zugrunde liegenden Daten im Speicher verweisen. Technisch gesehen liest das objektorientierte Back-End von Kiwix, das in reinem clientseitigem JavaScript geschrieben ist, nach Bedarf kleine Teile des großen Archivs. Wenn diese Slices dekomprimiert werden müssen, übergibt das Back-End sie an den Wasm-Dekomprimierungor und erhält auf Anfrage weitere Slices, 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 eingelesen werden muss.

Die File API hat einen Nachteil, der Kiwix JS-Apps im Vergleich zu nativen Apps umständlich und altmodisch erscheint: Der Nutzer muss Archive über die Dateiauswahl auswählen oder bei jedem Start der App eine Datei per Drag-and-drop in die App einfügen, da es mit dieser API keine Möglichkeit gibt, Zugriffsberechtigungen von einer Sitzung zur nächsten beizubehalten.

Wie viele Entwickler haben die Kiwix JS-Entwickler daher zuerst den Elektron-Pfad verwendet, um diese schlechte Benutzeroberfläche abzuschwächen. ElectronJS ist ein herausragendes Framework, das leistungsstarke Funktionen bietet, einschließlich des vollständigen Zugriffs auf das Dateisystem mithilfe von Node APIs. Es hat jedoch einige bekannte Nachteile:

  • Sie läuft nur auf Desktop-Betriebssystemen.
  • Es ist groß und umfangreich (70 MB–100 MB).

Da jede App eine vollständige Kopie von Chromium enthält, ist die Größe der Elektron-Apps im Vergleich zu nur 5, 1 MB für die minimierte und gebündelte PWA sehr negativ.

Konnte Kiwix also die Situation für die Nutzenden der PWA verbessern?

Die File System Access API kommt hier zu Hilfe.

Kiwix wurde etwa 2019 auf eine neue API aufmerksam, die in Chrome 78 getestet wurde und dann als Native File System API hieß. Es wurde die Möglichkeit geboten, ein Datei-Handle für eine Datei oder einen Ordner zu erhalten und diese in einer IndexedDB-Datenbank zu speichern. Entscheidend ist, dass dieser Handle zwischen den App-Sitzungen bestehen bleibt, sodass der Nutzer die Datei oder den Ordner beim Neustart der App nicht noch einmal auswählen muss (obwohl er auf eine schnelle Berechtigungsaufforderung antworten muss). Als das Produkt in der Produktion war, hatte es den Namen File System Access API und die Kernteile, die von der WASWG standardisiert wurden, als File System API (FSA) standardisiert.

Wie funktioniert also der Dateisystemzugriff der API? Ein paar wichtige Hinweise:

  • Es ist eine asynchrone API (außer bei speziellen Funktionen in Web Workers).
  • Die Datei- oder Verzeichnisauswahl muss programmatisch durch Erfassung einer Nutzergeste (auf ein UI-Element geklickt oder getippt) gestartet werden.
  • 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 weigert sich tatsächlich, die Berechtigungsaufforderung anzuzeigen, wenn sie nicht durch eine Nutzergeste ausgelöst wird.

Der Code ist relativ einfach, abgesehen davon, dass zum Speichern von Datei- und Verzeichnis-Handles die umständliche IndexedDB API verwendet werden muss. Die gute Nachricht ist, dass es einige Bibliotheken gibt, die einen Großteil der Arbeit für Sie übernehmen, z. B. browser-fs-access. Wir bei Kiwix JS haben 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 wird Promises verwendet. Wenn Sie async/await-Zucker bevorzugen, lesen Sie die Anleitung zu Chrome für Entwickler):

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 mit diesem Code nur die erste ausgewählte Datei verarbeitet. Es kann auch nicht mehr als eine Datei ausgewählt werden. Wenn Sie die Auswahl mehrerer Dateien mit { multiple: true } zulassen möchten, fassen Sie einfach alle Promise-Objekte, die jeden Handle verarbeiten, in einer Promise.all().then(...)-Anweisung zusammen. Beispiel:

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, mehrere Dateien auszuwählen, wenn Sie den Nutzer bitten, das Verzeichnis auszuwählen, das diese Dateien enthält, und nicht die einzelnen Dateien darin. Insbesondere da Kiwix-Nutzer tendenziell alle ihre ZIM-Dateien im selben Verzeichnis organisieren. Der Code zum Starten der Verzeichnisauswahl ist fast der gleiche wie oben, nur dass Sie window.showDirectoryPicker.then(function (dirHandle) { … }); verwenden.

Datei- oder Verzeichnis-Handle verarbeiten

Sobald 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 Datei-Handles bereitstellen. Hierfür gibt es keine praktischen Methoden, außer 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 von Handles von Dateien oder Ordnern verwendet wird.

Die Verarbeitung von Verzeichnissen ist 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 später für jeden Eintrag im entryList die Datei mit entry.getFile().then(function (file) { … }) abrufen müssen, wenn Sie sie benötigen, oder die Entsprechung, indem Sie const file = await entry.getFile() in einem async function verwenden.

Können wir noch mehr gehen?

Wenn der Nutzer bei nachfolgenden App-Starts mit einer Nutzerbewegung Berechtigungen erteilen muss, gestaltet sich das (erneute) Öffnen von Dateien und Ordnern etwas komplizierter. Es ist jedoch viel flüssiger, als eine Datei noch einmal auswählen zu müssen. Chromium-Entwickler finalisieren Code, der dauerhafte Berechtigungen für installierte PWAs ermöglicht. Dies ist etwas, das viele PWA-Entwickler gefordert haben und das sehr erwartet wird.

Aber was ist, wenn wir nicht warten müssen?! Kiwix-Entwickler haben kürzlich festgestellt, dass es derzeit möglich ist, alle Berechtigungsaufforderungen zu entfernen, indem eine nagelneue Funktion der File Access API verwendet wird, die sowohl von Chromium als auch von Firefox unterstützt wird und teilweise von Safari unterstützt wird, aber immer noch FileSystemWritableFileStream fehlt. Diese neue Funktion heißt Origin Private File System.

Komplett 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 darauf, Nutzer zu ermutigen, es auszuprobieren, da es weitgehend die Lücke zwischen nativen Apps und Webanwendungen schließt. Dies sind die wichtigsten Vorteile:

  • Auf Archive im OPFS kann ohne Berechtigungsaufforderung zugegriffen werden, auch beim Start. Nutzer können ohne Probleme mit dem Lesen eines Artikels und dem Durchsuchen eines Archivs an der Stelle fortfahren, an der sie in der vorherigen Sitzung aufgehört haben.
  • Sie ermöglicht einen optimierten Zugriff auf die darin gespeicherten Dateien: Unter Android konnten wir die Geschwindigkeit zwischen fünf und zehnmal schneller verbessern.

Der Standardzugriff auf Dateien in Android mit der File API ist extrem langsam, insbesondere (wie das häufig bei Kiwix-Nutzern der Fall ist), wenn große Archive auf einer microSD-Karte statt im Gerätespeicher gespeichert sind. Mit dieser neuen API ändert sich das. Die meisten Nutzer können keine 97-GB-Datei im OPFS speichern, das Gerätespeicher und nicht microSD-Kartenspeicher belegt, ist jedoch perfekt für kleine bis mittelgroße Archive geeignet. Sie möchten die vollständige medizinische Enzyklopädie von WikiProject Medicine? Kein Problem, mit 1,7 GB passt es problemlos in das OPFS! (Tipp: Suchen Sie nach othermdwiki_en_all_maxi in der In-App-Bibliothek.)

So funktioniert das OPFS

Das OPFS ist ein vom Browser bereitgestelltes Dateisystem, das für jeden Ursprung getrennt ist und dem App-bezogenen Speicher unter Android ähnelt. Dateien können aus dem für den Nutzer sichtbaren Dateisystem in das OPFS importiert oder direkt dort heruntergeladen werden (die API ermöglicht auch das Erstellen von Dateien im OPFS). Im OPFS sind sie vom Rest des Geräts isoliert. In Chromium-basierten Browsern auf dem Computer ist es auch möglich, Dateien aus dem OPFS-Dateisystem zurück in das für den Nutzer sichtbare Dateisystem zu exportieren.

Wenn Sie das OPFS verwenden möchten, fordern Sie zuerst mit navigator.storage.getDirectory() Zugriff darauf an. 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 daraus erhalten, ist der gleiche FileSystemDirectoryHandle-Typ, den Sie von window.showDirectoryPicker() oben erwähnt haben. Sie können also den Code wiederverwenden, der das verarbeitet. Sie müssen sie also nicht in indexedDB speichern. Rufen Sie sie einfach ab, wenn Sie sie benötigen. Angenommen, Sie haben bereits einige Dateien im OPFS und möchten diese verwenden. Dann können Sie mit der zuvor gezeigten Funktion iterateAsyncDirEntries() so vorgehen:

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

Sie müssen getFile() trotzdem für jeden Eintrag aus dem archiveList-Array verwenden, den Sie verwenden möchten.

Dateien in das OPFS importieren

Wie gelangen Dateien überhaupt in das OPFS? Nicht so schnell! Zuerst müssen Sie den Speicherplatz schätzen, mit dem Sie arbeiten müssen. Nutzer sollten nicht versuchen, eine Datei mit 97 GB Speicherplatz zu speichern, wenn diese nicht passt.

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

Bereich mit dem verwendeten Speicher in Prozent und dem verbleibenden verfügbaren Speicher in Gigabyte.

Der Bereich wird mit estimate.quota und estimate.usage ausgefüllt. Beispiel:

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 den Nutzer sichtbaren Dateisystem hinzufügen können. Die gute Nachricht: Sie können einfach die File API verwenden, um das erforderliche Dateiobjekt (oder die erforderlichen Objekte) 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 Legacy-Dateiauswahl, aber click()eine versteckte Legacy-Auswahl (<input type="file" multiple … />-Element), wenn darauf geklickt oder getippt wird. Die Anwendung erfasst dann nur das change-Ereignis der versteckten 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 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 privaten Dateisystem des Ursprungs eine Liste von Zim-Dateien hinzufügen möchte.

Da das Importieren von Archiven bei einigen Betriebssystemen wie Android nicht die schnellste Methode ist, zeigt Kiwix während des Imports der Archive auch ein Banner und ein kleines rotierendes Ladesymbol an. Das Team hat für diesen Vorgang keinen Fortschrittsindikator hinzugefügt: Antworten bitte auf einer Postkarte!

Wie hat Kiwix die Funktion importOPFSEntries() 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 Promises hier aus Gründen im Zusammenhang mit unserer Legacy-Codebasis, es muss jedoch erwähnt werden, dass await in diesem Fall eine einfachere Syntax erzeugt und die Doom-Effekt-Pyramide 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 einen Verzeichnis-Handle haben (d. h. Verzeichnisse, die mit window.showDirectoryPicker() ausgewählt wurden). Dabei werden die gleichen Prinzipien wie der obige Code verwendet, aber es wird ein Response erstellt, der aus einer ReadableStream und einem Controller besteht, der die aus der Remote-Datei gelesenen Byte in die Warteschlange stellt. Die resultierende Response.body wird dann an den Autor der neuen Datei im OPFS übergeleitet.

In diesem Fall kann Kiwix die Byte zählen, die den ReadableStream durchlaufen, und dem Nutzer eine Fortschrittsanzeige zur Verfügung stellen. Außerdem kann Kiwix den Nutzer warnen, die App während des Downloads nicht zu beenden. Der Code ist etwas zu komplex, um hier angezeigt zu werden. Da es sich bei unserer Anwendung jedoch um eine FOSS-Anwendung handelt, können Sie sich die Quelle ansehen, wenn Sie Ähnliches tun möchten. So sieht die Kiwix-Benutzeroberfläche aus (die unterschiedlichen Fortschrittswerte, die unten dargestellt sind, sind darauf zurückzuführen, dass das Banner nur aktualisiert wird, wenn sich der Prozentsatz ändert, aber der Bereich Downloadfortschritt regelmäßiger aktualisiert wird):

Kiwix-Benutzeroberfläche mit einer Leiste am unteren Rand, in der der Nutzer darauf gewarnt wird, die App nicht zu schließen, und der Download-Fortschritt des Zim-Archivs wird angezeigt.

Da der Download-Vorgang ziemlich lang sein kann, ermöglicht Kiwix den Nutzern, die App während des Vorgangs frei zu verwenden. Dabei wird aber immer sichergestellt, dass das Banner angezeigt wird, sodass Nutzer daran erinnert werden, die App erst zu schließen, wenn der Download-Vorgang abgeschlossen ist.

Implementierung eines Minidateimanagers in der App

Den Entwicklern von Kiwix PWAs wurde klar, dass es nicht ausreicht, Dateien zum OPFS hinzuzufügen. Die App musste den Nutzern auch die Möglichkeit geben, nicht mehr benötigte Dateien aus diesem Speicherbereich zu löschen und idealerweise auch alle im OPFS gesperrten Dateien zurück in das für den Nutzer sichtbare Dateisystem zu exportieren. Es war notwendig, innerhalb der Anwendung ein Mini-Dateiverwaltungssystem zu implementieren.

Ein kurzer Hinweis auf die fantastische Erweiterung OPFS Explorer für Chrome (sie funktioniert auch in Edge). Es wird ein Tab in den Entwicklertools hinzugefügt, mit dem Sie genau sehen können, was sich im OPFS befindet, und auch fehlerhafte oder fehlerhafte Dateien löschen können. Es war von unschätzbarem Wert, um zu überprüfen, ob der Code funktionierte, das Verhalten von Downloads zu überwachen und unsere Entwicklungsexperimente allgemein zu bereinigen.

Der Dateiexport hängt davon ab, dass ein Datei-Handle für eine ausgewählte Datei oder ein ausgewähltes Verzeichnis, in dem Kiwix die exportierte Datei speichert, abgerufen werden kann. Dies funktioniert daher nur in Kontexten, in denen die Methode window.showSaveFilePicker() verwendet werden kann. Wären die Kiwix-Dateien kleiner als mehrere GB, könnten wir ein Blob im Arbeitsspeicher erstellen, ihm eine URL zuweisen und es dann in das für den Nutzer sichtbare Dateisystem herunterladen. Leider ist das bei so großen Archiven nicht möglich. Wenn dies unterstützt wird, ist der Export ziemlich einfach: praktisch genauso, in umgekehrter Richtung wie beim Speichern einer Datei im OPFS (ein Handle für die zu speichernde Datei abrufen, den Nutzer bitten, einen Speicherort mit window.showSaveFilePicker() auszuwählen und dann createWriteable() für saveHandle zu verwenden). Den Code findest du im Repository.

Das Löschen von Dateien wird von allen Browsern unterstützt und ist mit einer einfachen dirHandle.removeEntry('filename') möglich. Im Fall von Kiwix haben wir es vorgezogen, die OPFS-Einträge wie oben beschrieben zu iterieren, um zuerst zu prüfen, ob die ausgewählte Datei vorhanden ist, und um eine Bestätigung zu bitten. Dies ist jedoch nicht für alle erforderlich. Auch hier können Sie bei Interesse unseren Code prüfen.

Dabei wurde beschlossen, die Kiwix-Benutzeroberfläche nicht mit Schaltflächen zu überladen, die diese Optionen bieten, sondern kleine Symbole direkt unter der Archivliste zu platzieren. Durch Tippen auf eines dieser Symbole ändert sich die Farbe der Archivliste. Dies ist ein visueller Hinweis für den Nutzer, was er tun wird. Der Nutzer klickt oder tippt dann auf eines der Archive und der entsprechende Vorgang (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 noch eine Screencast-Demo aller oben behandelten Dateiverwaltungsfunktionen: Eine Datei zum OPFS hinzufügen, eine Datei direkt in sie herunterladen, eine Datei löschen und in das für den Nutzer sichtbare Dateisystem exportieren.

Die Arbeit eines Entwickelnden ist nie erledigt

Das OPFS ist eine großartige Innovation für Entwickler von PWAs und bietet leistungsstarke Dateiverwaltungsfunktionen, die die Lücke zwischen nativen Apps und Webanwendungen nachhaltig schließen. Aber die Entwickler sind ein unglückseliges Haufen – sie sind nie ganz zufrieden! Das OPFS ist fast perfekt, aber nicht ganz... Es ist gut, dass die Hauptfunktionen sowohl in Chromium als auch in Firefox funktionieren und sowohl auf Android- als auch auf Desktop-Computern implementiert werden. Wir hoffen, dass alle Funktionen bald auch in Safari und iOS verfügbar sein werden. Die folgenden Probleme bestehen weiterhin:

  • In Firefox gilt aktuell eine Obergrenze von 10 GB für das OPFS-Kontingent, unabhängig davon, wie viel Speicherplatz zugrunde liegt. Für die meisten PWA-Autoren ist dies vielleicht nur wenig, für Kiwix ist dies jedoch recht restriktiv. Zum Glück sind Chromium-Browser viel großzügiger.
  • Es ist derzeit nicht möglich, große Dateien aus dem OPFS in das für Nutzer sichtbare Dateisystem in mobilen Browsern oder in Desktop-Firefox zu exportieren, da window.showSaveFilePicker() nicht implementiert ist. In diesen Browsern werden große Dateien effektiv im OPFS erfasst. Dies widerspricht dem Kiwix-Ethos des offenen Zugriffs auf Inhalte und der Möglichkeit, 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 verbraucht. Dies ist besonders auf Mobilgeräten problematisch, auf denen Nutzer zwar viel Speicherplatz auf einer microSD-Karte haben, aber nur sehr wenig Speicherplatz.

Insgesamt sind dies jedoch kleine Schwächen bei dem, was beim Dateizugriff in PWAs sonst ein großer Fortschritt wäre. Das PWA-Team von Kiwix ist den Chromium-Entwicklern und Verfechtern, die die File System Access API vorgeschlagen und entworfen haben, sehr dankbar. Außerdem ist es für die harte Arbeit, den Konsens zwischen den Browseranbietern zu erreichen, über die Bedeutung des Origin Private File System, zu erreichen. Bei Kiwix JS PWA wurden damit viele UX-Probleme gelöst, die in der Vergangenheit Probleme mit der App hatten. Dies hilft uns bei unserem Bestreben, die Zugänglichkeit von Kiwix-Inhalten für alle zu verbessern. Probieren Sie doch mal die Kiwix-PWA aus und sagen Sie den Entwicklern Ihre Meinung dazu.

Auf diesen Websites finden Sie nützliche Informationen zu den Funktionen von PWAs: