Das private Dateisystem des Ursprungs

Der File System Standard führt ein ursprungsprivates Dateisystem (Origin Private File System, 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 die Leistung optimiert ist.

Unterstützte Browser

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

Browser Support

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

Source

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 befindet sich die To-do-Liste eines Nutzers namens Tom beispielsweise unter C:\Users\Tom\Documents\ToDo.txt. In diesem Beispiel ist ToDo.txt der Dateiname und Users, Tom und Documents sind Ordnernamen. `C:` unter Windows steht für das Stammverzeichnis des Laufwerks.

Typische 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 oder öffnet sie auf dem Client mit <input type="file">.
  2. Der Nutzer nimmt die Änderungen vor und lädt dann die resultierende Datei mit einem eingefügten <a download="ToDo.txt> herunter, das Sie programmatisch click() mit JavaScript.
  3. Zum Öffnen von Ordnern verwenden Sie ein spezielles Attribut in <input type="file" webkitdirectory>, das trotz seines proprietären Namens praktisch universelle Browserunterstützung bietet.

Moderne Arbeitsweise mit Dateien im Web

Dieser Ablauf entspricht nicht der Art und Weise, wie Nutzer Dateien bearbeiten, und führt dazu, dass Nutzer heruntergeladene Kopien ihrer Eingabedateien erhalten. Daher wurden mit der File System Access API drei Auswahlmethoden eingeführt:

Sie ermöglichen folgenden Ablauf:

  1. Öffnen Sie ToDo.txt mit showOpenFilePicker() und rufen Sie ein FileSystemFileHandle-Objekt ab.
  2. Rufen Sie aus dem FileSystemFileHandle-Objekt eine File ab, indem Sie die Methode getFile() des Dateihandles aufrufen.
  3. Ändern Sie die Datei und rufen Sie dann requestPermission({mode: 'readwrite'}) für das 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. Für wiederholte Speichervorgänge können Sie das Dateihandle beibehalten, damit Sie das Dialogfeld zum Speichern der Datei nicht noch einmal anzeigen müssen.

Einschränkungen bei der Arbeit mit Dateien im Web

Dateien und Ordner, auf die mit diesen Methoden zugegriffen werden kann, befinden sich im sogenannten nutzersichtbaren Dateisystem. Aus dem Web gespeicherte Dateien und ausführbare Dateien insbesondere sind mit dem Zeichen des Webs gekennzeichnet, sodass das Betriebssystem eine zusätzliche Warnung anzeigen kann, bevor eine potenziell gefährliche Datei ausgeführt wird. Als zusätzliche Sicherheitsfunktion werden aus dem Web abgerufene Dateien auch durch Safe Browsinggeschützt. Der Einfachheit halber und im Kontext dieses Dokuments können Sie sich Safe Browsing als cloudbasierten Virenscan vorstellen. Wenn Sie mit der File System Access API Daten in eine Datei schreiben, werden die Schreibvorgänge nicht direkt ausgeführt, sondern es wird eine temporäre Datei verwendet. Die Datei selbst wird erst geändert, wenn sie alle diese Sicherheitsprüfungen bestanden hat.

Diese Vorgehensweise macht Dateivorgänge relativ langsam, obwohl nach Möglichkeit Verbesserungen vorgenommen wurden , z. B. unter macOS. Dennoch ist jeder write() Aufruf in sich abgeschlossen. Im Hintergrund wird die Datei geöffnet, der angegebene Offset gesucht und schließlich werden Daten geschrieben.

Dateien als Grundlage der Verarbeitung

Gleichzeitig sind Dateien eine hervorragende Möglichkeit, Daten aufzuzeichnen. SQLite speichert beispielsweise gesamte Datenbanken in einer einzigen Datei. Ein weiteres Beispiel sind Mipmaps, die in der Bild verarbeitung verwendet werden. Mipmaps sind vorab berechnete, optimierte Bildsequenzen, von denen jede eine Darstellung des vorherigen Bildes mit einer immer geringeren Auflösung ist. Dadurch werden viele Vorgänge wie das Zoomen beschleunigt. Wie können Webanwendungen also die Vorteile von Dateien nutzen, ohne die Leistungseinbußen der webbasierten Dateiverarbeitung in Kauf nehmen zu müssen? Die Antwort ist das ursprungsprivate Dateisystem.

Das nutzersichtbare Dateisystem im Vergleich zum ursprungsprivaten Dateisystem

Im Gegensatz zum nutzersichtbaren Dateisystem, das mit dem Datei-Explorer des Betriebssystems durchsucht wird und in dem Sie Dateien und Ordner lesen, schreiben, verschieben und umbenennen können, ist das ursprungsprivate Dateisystem nicht für Nutzer gedacht. Dateien und Ordner im ursprungsprivaten Dateisystem sind, wie der Name schon sagt, privat und genauer gesagt privat für den Ursprung einer Website. Sie können den Ursprung einer Seite ermitteln, indem Sie location.origin in die Entwicklertools-Konsole eingeben. Der Ursprung der Seite https://developer.chrome.com/articles/ ist beispielsweise https://developer.chrome.com. Weitere Informationen zur Theorie der Ursprünge finden Sie unter „Same-site“ und „same-origin“.

Alle Seiten mit demselben Ursprung können dieselben Daten des ursprungsprivaten Dateisystems sehen. https://developer.chrome.com/docs/extensions/mv3/getstarted/extensions-101/ kann also dieselben Details wie im vorherigen Beispiel sehen. Jeder Ursprung hat ein eigenes unabhängiges ursprungsprivates Dateisystem. Das ursprungsprivate Dateisystem von https://developer.chrome.com unterscheidet sich also vollständig von dem von beispielsweise https://web.dev. Unter Windows ist das Stammverzeichnis des nutzersichtbaren Dateisystems C:\\.

Das Äquivalent für das ursprungsprivate Dateisystem ist ein anfangs leeres Stamm verzeichnis pro Ursprung, auf das durch Aufrufen der asynchronen Methode navigator.storage.getDirectory() zugegriffen wird.

Einen Vergleich des nutzersichtbaren Dateisystems und des ursprungsprivaten Dateisystems finden Sie im folgenden Diagramm. Das Diagramm zeigt, dass abgesehen vom Stammverzeichnis alles konzeptionell gleich ist, mit einer Hierarchie von Dateien und Ordnern, die Sie nach Bedarf für Ihre Daten und Speicheranforderungen organisieren und anordnen können.

Diagramm des für den Nutzer sichtbaren Dateisystems und des privaten Dateisystems des Ursprungs mit zwei beispielhaften Dateihierarchien.
Der Einstiegspunkt für das nutzersichtbare Dateisystem ist eine symbolische Festplatte, der Einstiegspunkt für das ursprungsprivate Dateisystem ist der Aufruf der Methode navigator.storage.getDirectory.

Besonderheiten des ursprungsprivaten Dateisystems

Wie andere Speichermechanismen im Browser (z. B. localStorage oder IndexedDB) unterliegt das ursprungsprivate Dateisystem Browserkontingentbeschränkungen. Wenn ein Nutzer alle Browserdaten oder alle Websitedaten, wird auch das ursprungsprivate Dateisystem gelöscht.

Rufen Sie navigator.storage.estimate() auf und sehen Sie im resultierenden Antwortobjekt den usage Eintrag, um zu sehen, wie viel Speicher Ihre App bereits belegt. Die Aufschlüsselung nach Speichermechanismus finden Sie im usageDetails Objekt. Dort sollten Sie sich speziell den fileSystem Eintrag ansehen. Da das ursprungsprivate Dateisystem für den Nutzer nicht sichtbar ist, gibt es keine Berechtigungsaufforderungen und keine Safe Browsing-Prüfungen.

Zugriff auf das Stammverzeichnis

Führen Sie den folgenden Befehl aus, um auf das Stammverzeichnis zuzugreifen. Sie erhalten ein leeres Verzeichnishandle, 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 ursprungsprivate Dateisystem 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, was im Hauptthread im Allgemeinen nicht zulässig ist. Synchrone APIs können schneller sein, da sie keine Promises verarbeiten 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, springen Sie zu Ursprungsprivates Dateisystem in einem Web Worker verwenden.

Ursprungsprivates Dateisystem im Hauptthread verwenden

Neue Dateien und Ordner erstellen

Sobald Sie einen Stammordner haben, können Sie mit den getFileHandle() und den getDirectoryHandle() Methoden Dateien und Ordner erstellen. Wenn Sie {create: true}, die Datei oder der Ordner erstellt, falls er noch nicht vorhanden ist. 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 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 abrufen, die einem Dateihandle zum Lesen zugeordnet ist

Ein FileSystemFileHandle stellt eine Datei im Dateisystem dar. Verwenden Sie die getFile()Methode, um die zugehörige Fileabzurufen. Ein File Objekt ist eine spezielle Art von Blob und kann in jedem Kontext verwendet werden, in dem ein Blob verwendet werden kann.

Insbesondere FileReader, URL.createObjectURL(), createImageBitmap(), und XMLHttpRequest.send() akzeptieren sowohl Blobs als auch Files. Wenn Sie ein File aus einem FileSystemFileHandle "freigeben" die Daten, sodass Sie darauf zugreifen und sie dem nutzersichtbaren Dateisystem zur Verfügung stellen können.

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

Daten per Streaming in eine Datei schreiben

Streamen Sie Daten in eine Datei, indem Sie createWritable() aufrufen. Dadurch wird ein FileSystemWritableFileStream erstellt, in den Sie dann den Inhalt write() . Am Ende müssen Sie close() den Stream.

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 entsprechende remove() Methode des Datei- oder Verzeichnishandles aufrufen. Wenn Sie einen Ordner einschließlich aller Unterordner löschen möchten, übergeben Sie die {recursive: true} Option.

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

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

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

Dateien und Ordner verschieben und umbenennen

Verwenden Sie die move() Methode, um Dateien und Ordner umzubenennen und zu verschieben. Das Verschieben und Umbenennen kann zusammen oder separat 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');
kopieren.

Pfad einer Datei oder eines Ordners auflösen

Verwenden Sie die resolve()Methode, um herauszufinden, wo sich eine bestimmte Datei oder ein bestimmter Ordner im Verhältnis zu einem Referenz Verzeichnis befindet. Übergeben Sie dazu ein FileSystemHandle als Argument. Verwenden Sie das Stammverzeichnis als Referenzverzeichnis, das mit navigator.storage.getDirectory() abgerufen wird, um den vollständigen Pfad einer Datei oder eines Ordners im ursprungsprivaten Dateisystem zu erhalten.

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

Prüfen, ob zwei Datei- oder Ordnerhandles 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 isSameEntry() Methode, um zu prüfen, ob dies der Fall ist.

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

Inhalt eines Ordners auflisten

FileSystemDirectoryHandle ist ein asynchroner Iterator , den Sie mit einer for await... of -Schleife durchlaufen. Als asynchroner Iterator unterstützt er auch die entries(), die values(), und die keys() Methoden, aus denen Sie je nach Bedarf auswä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()) {}

Inhalt eines Ordners und aller Unterordner rekursiv auflisten

Der Umgang mit asynchronen Schleifen und Funktionen in Kombination mit Rekursion ist nicht einfach. Die folgende Funktion kann als Ausgangspunkt dienen, um den Inhalt eines Ordners und aller Unterordner einschließlich aller Dateien und ihrer Größen aufzulisten. Sie können die Funktion vereinfachen, wenn Sie die Dateigrößen nicht benötigen. Dazu müssen Sie an der Stelle, an der es heißt directoryEntryPromises.push, nicht das handle.getFile() Promise, sondern direkt das handle pushen.

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

Ursprungsprivates Dateisystem in einem Web Worker verwenden

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

Synchrones Zugriffshandle abrufen

Der Einstiegspunkt für die schnellstmöglichen Dateivorgänge ist ein FileSystemSyncAccessHandle, das Sie von einem regulären FileSystemFileHandle abrufen, indem Sie createSyncAccessHandle() aufrufen.

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

Synchrone In-place-Dateimethoden

Sobald Sie ein synchrones Zugriffshandle haben, können Sie auf schnelle In-place-Dateimethoden zugreifen, die alle synchron sind.

  • getSize(): Gibt die Größe der Datei in Byte zurück.
  • write(): Schreibt den Inhalt eines Puffers in die Datei, optional an einem bestimmten Offset, und gibt die Anzahl der geschriebenen Byte zurück. Durch Prüfen der zurückgegebenen Anzahl der geschriebenen Byte können Aufrufer Fehler und Teilschreibvorgänge erkennen und verarbeiten.
  • read(): Liest den Inhalt der Datei in einen Puffer, optional an einem bestimmten Offset.
  • truncate(): Ändert die Größe der Datei auf die angegebene Größe.
  • flush(): Stellt sicher, dass der Inhalt der Datei alle Änderungen enthält, die mit write() vorgenommen wurden.
  • close(): Schließt das Zugriffshandle.

Hier ist ein Beispiel, in dem jede Methode verwendet wird.

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 ursprungsprivaten Dateisystem in das nutzersichtbare Dateisystem kopieren

Wie oben erwähnt, ist es nicht möglich, Dateien aus dem ursprungsprivaten Dateisystem in das nutzersichtbare Dateisystem zu verschieben. Sie können sie jedoch kopieren. Da showSaveFilePicker() nur im Hauptthread, nicht aber im Worker-Thread verfügbar ist, müssen 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);
}

Ursprungsprivates Dateisystem debuggen

Bis die integrierte Entwicklertools-Unterstützung hinzugefügt wird (siehe crbug/1284595), verwenden Sie die OPFS Explorer Chrome-Erweiterung, um das ursprungsprivate Dateisystem zu debuggen. Der Screenshot 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. Dann können Sie die Dateihierarchie prüfen. Klicken Sie auf den Dateinamen, um Dateien aus dem ursprungsprivaten Dateisystem im nutzersichtbaren Dateisystem zu speichern. Klicken Sie auf das Papierkorbsymbol, um Dateien und Ordner zu löschen.

Demo

In einer Demo, in der das ursprungsprivate Dateisystem als Backend für eine in WebAssembly kompilierte SQLite-Datenbank verwendet wird, können Sie es in Aktion sehen (wenn Sie die Erweiterung OPFS Explorer installieren). Den Quellcode finden Sie auf GitHub. Beachten Sie, dass die eingebettete Version das ursprungsprivate Dateisystem-Backend nicht verwendet, da der Iframe ursprungsübergreifend ist. Wenn Sie die Demo jedoch in einem separaten Tab öffnen, wird es verwendet.

Fazit

Das ursprungsprivate Dateisystem, wie von der WHATWG festgelegt, hat die Art und Weise verändert, wie wir Dateien im Web verwenden und mit ihnen interagieren. Es hat neue Anwendungsfälle ermöglicht, die mit dem nutzersichtbaren Dateisystem nicht möglich waren. Alle großen Browseranbieter – Apple, Mozilla und Google – sind dabei und haben eine gemeinsame Vision. Die Entwicklung des ursprungsprivaten Dateisystems ist eine Gemeinschaftsleistung und Feedback von Entwicklern und Nutzern ist für den Fortschritt unerlässlich.

Wir arbeiten weiter daran, den Standard zu verfeinern und zu verbessern. Feedback zum whatwg/fs-Repository in Form von Issues oder Pull Requests ist willkommen.

Danksagungen

Dieses Dokument wurde von Austin Sully, Etienne Noël und Rachel Andrew geprüft.