带有源私有文件系统的文件系统API

应用程序与本地文件交互非常常见。例如,一个常见的工作流程是打开文件、进行一些更改并保存文件。对于Web应用程序来说,这可能很难实现。可以使用IndexedDB API、带有“file”类型的HTML输入元素、带有download属性的HTML锚点元素等来模拟文件操作,但这需要对这些标准有很好的理解,并需要精心设计以提供良好的用户体验。此外,对于频繁操作和大文件,性能可能不尽如人意。

文件系统API使Web应用程序能够轻松高效地访问文件。它提供了一种直接创建、打开、读取和写入文件的方式。此外,它还允许应用程序创建目录并枚举其内容。

源私有文件系统

WebKit已添加对文件系统标准的支持。这提供了一个源私有文件系统——一个指向某个的私有存储端点。从概念上讲,每个源都拥有一个独立的目录,页面只能访问其源目录中的文件或目录。例如,https://webkit.ac.cn无法读取由https://apple.com创建的文件。

根据不同浏览器的实现,源私有文件系统中的一个条目不一定映射到用户本地文件系统中的条目——它可以是存储在某个数据库中的对象。这意味着通过文件系统API创建的文件或目录可能无法轻易地从浏览器外部检索到。

持久性

该API目前在Safari的隐私浏览模式窗口中不可用。在可用的情况下,其存储生命周期与其他持久性存储类型(如IndexedDB和localStorage)相同。存储策略将符合存储标准。Safari用户可以通过macOS上的“偏好设置”或iOS上的“设置”来查看和删除某个站点的文件系统存储。

浏览器支持

242951@main版本开始,WebKit中已启用带有源私有文件系统的文件系统。它在Safari中可用,适用于:

  • macOS 12.2及更高版本
  • iOS 15.2及更高版本

在macOS 12.4和iOS 15.4上的Safari中,我们引入了FileSystemFileHandle的getFile()方法。

API

WebKit目前支持文件系统标准的四种接口:

  • FileSystemHandle,它表示文件系统中的一个条目。它在Worker中可用,并且
  • FileSystemFileHandle,它继承自FileSystemHandle并表示一个文件条目。
  • FileSystemDirectoryHandle,它继承自FileSystemHandle并表示一个目录条目。
  • FileSystemSyncAccessHandle,它提供一个用于同步读写条目的独占双工流。与上述接口(它们在Window和Worker上下文中都存在)不同,FileSystemSyncAccessHandle仅在Worker中可用。

了解了这些基本接口后,让我们通过一些示例来了解如何使用它们。

示例

访问源私有文件系统

在源私有文件系统中,FileSystemHandle表示源空间的根目录,或根目录的后代。因此,第一步是获取根FileSystemDirectoryHandle。这通过StorageManager接口完成。

const root = await navigator.storage.getDirectory();

创建目录或文件

使用像root这样的FileSystemDirectoryHandle对象,您可以使用getDirectoryHandle()getFileHandle()方法访问其具有特定名称的子级。

// Create a file named *Untiled.txt* under root directory.
const untitledFile = await root.getFileHandle("Untitled.txt", { "create" : true });
// Get access to existing *Untitled.txt* file.
// untitledFile and existingUntitledFile point to the same entry.
const existingUntitledFile = await root.getFileHandle("Untitled.txt");
// Create a directory named *Diary Folder*.
const diaryDirectory = await root.getDirectoryHandle("Diary Folder", { "create" : true });

移动或重命名目录或文件

要移动FileSystemHandle所代表的文件或目录,您可以使用move()方法。第一个参数是表示目标父目录的FileSystemDirectoryHandle,第二个参数是表示目标文件名的USVString。该字符串必须是有效的文件名

// Move *Untitled.txt* from /root/ to /root/Diary Folder/.
await untitledFile.move(diaryDirectory, untitledFile.name);
// Rename *Untitled.txt* to *Feb_01.txt*
await untitledFile.move(diaryDirectory, "Feb_01.txt");
// The two steps above can be combined as:
// await untitledFile.move(diaryDirectory, "Feb_01.txt");

解析从目录条目到其后代的路径

要判断一个FileSystemHandle是否是现有FileSystemDirectoryHandle的后代,并获取它们的相对路径,您可以使用resolve()方法。结果是一个组件名称数组,它们构成了路径。

// Get access to *`Feb_01.txt`* in *Diary Folder*.
const diaryFile = await diaryDirectory.getFileHandle("Feb_01.txt");
// Resolve path between Feb_01.txt and root.
const relativePath = await root.resolve(diaryFile);
// relativePath is ["Diary Folder", "Feb_01.txt"].

枚举目录中的内容

上面介绍的方法需要您知道目标的名称,但如果您不知道名称,仍然可以通过`keys()`、`values()`和`entries()`方法返回的异步迭代器来枚举现有目录的内容以获取它。

// Create a directory named `*Trash*` under root directory.
const trashDirectory = await root.getDirectoryHandle("Trash", { "create" : true });
// Find directories under root/ and print their names.
const directoryNames = [];
for await (const handle of root.values()) {
    if (handle.kind == "directory") {
        directoryNames.push(handle.name);
    }
}
// directoryNames is ["Trash", "Diary Folder"].

删除目录或文件

使用FileSystemDirectoryHandle对象,您可以使用removeEntry()方法按名称删除其子条目。

// Delete *Feb_01.txt* in *Diary Folder*.
await diaryDirectory.removeEntry(diaryFile.name);
// Delete *Trash* and all its descendants.
await root.removeEntry(trashDirectory.name, { "recursive" : true });

读取文件

一旦您拥有代表目标文件的FileSystemFileHandle,您就可以使用getFile()方法将其转换为File对象,从而读取其属性和内容。您可以使用File的接口获取文件信息和内容。

const fileHandle = await root.getFileHandle("Draft.txt", { "create" : true });
const file = await fileHandle.getFile();

在Worker线程中读写文件

读取文件的另一种方法是使用`FileSystemSyncAccessHandle`接口的`read()`方法。您可以使用`createSyncAccessHandle()`方法从`FileSystemFileHandle`对象创建`FileSystemSyncAccessHandle`。由于`FileSystemSyncAccessHandle`仅在Worker上下文中可用,因此您需要首先创建一个专用Worker

与返回Promise的`getFile()`不同,`read()`是同步的,因此提供了更好的性能。如果您的目标是最高效的文件访问,那么`FileSystemSyncAccessHandle`是首选。

要写入文件,您可以使用`FileSystemSyncAccessHandle`的同步`write()`方法。在当前实现中,这是在WebKit中写入文件的唯一方法。

为了实现同步读写操作,`FileSystemSyncAccessHandle`必须对文件条目具有独占访问权限。因此,如果前一个`FileSystemSyncAccessHandle`未正确关闭,则尝试在同一条目上创建第二个`FileSystemSyncAccessHandle`将失败。

// Get access to the existing `*Draft.txt* file`.
const root = await navigator.storage.getDirectory();
const draftFile = await root.getFileHandle("Draft.txt");
// Create FileSystemSyncAccessHandle on the file.
const accessHandle = await draftFile.createSyncAccessHandle();
// Get size of the file.
const fileSize = accessHandle.getSize();
// Read file content to a buffer.
const readBuffer = new ArrayBuffer(fileSize);
const readSize = accessHandle.read(readBuffer, { "at": 0 });
// Write a sentence to the end of the file.
const encoder = new TextEncoder();
const writeBuffer = encoder.encode("Thank you for reading this.");
const writeSize = accessHandle.write(writeBuffer, { "at" : readSize });
// Truncate file to 1 byte.
accessHandle.truncate(1);
// Persist changes to disk.
accessHandle.flush();
// Always close FileSystemSyncAccessHandle if done.
accessHandle.close();

总结

如果您的Web应用程序需要与文件交互,您应该尝试新的文件系统API。它提供了与原生文件系统API类似的接口,并具有优化的性能。

随着标准的演进和我们的开发进展,我们将根据文件系统标准持续添加或更新接口和方法。如果您在使用此API时遇到任何问题,请在bugs.webkit.org的“Website Storage”组件下提交错误报告。您也可以提交新的错误报告作为功能请求,描述您的用例以及为什么该功能很重要。如果您对API本身有任何问题或建议,可以在WICG仓库中提交规范问题。您的反馈对我们非常重要。