Der Dateisystemstandard führt ein ursprungsbezogenes privates 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. Er bietet optionalen Zugriff auf eine spezielle Art von Datei, die für die 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.
Motivation
Wenn Sie an Dateien auf Ihrem Computer denken, stellen Sie sich wahrscheinlich eine Dateihierarchie vor: 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 mit dem Namen 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.
Traditionelle Arbeitsweise mit Dateien im Web
So bearbeiten Sie die Aufgabenliste in einer Webanwendung:
- Der Nutzer lädt die Datei auf einen Server hoch oder öffnet sie auf dem Client mit
<input type="file">
. - 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 über JavaScriptclick()
. - 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 Art, mit Dateien im Web zu arbeiten
Dieser Ablauf entspricht nicht der Art und Weise, wie Nutzer Dateien bearbeiten möchten, und führt dazu, dass Nutzer Kopien ihrer Eingabedateien herunterladen. 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 folgt:
- Öffnen Sie
ToDo.txt
mitshowOpenFilePicker()
und rufen Sie einFileSystemFileHandle
-Objekt ab. - Rufen Sie aus dem
FileSystemFileHandle
-Objekt einFile
ab, indem Sie die MethodegetFile()
des Dateihandles aufrufen. - Ändern Sie die Datei und rufen Sie dann
requestPermission({mode: 'readwrite'})
für den Handle auf. - Wenn der Nutzer die Berechtigungsanfrage akzeptiert, speichern Sie die Änderungen in der Originaldatei.
- 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 wiederholten Speichervorgängen können Sie das Dateihandle beibehalten, sodass das Dialogfeld zum Speichern der Datei nicht noch einmal angezeigt werden muss.
Einschränkungen bei der Arbeit mit Dateien im Web
Dateien und Ordner, auf die über diese Methoden zugegriffen werden kann, befinden sich im nutzersichtbaren Dateisystem. Aus dem Web gespeicherte Dateien und insbesondere ausführbare Dateien werden mit dem Mark of the Web gekennzeichnet. So kann das Betriebssystem eine zusätzliche Warnung anzeigen, bevor eine potenziell gefährliche Datei ausgeführt wird. Als zusätzliche Sicherheitsfunktion werden Dateien aus dem Web auch durch Safe Browsing geschützt. Vereinfacht gesagt und im Kontext dieses Artikels können Sie sich das als cloudbasierten Virenscan vorstellen. Wenn Sie Daten mit der File System Access API in eine Datei schreiben, werden die Daten nicht direkt in die Datei geschrieben, sondern in eine temporäre Datei. Die Datei selbst wird erst geändert, wenn sie alle diese Sicherheitsprüfungen besteht. Wie Sie sich vorstellen können, macht diese Arbeit 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, zum angegebenen Offset gesucht und schließlich werden Daten geschrieben.
Dateien als Grundlage für die Verarbeitung
Gleichzeitig sind Dateien eine hervorragende Möglichkeit, Daten aufzuzeichnen. SQLite speichert beispielsweise ganze Datenbanken in einer einzigen Datei. Ein weiteres Beispiel sind Mipmaps, die bei der Bildverarbeitung verwendet werden. Mipmaps sind vorkalkulierte, optimierte Bildsequenzen, die jeweils eine Darstellung des vorherigen Bildes mit einer immer geringeren Auflösung enthalten. 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 private Dateisystem des Ursprungs.
Das für Nutzer sichtbare Dateisystem im Vergleich zum privaten Dateisystem des Ursprungs
Im Gegensatz zum für Nutzer sichtbaren Dateisystem, das mit dem Datei-Explorer des Betriebssystems durchsucht wird und in dem Dateien und Ordner gelesen, geschrieben, verschoben und umbenannt werden können, ist das private Dateisystem des Ursprungs nicht für Nutzer vorgesehen. Dateien und Ordner im privaten Dateisystem des Ursprungs sind, wie der Name schon sagt, privat und genauer gesagt privat für den Ursprung einer Website. Geben Sie location.origin
in die DevTools-Konsole ein, um den Ursprung einer Seite zu ermitteln. Der Ursprung der Seite https://developer.chrome.com/articles/
ist beispielsweise https://developer.chrome.com
. Der 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, die denselben Ursprung haben, können dieselben privaten Dateisystemdaten des Ursprungs sehen. https://developer.chrome.com/docs/extensions/mv3/getstarted/extensions-101/
kann also dieselben Details wie im vorherigen Beispiel sehen. Jeder Ursprung hat sein eigenes unabhängiges privates Dateisystem. Das private Dateisystem des Ursprungs von https://developer.chrome.com
unterscheidet sich also vollständig von dem von z. B. https://web.dev
. Unter Windows ist das Stammverzeichnis des für 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.
Ein Vergleich des für Nutzer sichtbaren Dateisystems und des privaten Dateisystems des Ursprungs ist im folgenden Diagramm dargestellt. Das Diagramm zeigt, dass abgesehen vom Stammverzeichnis alles andere konzeptionell gleich ist. Es gibt eine Hierarchie von Dateien und Ordnern, die nach Bedarf für Ihre Daten und Speicheranforderungen organisiert und angeordnet werden können.
Besonderheiten des privaten Dateisystems des Ursprungs
Wie andere Speichermechanismen im Browser (z. B. localStorage oder IndexedDB) unterliegt das ursprungsbezogene private Dateisystem Browserkontingentbeschränkungen. Wenn ein Nutzer alle Browserdaten oder alle Websitedaten löscht, wird auch das private Dateisystem des Ursprungs 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. Dort sollten Sie sich insbesondere den Eintrag fileSystem
ansehen. 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 auf das Stammverzeichnis zuzugreifen. 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 Web Worker. Web Workers können den Hauptthread nicht blockieren. Das bedeutet, dass APIs in diesem Kontext synchron sein können. Dies ist im Hauptthread in der Regel nicht zulässig. Synchrone APIs können schneller sein, da keine Promises verarbeitet werden müssen. Dateioperationen 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 Das ursprungsbezogene private Dateisystem in einem Web Worker verwenden. Andernfalls können Sie weiterlesen.
Origin Private File System im Hauptthread verwenden
Neue Dateien und Ordner erstellen
Nachdem Sie einen Stammordner haben, können Sie Dateien und Ordner mit den Methoden getFileHandle()
bzw. getDirectoryHandle()
erstellen. 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 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});
Auf vorhandene Dateien und Ordner zugreifen
Wenn Sie den Namen kennen, können Sie mit den Methoden getFileHandle()
oder getDirectoryHandle()
auf zuvor erstellte Dateien und Ordner zugreifen 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 Dateihandle zugeordnet ist, zum Lesen abrufen
Ein FileSystemFileHandle
repräsentiert eine Datei im Dateisystem. Verwenden Sie die Methode getFile()
, um die zugehörige File
abzurufen. 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 akzeptieren FileReader
, URL.createObjectURL()
, createImageBitmap()
und XMLHttpRequest.send()
sowohl Blobs
als auch Files
. Wenn Sie eine File
von einem FileSystemFileHandle
erhalten, werden die Daten „freigegeben“, sodass Sie darauf zugreifen und sie dem für Nutzer sichtbaren Dateisystem zur Verfügung stellen können.
const file = await fileHandle.getFile();
console.log(await file.text());
In eine Datei schreiben (Streaming)
Streamen Sie Daten in eine Datei, indem Sie createWritable()
aufrufen. Dadurch wird ein FileSystemWritableFileStream
erstellt, in das Sie dann den Inhalt write()
. 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
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 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
Benennen Sie Dateien und Ordner um und verschieben Sie sie mit der Methode move()
. Verschieben und Umbenennen können gleichzeitig oder unabhängig 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 in Bezug auf ein 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 privaten Dateisystem des Ursprungs abrufen möchten, verwenden Sie das Stammverzeichnis als Referenzverzeichnis, das über navigator.storage.getDirectory()
abgerufen wird.
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 Handles und wissen nicht, ob sie auf dieselbe Datei oder denselben Ordner verweisen. Verwenden Sie die Methode isSameEntry()
, 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 werden auch die Methoden entries()
, values()
und keys()
unterstützt, aus denen Sie je nach 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()) {}
Inhalte eines Ordners und aller Unterordner rekursiv auflisten
Asynchrone Schleifen und Funktionen in Kombination mit Rekursion sind nicht ganz einfach zu handhaben. Die folgende Funktion kann als Ausgangspunkt für das Auflisten der Inhalte eines Ordners und aller seiner Unterordner dienen, einschließlich aller Dateien und ihrer Größen. Sie können die Funktion vereinfachen, wenn Sie die Dateigrößen nicht benötigen. Anstelle des handle.getFile()
-Promise wird dann direkt handle
übertragen.directoryEntryPromises.push
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;
};
Origin Private File System in einem Web Worker verwenden
Wie bereits erwähnt, können Web Workers 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
, 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 In-Place-Dateimethoden
Sobald Sie ein Handle für den synchronen Zugriff 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 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 Teilvorgä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()
: Sorgt dafür, dass der Inhalt der Datei alle Änderungen enthält, die überwrite()
vorgenommen wurden.close()
: Schließt das Zugriffshandle.
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);
Datei aus dem privaten Dateisystem des Ursprungs in das für den Nutzer sichtbare Dateisystem kopieren
Wie oben erwähnt, ist es nicht möglich, Dateien aus dem ursprünglichen privaten Dateisystem in das für den Nutzer sichtbare Dateisystem zu verschieben. Sie können Dateien jedoch kopieren. Da showSaveFilePicker()
nur im Hauptthread und 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);
}
Privates Dateisystem des Ursprungs debuggen
Bis die integrierte DevTools-Unterstützung hinzugefügt wird (siehe crbug/1284595), können Sie die Chrome-Erweiterung OPFS Explorer verwenden, um das private Dateisystem des Ursprungs zu debuggen. Der Screenshot oben im Abschnitt Neue Dateien und Ordner erstellen stammt übrigens direkt aus der Erweiterung.
Öffnen Sie nach der Installation der Erweiterung die Chrome-Entwicklertools und wählen Sie den Tab OPFS Explorer aus. Jetzt können Sie die Dateihierarchie untersuchen. Sie können Dateien aus dem privaten Dateisystem des Ursprungs in das für den Nutzer sichtbare Dateisystem speichern, indem Sie auf den Dateinamen klicken. Dateien und Ordner lassen sich löschen, indem Sie auf das Papierkorbsymbol klicken.
Demo
Wenn Sie die OPFS Explorer-Erweiterung installieren, können Sie das private Dateisystem des Ursprungs in einer Demo in Aktion sehen, in der es als Backend für eine in WebAssembly kompilierte SQLite-Datenbank verwendet wird. Sehen Sie sich unbedingt den Quellcode auf Glitch an. Beachten Sie, dass die eingebettete Version unten 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, wird es verwendet.
Zusammenfassung
Das private Dateisystem des Ursprungs, wie von der WHATWG angegeben, hat die Art und Weise geprägt, wie wir Dateien im Web verwenden und mit ihnen interagieren. Dadurch wurden neue Anwendungsfälle ermöglicht, die mit dem für Nutzer sichtbaren Dateisystem nicht möglich waren. Alle wichtigen Browseranbieter – Apple, Mozilla und Google – sind mit an Bord und teilen eine gemeinsame Vision. Die Entwicklung des Origin Private File System ist ein Gemeinschaftsprojekt. Feedback von Entwicklern und Nutzern ist für den Fortschritt unerlässlich. Wir arbeiten daran, den Standard weiter zu verfeinern und zu verbessern. Feedback im whatwg/fs-Repository in Form von Issues oder Pull Requests ist willkommen.
Weitere Informationen
- Dateisystem – Standardspezifikation
- Standard-Repository für das Dateisystem
- The File System API with Origin Private File System WebKit post
- OPFS Explorer-Erweiterung
Danksagungen
Dieser Artikel wurde von Austin Sully, Etienne Noël und Rachel Andrew geprüft. Hero-Image von Christina Rumpf auf Unsplash.