新的 API 可以让网页直接读写硬盘上的文件?

10,756 阅读8分钟

从前,网页中想要实现文件的读写是比较麻烦的,为了读取一个文件,我们需要创建一个 type="file"input 标签让用户选择一个文件,而为了保存一些内容到本地硬盘,我们则需要使用 a 标签的 download 属性去触发文件保存。

而连续的文件保存则无法实现了,因为向本地硬盘写入文件是一定要触发“保存”弹框的。要遍历一个本地的目录也是无法实现的。

在最新版的 Chrome 78 中,Google 开启了一项实验性的特性:Native File System,这是一个全新的本地文件读写 API,当前还未开发完成,因此默认并没有开启这项功能。

如果想要体验这个 API 的话,可以在 chrome://flags 这个地址中搜索 Native File System API 进行启用。

关于这个 API 的简介,可以参考:github.com/WICG/native…

这是一个全新的文件读写 API,它带来了更加方便的本地文件读写方式,但是也带来了很大的安全风险,因此这个 API 只能在安全的页面中(比如部署了有效的 https 的页面) 调用,并且必须是通过用户的选择进行操作(也就是不能完全后台自动执行)。

打开并读取文件内容

为了读取本地文件,我们需要调用 window.chooseFileSystemEntries(option) 接口,它接受一个 option 作为参数。

option 是一个对象,包含 4 个可以配置的配置项:

配置项 取值 解释
type 'openFile'/'saveFile'/'openDirectory' 操作类型,是打开文件,保存文件还是打开文件夹,默认是 'open-file'
multiple boolean 允许用户选择单个文件还是选择多个文件,默认为 false,即只允许选择单个文件
accepts object[] 指定弹出的文件选择框中允许选择的文件类型
accepts.description string 描述
accepts.mimeTypes string[] 允许选择的文件的 MIME 类型
accepts.extensions string[] 允许选择的文件的扩展名
excludeAcceptAllOption boolean 是否允许选择不在上面 accepts 列表中允许的文件类型,默认为 false,即允许选择任意文件(*.*

window.chooseFileSystemEntries(option) 返回一个 FileSystemHandle 对象(若 optionmultiple 被指定为 true,则返回一个 FileSystemHandle 数组)。

FileSystemHandle 是一个基类,实际上 chooseFileSystemEntries 根据 option 中指定的 type,返回的是它的一个子类 FileSystemFileHandleFileSystemDirectoryHandle

若指定的 option 是打开一个文件夹,则返回 FileSystemDirectoryHandle,可以进行目录的遍历,也可以打开一个指定的文件,打开文件将返回一个 FileSystemFileHandle。若指定的 option 是打开/保存一个文件,则直接返回 FileSystemFileHandle

通过 FileSystemFileHandle 上的 getFile() 函数,就可以得到一个 File 对象了。

这里得到 File 对象与 type="file"input 返回的 File 一样,你可以通过通过通过 File 对象的 API 获取文件的具体内容,比如对于文本文件,可以直接调用 .text() 得到字符串,对于二进制文件,可以调用 .arrayBuffer() 得到 ArrayBuffer 对象,也可以调用 .stream() 得到一个 ReadableStream

const handler = await window.chooseFileSystemEntries({
  type: 'openFile',
  accepts: [
    { description: '文本文件', extensions: ['txt'] },
  ],
});

const file = await handler.getFile();
const text = await file.text();
console.log(text);

保存文件到本地硬盘

为了将文件保存到硬盘上,我们需要调用 FileSystemFileHandle 对象上的 createWriter() 函数。

保存文件通常有两种模式,一种是“保存”,即覆盖原始文件,另一种是“另存为”,即创建一个新的文件。

“另存为”的形式,我们只需要重新调用 window.chooseFileSystemEntries() 提供保存文件的选项即可。而“保存”文件,我们直接使用之前打开的文件的 FileSystemFileHandle 对象即可。

调用 createWriter() 函数后会返回一个 FileSystemWriter 对象,它包含三个函数:write(position, data)truncate(size)close()

write 函数的第一个参数 position 为一个数字,指示开始写出的位置,第二个参数为要写出的数据,可以是 ArrayBuffer 对象、ArrayBufferView 对象(Uint8ArrayDataView之类的)、Blob 对象或者直接提供一个要写出的字符串。

truncate 函数会将文件截断为指定长度。通常发生在要保存的文件比原始文件小,如果直接调用 write 函数,文件将只有前面一部分被改写,而后面的原始文件内容还是会残留下来。

在调用 createWriter() 函数的时候,浏览器会先检查用户是否已经授权了文件写出权限,如果之前是以“打开文件”或是“打开文件夹”的形式打开的文件,则默认是没有文件写出权限的,则此时浏览器会提示用户以申请文件写出权限。如果用户拒绝了申请的权限,则将会抛出一个异常。

const handler = await window.chooseFileSystemEntries({
  type: 'saveFile',
  accepts: [
    { description: '文本文件', extensions: ['txt'] },
  ],
});

const writer = await handler.createWriter();
await writer.truncate(0);  // 清空文件
await writer.write(0, 'Hello World!');  // 在文件头写入字符串
await writer.close();

const file = await handler.getFile();
const text = await file.text();
console.log(text);

遍历本地目录

在前面提到的 window.chooseFileSystemEntries() 函数中,option 可以指定打开一个文件夹,此时浏览器会向用户申请目录遍历权限,获得用户同意之后,就可以得到一个 FileSystemDirectoryHandle 对象。

FileSystemDirectoryHandle 对象上拥有一个 getEntries() 函数,它返回一个异步迭代器,可以通过 for await ... of ... 循环对目录进行遍历。每次遍历得到的都是一个 FileSystemHandle 对象,可以根据 .isFile 属性与 .isDirectory 属性判断具体是 FileSystemFileHandle 对象还是 FileSystemDirectoryHandle 对象。

除了使用 getEntries() 进行遍历,还可以通过 FileSystemDirectoryHandle 对象上的 getDirectory 函数打开一个指定的子目录,子目录同样是一个 FileSystemDirectoryHandle 对象;也可以通过 getFile 函数打开一个指定的文件,得到 FileSystemFileHandle 对象。

const handler = await window.chooseFileSystemEntries({
  type: 'openDirectory',
});

const ls = async (path, handler) => {
  const result = [];
  for await (const h of await handler.getEntries()) {
    if (h.isFile) {
      result.push(`${path}/${h.name}`);
    } else if (h.isDirectory) {
      const subDirectory = await ls(`${path}/${h.name}`, h);
      result.push(...subDirectory);
    }
  }
  return result;
};

const files = await ls(handler.name, handler);
console.log(files);

安全性与权限

这个新的 Native File System API 的所有 API 均以 Promise 的形式进行返回,这使得浏览器可以在函数返回之前进行一切权限的申请与检查而不会阻塞其他代码的执行。在权限不足的时候,浏览器可以在 Promise 等待的阶段向用户申请权限。

为了避免恶意网站滥用这个 API 向用户电脑写入恶意文件,window.chooseFileSystemEntries() 函数的调用必须是在安全的页面中,由用户主动触发的,也就是必须放在用户交互的回调中(比如按钮的点击事件),这与 type="file"input 类似。

浏览器可能会处于保护的目的对一些与操作系统相关的文件夹进行限制访问。并且在网站尝试向本地文件夹写入文件的时候,浏览器会给出通知。

为了避免频繁地弹出权限警告,浏览器将会保存用户授予的权限,一旦用户授予网页访问某一文件,网页在被关闭之前都将拥有这个权限而不会过期。

一旦网页被关闭,所有的权限都将被回收,用户在下一次打开相同网站时,网站需要重新申请权限。

未来,网页可以将授权信息存入网页内置的数据库(IndexedDB)中,以持久化权限请求。

总结

到目前为止,这个 API 还没有开发完成,标准任然可能被修改或删除。但是这个 API 的出现无疑为 WebIDE 的开发提供了可能,未来可能可以不再需要安装本地 IDE,直接在网页中打开本地的工程目录,对代码进行编辑、保存,并直接使用线上虚拟化环境进行代码的执行测试。

但是,这个 API 的出现是否会带来严重的安全问题,现在还不得而知,毕竟对于广大小白用户来说,即便网站给出了安全警告,他们依旧会直接忽略。不是他们不重视,是他们根本看不懂。

比如即便是网站 HTTPS 配置错误,展示出了可能存在风险的警告,用户还是会习惯性的选择“高级”-“继续进入”;所以出现了 HSTS,在网站出现 HTTPS 错误时,禁止用户继续使用网站。

如此可见,即便是浏览器明确提示你存在安全问题,用户还是选择性忽略,所以对于 Native File System API 给出的权限申请,展示的只是个“提示框”,对于广大普通用户来说根本“不会放在眼里”,直接无脑确定就是了。

若用户访问到恶意网站,一个权限申请,一旦点击了“确定”,整个硬盘的数据都被加密……

安全,与便捷,还是需要一个平衡点!

emmmm……是不是跑题了。。。

附:标准文档:wicg.github.io/native-file…

一个 Chrome 实验室提供的纯文本编辑器 Demo:googlechromelabs.github.io/text-editor…