# File System Access API：读写本地文件

> showOpenFilePicker/showSaveFilePicker/showDirectoryPicker 的工作方式、文件与目录句柄、权限模型、句柄持久化，以及 Safari/Firefox 回退方案。

import CompatTable from '@components/CompatTable.astro';

**一句话：** File System Access API 让 Web 应用通过 `showOpenFilePicker`、
`showSaveFilePicker` 与 `showDirectoryPicker` 打开、编辑并保存用户设备上的真实文件与
文件夹。它需要安全上下文与用户手势，每次写入都受显式权限提示约束，且仅在 Chromium
浏览器中提供——所以 Safari 与 Firefox 需要回退方案。

## 选择器与句柄

- **`showOpenFilePicker()`** 返回一组 `FileSystemFileHandle`。调用 `handle.getFile()`
  读取 `File`（一个 `Blob`）——用 `.text()`、`.arrayBuffer()` 或 `.stream()` 获取内容。
- **`showSaveFilePicker()`** 返回一个指向新建或选定文件的 `FileSystemFileHandle`。调用
  `handle.createWritable()` 获取 `FileSystemWritableFileStream`，向其 `write()`，再
  `close()` 提交。写入先进临时文件，关闭时原子替换目标文件。
- **`showDirectoryPicker()`** 返回一个 `FileSystemDirectoryHandle`。用
  `for await (const [name, handle] of dir.entries())` 遍历条目，并通过
  `getFileHandle(name, { create })` / `getDirectoryHandle(name, { create })` 创建或获取子项。

三个选择器都是异步的，用户取消时以 `AbortError` 拒绝，且必须在安全上下文（HTTPS 或
`localhost`）中由用户手势（点击、按键）触发。

## 权限模型

- 用户选择文件或文件夹时即授予读取权限。写入会再次提示——首次 `createWritable()`
  （或显式 `requestPermission({ mode: 'readwrite' })`）会触发权限对话框。
- 用 `handle.queryPermission({ mode })` 在不提示的情况下查询当前状态；用
  `handle.requestPermission({ mode })`（必须在用户手势内）请求权限。两者都解析为
  `'granted'`、`'denied'` 或 `'prompt'`。
- 授权按源（origin）限定且并非永久——通常只在标签页/会话期间有效，页面完全关闭后
  重置，因此再次访问时重新提示是预期行为。
- 某些敏感位置（系统文件夹、某些情况下的下载目录）会被直接禁止访问。

## 跨会话持久化句柄

`FileSystemFileHandle` 与 `FileSystemDirectoryHandle` 是**可结构化克隆的**，因此你可以
把它们存入 IndexedDB，并在之后的访问中取回——用户无需重新选择同一文件。句柄会持久化，
但权限不会：恢复句柄后，需调用 `queryPermission()`，并在必要时由新的用户手势触发
`requestPermission()` 再进行读写。这一模式正是 Web 端编辑器与 IDE 中"最近文件"列表的
实现基础。

## 源私有文件系统（OPFS）

OPFS 是本 API 中广泛可用的子集。`navigator.storage.getDirectory()` 返回一个根植于私有、
按源限定沙箱的 `FileSystemDirectoryHandle`——无选择器、无权限提示，且不在用户文件管理器
中可见。它在**包括 Safari 与 Firefox 在内的所有现代引擎**中均可用，支持通过
`createSyncAccessHandle()` 在 Web Worker 中进行高性能同步访问，非常适合浏览器内 SQLite、
缓存与大型工作数据。需要快速本地存储时用 OPFS；需要用户看见并拥有真实文件时用选择器。

## 浏览器与生态支持

<CompatTable feature="file-system-access" />

## 决策判定框架

| 决策问题 | 建议行为 | 理由 |
|---|---|---|
| 需要用户打开并重新保存*自己的*文件（编辑器、IDE）？ | 用选择器 + 把句柄存入 IndexedDB。 | 真正的就地编辑；"最近文件"可跨会话工作。 |
| 同时面向 Safari 或 Firefox？ | 用 `<input type="file">` 打开、用 `<a download>` blob URL 保存作为回退。 | 选择器仅限 Chromium；此方案覆盖所有引擎且体验平滑。 |
| 只需快速私有存储（缓存、数据库、暂存空间）？ | 用 `navigator.storage.getDirectory()` 走 OPFS。 | 随处可用、无提示、Worker 同步访问保证性能。 |
| 写入大文件或流式输出？ | `createWritable()` 分块 `write()`，再 `close()`。 | 关闭时原子替换；避免把整个文件缓冲进内存。 |
| 在之后的访问中恢复已保存句柄？ | 在点击处理器内 `queryPermission()`，再 `requestPermission()`。 | 句柄持久但授权不持久；必须重新提示。 |

## 实践清单

- [ ] 仅在用户手势内、且仅在 HTTPS/`localhost` 下调用选择器。
- [ ] 捕获 `AbortError`——用户取消选择器是正常现象，不是错误。
- [ ] 始终 `close()` 可写流，以便原子写入提交。
- [ ] 把句柄存入 IndexedDB，但返回时用 `query`/`requestPermission` 重新核验权限。
- [ ] 为 Safari 与 Firefox 提供 `<input type="file">` + 下载链接回退。
- [ ] 需要私有、跨引擎、高性能存储而非用户可见文件时，选用 OPFS。