Lesson 0002 · ~12 minutes
One skill: open the OPFS root, get file/directory handles, and perform main-thread create / read / write / delete — without confusing this with user-visible pickers or worker sync I/O.
You can sketch
getDirectory → getFileHandle → createWritable / getFile,
know how folders nest, and pick the right delete path — still without
sync access handles.
Entry point (no picker, no permission prompt for OPFS itself):
navigator.storage.getDirectory()
returns a
FileSystemDirectoryHandle
for the origin’s private root
(web.dev OPFS):
const root = await navigator.storage.getDirectory();
// root.kind === "directory", root.name === ""
Same root for every page (and worker) on that origin. Clearing site data clears OPFS. This is still not a path under the user’s Desktop.
Treat the root as an app-owned disk: design a
folder layout (e.g. exports/,
media/) instead of dumping every blob at "".
Names are strings you invent — version or namespace them if schemas
evolve.
You never pass OS paths. You walk handles. On a directory handle (MDN FileSystemDirectoryHandle):
Child file by name. Pass
{ create: true } to create if missing; omit to open
existing only (rejects if absent).
Child folder by name. Same
{ create: true } pattern. Nest by calling again on the
child directory.
const root = await navigator.storage.getDirectory();
const fileHandle = await root.getFileHandle("notes.txt", { create: true });
const folder = await root.getDirectoryHandle("exports", { create: true });
const nested = await folder.getFileHandle("report.pdf", { create: true });
// Existing only — fails if missing
const existing = await root.getFileHandle("notes.txt");
A
FileSystemFileHandle
is a capability to that file, not the bytes themselves. You still need
read/write steps.
Main-thread (and worker-safe async) path from web.dev / MDN:
| Job | Call | Notes |
|---|---|---|
| Read bytes | await fileHandle.getFile() |
Returns a File (a Blob). Then
text(), arrayBuffer(),
stream().
|
| Write bytes |
createWritable() →
write(...) → close()
|
close() persists. Without close, don’t assume data
stuck.
|
const handle = await root.getFileHandle("notes.txt", { create: true });
// Write
const writable = await handle.createWritable();
await writable.write("hello OPFS");
await writable.close();
// Read
const file = await handle.getFile();
console.log(await file.text()); // "hello OPFS"
createSyncAccessHandle() is the high-throughput,
in-place path for dedicated workers (Wasm/DB
engines). Different mental model — later lesson. Do not reach for it
for ordinary main-thread text/blob writes
(MDN).
Two delete styles (web.dev OPFS):
await handle.remove() — remove this entry (directories:
{ recursive: true }). Check support:
"remove" in FileSystemFileHandle.prototype (Chrome-first
historically).
await dir.removeEntry("child-name") — parent removes by
name (portable pattern).
await fileHandle.remove();
await folder.remove({ recursive: true });
await root.removeEntry("notes.txt");
// Nuclear: wipe whole OPFS
// await (await navigator.storage.getDirectory()).remove({ recursive: true });
List children: directory handles are async-iterable (MDN):
for await (const [name, handle] of root.entries()) {
console.log(name, handle.kind); // "file" | "directory"
}
Choose the best default. Equal-length options so formatting doesn’t hint. Feedback is immediate.
You need the origin’s private OPFS root handle (create nothing else yet).
Under the OPFS root, create draft.bin if missing and
get its file handle.
On the main thread, replace the entire contents of an existing OPFS file handle with a short string.
Read the full text of a file you already have a
FileSystemFileHandle for.
You have the parent folder handle and the child name
tmp.dat. Delete that child only.
navigator.storage.getDirectory() → OPFS root (private,
per origin).
getFileHandle / getDirectoryHandle (+
{ create: true }) build the tree.
createWritable + close; read with
getFile.
remove / removeEntry; list with
async iteration. Sync access handles = later.
web.dev — The origin private file system (Thomas Steiner). Best short primary for this lesson’s handle map. Still useful (deferred from lesson 1): Storage for the web for the spectrum you already practiced.