Das private Dateisystem des Ursprungs

Mit dem Dateisystemstandard wird ein privates Dateisystem (Origin Private File System, OPFS) als Speicherendpunkt eingeführt, der nur dem Ursprung der Seite zugeordnet und für den Nutzer nicht sichtbar ist. Er bietet den optionalen Zugriff auf eine spezielle Art von Datei, die hochgradig leistungsoptimiert ist.

Unterstützte Browser

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

Unterstützte Browser

  • 86
  • 86
  • 111
  • 15.2

Quelle

Ziel

Wenn Sie an Dateien auf Ihrem Computer denken, denken Sie wahrscheinlich an eine Dateihierarchie: Dateien, die in Ordnern organisiert sind und die Sie mit dem Datei-Explorer Ihres Betriebssystems erkunden können. Unter Windows könnte sich die To-do-Liste eines Nutzers namens Tom beispielsweise in 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.

Herkömmliche Art der Arbeit mit Dateien im Web

So bearbeiten Sie die Aufgabenliste in einer Webanwendung:

  1. Der Nutzer lädt die Datei auf einen Server hoch oder öffnet sie auf dem Client mit <input type="file">.
  2. Der Nutzer nimmt Änderungen vor und lädt dann die resultierende Datei mit einer eingeschleusten <a download="ToDo.txt> herunter, die du programmatisch über JavaScript click() click() hast.
  3. Zum Öffnen von Ordnern verwenden Sie ein spezielles Attribut in <input type="file" webkitdirectory>, das trotz des proprietären Namens praktisch universelle Browserunterstützung hat.

Moderne Art, mit Dateien im Web zu arbeiten

Dieser Ablauf ist nicht repräsentativ dafür, wie Nutzer Dateien bearbeiten, sondern dass sie am Ende Kopien ihrer Eingabedateien erhalten. Daher wurden für die File System Access API drei Auswahlmethoden eingeführt: showOpenFilePicker(), showSaveFilePicker() und showDirectoryPicker(), die genau das tun, was der Name schon sagt. Sie ermöglichen einen Ablauf:

  1. Öffnen Sie ToDo.txt mit showOpenFilePicker() und rufen Sie ein FileSystemFileHandle-Objekt ab.
  2. Rufen Sie aus dem FileSystemFileHandle-Objekt ein File ab, indem Sie die Methode getFile() des Datei-Handles aufrufen.
  3. Ändere die Datei und rufe dann requestPermission({mode: 'readwrite'}) für den Handle auf.
  4. Wenn der Nutzer die Berechtigungsanfrage annimmt, speichern Sie die Änderungen wieder in der Originaldatei.
  5. Alternativ können Sie showSaveFilePicker() aufrufen und dem Nutzer die Möglichkeit geben, eine neue Datei auszuwählen. Wenn der Nutzer eine zuvor geöffnete Datei auswählt, wird ihr Inhalt überschrieben. Bei wiederholten Speichern können Sie den Handle beibehalten, damit Sie das Dialogfeld zum Speichern der Datei nicht noch einmal aufrufen müssen.

Einschränkungen beim Arbeiten mit Dateien im Web

Dateien und Ordner, auf die über diese Methoden zugegriffen werden kann, befinden sich im sogenannten für Nutzer sichtbaren Dateisystem. Aus dem Web gespeicherte Dateien, insbesondere ausführbare Dateien, sind mit dem Zeichen für das Web gekennzeichnet. Dadurch wird vom Betriebssystem eine zusätzliche Warnung ausgegeben, bevor eine potenziell gefährliche Datei ausgeführt wird. Als zusätzliche Sicherheitsfunktion werden aus dem Web abgerufene Dateien auch durch Safe Browsing geschützt. Der Einfachheit halber und im Kontext dieses Artikels können Sie es sich als einen cloudbasierten Virenscan vorstellen. Wenn Sie Daten mit der File System Access API in eine Datei schreiben, erfolgen die Schreibvorgänge nicht direkt, sondern verwenden eine temporäre Datei. Die Datei selbst wird nur geändert, wenn sie alle diese Sicherheitsprüfungen besteht. Wie Sie sich vorstellen können, werden Dateivorgänge durch diese Arbeit relativ langsam, obwohl Verbesserungen nach Möglichkeit umgesetzt wurden, z. B. unter macOS. Dennoch ist jeder write()-Aufruf eigenständig. Daher wird die Datei im Hintergrund geöffnet, nach dem angegebenen Offset gesucht und schließlich Daten geschrieben.

Dateien als Grundlage für die Verarbeitung

Gleichzeitig sind Dateien eine hervorragende Möglichkeit, Daten aufzuzeichnen. Zum Beispiel speichert SQLite ganze Datenbanken in einer einzigen Datei. Ein weiteres Beispiel sind Mipmaps, die bei der Bildverarbeitung verwendet werden. Mipmaps sind vorab berechnete, optimierte Abfolgen von Bildern, von denen jedes eine zunehmend geringere Auflösung des vorherigen Bilds darstellt, was viele Vorgänge wie das Zoomen beschleunigt. Wie können also Webanwendungen die Vorteile von Dateien nutzen, ohne die Leistungskosten einer webbasierten Dateiverarbeitung zu verschwenden? Die Antwort ist das private Dateisystem des Ursprungs.

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

Im Gegensatz zum für Nutzer sichtbaren Dateisystem, das über den Datei-Explorer des Betriebssystems durchsucht wird, mit Dateien und Ordnern, die Sie lesen, schreiben, verschieben und umbenennen 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 und konkret auf den Ursprung einer Website beschränkt. Du kannst den Ursprung einer Seite ermitteln, indem du in der Entwicklertools-Konsole location.origin eingibst. Der Ursprung der Seite https://developer.chrome.com/articles/ ist beispielsweise https://developer.chrome.com, d. h., Teil /articles ist nicht Teil des Ursprungs. Weitere Informationen zur Theorie der Ursprünge finden Sie unter „same-site“ und „same-origin“ verstehen. Alle Seiten desselben Ursprungs können dieselben privaten Dateisystemdaten sehen, sodass https://developer.chrome.com/docs/extensions/mv3/getstarted/extensions-101/ dieselben Details wie im vorherigen Beispiel sehen kann. Jede Quelle hat ein eigenes, unabhängiges privates Dateisystem. Das bedeutet, dass sich das private Dateisystem von https://developer.chrome.com vollständig von dem System von z. B. https://web.dev unterscheidet. Unter Windows ist das Stammverzeichnis des für den Nutzer sichtbaren Dateisystems C:\\. Das Äquivalent für das private Dateisystem des Ursprungs ist ein anfangs leeres Stammverzeichnis pro Ursprung, auf das durch Aufrufen der asynchronen Methode navigator.storage.getDirectory() zugegriffen wird. Im folgenden Diagramm finden Sie einen Vergleich des für den Nutzer sichtbaren Dateisystems und des privaten Dateisystems. Das Diagramm zeigt, dass abgesehen vom Stammverzeichnis alles andere im Prinzip gleich ist, mit einer Hierarchie von Dateien und Ordnern, die Sie entsprechend Ihren Daten- und Speicheranforderungen organisieren und anordnen können.

Diagramm des für Nutzer sichtbaren Dateisystems und des privaten Ursprungsdateisystems mit zwei beispielhaften Dateihierarchien. Der Einstiegspunkt für das für den Nutzer sichtbare Dateisystem ist eine symbolische Festplatte, der Einstiegspunkt für das ursprüngliche private Dateisystem ruft die Methode „navigator.storage.getDirectory“ auf.

Details des privaten Dateisystems des Ursprungs

Genau wie andere Speichermechanismen im Browser (z. B. localStorage oder IndexedDB) unterliegt das private Dateisystem den Einschränkungen des Browserkontingents. 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 usage-Eintrag an, um zu sehen, wie viel Speicher Ihre App bereits verbraucht. Dieser wird nach Speichermechanismus im Objekt usageDetails aufgeschlüsselt, wo Sie sich speziell den Eintrag fileSystem ansehen möchten. Da das private Dateisystem des Ursprungs für den Nutzer nicht sichtbar ist, gibt es keine Berechtigungsaufforderungen und keine Safe Browsing-Prüfungen.

Zugriff auf das Stammverzeichnis erhalten

Führen Sie den folgenden Befehl aus, um Zugriff auf das Stammverzeichnis zu erhalten. Sie haben am Ende 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 Web Worker. Web Worker können den Hauptthread nicht blockieren. Das bedeutet, dass APIs in diesem Kontext synchron sein können, ein Muster, das für den Hauptthread in der Regel nicht zulässig ist. Synchrone APIs können schneller sein, da sie keine Promise verarbeiten müssen, und Dateivorgänge sind in Sprachen wie C in der Regel synchron, die in WebAssembly kompiliert werden können.

// 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 ursprüngliches Dateisystem in einem Web Worker verwenden fort. Ansonsten kannst du weiterlesen.

Privates Ursprungsdateisystem im Hauptthread verwenden

Neue Dateien und Ordner erstellen

Sobald Sie einen Stammordner haben, erstellen Sie Dateien und Ordner mit den Methoden getFileHandle() bzw. getDirectoryHandle(). Wenn Sie {create: true} übergeben, wird die Datei oder der Ordner erstellt, falls noch nicht vorhanden. Erstellen Sie eine Hierarchie von Dateien, 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 daraus resultierende Dateihierarchie aus dem vorherigen Codebeispiel.

Auf vorhandene Dateien und Ordner zugreifen

Wenn Sie den Namen kennen, können Sie auf zuvor erstellte Dateien und Ordner zugreifen, indem Sie die Methoden getFileHandle() oder getDirectoryHandle() aufrufen und den Namen der Datei oder des Ordners übergeben.

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

Datei mit einem Datei-Handle zum Lesen verknüpfen

Ein FileSystemFileHandle steht für eine Datei im Dateisystem. Verwenden Sie die Methode getFile(), um die zugehörige File abzurufen. Ein File-Objekt ist eine bestimmte Art von Blob und kann in jedem Kontext verwendet werden, so wie das auch mit einem Blob möglich ist. Insbesondere akzeptieren FileReader, URL.createObjectURL(), createImageBitmap() und XMLHttpRequest.send() sowohl Blobs als auch Files. In diesem Fall werden die Daten durch Abrufen eines File von einem FileSystemFileHandle „freigegeben“, damit Sie darauf zugreifen und sie dem für den Nutzer sichtbaren Dateisystem zur Verfügung stellen können.

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

Per Streaming in eine Datei schreiben

Streame Daten in eine Datei, indem du createWritable() aufrufst. Dadurch wird ein FileSystemWritableFileStream erstellt. Anschließend write() wird der Inhalt erstellt. Am Ende müssen Sie 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

Löschen Sie Dateien und Ordner, indem Sie die spezifische remove()-Methode des Datei- oder Verzeichnis-Handles aufrufen. Wenn Sie einen Ordner mit allen Unterordnern löschen möchten, übergeben Sie die Option {recursive: true}.

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

Wenn Sie den Namen der zu löschenden Datei oder des zu löschenden Ordners in einem Verzeichnis kennen, können Sie alternativ die Methode removeEntry() verwenden.

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

Dateien und Ordner verschieben und umbenennen

Mit der Methode move() können Sie Dateien und Ordner umbenennen und verschieben. Das Verschieben und Umbenennen kann zusammen oder isoliert 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 in einem Referenzverzeichnis befindet, verwenden Sie die Methode resolve() und übergeben Sie dabei ein FileSystemHandle als Argument. Verwenden Sie das Stammverzeichnis als Referenzverzeichnis, das Sie über navigator.storage.getDirectory() abrufen, um den vollständigen Pfad einer Datei oder eines Ordners im privaten Dateisystem des Ursprungs zu erhalten.

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

Prüfen, ob zwei Ziehpunkte für Dateien oder Ordner auf dieselbe Datei oder denselben Ordner verweisen

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

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

Inhalte eines Ordners auflisten

FileSystemDirectoryHandle ist ein asynchroner Iterator, den Sie mit einer for await…of-Schleife iterieren. Als asynchroner Iterator unterstützt er auch die Methoden entries(), values() und keys(), aus denen Sie abhängig von den benötigten Informationen wählen können:

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()) {}

Den Inhalt eines Ordners und aller Unterordner rekursiv auflisten

Beim Umgang mit asynchronen Schleifen und Funktionen in Kombination mit Rekursions kann leicht ein Fehler gemacht werden. Die folgende Funktion kann als Ausgangspunkt für das Auflisten der Inhalte eines Ordners und aller Unterordner, einschließlich aller Dateien und deren Größen, dienen. Sie können die Funktion vereinfachen, wenn Sie die Dateigrößen an der Stelle directoryEntryPromises.push nicht benötigen, ohne das Versprechen handle.getFile() zu übertragen, sondern direkt das handle.

  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 Ursprungssystem in einem Web Worker verwenden

Wie bereits erwähnt, können Web Worker 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() von einem regulären FileSystemFileHandle abgerufen wird.

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

Methoden für synchrone In-Place-Dateien

Sobald Sie über einen synchronen Zugriff verfügen, erhalten Sie Zugriff auf schnelle direkte Dateimethoden, die alle synchron sind.

  • getSize(): gibt die Größe der Datei in Byte zurück.
  • write(): Schreibt den Inhalt eines Zwischenspeichers in die Datei, optional mit einem bestimmten Offset, und gibt die Anzahl der geschriebenen Byte zurück. Die Überprüfung der zurückgegebenen Anzahl geschriebener Byte ermöglicht es Aufrufern, Fehler und Teilschreibvorgänge zu erkennen und zu verarbeiten.
  • read(): Liest den Inhalt der Datei in einen Zwischenspeicher, optional mit einem bestimmten Offset.
  • truncate(): Die Datei wird an die angegebene Größe angepasst.
  • flush(): Stellt sicher, dass der Inhalt der Datei alle Änderungen enthält, die über write() vorgenommen wurden.
  • close(): Schließt den Zugriffs-Handle.

Hier ist ein Beispiel, das alle oben genannten Methoden verwendet.

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

Datei aus dem privaten Dateisystem in das für den Nutzer sichtbare Dateisystem kopieren

Wie bereits erwähnt, können Dateien nicht aus dem privaten Dateisystem in das für Nutzer sichtbare Dateisystem verschoben werden. Sie können jedoch Dateien kopieren. Da showSaveFilePicker() nur im Hauptthread, aber nicht im Worker-Thread verfügbar gemacht wird, sollten Sie den Code dort ausführen.

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

Fehler im privaten Dateisystem des Ursprungs beheben

Bis die integrierte Unterstützung für die Entwicklertools hinzugefügt wird (siehe crbug/1284595), verwenden Sie die Chrome-Erweiterung OPFS Explorer, um Fehler im privaten Dateisystem des Ursprungs zu beheben. 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, wählen Sie den Tab OPFS Explorer aus und prüfen Sie die Dateihierarchie. Sie können Dateien aus dem privaten Dateisystem im für den Nutzer sichtbaren Dateisystem speichern, indem Sie auf den Dateinamen klicken und Dateien und Ordner löschen, indem Sie auf das Papierkorbsymbol klicken.

Demo

Sehen Sie sich das private Dateisystem des Ursprungs in Aktion (wenn Sie die Erweiterung OPFS Explorer installieren) in einer Demo an, die es als Back-End für eine SQLite-Datenbank verwendet, die in WebAssembly kompiliert wurde. Sieh dir auch den Quellcode auf Glitch an. Beachten Sie, dass die folgende eingebettete Version nicht das Backend des privaten Dateisystems des Ursprungs verwendet (da der iFrame ursprungsübergreifend ist). Wenn Sie die Demo jedoch in einem separaten Tab öffnen, geschieht dies.

Ergebnisse

Das von der WHATWG festgelegte private Dateisystem hat die Art und Weise geprägt, wie wir Dateien im Web verwenden und mit ihnen interagieren. Es hat neue Anwendungsfälle ermöglicht, die mit dem für die Nutzer sichtbaren Dateisystem unmöglich waren. Alle großen Browser-Anbieter – Apple, Mozilla und Google – sind an Bord und teilen eine gemeinsame Vision. Bei der Entwicklung des privaten Dateisystems geht viel mehr um die Zusammenarbeit. Feedback von Entwicklern und Nutzern ist dabei sehr wichtig. Wir arbeiten weiter daran, den Standard weiter zu verfeinern und zu verbessern. Wir freuen uns über Feedback zum whatwg/fs-Repository in Form von Problemen oder Pull-Anfragen.

Danksagungen

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