# navigator.storage.getDirectory()（OPFS）

> getDirectory() 如何打开源私有文件系统、createSyncAccessHandle() 如何让专用 worker 获得同步读写，以及与锁相关的异常。

`navigator.storage.getDirectory()` 解析为源私有文件系统（OPFS）的根 `FileSystemDirectoryHandle`。OPFS 是受配额管理的文件与目录存储，没有选择器或权限提示把关，用户在文件管理器里也看不到它。在专用 worker 内，`FileSystemFileHandle.createSyncAccessHandle()` 再加上同步、按偏移寻址的读写，这正是 SQLite 等 Wasm 数据库能在浏览器里实用的原因。

## 语法

```js
navigator.storage.getDirectory()
fileHandle.createSyncAccessHandle()
fileHandle.createSyncAccessHandle({ mode })
```

`getDirectory()` 在窗口和 worker 中都可用（BCD `api.StorageManager.getDirectory`：Chrome 86、Firefox 111、Safari 15.2）。`createSyncAccessHandle()` 只存在于专用 worker（Chrome 102、Firefox 111、Safari 15.2），而且名字虽叫 sync，本身返回的是 Promise；真正同步的是返回的 `FileSystemSyncAccessHandle` 上的方法（`read`、`write`、`getSize`、`truncate`、`flush`、`close`）。OPFS 文件句柄上的异步 `createWritable()` 到 Safari 26 才加入，所以在 Safari 15.2 到 18 上同步访问句柄是唯一的写入路径。

## 参数

| 方法 | 参数 | 类型 | 含义 |
|---|---|---|---|
| `getDirectory` | 无 | | 返回该源的 OPFS 根。每个源以及被嵌入源的每个存储分区各有一个根。 |
| `createSyncAccessHandle` | `options.mode` | string，可选 | 锁模式。`"readwrite"`（默认）：每个文件同时只能有一个句柄。`"read-only"`：可同时打开任意多个句柄，但只能调用 `read()`、`getSize()`、`close()`。`"readwrite-unsafe"`：可同时打开任意多个可写句柄，没有任何协调，一致性由调用方负责。 |

根下的文件和目录通过 `getFileHandle(name, { create })` 与 `getDirectoryHandle(name, { create })` 获取，通过 `removeEntry(name, { recursive })` 删除。用量计入该源的配额并体现在 [`navigator.storage.estimate()`](/zh/reference/storage/quota-estimate/) 中；清除站点数据会删除整棵树。

## 异常

| 异常 | 位置 | 触发条件 |
|---|---|---|
| `SecurityError` `DOMException` | `getDirectory()` | 浏览器无法为该源映射目录，例如不透明源或存储被禁用。 |
| `NotFoundError` `DOMException` | `getFileHandle()`、`createSyncAccessHandle()` | 具名条目不存在且未传 `create: true`，或文件在取得句柄后被删除。 |
| `TypeMismatchError` `DOMException` | `getFileHandle()`、`getDirectoryHandle()` | 该名称存在，但是另一种类型的条目。 |
| `NoModificationAllowedError` `DOMException` | `createSyncAccessHandle()` | 拿不到锁：另一个 `"readwrite"` 模式的句柄仍打开着，或该文件上有打开的 `FileSystemWritableFileStream`。常见原因是第二个标签页的 worker 打开了同一个数据库文件。 |
| `InvalidStateError` `DOMException` | `createSyncAccessHandle()`、已关闭句柄上的任何方法 | 句柄不指向 OPFS（来自选择器的句柄），或已经调用过 `close()`。 |
| `NotAllowedError` `DOMException` | `createSyncAccessHandle()` | `"readwrite"` 模式下该句柄的权限状态不是 `granted`。 |
| `QuotaExceededError` `DOMException` | `write()`、`truncate()` | 该源配额已用尽。文件保留失败之前已写入的字节。 |

`write()` 返回写入的字节数，但在 `flush()` 返回之前不保证落盘；在两者之间被终止的 worker 可能丢掉写入的尾部。各异常的完整条件见 [FileSystemFileHandle: createSyncAccessHandle() method](https://developer.mozilla.org/en-US/docs/Web/API/FileSystemFileHandle/createSyncAccessHandle)（developer.mozilla.org）。

## 示例

worker 示例假定用 `new Worker()` 创建的专用 worker；共享 worker 和 Service Worker 拿不到同步句柄。

### 在专用 worker 中向日志文件追加

主线程发来一行文本；worker 在页面生命周期内持有一个 `"readwrite"` 句柄，并在当前大小处追加。按消息开关句柄也可以，但每次都要付出一次加锁往返。

```js
// log-worker.js（专用 worker）
let handle;

self.onmessage = async ({ data: line }) => {
  if (!handle) {
    const root = await navigator.storage.getDirectory();
    const file = await root.getFileHandle('app.log', { create: true });
    handle = await file.createSyncAccessHandle();
  }
  const bytes = new TextEncoder().encode(`${line}\n`);
  handle.write(bytes, { at: handle.getSize() });
  handle.flush();
};
```

句柄是独占的，第二个标签页启动同一个 worker 时 `createSyncAccessHandle()` 会抛出 `NoModificationAllowedError`；页面应捕获它，然后要么改用 `"readwrite-unsafe"` 并自己加锁（例如 [Web Locks API](/zh/reference/capabilities/web-locks/)），要么把写入集中到一个标签页。

### 在主线程读回文件

API 的异步部分在窗口里可用。`getFile()` 返回 `File`，所以普通的 `text()`、`arrayBuffer()`、`stream()` 读取方式都适用。

```js
async function readLog() {
  const root = await navigator.storage.getDirectory();
  try {
    const file = await (await root.getFileHandle('app.log')).getFile();
    return await file.text();
  } catch (err) {
    if (err.name === 'NotFoundError') return ''; // 还没写过日志
    throw err;
  }
}
```

来自 OPFS 的 `File` 是快照：`getFile()` 兑现之后 worker 追加的字节通过它看不到，要读新内容就重新从句柄取文件。

### 检测支持并选择存储

`createSyncAccessHandle` 只暴露给专用 worker，在主线程上检测即使在 Chrome 里也是 `false`。把探测放进 worker 并把结果发回，由页面据此选择存储。

```js
// probe-worker.js（专用 worker）
(async () => {
  if (!('storage' in navigator) || typeof navigator.storage.getDirectory !== 'function') {
    postMessage('none'); // 没有 OPFS：页面退回 IndexedDB
    return;
  }
  const sync = typeof FileSystemFileHandle !== 'undefined'
    && 'createSyncAccessHandle' in FileSystemFileHandle.prototype;
  postMessage(sync ? 'sync' : 'async');
})();
```

`'sync'` 表示上面的日志 worker 路径可用；`'async'` 对应暴露了根但没有同步句柄的引擎，这时数据库仍然更适合放在 IndexedDB。

:::observed
Chrome DevTools 的 Application 面板没有 OPFS 查看器，文件存在的唯一内置证据是 Application > Storage 中 **Usage** 数值在写入后发生变化；Safari 的 Web Inspector 同样不列出 OPFS 条目。实用的检查是在 Console 里执行一行 `(await (await (await navigator.storage.getDirectory()).getFileHandle('app.log')).getFile()).size`，它返回字节数，或抛出 `NotFoundError: A requested file or directory could not be found at the time an operation was processed.`，即 Chromium 为 `NotFoundError` 定义的默认 `DOMException` 文本。
:::

## 另请参阅

- [File System Standard: createSyncAccessHandle()](https://fs.spec.whatwg.org/#api-filesystemfilehandle-createsyncaccesshandle)（whatwg.org）
- [Origin private file system](https://developer.mozilla.org/en-US/docs/Web/API/File_System_API/Origin_private_file_system)（developer.mozilla.org）
- [File System Access API](/zh/reference/capabilities/file-system-access/)
- [StorageManager.estimate()](/zh/reference/storage/quota-estimate/)
- [IndexedDB](/zh/reference/storage/indexeddb/)