Save file in Chrome

Chrome

在 Web 环境中,一般对内容的存储都是依托于 Cookie 或是 LocalStorage 进行的(个别会使用 IndexDB)。其实,在早些时候,Web 曾推出过一个 FileSystem 的标准(已经废弃),用于将数据直接存储到本地的沙盒环境中,方便日后的使用。这个 API 目前只有 Chrome 进行了实现。

这篇文章 针对 FileSystem API 做了详细的介绍。这个 GitHub 仓库 则在 FileSystem 原生 API 的基础上,进行了二次封装。(注:第一个链接给出的文章,部分代码有误,可能无法正常运行。实际使用过程中,可以参考第二个链接给出的 GitHub 仓库中的相关代码进行调整)

假设,需要实现一个分片的文件下载功能,即文件被服务器分割成很多块,通过 JavaScript 依次下载这些内容,再在本地拼接后提交给用户。这里,考虑到文件可能非常大,如果只是存储在内存中,一旦用户刷新页面或是遇到其他问题,已经下载的内容就都失效了,只能重新再来一次。这种情况下,可以考虑使用 FileSystem API 将分片的文件内容下载后先存放在本地的沙盒文件中,等到全部下载完成之后,再将拼接好的内容提交给用户。

下面给出一个实例代码,用以介绍 FileSystem API 的可能使用方法:

/**
 * 实际中 Chrome 给出的 API 只用 window.webkitRequestFileSystem
 */
const requestFileSystem = window.requestFileSystem ||
  window.webkitRequestFileSystem;

/**
 * 下载 Link 并保存文件为 filename
 * 只是示例代码,实际的可行方案请参考 file-saver 的实现
 */
function download(link, filename) {
  const a = document.createElement('a');
  a.href = link;
  a.target = '_blank';
  a.download = filename;
  a.click();
}

function save(blob, filename) {
  function errorHandler(e) {
    console.log(e);
  }
  function handler(fs) {
    /**
     * 获取名为 filename 的文件,{ create: true } 表示如果文件不存在,就创建一个
     * fileEntry 中包含的 API 可以用于对这个文件进行操作
     */
    fs.root.getFile(filename, { create: true }, (fileEntry) => {
      fileEntry.createWriter(writer => {
        /**
         * 指定文件的写入位置在当前文件内容的末尾
         */
        writer.seek(writer.length);
        writer.onwriteend = () => {
          /**
           * FileSystem 中的文件,可以通过类似如下的 Link 获取到:
           * filesystem:https://xxx.com/persistent/filename
           * 具体的 URL 地址通过 `fileEntry.toURL()` 获取
           */
          const url = fileEntry.toURL();
          download(url, filename);
        };
        writer.onerror = console.error;
        writer.write(blob);
      }, errorHandler);
    }, errorHandler);
  }
  /**
   * 对于 PERSISTENT 存储的文件,需要事先通过浏览器询问权限
   * 声明需要使用的大小为 blob.size
   * 第二个参数是 success callback,在成功后调用,可以在这里进行文件读写
   * 第三个参数是 error callback,用于处理报错
   */
  navigator.webkitPersistentStorage.requestQuota(
    blob.size,
    grantedBytes => {
      /**
       * 以 PERSISTENT 的方式,写入 grantedBytes 这么多的内容
       * 允许写入会执行 handler,否则会执行 errorHandler
       */
      requestFileSystem(window.PERSISTENT, grantedBytes, handler, errorHandler);
    },
    console.error
  );
}

/**
 * 示例代码的调用,将 hello world 写入到 output.txt 文件中
 */
save(new Blob(['hello world'], { type: 'text/plain' }), 'output.txt')

上面这个例子,展示了如何将 Blob / File 写入到本地沙盒的文件中(例子中写入到了 output.txt 文件内)。有几点需要注意:

  1. 文件是写入到沙盒环境中的,因而虽然 fileEntry.fullPath 的值是 /output.txt,并不代表真的可以在根目录下找到 output.txt 文件
  2. window.PERSISTENTwindow.TEMPORARY 是两种可能的存储方式。如果是 PERSISTENT 的,那么需要用户授权(也就是 requestQuota 做的事情)且清理需要程序或用户手动执行;如果是 TEMPORARY 类型的存储方式,那么浏览器可能会在某些情况下自动清理文件(比如,当空间不够的时候)
  3. 通过 fileEntry.toURL API 可以拿到当前文件存储对应的 URL 地址,进而可以通过常规手段将这个内容下载到本地
  4. 代码中的 errorHandler 函数写的比较粗糙,更丰富的 Error Handler 写法,可以参考 chromestore.js 中的代码

MyAirBridge 网站可能使用了类似上面提到的技术来存储下载中的文件内容。