源私有文件系统

Baseline 已广泛支持

此功能已成熟,并可在许多设备和浏览器版本上运行。自 2023 年 3 月以来,它已在各种浏览器中可用。

安全上下文: 此功能仅在安全上下文(HTTPS)中可用,且支持此功能的浏览器数量有限。

注意:此功能在 Web Workers 中可用。

源私有文件系统(OPFS)是作为文件系统 API 的一部分提供的存储端点,它仅对页面的源可见,不像常规文件系统那样对用户可见。它提供了对一种特殊文件的访问,这种文件在性能方面经过高度优化,并允许对其内容进行原地写入访问。

使用文件系统访问 API 处理文件

扩展了文件系统 API文件系统访问 API,通过选择器方法提供对文件的访问。例如:

  1. Window.showOpenFilePicker() 允许用户选择要访问的文件,这将返回一个FileSystemFileHandle 对象。
  2. 调用FileSystemFileHandle.getFile() 来访问文件内容,然后使用FileSystemFileHandle.createWritable() / FileSystemWritableFileStream.write() 修改内容。
  3. 使用FileSystemHandle.requestPermission({mode: 'readwrite'}) 请求用户保存更改的权限。
  4. 如果用户接受权限请求,更改将保存回原始文件。

这可行,但有一些限制。这些更改是针对用户可见的文件系统进行的,因此存在许多安全检查(例如,Chrome 中的安全浏览)来防止恶意内容写入该文件系统。这些写入不是原地进行的,而是使用临时文件。原始文件只有在通过所有安全检查后才会被修改。

因此,这些操作相当缓慢。在进行小的文本更新时,这不太明显,但在进行更重大的、大规模的文件更新时,例如SQLite 数据库修改时,性能会受到影响。

OPFS 如何解决这些问题?

OPFS 提供低级别的、逐字节的文件访问,它仅对页面的源可见,对用户不可见。因此,它不需要同样一系列的安全检查和权限授予,从而比文件系统访问 API 调用更快。它还提供了一组同步调用(其他文件系统 API 调用是异步的),这些调用只能在 Web Worker 中运行,以免阻塞主线程。

总结 OPFS 与用户可见文件系统的区别

  • 与任何其他源分区存储机制(例如IndexedDB API)一样,OPFS 受浏览器存储配额限制。您可以通过navigator.storage.estimate() 访问 OPFS 使用的存储空间量。
  • 清除网站的存储数据会删除 OPFS。
  • 访问 OPFS 中的文件不需要权限提示和安全检查。
  • 浏览器将 OPFS 的内容持久化到磁盘的某个位置,但您不能期望找到一一对应的创建的文件。OPFS 不打算对用户可见。

如何访问 OPFS?

要访问 OPFS,首先需要调用navigator.storage.getDirectory() 方法。这将返回一个FileSystemDirectoryHandle 对象引用,该对象代表 OPFS 的根目录。

在主线程中操作 OPFS

从主线程访问 OPFS 时,将使用异步的、基于Promise 的 API。通过在代表 OPFS 根目录(以及创建的子目录)的FileSystemDirectoryHandle 对象上调用FileSystemDirectoryHandle.getFileHandle()FileSystemDirectoryHandle.getDirectoryHandle(),可以分别访问文件(FileSystemFileHandle)和目录(FileSystemDirectoryHandle)句柄。

注意:{ create: true } 传递给上述方法会导致在文件或文件夹不存在时创建它们。

js
// Create a hierarchy of files and folders
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 },
);

// Access existing files and folders via their names
const existingFileHandle = await opfsRoot.getFileHandle("my first file");
const existingDirectoryHandle =
  await opfsRoot.getDirectoryHandle("my first folder");

读取文件

  1. 调用FileSystemDirectoryHandle.getFileHandle() 返回一个FileSystemFileHandle 对象。
  2. 调用FileSystemFileHandle.getFile() 对象返回一个File 对象。这是一种特殊的Blob 类型,因此可以像任何其他 Blob 一样对其进行操作。例如,您可以通过Blob.text() 直接访问文本内容。

写入文件

  1. 调用FileSystemDirectoryHandle.getFileHandle() 返回一个FileSystemFileHandle 对象。
  2. 调用FileSystemFileHandle.createWritable() 返回一个FileSystemWritableFileStream 对象,这是一种特殊的WritableStream 类型。
  3. 使用FileSystemWritableFileStream.write() 调用向其中写入内容。
  4. 使用WritableStream.close() 关闭流。

删除文件或文件夹

您可以对父目录调用FileSystemDirectoryHandle.removeEntry(),并将要删除的项的名称作为参数传递。

js
directoryHandle.removeEntry("my first nested file");

您还可以对代表要删除的项的FileSystemFileHandleFileSystemDirectoryHandle 调用FileSystemHandle.remove()。要删除一个文件夹及其所有子文件夹,请传递 { recursive: true } 选项。

js
await fileHandle.remove();
await directoryHandle.remove({ recursive: true });

以下提供了一种快速清空整个 OPFS 的方法

js
await (await navigator.storage.getDirectory()).remove({ recursive: true });

列出文件夹内容

FileSystemDirectoryHandle 是一个异步迭代器。因此,您可以使用 for await...of 循环和标准方法(如entries()values()keys())对其进行迭代。

例如

js
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()) {
}

在 Web Worker 中操作 OPFS

Web Workers 不会阻塞主线程,这意味着您可以在此上下文中执行同步文件访问 API。同步 API 更快,因为它们避免了处理 Promise。

您可以通过在常规FileSystemFileHandle 上调用FileSystemFileHandle.createSyncAccessHandle() 来同步访问文件。

注意:尽管其名称中包含“Sync”(同步),但 createSyncAccessHandle() 方法本身是异步的。

js
const opfsRoot = await navigator.storage.getDirectory();
const fileHandle = await opfsRoot.getFileHandle("my-high-speed-file.txt", {
  create: true,
});
const syncAccessHandle = await fileHandle.createSyncAccessHandle();

在返回的FileSystemSyncAccessHandle 上有许多*同步*方法可用:

  • getSize():返回文件的大小(以字节为单位)。
  • write():将缓冲区的内容写入文件,可以选择在给定偏移量处写入,并返回写入的字节数。检查返回的写入字节数允许调用者检测和处理错误及部分写入。
  • read():将文件内容读取到缓冲区中,可以选择在给定偏移量处读取。
  • truncate():将文件大小调整为给定大小。
  • flush():确保文件内容包含通过 write() 进行的所有修改。
  • close():关闭访问句柄。

以下是一个使用上述所有方法的示例

js
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, { at: 0 });
// 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);

浏览器兼容性

另见