File System Access API:简化对本地文件的访问

借助 File System Access API,Web 应用可以直接读取用户设备上的文件和文件夹,或保存对文件和文件夹的更改。

什么是 File System Access API?

借助 File System Access API,开发者可以构建功能强大的 Web 应用, 用户本地设备上的文件,例如 IDE、照片和视频编辑器、文本编辑器等。更新后 当用户向 Web 应用授予访问权限时,此 API 可让用户直接读取或保存对文件的更改,以及 文件夹。除了读取和写入文件以外,File System Access API 还提供了 能够打开目录并枚举其内容。

如果你以前读写过文件,那么我今天要分享的大部分内容 但还是建议您继续阅读,因为并非所有系统都是一样的。

大多数 Chromium 浏览器都支持 File System Access API, Windows、macOS、ChromeOS 和 Linux。一个值得注意的例外情况是 Brave, 目前仅在一个标志后面可用。我们正在通过 crbug.com/1011535 发布对 Android 的支持。

使用 File System Access API

为了展示 File System Access API 的强大功能和实用性,我编写了一个单文件文本 编辑器。它支持打开文本文件、编辑文件、将更改保存回磁盘或开始 一个新文件并将更改保存到磁盘。虽然没有什么可言而喻,但能提供充分的帮助 了解这些概念。

浏览器支持

浏览器支持

  • Chrome:86。 <ph type="x-smartling-placeholder">
  • Edge:86。 <ph type="x-smartling-placeholder">
  • Firefox:不支持。 <ph type="x-smartling-placeholder">
  • Safari:不支持。 <ph type="x-smartling-placeholder">

来源

功能检测

如需了解系统是否支持 File System Access API,请检查选择器方法 您感兴趣的内容。

if ('showOpenFilePicker' in self) {
  // The `showOpenFilePicker()` method of the File System Access API is supported.
}

试试看

请参阅 文本编辑器演示。

从本地文件系统中读取文件

我要处理的第一个用例是要求用户选择一个文件,然后打开并读取该文件 从磁盘读取文件。

让用户选择要读取的文件

File System Access API 的入口点是 window.showOpenFilePicker()。被调用时,它会显示一个文件选择器对话框, 并提示用户选择文件。用户选择文件后,该 API 会返回一个文件数组 标识名。您可以使用可选的 options 参数来影响文件选择器的行为,例如 例如,允许用户选择多个文件、目录或不同的文件类型。 在未指定任何选项的情况下,文件选择器允许用户选择单个文件。这是 非常适合文本编辑器。

与许多其他强大的 API 一样,调用 showOpenFilePicker() 必须在安全的 上下文进行调用,必须从用户手势中调用。

let fileHandle;
butOpenFile.addEventListener('click', async () => {
  // Destructure the one-element array.
  [fileHandle] = await window.showOpenFilePicker();
  // Do something with the file handle.
});

用户选择文件后,showOpenFilePicker() 会返回一个句柄数组,在本例中为 一个单元素数组,其中有一个 FileSystemFileHandle,该数组包含属性和 与文件交互所需的方法。

最好保留对文件句柄的引用,以便日后使用。时间是 保存对文件所做的更改或执行任何其他文件操作所需的操作。

从文件系统中读取文件

现在,您已经有了文件的句柄,可以获取文件的属性,或者访问文件本身。 现在,我会读出里面的内容。调用 handle.getFile() 会返回 File 对象,其中包含一个 Blob。要从 Blob 中获取数据,请调用 Blob 的其中一个 方法slice()stream(), text(),或 arrayBuffer())。

const file = await fileHandle.getFile();
const contents = await file.text();

FileSystemFileHandle.getFile() 返回的 File 对象只有 没有更改如果磁盘上的文件被修改,File 对象会变为 不可读取,您需要再次调用 getFile() 以获取新的 File 对象来读取更改后的内容 数据。

综合应用

当用户点击 Open 按钮时,浏览器会显示一个文件选择器。用户选择文件后, 应用读取内容,并将其放入 <textarea> 中。

let fileHandle;
butOpenFile.addEventListener('click', async () => {
  [fileHandle] = await window.showOpenFilePicker();
  const file = await fileHandle.getFile();
  const contents = await file.text();
  textArea.value = contents;
});

将文件写入本地文件系统

在文本编辑器中,有两种保存文件的方法:保存另存为保存 使用之前检索到的文件句柄将更改写回原始文件。但节省 As 会创建一个新文件,因此需要新的文件句柄。

创建新文件

如需保存文件,请调用 showSaveFilePicker(),这样会显示文件选择器 在“保存”中模式,以便用户选择要用于保存的新文件。对于文本 我还希望它自动添加 .txt 扩展程序,因此我提供了一些额外的 参数。

async function getNewFileHandle() {
  const options = {
    types: [
      {
        description: 'Text Files',
        accept: {
          'text/plain': ['.txt'],
        },
      },
    ],
  };
  const handle = await window.showSaveFilePicker(options);
  return handle;
}

将更改保存到磁盘

如需查找用于保存对文件所做的更改的所有代码,请访问文本编辑器 GitHub。核心文件系统交互包括 fs-helpers.js。简单来说,该过程如以下代码所示。 我将逐一演示每个步骤并加以说明。

// fileHandle is an instance of FileSystemFileHandle..
async function writeFile(fileHandle, contents) {
  // Create a FileSystemWritableFileStream to write to.
  const writable = await fileHandle.createWritable();
  // Write the contents of the file to the stream.
  await writable.write(contents);
  // Close the file and write the contents to disk.
  await writable.close();
}

将数据写入磁盘会使用 FileSystemWritableFileStream 对象(子类) 共 WritableStream 个。通过对文件调用 createWritable() 来创建数据流 处理对象。调用 createWritable() 时,浏览器会首先检查用户是否已授予 写入权限。如果未授予写入权限,浏览器会提示 向用户请求权限。如果未授予权限,createWritable() 会抛出 DOMException,并且该应用无法向该文件写入数据。在文本编辑器中, DOMException 对象通过 saveFile() 方法进行处理。

write() 方法接受一个字符串,这是文本编辑器所需的字符串。但也可能需要 BufferSourceBlob。例如,您可以通过管道 :

async function writeURLToFile(fileHandle, url) {
  // Create a FileSystemWritableFileStream to write to.
  const writable = await fileHandle.createWritable();
  // Make an HTTP request for the contents.
  const response = await fetch(url);
  // Stream the response into the file.
  await response.body.pipeTo(writable);
  // pipeTo() closes the destination pipe by default, no need to close it.
}

你还可以在信息流中点击 seek()truncate(),以更新 或调整文件的大小。

指定建议的文件名和启动目录

在许多情况下,您可能希望应用提供默认文件名或位置建议。例如,一段文本 编辑器可能想要建议一个默认文件名,即 Untitled Text.txt,而不是 Untitled。您 可以通过将 suggestedName 属性作为 showSaveFilePicker 选项的一部分传递来实现此目的。

const fileHandle = await self.showSaveFilePicker({
  suggestedName: 'Untitled Text.txt',
  types: [{
    description: 'Text documents',
    accept: {
      'text/plain': ['.txt'],
    },
  }],
});

默认启动目录也是如此。如果您正在构建文本编辑器,则可能需要 在默认的 documents 文件夹中启动文件保存或文件打开对话框,而对于图片 不妨从默认的 pictures 文件夹中启动。您可以建议默认启动时间 方法是将 startIn 属性传递给 showSaveFilePickershowDirectoryPicker()showOpenFilePicker 方法,如下所示。

const fileHandle = await self.showOpenFilePicker({
  startIn: 'pictures'
});

下面是众所周知的系统目录列表:

  • desktop:用户的桌面目录(如果存在)。
  • documents:用户创建的文档通常用于存储的目录。
  • downloads:通常用于存储已下载文件的目录。
  • music:通常用于存储音频文件的目录。
  • pictures:通常用于存储照片和其他静态图片的目录。
  • videos:通常用于存储视频或电影的目录。

除了众所周知的系统目录之外,您还可以将现有的文件或目录句柄作为 startIn 的值。然后,此对话框就会在同一目录中打开。

// Assume `directoryHandle` is a handle to a previously opened directory.
const fileHandle = await self.showOpenFilePicker({
  startIn: directoryHandle
});

指定不同文件选择器的用途

有时,应用会出于不同目的而使用不同的选择器。例如,富文本 编辑器可以允许用户打开文本文件,还允许导入图片。默认情况下,每个文件 选择器就会在您最后一次记住的位置打开。您可以通过存储 id 值来规避此问题。 为每种类型的选择器选择如果指定了 id,文件选择器实现会记住 该 id 的上次使用的目录。

const fileHandle1 = await self.showSaveFilePicker({
  id: 'openText',
});

const fileHandle2 = await self.showSaveFilePicker({
  id: 'importImage',
});

在 IndexedDB 中存储文件句柄或目录句柄

文件句柄和目录句柄是可序列化的,这意味着您可以保存文件或 将目录句柄添加到 IndexedDB,或调用 postMessage() 在同一顶级数据库之间发送这些路径和 来源。

将文件或目录句柄保存到 IndexedDB 意味着您可以存储状态,或记住 文件或目录这样,您就能以列表形式保存 或编辑过的文件,在应用打开时询问是否重新打开最后一个文件,恢复以前工作的 目录等我会在文本编辑器中存储用户最近访问过的五个文件的列表 已打开,这样即可再次访问这些文件。

以下代码示例展示了如何存储和检索文件句柄和目录句柄。您可以 请在 Glitch 上查看实际应用示例。(我使用的是 idb-keyval 库)

import { get, set } from 'https://unpkg.com/idb-keyval@5.0.2/dist/esm/index.js';

const pre1 = document.querySelector('pre.file');
const pre2 = document.querySelector('pre.directory');
const button1 = document.querySelector('button.file');
const button2 = document.querySelector('button.directory');

// File handle
button1.addEventListener('click', async () => {
  try {
    const fileHandleOrUndefined = await get('file');
    if (fileHandleOrUndefined) {
      pre1.textContent = `Retrieved file handle "${fileHandleOrUndefined.name}" from IndexedDB.`;
      return;
    }
    const [fileHandle] = await window.showOpenFilePicker();
    await set('file', fileHandle);
    pre1.textContent = `Stored file handle for "${fileHandle.name}" in IndexedDB.`;
  } catch (error) {
    alert(error.name, error.message);
  }
});

// Directory handle
button2.addEventListener('click', async () => {
  try {
    const directoryHandleOrUndefined = await get('directory');
    if (directoryHandleOrUndefined) {
      pre2.textContent = `Retrieved directroy handle "${directoryHandleOrUndefined.name}" from IndexedDB.`;
      return;
    }
    const directoryHandle = await window.showDirectoryPicker();
    await set('directory', directoryHandle);
    pre2.textContent = `Stored directory handle for "${directoryHandle.name}" in IndexedDB.`;
  } catch (error) {
    alert(error.name, error.message);
  }
});

存储的文件或目录句柄和权限

由于权限并非总是在会话之间持久保留,因此您应验证用户是否 已使用 queryPermission() 授予对文件或目录的权限。否则,请调用 requestPermission() 来(重新)请求该请求。这对文件和目录句柄是相同的。您 需要运行 fileOrDirectoryHandle.requestPermission(descriptor)fileOrDirectoryHandle.queryPermission(descriptor)

在文本编辑器中,我创建了一个 verifyPermission() 方法,用于检查用户是否已 并视需要发出请求。

async function verifyPermission(fileHandle, readWrite) {
  const options = {};
  if (readWrite) {
    options.mode = 'readwrite';
  }
  // Check if permission was already granted. If so, return true.
  if ((await fileHandle.queryPermission(options)) === 'granted') {
    return true;
  }
  // Request permission. If the user grants permission, return true.
  if ((await fileHandle.requestPermission(options)) === 'granted') {
    return true;
  }
  // The user didn't grant permission, so return false.
  return false;
}

通过使用读取请求请求写入权限,我减少了权限提示的数量; 用户在打开文件时会看到一条提示,并且向其授予读取和写入权限。

打开目录并枚举其内容

如需枚举目录中的所有文件,请调用 showDirectoryPicker()。用户 在选择器中选择目录后,系统会先后 FileSystemDirectoryHandle 返回,以便枚举和访问该目录的文件。默认情况下,您已经阅读了 对目录中的文件的访问权限,但如果需要写入权限,可以传递 { mode: 'readwrite' }

butDir.addEventListener('click', async () => {
  const dirHandle = await window.showDirectoryPicker();
  for await (const entry of dirHandle.values()) {
    console.log(entry.kind, entry.name);
  }
});

如果您还需要使用 getFile() 访问每个文件,例如需要获取单独的 文件大小,不要依序对每个结果使用 await,而是应以 例如使用 Promise.all()

butDir.addEventListener('click', async () => {
  const dirHandle = await window.showDirectoryPicker();
  const promises = [];
  for await (const entry of dirHandle.values()) {
    if (entry.kind !== 'file') {
      continue;
    }
    promises.push(entry.getFile().then((file) => `${file.name} (${file.size})`));
  }
  console.log(await Promise.all(promises));
});

在目录中创建或访问文件和文件夹

在目录中,您可以使用 getFileHandle() 或分别为 getDirectoryHandle() 方法。传入一个可选的 options 对象,其键为 create,布尔值为 truefalse,您可以确定是否应创建新文件或文件夹(如果该文件或文件夹不存在)。

// In an existing directory, create a new directory named "My Documents".
const newDirectoryHandle = await existingDirectoryHandle.getDirectoryHandle('My Documents', {
  create: true,
});
// In this new directory, create a file named "My Notes.txt".
const newFileHandle = await newDirectoryHandle.getFileHandle('My Notes.txt', { create: true });

解析目录中项的路径

处理目录中的文件或文件夹时,解析相应项的路径非常有用 问题。这可以通过适当命名的 resolve() 方法来实现。为解决此问题, 项可以是目录的直接或间接子级。

// Resolve the path of the previously created file called "My Notes.txt".
const path = await newDirectoryHandle.resolve(newFileHandle);
// `path` is now ["My Documents", "My Notes.txt"]

删除目录中的文件和文件夹

如果您已获得某个目录的访问权限,则可以使用 removeEntry() 方法。对于文件夹,可以选择递归删除,并包括 所有子文件夹及其中包含的文件。

// Delete a file.
await directoryHandle.removeEntry('Abandoned Projects.txt');
// Recursively delete a folder.
await directoryHandle.removeEntry('Old Stuff', { recursive: true });

直接删除文件或文件夹

如果您有权访问某个文件或目录句柄,请对 FileSystemFileHandle 调用 remove(),或 按 FileSystemDirectoryHandle 可将其移除。

// Delete a file.
await fileHandle.remove();
// Delete a directory.
await directoryHandle.remove();

重命名和移动文件和文件夹

您可以对文件和文件夹调用 move(),以重命名或移动到新位置 FileSystemHandle 接口。FileSystemHandle 具有子接口 FileSystemFileHandleFileSystemDirectoryHandlemove() 方法接受一个或两个参数。第一种方式可以是 是具有新名称的字符串或目标文件夹的 FileSystemDirectoryHandle。在 在后一种情况下,可选的第二个参数是一个具有新名称的字符串,因此移动和重命名 一步完成。

// Rename the file.
await file.move('new_name');
// Move the file to a new directory.
await file.move(directory);
// Move the file to a new directory and rename it.
await file.move(directory, 'newer_name');

拖放集成

通过 HTML 拖放界面 启用 Web 应用以接受 拖放的文件 。在执行拖放操作的过程中,拖动的文件和目录项会 分别是文件条目和目录条目DataTransferItem.getAsFileSystemHandle() 如果拖动的项是文件,该方法会返回带有 FileSystemFileHandle 对象的 promise promise 与 FileSystemDirectoryHandle 对象相关联(如果拖动的项是目录)。以下列表 显示了实际操作示例。请注意,拖放界面的 DataTransferItem.kind"file"(针对文件和目录),而 File System Access API 的 FileSystemHandle.kind "file" 表示文件,"directory" 表示目录。

elem.addEventListener('dragover', (e) => {
  // Prevent navigation.
  e.preventDefault();
});

elem.addEventListener('drop', async (e) => {
  e.preventDefault();

  const fileHandlesPromises = [...e.dataTransfer.items]
    .filter((item) => item.kind === 'file')
    .map((item) => item.getAsFileSystemHandle());

  for await (const handle of fileHandlesPromises) {
    if (handle.kind === 'directory') {
      console.log(`Directory: ${handle.name}`);
    } else {
      console.log(`File: ${handle.name}`);
    }
  }
});

访问源专用文件系统

源专用文件系统是一个存储端点,顾名思义,就是 网页的来源虽然浏览器通常通过在 YAML 文件中持久保留此 源私有文件系统转移到某处的磁盘,那么相应内容是被用户 可访问性。同样,不要期望文件名与名称中的 原始私有文件系统的子级的名称存在。虽然浏览器可能看起来 还是内部存在文件(由于这是一个源私有文件系统),因此浏览器可能会存储 这些“文件”数据库或任何其他数据结构中的代码。从本质上讲,如果您使用此 API 不认为在硬盘上的某处找到创建的文件一对一匹配。您可以在 访问根 FileSystemDirectoryHandle 后,即可访问源专用文件系统。

const root = await navigator.storage.getDirectory();
// Create a new file handle.
const fileHandle = await root.getFileHandle('Untitled.txt', { create: true });
// Create a new directory handle.
const dirHandle = await root.getDirectoryHandle('New Folder', { create: true });
// Recursively remove a directory.
await root.removeEntry('Old Stuff', { recursive: true });

浏览器支持

  • Chrome:86。 <ph type="x-smartling-placeholder">
  • Edge:86。 <ph type="x-smartling-placeholder">
  • Firefox:111。 <ph type="x-smartling-placeholder">
  • Safari:15.2. <ph type="x-smartling-placeholder">

来源

通过源专用文件系统访问针对性能进行了优化的文件

源私有文件系统提供对一种特殊类型的文件的可选访问,该类型文件高度 针对性能进行了优化,例如,通过提供对文件目录的就地和独占写入访问 内容。在 Chromium 102 及更高版本中,源专用文件系统中还有一个适用于 简化文件访问:createSyncAccessHandle()(用于同步读写操作)。 它在 FileSystemFileHandle 上公开,但仅在 Web Worker

// (Read and write operations are synchronous,
// but obtaining the handle is asynchronous.)
// Synchronous access exclusively in Worker contexts.
const accessHandle = await fileHandle.createSyncAccessHandle();
const writtenBytes = accessHandle.write(buffer);
const readBytes = accessHandle.read(buffer, { at: 1 });

Polyfilling

无法完全对 File System Access API 方法执行 polyfill 操作。

  • showOpenFilePicker() 方法可以用 <input type="file"> 元素近似计算。
  • 您可以使用 <a download="file_name"> 元素模拟 showSaveFilePicker() 方法, 但这会触发程序化下载,并且不允许覆盖现有文件。
  • 可以使用非标准 showDirectoryPicker() 方法模拟 <input type="file" webkitdirectory> 元素。

我们开发了一个名为 browser-fs-access 的库,该库使用 System Access API 会尽可能回退到 案例

安全与权限

Chrome 团队按照核心原则设计和实施 File System Access API (如控制对强大的 Web 平台功能的访问权限中所述),包括 控制力、透明度,以及用户工效学设计。

打开文件或保存新文件

<ph type="x-smartling-placeholder">
</ph> 用于打开文件以供阅读的文件选择器 <ph type="x-smartling-placeholder">
</ph> 用于打开现有文件进行读取的文件选择器。

打开文件时,用户提供使用文件选择器读取文件或目录的权限。 只有通过安全的 上下文。如果用户改变了主意,可以在文件中取消选择 不会获得任何访问权限这与 <input type="file"> 元素。

<ph type="x-smartling-placeholder">
</ph> 用于将文件保存到磁盘的文件选择器。 <ph type="x-smartling-placeholder">
</ph> 用于将文件保存到磁盘的文件选择器。

同样,当 Web 应用想要保存新文件时,浏览器会显示保存文件选择器, 允许用户指定新文件的名称和位置。由于在保存新文件 发送到设备(而不是覆盖现有文件),则文件选择器会向应用授予以下权限: 写入文件。

受限文件夹

为了帮助保护用户及其数据,浏览器可能会限制用户保存至特定 文件夹,例如 Windows 等核心操作系统文件夹、macOS“库”文件夹。 在这种情况下,浏览器会显示一条提示,要求用户选择其他 文件夹中。

修改现有文件或目录

未经用户的明确许可,Web 应用无法修改磁盘上的文件。

权限提示

如果用户想要保存对之前授予读取权限的文件所做的更改,浏览器 ,显示权限提示,请求网站将更改写入磁盘的权限。 权限请求只能通过用户手势触发,例如,通过点击“保存” 按钮。

<ph type="x-smartling-placeholder">
</ph> 保存文件前显示的权限提示。 <ph type="x-smartling-placeholder">
</ph> 在浏览器被授予写入权限之前向用户显示的提示 权限。

或者,可以编辑多个文件的 Web 应用(例如 IDE)也可以请求保存 会在开盘时更改。

如果用户选择“取消”,并且未授予写入权限,则 Web 应用无法保存对 本地文件。它应该为用户提供另一种保存数据的方法, 从而为用户提供“下载”文件或将数据保存到云端。

透明度

<ph type="x-smartling-placeholder">
</ph> 多功能框图标 <ph type="x-smartling-placeholder">
</ph> 地址栏图标表示用户已向网站授予以下权限: 保存到本地文件。

用户向 Web 应用授予保存本地文件的权限后,浏览器会显示一个图标 。点击该图标会打开一个弹出式窗口,其中列出了用户提供的文件 访问权限。用户可以随时选择撤消访问权限。

权限保留

在文件的所有标签页之前,Web 应用可以继续保存对文件的更改,而不会出现提示。 已关闭一旦关闭某个标签页,该网站将失去所有访问权限。用户下次使用 Web 应用,系统会重新提示他们访问文件。

反馈

我们希望了解您使用 File System Access API 的体验。

向我们介绍 API 设计

API 是否有什么无法按预期运行?或者是否缺少方法 需要哪些资源或属性来实现您的想法?对安全性有疑问或意见 模型?

实施时遇到问题?

您在 Chrome 的实现过程中是否发现了错误?或者,实现是否与规范不同?

  • 访问 https://new.crbug.com 提交 bug。请务必提供尽可能多的细节信息 并将 Components 设为 Blink>Storage>FileSystemGlitch 非常适合用于分享快速重现。

打算使用该 API?

打算在您的网站上使用 File System Access API?您的公开支持有助于我们确定各项工作的轻重缓急 并向其他浏览器供应商展示支持这些功能的重要性。

实用链接

致谢

File System Access API 规范由 Marijn Kruisselbrink