Das private Dateisystem des Ursprungs

Der Dateisystemstandard führt ein origin privates Dateisystem (OPFS) als Speicherendpunkt ein, der für den Ursprung der Seite privat und für den Nutzer nicht sichtbar ist. Es bietet optionalen Zugriff auf eine spezielle Art von Datei, die für eine hohe Leistung optimiert ist.

Unterstützte Browser

Das private Dateisystem des Ursprungs wird von modernen Browsern unterstützt und von der Web Hypertext Application Technology Working Group (WHATWG) im File System Living Standard standardisiert.

Unterstützte Browser

  • Chrome: 86.
  • Edge: 86.
  • Firefox: 111.
  • Safari: 15.2.

Quelle

Motivation

Wenn Sie an Dateien auf Ihrem Computer denken, denken Sie wahrscheinlich an eine Dateihierarchie: Dateien, die in Ordnern organisiert sind, die Sie mit dem Datei-Explorer Ihres Betriebssystems durchsuchen können. Unter Windows könnte sich die To-do-Liste eines Nutzers namens Tom beispielsweise unter C:\Users\Tom\Documents\ToDo.txt befinden. In diesem Beispiel ist ToDo.txt der Dateiname und Users, Tom und Documents sind Ordnernamen. „C:“ steht unter Windows für das Stammverzeichnis des Laufwerks.

Traditionelle Arbeitsweise mit Dateien im Web

So bearbeiten Sie die To-do-Liste in einer Webanwendung:

  1. Der Nutzer lädt die Datei auf einen Server hoch oder öffnet sie mit <input type="file"> auf dem Client.
  2. Der Nutzer nimmt die Änderungen vor und lädt die resultierende Datei mit einem eingefügten <a download="ToDo.txt> herunter, das Sie programmatisch click() über JavaScript eingefügt haben.
  3. Zum Öffnen von Ordnern verwenden Sie ein spezielles Attribut in <input type="file" webkitdirectory>, das trotz seines proprietären Namens praktisch universell von Browsern unterstützt wird.

Moderne Arbeitsweise mit Dateien im Web

Dieser Ablauf entspricht nicht der Vorstellung der Nutzer von der Bearbeitung von Dateien und führt dazu, dass Nutzer heruntergeladene Kopien ihrer Eingabedateien erhalten. Daher wurden in der File System Access API drei Auswahlmethoden eingeführt: showOpenFilePicker(), showSaveFilePicker() und showDirectoryPicker(). Sie tun genau das, was ihr Name vermuten lässt. Sie ermöglichen einen Ablauf wie hier beschrieben:

  1. Öffnen Sie ToDo.txt mit showOpenFilePicker() und rufen Sie ein FileSystemFileHandle-Objekt ab.
  2. Rufen Sie die Methode getFile() des Dateihandles auf, um ein File-Objekt aus dem FileSystemFileHandle-Objekt abzurufen.
  3. Ändern Sie die Datei und rufen Sie dann requestPermission({mode: 'readwrite'}) auf den Handle auf.
  4. Wenn der Nutzer die Berechtigungsanfrage akzeptiert, speichern Sie die Änderungen in der Originaldatei.
  5. Alternativ können Sie showSaveFilePicker() aufrufen und den Nutzer eine neue Datei auswählen lassen. Wenn der Nutzer eine zuvor geöffnete Datei auswählt, wird ihr Inhalt überschrieben. Bei wiederholtem Speichern können Sie den Dateihandle beibehalten, damit das Dialogfeld zum Speichern der Datei nicht noch einmal angezeigt wird.

Einschränkungen bei der Arbeit mit Dateien im Web

Dateien und Ordner, auf die über diese Methoden zugegriffen werden kann, befinden sich im sogenannten nutzersichtbaren Dateisystem. Aus dem Web gespeicherte Dateien, insbesondere ausführbare Dateien, sind mit dem Websymbol gekennzeichnet. Das Betriebssystem kann also eine zusätzliche Warnung anzeigen, bevor eine potenziell gefährliche Datei ausgeführt wird. Als zusätzliche Sicherheitsfunktion werden Dateien, die aus dem Web stammen, auch durch Safe Browsing geschützt. Sie können sich das im Rahmen dieses Artikels zweckmäßigerweise als cloudbasierten Virenscan vorstellen. Wenn Sie Daten mit der File System Access API in eine Datei schreiben, erfolgt dies nicht an Ort und Stelle, sondern mithilfe einer temporären Datei. Die Datei selbst wird nur dann geändert, wenn sie alle diese Sicherheitsprüfungen besteht. Wie Sie sich vorstellen können, führt diese Arbeit dazu, dass Dateivorgänge relativ langsam sind, obwohl nach Möglichkeit Verbesserungen vorgenommen werden, z. B. unter macOS. Dennoch ist jeder write()-Aufruf in sich geschlossen. Im Hintergrund wird also die Datei geöffnet, zum angegebenen Offset gesprungen und schließlich werden Daten geschrieben.

Dateien als Grundlage der Verarbeitung

Gleichzeitig sind Dateien eine hervorragende Möglichkeit, Daten zu erfassen. SQLite speichert beispielsweise ganze Datenbanken in einer einzigen Datei. Ein weiteres Beispiel sind Mipmaps, die in der Bildverarbeitung verwendet werden. Mipmaps sind vorab berechnete, optimierte Bildsequenzen, von denen jede eine Darstellung mit zunehmend niedrigerer Auflösung des vorherigen Bildes ist. Dadurch werden viele Vorgänge wie das Zoomen beschleunigt. Wie können Webanwendungen also die Vorteile von Dateien nutzen, ohne die Leistungskosten der webbasierten Dateiverarbeitung? Die Antwort lautet ursprüngliches privates Dateisystem.

Das für den Nutzer sichtbare und das private Dateisystem des Ursprungs

Im Gegensatz zum nutzersichtbaren Dateisystem, das über den Datei-Explorer des Betriebssystems aufgerufen wird und dessen Dateien und Ordner gelesen, geschrieben, verschoben und umbenannt werden können, ist das ursprüngliche private Dateisystem nicht für Nutzer gedacht. Dateien und Ordner im privaten Dateisystem des Ursprungs sind, wie der Name schon sagt, privat, genauer gesagt für den Ursprung einer Website. Geben Sie in der DevTools-Konsole location.origin ein, um den Ursprung einer Seite zu ermitteln. Beispiel: Der Ursprung der Seite https://developer.chrome.com/articles/ ist https://developer.chrome.com. Der Teil /articles ist also nicht Teil des Ursprungs. Weitere Informationen zur Theorie der Ursprünge finden Sie unter „Auf derselben Website“ und „Mit gleicher Herkunft“. Alle Seiten mit demselben Ursprung können dieselben privaten Dateisystemdaten des Ursprungs sehen. Daher kann https://developer.chrome.com/docs/extensions/mv3/getstarted/extensions-101/ dieselben Details wie im vorherigen Beispiel sehen. Jede Quelle hat ein eigenes unabhängiges privates Dateisystem. Das bedeutet, dass das private Dateisystem der Quelle https://developer.chrome.com sich vollständig von dem von https://web.dev unterscheidet. Unter Windows ist das Stammverzeichnis des für Nutzer sichtbaren Dateisystems C:\\. Das Äquivalent für das private Dateisystem der Quelle ist ein ursprünglich leeres Stammverzeichnis pro Quelle, auf das über den Aufruf der asynchronen Methode navigator.storage.getDirectory() zugegriffen wird. Im folgenden Diagramm wird das nutzersichtbare Dateisystem mit dem privaten Dateisystem des Ursprungs verglichen. Das Diagramm zeigt, dass abgesehen vom Stammverzeichnis alles andere konzeptionell gleich ist. Es gibt eine Hierarchie von Dateien und Ordnern, die Sie nach Bedarf für Ihre Daten- und Speicheranforderungen organisieren und anordnen können.

Diagramm des für Nutzer sichtbaren Dateisystems und des ursprünglichen privaten Dateisystems mit zwei Beispieldateihierarchien. Der Einstiegspunkt für das für den Nutzer sichtbare Dateisystem ist eine symbolische Festplatte. Der Einstiegspunkt für das private Quelldateisystem ist der Aufruf der Methode „navigator.storage.getDirectory“.

Details zum privaten Quelldateisystem

Wie andere Speichermechanismen im Browser (z. B. localStorage oder IndexedDB) unterliegt auch das private Dateisystem des Ursprungs den Browserkontingentbeschränkungen. Wenn ein Nutzer alle Browserdaten oder alle Websitedaten löscht, wird auch das ursprüngliche private Dateisystem gelöscht. Rufen Sie navigator.storage.estimate() auf und sehen Sie sich im resultierenden Antwortobjekt den Eintrag usage an, um zu sehen, wie viel Speicherplatz Ihre App bereits belegt. Dieser wird im Objekt usageDetails nach Speichermechanismus aufgeschlüsselt. Sehen Sie sich dabei insbesondere den Eintrag fileSystem an. Da das private Dateisystem des Ursprungs für den Nutzer nicht sichtbar ist, werden keine Berechtigungsanfragen und keine Safe Browsing-Prüfungen angezeigt.

Zugriff auf das Stammverzeichnis erhalten

Führen Sie den folgenden Befehl aus, um Zugriff auf das Stammverzeichnis zu erhalten. Sie erhalten ein leeres Verzeichnis-Handle, genauer gesagt ein FileSystemDirectoryHandle.

const opfsRoot = await navigator.storage.getDirectory();
// A FileSystemDirectoryHandle whose type is "directory"
// and whose name is "".
console.log(opfsRoot);

Hauptthread oder Web Worker

Es gibt zwei Möglichkeiten, das private Dateisystem des Ursprungs zu verwenden: im Hauptthread oder in einem Webworker. Webworker können den Hauptthread nicht blockieren. Das bedeutet, dass APIs in diesem Kontext synchron sein können, was im Hauptthread normalerweise nicht zulässig ist. Synchrone APIs können schneller sein, da keine Zusagen verarbeitet werden müssen. Dateivorgänge sind in Sprachen wie C, die in WebAssembly kompiliert werden können, in der Regel synchron.

// This is synchronous C code.
FILE *f;
f = fopen("example.txt", "w+");
fputs("Some text\n", f);
fclose(f);

Wenn Sie die schnellstmöglichen Dateivorgänge benötigen oder mit WebAssembly arbeiten, fahren Sie mit Privates Dateisystem des Ursprungs in einem Webworker verwenden fort. Andernfalls können Sie weiterlesen.

Origin-privates Dateisystem im Hauptthread verwenden

Neue Dateien und Ordner erstellen

Nachdem Sie einen Stammordner erstellt haben, können Sie mit den Methoden getFileHandle() und getDirectoryHandle() Dateien und Ordner erstellen. Wenn Sie {create: true} übergeben, wird die Datei oder der Ordner erstellt, falls sie bzw. er noch nicht vorhanden ist. Erstellen Sie eine Dateihierarchie, indem Sie diese Funktionen mit einem neu erstellten Verzeichnis als Ausgangspunkt aufrufen.

const fileHandle = await opfsRoot
    .getFileHandle('my first file', {create: true});
const directoryHandle = await opfsRoot
    .getDirectoryHandle('my first folder', {create: true});
const nestedFileHandle = await directoryHandle
    .getFileHandle('my first nested file', {create: true});
const nestedDirectoryHandle = await directoryHandle
    .getDirectoryHandle('my first nested folder', {create: true});

Die resultierende Dateihierarchie aus dem vorherigen Codebeispiel.

Auf vorhandene Dateien und Ordner zugreifen

Wenn Sie den Namen kennen, rufen Sie die Methoden getFileHandle() oder getDirectoryHandle() auf und geben Sie den Namen der Datei oder des Ordners an, um auf zuvor erstellte Dateien und Ordner zuzugreifen.

const existingFileHandle = await opfsRoot.getFileHandle('my first file');
const existingDirectoryHandle = await opfsRoot
    .getDirectoryHandle('my first folder');

Datei abrufen, die mit einem Dateihandle zum Lesen verknüpft ist

Ein FileSystemFileHandle steht für eine Datei im Dateisystem. Mit der Methode getFile() können Sie die zugehörige File abrufen. Ein File-Objekt ist eine spezielle Art von Blob und kann in jedem Kontext verwendet werden, in dem auch ein Blob verwendet werden kann. Insbesondere werden in FileReader, URL.createObjectURL(), createImageBitmap() und XMLHttpRequest.send() sowohl Blobs als auch Files akzeptiert. Wenn Sie eine File aus einer FileSystemFileHandle abrufen, werden die Daten „freigegeben“, sodass Sie darauf zugreifen und sie für das sichtbare Dateisystem des Nutzers verfügbar machen können.

const file = await fileHandle.getFile();
console.log(await file.text());

Durch Streaming in eine Datei schreiben

Daten in eine Datei streamen, indem createWritable() aufgerufen wird. Dadurch wird eine FileSystemWritableFileStream erstellt, in die der Inhalt dann write() wird. Am Ende musst du den Stream close().

const contents = 'Some text';
// Get a writable stream.
const writable = await fileHandle.createWritable();
// Write the contents of the file to the stream.
await writable.write(contents);
// Close the stream, which persists the contents.
await writable.close();

Dateien und Ordner löschen

Dateien und Ordner können durch Aufrufen der entsprechenden remove()-Methode des Datei- oder Verzeichnishandles gelöscht werden. Wenn Sie einen Ordner einschließlich aller Unterordner löschen möchten, übergeben Sie die Option {recursive: true}.

await fileHandle.remove();
await directoryHandle.remove({recursive: true});

Wenn Sie den Namen der Datei oder des Ordners kennen, die bzw. der gelöscht werden soll, können Sie auch die Methode removeEntry() verwenden.

directoryHandle.removeEntry('my first nested file');

Dateien und Ordner verschieben und umbenennen

Dateien und Ordner mit der Methode move() umbenennen und verschieben Verschieben und Umbenennen können gleichzeitig oder getrennt voneinander erfolgen.

// Rename a file.
await fileHandle.move('my first renamed file');
// Move a file to another directory.
await fileHandle.move(nestedDirectoryHandle);
// Move a file to another directory and rename it.
await fileHandle
    .move(nestedDirectoryHandle, 'my first renamed and now nested file');

Pfad einer Datei oder eines Ordners auflösen

Wenn Sie wissen möchten, wo sich eine bestimmte Datei oder ein bestimmter Ordner im Verhältnis zu einem Referenzverzeichnis befindet, verwenden Sie die Methode resolve() und übergeben Sie ihr ein FileSystemHandle als Argument. Wenn Sie den vollständigen Pfad einer Datei oder eines Ordners im ursprünglichen privaten Dateisystem abrufen möchten, verwenden Sie das Stammverzeichnis als Referenzverzeichnis, das über navigator.storage.getDirectory() abgerufen wurde.

const relativePath = await opfsRoot.resolve(nestedDirectoryHandle);
// `relativePath` is `['my first folder', 'my first nested folder']`.

Prüfen, ob zwei Datei- oder Ordner-Handle auf dieselbe Datei oder denselben Ordner verweisen

Manchmal haben Sie zwei Handles und wissen nicht, ob sie auf dieselbe Datei oder denselben Ordner verweisen. Verwenden Sie die Methode isSameEntry(), um dies zu prüfen.

fileHandle.isSameEntry(nestedFileHandle);
// Returns `false`.

Inhalt eines Ordners auflisten

FileSystemDirectoryHandle ist ein asynchroner Iterator, über den Sie mit einer for await…of-Schleife iterieren. Als asynchroner Iterator unterstützt er auch die Methoden entries(), values() und keys(). Sie können je nach Bedarf eine davon auswählen:

for await (let [name, handle] of directoryHandle) {}
for await (let [name, handle] of directoryHandle.entries()) {}
for await (let handle of directoryHandle.values()) {}
for await (let name of directoryHandle.keys()) {}

Inhalt eines Ordners und aller Unterordner rekursiv auflisten

Bei der Arbeit mit asynchronen Schleifen und Funktionen, die mit Rekursion kombiniert werden, kann es leicht zu Fehlern kommen. Die folgende Funktion kann als Ausgangspunkt für das Auflisten des Inhalts eines Ordners und aller seiner Unterordner dienen, einschließlich aller Dateien und ihrer Größe. Wenn Sie die Dateigrößen nicht benötigen, können Sie die Funktion vereinfachen, indem Sie anstelle des handle.getFile()-Versprechens an der Stelle, an der directoryEntryPromises.push steht, direkt das handle-Versprechen senden.

  const getDirectoryEntriesRecursive = async (
    directoryHandle,
    relativePath = '.',
  ) => {
    const fileHandles = [];
    const directoryHandles = [];
    const entries = {};
    // Get an iterator of the files and folders in the directory.
    const directoryIterator = directoryHandle.values();
    const directoryEntryPromises = [];
    for await (const handle of directoryIterator) {
      const nestedPath = `${relativePath}/${handle.name}`;
      if (handle.kind === 'file') {
        fileHandles.push({ handle, nestedPath });
        directoryEntryPromises.push(
          handle.getFile().then((file) => {
            return {
              name: handle.name,
              kind: handle.kind,
              size: file.size,
              type: file.type,
              lastModified: file.lastModified,
              relativePath: nestedPath,
              handle
            };
          }),
        );
      } else if (handle.kind === 'directory') {
        directoryHandles.push({ handle, nestedPath });
        directoryEntryPromises.push(
          (async () => {
            return {
              name: handle.name,
              kind: handle.kind,
              relativePath: nestedPath,
              entries:
                  await getDirectoryEntriesRecursive(handle, nestedPath),
              handle,
            };
          })(),
        );
      }
    }
    const directoryEntries = await Promise.all(directoryEntryPromises);
    directoryEntries.forEach((directoryEntry) => {
      entries[directoryEntry.name] = directoryEntry;
    });
    return entries;
  };

Privates Dateisystem des Ursprungs in einem Webworker verwenden

Wie bereits erwähnt, können Webworker den Hauptthread nicht blockieren. Deshalb sind in diesem Kontext synchrone Methoden zulässig.

Synchronen Zugriffs-Handle abrufen

Der Einstiegspunkt für die schnellstmöglichen Dateivorgänge ist ein FileSystemSyncAccessHandle, der durch Aufrufen von createSyncAccessHandle() aus einem regulären FileSystemFileHandle abgerufen wird.

const fileHandle = await opfsRoot
    .getFileHandle('my highspeed file.txt', {create: true});
const syncAccessHandle = await fileHandle.createSyncAccessHandle();

Synchrone Methoden für In-Place-Dateien

Sobald Sie einen synchronen Zugriffs-Handle haben, erhalten Sie Zugriff auf schnelle In-Place-Dateimethoden, die alle synchron sind.

  • getSize(): Gibt die Größe der Datei in Byte zurück.
  • write(): Schreibt den Inhalt eines Buffers in die Datei, optional an einem bestimmten Offset, und gibt die Anzahl der geschriebenen Byte zurück. Durch die Prüfung der zurückgegebenen Anzahl der geschriebenen Byte können Aufrufer Fehler und teilweise Schreibvorgänge erkennen und behandeln.
  • read(): Liest den Inhalt der Datei in einen Puffer, optional mit einem bestimmten Offset.
  • truncate(): Ändert die Größe der Datei auf die angegebene Größe.
  • flush(): Damit wird sichergestellt, dass der Inhalt der Datei alle über write() vorgenommenen Änderungen enthält.
  • close(): Schließt den Zugriffs-Handle.

Hier ist ein Beispiel, in dem alle oben genannten Methoden verwendet werden.

const opfsRoot = await navigator.storage.getDirectory();
const fileHandle = await opfsRoot.getFileHandle('fast', {create: true});
const accessHandle = await fileHandle.createSyncAccessHandle();

const textEncoder = new TextEncoder();
const textDecoder = new TextDecoder();

// Initialize this variable for the size of the file.
let size;
// The current size of the file, initially `0`.
size = accessHandle.getSize();
// Encode content to write to the file.
const content = textEncoder.encode('Some text');
// Write the content at the beginning of the file.
accessHandle.write(content, {at: size});
// Flush the changes.
accessHandle.flush();
// The current size of the file, now `9` (the length of "Some text").
size = accessHandle.getSize();

// Encode more content to write to the file.
const moreContent = textEncoder.encode('More content');
// Write the content at the end of the file.
accessHandle.write(moreContent, {at: size});
// Flush the changes.
accessHandle.flush();
// The current size of the file, now `21` (the length of
// "Some textMore content").
size = accessHandle.getSize();

// Prepare a data view of the length of the file.
const dataView = new DataView(new ArrayBuffer(size));

// Read the entire file into the data view.
accessHandle.read(dataView);
// Logs `"Some textMore content"`.
console.log(textDecoder.decode(dataView));

// Read starting at offset 9 into the data view.
accessHandle.read(dataView, {at: 9});
// Logs `"More content"`.
console.log(textDecoder.decode(dataView));

// Truncate the file after 4 bytes.
accessHandle.truncate(4);

Dateien aus dem privaten Quelldateisystem in das sichtbare Dateisystem des Nutzers kopieren

Wie bereits erwähnt, ist es nicht möglich, Dateien aus dem privaten Quelldateisystem in das für Nutzer sichtbare Dateisystem zu verschieben. Sie können sie jedoch kopieren. Da showSaveFilePicker() nur im Hauptthread, aber nicht im Worker-Thread verfügbar ist, muss der Code dort ausgeführt werden.

// On the main thread, not in the Worker. This assumes
// `fileHandle` is the `FileSystemFileHandle` you obtained
// the `FileSystemSyncAccessHandle` from in the Worker
// thread. Be sure to close the file in the Worker thread first.
const fileHandle = await opfsRoot.getFileHandle('fast');
try {
  // Obtain a file handle to a new file in the user-visible file system
  // with the same name as the file in the origin private file system.
  const saveHandle = await showSaveFilePicker({
    suggestedName: fileHandle.name || ''
  });
  const writable = await saveHandle.createWritable();
  await writable.write(await fileHandle.getFile());
  await writable.close();
} catch (err) {
  console.error(err.name, err.message);
}

Quell-Dateisystem debuggen

Bis die integrierte DevTools-Unterstützung hinzugefügt wird (siehe crbug/1284595), verwenden Sie die Chrome-Erweiterung OPFS Explorer, um das ursprüngliche private Dateisystem zu debuggen. Der Screenshot oben aus dem Abschnitt Neue Dateien und Ordner erstellen stammt übrigens direkt aus der Erweiterung.

Die Chrome-Entwicklertools-Erweiterung „OPFS Explorer“ im Chrome Web Store.

Öffnen Sie nach der Installation der Erweiterung die Chrome-Entwicklertools und wählen Sie den Tab OPFS Explorer aus. Sie können dann die Dateihierarchie prüfen. Sie können Dateien aus dem ursprünglichen privaten Dateisystem im für Nutzer sichtbaren Dateisystem speichern, indem Sie auf den Dateinamen klicken. Dateien und Ordner lassen sich durch Klicken auf das Papierkorbsymbol löschen.

Demo

Wenn Sie die OPFS Explorer-Erweiterung installieren, können Sie das Origin Private File System in einer Demo in Aktion sehen, in der es als Backend für eine in WebAssembly kompilierte SQLite-Datenbank verwendet wird. Sehen Sie sich den Quellcode auf Glitch an. Beachten Sie, dass in der eingebetteten Version unten das private Dateisystem-Backend des Ursprungs nicht verwendet wird, da der Iframe plattformübergreifend ist. Wenn Sie die Demo jedoch in einem separaten Tab öffnen, wird es verwendet.

Ergebnisse

Das von der WHATWG festgelegte private Dateisystem hat die Art und Weise geprägt, wie wir Dateien im Web verwenden und damit interagieren. Es ermöglicht neue Anwendungsfälle, die mit dem für Nutzer sichtbaren Dateisystem nicht möglich waren. Alle großen Browseranbieter – Apple, Mozilla und Google – sind an Bord und teilen eine gemeinsame Vision. Die Entwicklung des origin-private-Dateisystems ist ein Gemeinschaftsprojekt und Feedback von Entwicklern und Nutzern ist für den Fortschritt unerlässlich. Wir arbeiten kontinuierlich daran, den Standard zu optimieren. Feedback zum whatwg/fs-Repository in Form von Issues oder Pull-Requests ist willkommen.

Danksagungen

Dieser Artikel wurde von Austin Sully, Etienne Noël und Rachel Andrew geprüft. Hero-Image von Christina Rumpf auf Unsplash.