Das private Dateisystem des Ursprungs

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

Unterstützte Browser

Das private Ursprungsdateisystem 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 <ph type="x-smartling-placeholder">
  • Edge: 86. <ph type="x-smartling-placeholder">
  • Firefox: 111 <ph type="x-smartling-placeholder">
  • Safari: 15.2 <ph type="x-smartling-placeholder">

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 erkunden können. Ein Beispiel: Unter Windows befindet sich die To-do-Liste eines Nutzers namens Tom möglicherweise in 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.

Herkömmliche Arbeitsweise 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 eingefügten <a download="ToDo.txt> herunter, die Sie programmatisch über JavaScript click() laden.
  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 spiegelt nicht wider, wie Nutzer von der Bearbeitung von Dateien denken. Vielmehr erhalten die Nutzer heruntergeladene Kopien ihrer Eingabedateien. Daher wurden im Rahmen der File System Access API drei Auswahlmethoden eingeführt: showOpenFilePicker(), showSaveFilePicker() und showDirectoryPicker(), die genau das tun, was ihr Name verrät. Sie ermöglichen einen Ablauf wie folgt:

  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 Alias 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 Auswahl einer neuen Datei überlassen. Wenn der Nutzer eine zuvor geöffnete Datei auswählt, wird ihr Inhalt überschrieben. Bei wiederholten Speichern können Sie das Datei-Handle 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 über diese Methoden zugegriffen werden kann, befinden sich in einem sogenannten für Nutzer sichtbaren Dateisystem. Aus dem Web gespeicherte Dateien und insbesondere ausführbare Dateien sind mit dem Symbol „Web“ gekennzeichnet. Das Betriebssystem zeigt also eine zusätzliche Warnung an, 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, das Sie der Einfachheit halber und im Kontext dieses Artikels als cloudbasierter Virenscan vorstellen können. Wenn Sie mit der File System Access API Daten in eine Datei schreiben, werden Schreibvorgänge nicht direkt durchgeführt, sondern verwenden eine temporäre Datei. Die Datei selbst wird nur geändert, wenn sie all diese Sicherheitsprüfungen besteht. Wie Sie sich vorstellen können, verlangsamt diese Arbeit Dateivorgänge relativ langsam, obwohl nach Möglichkeit Verbesserungen angewendet wurden, z. B. unter macOS. Da jeder write()-Aufruf eigenständig ist, wird die Datei geöffnet, zum angegebenen Offset gesucht und schließlich Daten geschrieben.

Dateien als Grundlage der Verarbeitung

Gleichzeitig sind Dateien eine hervorragende Möglichkeit, um Daten aufzuzeichnen. 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 Sequenzen von Bildern, von denen jedes eine immer geringere Auflösung ist und viele Vorgänge wie das Zoomen beschleunigt. Wie können also Webanwendungen von den Vorteilen von Dateien profitieren, aber ohne die Leistungskosten einer webbasierten Dateiverarbeitung? Die Antwort ist das private Ursprungsdateisystem.

Vergleich zwischen dem sichtbaren und dem ursprünglichen Dateisystem des privaten Dateisystems

Im Gegensatz zum für den Nutzer sichtbaren Dateisystem, das mit dem Datei-Explorer des Betriebssystems durchsucht wird, ist das private Ursprungsdateisystem mit Dateien und Ordnern, die Sie lesen, schreiben, verschieben und umbenennen können, nicht für Nutzer sichtbar. Dateien und Ordner im privaten Ursprungsdateisystem sind, wie der Name schon sagt, privat und somit privat für den Ursprung einer Website. Gib location.origin in der Entwicklertools-Konsole ein, um den Ursprung einer Seite zu ermitteln. Der Ursprung der Seite https://developer.chrome.com/articles/ ist beispielsweise https://developer.chrome.com, d. h. der Teil /articles ist nicht Teil des Ursprungs. Weitere Informationen zur Entstehungstheorie finden Sie unter Informationen zu derselben Website und „same-origin“ angegeben. Alle Seiten mit demselben Ursprung können dieselben Ursprungsdaten des privaten Dateisystems sehen, sodass https://developer.chrome.com/docs/extensions/mv3/getstarted/extensions-101/ dieselben Details wie im vorherigen Beispiel sehen kann. Jeder Ursprung hat sein eigenes unabhängiges privates Dateisystem für den Ursprung. Das bedeutet, dass sich das private Ursprungsdateisystem von https://developer.chrome.com vollständig von dem System von 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 anfänglich leeres Stammverzeichnis pro Ursprung, auf den durch Aufrufen der asynchronen Methode zugegriffen wird. navigator.storage.getDirectory() Im folgenden Diagramm finden Sie einen Vergleich des für den Nutzer sichtbaren Dateisystems und des privaten Ursprungsdateisystems. Das Diagramm zeigt, dass mit Ausnahme des Stammverzeichnisses alles andere konzeptionell identisch ist, mit einer Hierarchie von Dateien und Ordnern, die entsprechend Ihren Daten- und Speicheranforderungen organisiert und angeordnet werden können.

Diagramm des für den 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 private Dateisystem des Ursprungs ruft die Methode „navigator.storage.getDirectory“ auf.

Details zum privaten Dateisystem des Ursprungs

Genau wie andere Speichermechanismen im Browser (z. B. localStorage oder IndexedDB) unterliegt das private Ursprungsdateisystem den Kontingentbeschränkungen des Browsers. Wenn ein Nutzer alle Browserdaten oder alle Websitedaten löscht, wird auch das private Ursprungsdateisystem 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 ist nach Speichermechanismus im Objekt usageDetails aufgeschlüsselt, wobei Sie speziell den Eintrag fileSystem betrachten möchten. Da das private Dateisystem des Ursprungs für den Nutzer nicht sichtbar ist, gibt es keine Aufforderungen zu Berechtigungen 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, sodass APIs in diesem Kontext synchron sein können. Ein Muster ist im Hauptthread normalerweise nicht zulässig. Synchrone APIs können schneller sein, da sie nicht mit Promise umgehen müssen, und Dateivorgänge in Sprachen wie C, die in WebAssembly kompiliert werden können, sind 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 möglichst schnelle Dateivorgänge benötigen oder WebAssembly nutzen möchten, fahren Sie mit Privates Ursprungsdateisystem 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 er nicht vorhanden ist. Erstellen Sie eine Hierarchie von Dateien, indem Sie diese Funktionen aufrufen und ein neu erstelltes Verzeichnis als Ausgangspunkt verwenden.

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 sich ergebende Dateihierarchie aus dem früheren Codebeispiel.

Auf vorhandene Dateien und Ordner zugreifen

Wenn Sie deren 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, die einem Datei-Handle zum Lesen zugeordnet ist, abrufen

Ein FileSystemFileHandle steht für eine Datei im Dateisystem. Die zugehörige File können Sie mit der Methode getFile() abrufen. Ein File-Objekt ist eine bestimmte Art von Blob und kann in jedem Kontext verwendet werden, den ein Blob kann. Insbesondere akzeptieren FileReader, URL.createObjectURL(), createImageBitmap() und XMLHttpRequest.send() sowohl Blobs als auch Files. Wenn dies der Fall ist, erhalten Sie ein File aus einem FileSystemFileHandle-Guthaben 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());

Durch Streaming in eine Datei schreiben

Sie können Daten in eine Datei streamen, indem Sie createWritable() aufrufen. Es wird ein FileSystemWritableFileStream erstellt, für das Sie dann write() den Inhalt erstellen. 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 entsprechende remove()-Methode des entsprechenden Datei- oder Verzeichnis-Handles aufrufen. 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 zu löschenden Datei oder des zu löschenden Ordners in einem Verzeichnis kennen, verwenden Sie alternativ die Methode removeEntry().

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 Bezug auf ein Referenzverzeichnis befindet, verwenden Sie die Methode resolve() und übergeben Sie FileSystemHandle als Argument. Um den vollständigen Pfad einer Datei oder eines Ordners im privaten Ursprungsdateisystem abzurufen, verwenden Sie das Stammverzeichnis als Referenzverzeichnis über navigator.storage.getDirectory().

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

Prüfen, ob zwei Datei- oder Ordner-Handles 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 das 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(), aus denen Sie je nach den benötigten Informationen 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 Rekursionen kann leicht schiefgehen. Die folgende Funktion kann als Ausgangspunkt dienen, um den Inhalt eines Ordners und aller Unterordner, einschließlich aller Dateien und ihrer Größe, aufzulisten. Wenn du die Dateigröße nicht benötigst, kannst du die Funktion vereinfachen, indem du directoryEntryPromises.push angibst. Das handle.getFile()-Promise wird nicht übertragen, sondern das handle direkt.

  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 Ursprungsdateisystem 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.

Alias für synchronen Zugriff abrufen

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

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

Synchrone In-Place-Dateimethoden

Sobald Sie ein synchrones 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 Zwischenspeichers in die Datei, optional mit einem bestimmten Offset, und gibt die Anzahl der geschriebenen Byte zurück. Durch Überprüfen der zurückgegebenen Anzahl geschriebener Byte können Aufrufer Fehler und Teilschreibvorgänge erkennen und verarbeiten.
  • read(): Liest den Inhalt der Datei in einen Zwischenspeicher, optional mit einem bestimmten Versatz.
  • truncate(): ändert die Größe der Datei an die angegebene Größe.
  • flush(): sorgt dafür, dass der Inhalt der Datei alle Änderungen enthält, die über write() vorgenommen wurden.
  • 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);

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

Wie bereits erwähnt, ist es nicht möglich, Dateien vom privaten Dateisystem in das für den Nutzer sichtbare Dateisystem zu verschieben. Sie können Dateien jedoch 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 Entwicklertools-Unterstützung verfügbar ist (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 wird übrigens direkt aus der Erweiterung erstellt.

Die Chrome DevTools-Erweiterung für OPFS Explorer im Chrome Web Store

Öffne nach der Installation der Erweiterung die Chrome-Entwicklertools, wähle den Tab OPFS Explorer aus und prüfe die Dateihierarchie. Speichern Sie Dateien aus dem privaten Ursprungsdateisystem im für den Nutzer sichtbaren Dateisystem, indem Sie auf den Dateinamen klicken. Löschen Sie Dateien und Ordner, indem Sie auf das Papierkorbsymbol klicken.

Demo

Sehen Sie sich in einer Demo das private Ursprungsdateisystem in Aktion an (wenn Sie die Erweiterung „OPFS Explorer“ installieren). Darin wird es als Back-End für eine in WebAssembly kompilierte SQLite-Datenbank verwendet. Sieh dir unbedingt auch den Quellcode zu Glitch an. Beachten Sie, dass die eingebettete Version unten nicht das Ursprungs-Back-End des privaten Dateisystems verwendet (da der iFrame ursprungsübergreifend ist). Wenn Sie die Demo jedoch in einem separaten Tab öffnen, wird sie geöffnet.

Ergebnisse

Das Ursprungssystem für private Dateien, wie von der WHATWG festgelegt, hat die Art und Weise geprägt, wie wir Dateien im Web nutzen und mit ihnen interagieren. Es hat neue Anwendungsfälle ermöglicht, die mit dem für den Nutzer sichtbaren Dateisystem nicht realisierbar waren. Alle großen Browseranbieter – Apple, Mozilla und Google – haben eine gemeinsame Vision. Die Entwicklung des privaten Ursprungsdateisystems ist sehr auf Zusammenarbeit ausgelegt und das Feedback von Entwicklern und Nutzern ist für den Fortschritt von entscheidender Bedeutung. Wir arbeiten kontinuierlich an der Verfeinerung und Verbesserung des Standards. 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 gelesen. Hero-Image von Christina Rumpf auf Unsplash