探索 FileSystem API

HTML5 Rocks

简介

我常常想,如果网络应用能够读取和写入文件与目录,将会非常方便。从离线转移到在线后,应用变得更加复杂,而文件系统方面的 API 的缺乏也一直阻碍着网络前进。存储二进制数据或与其进行交互不应局限于桌面。令人欣慰的是,由于FileSystem API 的出现,这一现状终于得到了改变。有了 FileSystem API,网络应用就可以创建、读取、导航用户本地文件系统中的沙盒部分以及向其中写入数据。

API 被分为以下不同的主题:

  • 读取和处理文件:File/BlobFileListFileReader
  • 创建和写入:BlobBuilderFileWriter
  • 目录和文件系统访问:DirectoryReaderFileEntry/DirectoryEntryLocalFileSystem

浏览器支持与存储限制

在写这篇文章时,只有 Google Chrome 浏览器可以实施此 FileSystem API。目前尚不存在专门用于文件/配额管理的浏览器用户界面。要在用户的系统上存储数据,您的应用可能需要请求配额。不过,可使用 --unlimited-quota-for-files 标记运行 Chrome 浏览器进行测试。此外,如果您要开发的是用于 Chrome 网上应用店的应用或扩展程序,可使用 unlimitedStorage 清单文件权限,而无需请求配额。最后,用户会收到授予、拒绝或为应用增加存储的权限对话框。

如果您要通过 file:// 调试您的应用,可能需要 --allow-file-access-from-files 标记。不使用这些标记会导致 SECURITY_ERR 或 QUOTA_EXCEEDED_ERRFileError。

请求文件系统

网络应用可通过调用 window.requestFileSystem() 请求对沙盒文件系统的访问权限:

// Note: The file system has been prefixed as of Google Chrome 12:
window.requestFileSystem  = window.requestFileSystem || window.webkitRequestFileSystem;

window.requestFileSystem(type, size, successCallback, opt_errorCallback)
type
文件存储是否应该是持久的。可能的值包括  window.TEMPORARY 和 window.PERSISTENT。通过  TEMPORARY 存储的数据可由浏览器自行决定删除(例如在需要更多空间的情况下)。要清除  PERSISTENT 存储,必须获得用户或应用的明确授权,并且需要用户向您的应用授予配额。请参阅 请求配额
size
应用需要用于存储的大小(以字节为单位)。
successCallback
文件系统请求成功时调用的回调。其参数为  FileSystem 对象。
opt_errorCallback
用于处理错误或获取文件系统的请求遭到拒绝时可选的回调。其参数为 FileError 对象。

如果您是首次调用 requestFileSystem(),系统会为您的应用创建新的存储。请注意,这是沙箱文件系统,也就是说,一个网络应用无法访问另一个应用的文件。这也意味着您无法在用户硬盘上的任意文件夹(例如“我的图片”、“我的文档”等)中读/写文件。

用法示例:

function onInitFs(fs) {
  console.log('Opened file system: ' + fs.name);
}

window.requestFileSystem(window.TEMPORARY, 5*1024*1024 /*5MB*/, onInitFs, errorHandler);

FileSystem 规范还定义了计划用于 Web Workers 的同步 API (LocalFileSystemSync) 接口。不过,本教程不涉及该同步 API。

在本文档的其余部分中,我们将使用相同的处理程序处理异步调用引发的错误:

function errorHandler(e) {
  var msg = '';

  switch (e.code) {
    case FileError.QUOTA_EXCEEDED_ERR:
      msg = 'QUOTA_EXCEEDED_ERR';
      break;
    case FileError.NOT_FOUND_ERR:
      msg = 'NOT_FOUND_ERR';
      break;
    case FileError.SECURITY_ERR:
      msg = 'SECURITY_ERR';
      break;
    case FileError.INVALID_MODIFICATION_ERR:
      msg = 'INVALID_MODIFICATION_ERR';
      break;
    case FileError.INVALID_STATE_ERR:
      msg = 'INVALID_STATE_ERR';
      break;
    default:
      msg = 'Unknown Error';
      break;
  };

  console.log('Error: ' + msg);
}

当然,这种错误回调非常通用,能让您充分理解,但您提供给用户的应是易于一般人理解的讯息。

请求存储配额

要使用 PERSISTENT 存储,您必须向用户取得存储持久数据的许可。由于浏览器可自行决定删除临时存储的数据,因此这一限制不适用于 TEMPORARY 存储。

为了将 PERSISTENT 存储与 FileSystem API 配合使用,Chrome 浏览器使用基于window.webkitStorageInfo 的新 API 以请求存储:

window.webkitStorageInfo.requestQuota(PERSISTENT, 1024*1024, function(grantedBytes) {
  window.requestFileSystem(PERSISTENT, grantedBytes, onInitFs, errorHandler);
}, function(e) {
  console.log('Error', e);
});

用户授予许可后,就不必再调用 requestQuota() 了。后续调用为无操作指令。

您还可以使用 API 查询源的当前配额使用情况和分配情况:window.webkitStorageInfo.queryUsageAndQuota()

使用文件

沙盒环境中的文件通过 FileEntry 接口表示。FileEntry 包含标准文件系统中会有的属性类型(nameisFile...)和方法(removemoveTocopyTo...)。

FileEntry 的属性和方法:

fileEntry.isFile === true
fileEntry.isDirectory === false
fileEntry.name
fileEntry.fullPath
...

fileEntry.getMetadata(successCallback, opt_errorCallback);
fileEntry.remove(successCallback, opt_errorCallback);
fileEntry.moveTo(dirEntry, opt_newName, opt_successCallback, opt_errorCallback);
fileEntry.copyTo(dirEntry, opt_newName, opt_successCallback, opt_errorCallback);
fileEntry.getParent(successCallback, opt_errorCallback);
fileEntry.toURL(opt_mimeType);

fileEntry.file(successCallback, opt_errorCallback);
fileEntry.createWriter(successCallback, opt_errorCallback);
...

为了更好地理解 FileEntry,本部分还提供了执行常规任务的众多技巧。

创建文件

您可以使用文件系统的 getFile()DirectoryEntry 接口的一种方法)查找或创建文件。请求文件系统后,系统会向成功回调传递 FileSystem 对象,其中包含指向该应用相应文件系统的根的 DirectoryEntry (fs.root)。

以下代码会在该应用相应文件系统的根中创建名为“log.txt”的空白文件:

function onInitFs(fs) {

  fs.root.getFile('log.txt', {create: true, exclusive: true}, function(fileEntry) {

    // fileEntry.isFile === true
    // fileEntry.name == 'log.txt'
    // fileEntry.fullPath == '/log.txt'

  }, errorHandler);

}

window.requestFileSystem(window.TEMPORARY, 1024*1024, onInitFs, errorHandler);

请求文件系统后,系统会向成功处理程序传递 FileSystem 对象。我们可以将回调中的 fs.root.getFile() 命名为要创建的文件的文件名。您可以传递绝对路径或相对路径,但该路径必须有效。例如,如果您尝试创建一个其直接父级文件不存在的文件,将会导致出错。getFile() 的第二个参数是在文件不存在时从字面上说明函数行为的对象。在此示例中,create: true 会在文件不存在时创建文件,并在文件存在时 (exclusive: true) 引发错误。如果 create: false,系统只会获取并返回文件。无论是哪种情况,系统都不会覆盖文件内容,因为我们只是获取相关文件的引用路径。

通过名称读取文件

以下代码会检索名为“log.txt”的文件,并使用 FileReader API 读取文件内容,然后将其附加到页面上新的 <textarea>。如果 log.txt 不存在,系统将引发错误。

function onInitFs(fs) {

  fs.root.getFile('log.txt', {}, function(fileEntry) {

    // Get a File object representing the file,
    // then use FileReader to read its contents.
    fileEntry.file(function(file) {
       var reader = new FileReader();

       reader.onloadend = function(e) {
         var txtArea = document.createElement('textarea');
         txtArea.value = this.result;
         document.body.appendChild(txtArea);
       };

       reader.readAsText(file);
    }, errorHandler);

  }, errorHandler);

}

window.requestFileSystem(window.TEMPORARY, 1024*1024, onInitFs, errorHandler);

写入到文件

以下代码会创建名为“log.txt”的空白文件(如果该文件不存在),并在文件中填入“Lorem Ipsum”文字。

function onInitFs(fs) {

  fs.root.getFile('log.txt', {create: true}, function(fileEntry) {

    // Create a FileWriter object for our FileEntry (log.txt).
    fileEntry.createWriter(function(fileWriter) {

      fileWriter.onwriteend = function(e) {
        console.log('Write completed.');
      };

      fileWriter.onerror = function(e) {
        console.log('Write failed: ' + e.toString());
      };

      // Create a new Blob and write it to log.txt.
      var bb = new BlobBuilder(); // Note: window.WebKitBlobBuilder in Chrome 12.
      bb.append('Lorem Ipsum');
      fileWriter.write(bb.getBlob('text/plain'));

    }, errorHandler);

  }, errorHandler);

}

window.requestFileSystem(window.TEMPORARY, 1024*1024, onInitFs, errorHandler);

此时,我们会调用 FileEntry 的 createWriter() 方法获取 FileWriter 对象。在成功回调中为 error 事件和 writeend 事件设置事件处理程序。通过以下操作将文字数据写入文件:创建 Blob,向 Blob 附加文字,然后将 Blob 传递到FileWriter.write()

向文件附加文字

以下代码会将“Hello World”文字附加到日志文件结尾。如果该文件不存在,系统将引发错误。

function onInitFs(fs) {

  fs.root.getFile('log.txt', {create: false}, function(fileEntry) {

    // Create a FileWriter object for our FileEntry (log.txt).
    fileEntry.createWriter(function(fileWriter) {

      fileWriter.seek(fileWriter.length); // Start write position at EOF.

      // Create a new Blob and write it to log.txt.
      var bb = new BlobBuilder(); // Note: window.WebKitBlobBuilder in Chrome 12.
      bb.append('Hello World');
      fileWriter.write(bb.getBlob('text/plain'));

    }, errorHandler);

  }, errorHandler);

}

window.requestFileSystem(window.TEMPORARY, 1024*1024, onInitFs, errorHandler);

复制用户选定的文件

以下代码可让用户使用 <input type="file" multiple /> 选择多个文件,并在应用的沙盒文件系统中复制这些文件。

<input type="file" id="myfile" multiple />
document.querySelector('#myfile').onchange = function(e) {
  var files = this.files;

  window.requestFileSystem(window.TEMPORARY, 1024*1024, function(fs) {
    // Duplicate each file the user selected to the app's fs.
    for (var i = 0, file; file = files[i]; ++i) {

      // Capture current iteration's file in local scope for the getFile() callback.
      (function(f) {
        fs.root.getFile(file.name, {create: true, exclusive: true}, function(fileEntry) {
          fileEntry.createWriter(function(fileWriter) {
            fileWriter.write(f); // Note: write() can take a File or Blob object.
          }, errorHandler);
        }, errorHandler);
      })(file);

    }
  }, errorHandler);

};

虽然我们通过输入导入文件,您也可以使用 HTML5 拖放功能轻松实现相同的目标。

正如评论中所说的,FileWriter.write() 可接受 Blob 或 File。这是因为 File继承自 Blob,所以文件对象也是 Blob。

删除文件

以下代码会删除“log.txt”文件。

window.requestFileSystem(window.TEMPORARY, 1024*1024, function(fs) {
  fs.root.getFile('log.txt', {create: false}, function(fileEntry) {

    fileEntry.remove(function() {
      console.log('File removed.');
    }, errorHandler);

  }, errorHandler);
}, errorHandler);

使用目录

沙盒中的目录通过 DirectoryEntry 接口表示,该接口共享了 FileEntry 的大部分属性(继承自常用 Entry 接口)。不过,DirectoryEntry 还可使用其他方法处理目录。

DirectoryEntry 的属性和方法:

dirEntry.isDirectory === true
// See the section on FileEntry for other inherited properties/methods.
...

var dirReader = dirEntry.createReader();
dirEntry.getFile(path, opt_flags, opt_successCallback, opt_errorCallback);
dirEntry.getDirectory(path, opt_flags, opt_successCallback, opt_errorCallback);
dirEntry.removeRecursively(successCallback, opt_errorCallback);
...

创建目录

使用 DirectoryEntry 的 getDirectory() 方法读取或创建目录。您可以递交名称或路径作为查找或创建所用的目录。

例如,以下代码会在根目录中创建名为“MyPictures”的目录:

window.requestFileSystem(window.TEMPORARY, 1024*1024, function(fs) {
  fs.root.getDirectory('MyPictures', {create: true}, function(dirEntry) {
    ...
  }, errorHandler);
}, errorHandler);
  

子目录

创建子目录的方法与创建其他任何目录的方法完全相同。不过,如果您尝试创建其直接父目录不存在的目录,API 将引发错误。相应的解决方法是,依次创建各级目录,而这对异步 API 而言非常麻烦。

以下代码会在系统创建父文件夹后以递归方式添加各个子文件夹,从而在应用相应 FileSystem 的根中创建新的层次结构 (music/genres/jazz)。

var path = 'music/genres/jazz/';

function createDir(rootDirEntry, folders) {
  // Throw out './' or '/' and move on to prevent something like '/foo/.//bar'.
  if (folders[0] == '.' || folders[0] == '') {
    folders = folders.slice(1);
  }
  rootDirEntry.getDirectory(folders[0], {create: true}, function(dirEntry) {
    // Recursively add the new subfolder (if we still have another to create).
    if (folders.length) {
      createDir(dirEntry, folders.slice(1));
    }
  }, errorHandler);
};

function onInitFs(fs) {
  createDir(fs.root, path.split('/')); // fs.root is a DirectoryEntry.
}

window.requestFileSystem(window.TEMPORARY, 1024*1024, onInitFs, errorHandler);

在“music/genres/jazz”处于合适的位置后,我们就可以将完整路径传递到getDirectory(),然后在其下方创建新的子文件夹。例如:

window.requestFileSystem(window.TEMPORARY, 1024*1024, function(fs) {
  fs.root.getFile('/music/genres/jazz/song.mp3', {create: true}, function(fileEntry) {
    ...
  }, errorHandler);
}, errorHandler);

读取目录内容

要读取目录的内容,可先创建 DirectoryReader,然后调用 readEntries() 方法。我们不能保证所有目录条目都能在仅调用一次 readEntries() 的情况下同时返回。也就是说,您需要一直调用 DirectoryReader.readEntries(),直到系统不再返回结果为止。以下代码对此作了说明:

<ul id="filelist"></ul>
function toArray(list) {
  return Array.prototype.slice.call(list || [], 0);
}

function listResults(entries) {
  // Document fragments can improve performance since they're only appended
  // to the DOM once. Only one browser reflow occurs.
  var fragment = document.createDocumentFragment();

  entries.forEach(function(entry, i) {
    var img = entry.isDirectory ? '<img src="folder-icon.gif">' :
                                  '<img src="file-icon.gif">';
    var li = document.createElement('li');
    li.innerHTML = [img, '<span>', entry.name, '</span>'].join('');
    fragment.appendChild(li);
  });

  document.querySelector('#filelist').appendChild(fragment);
}

function onInitFs(fs) {

  var dirReader = fs.root.createReader();
  var entries = [];

  // Call the reader.readEntries() until no more results are returned.
  var readEntries = function() {
     dirReader.readEntries (function(results) {
      if (!results.length) {
        listResults(entries.sort());
      } else {
        entries = entries.concat(toArray(results));
        readEntries();
      }
    }, errorHandler);
  };

  readEntries(); // Start reading dirs.

}

window.requestFileSystem(window.TEMPORARY, 1024*1024, onInitFs, errorHandler);

删除目录

DirectoryEntry.remove() 方法的行为与 FileEntry 相应方法的行为非常相似。差别在于:尝试删除非空目录时会引发错误。

以下代码会从“/music/genres/”删除空的“jazz”目录:

window.requestFileSystem(window.TEMPORARY, 1024*1024, function(fs) {
  fs.root.getDirectory('music/genres/jazz', {}, function(dirEntry) {

    dirEntry.remove(function() {
      console.log('Directory removed.');
    }, errorHandler);

  }, errorHandler);
}, errorHandler);
以递归方式删除目录

如果您不需要某个包含条目的目录,不妨使用 removeRecursively()。该方法将以递归方式删除目录及其内容。

以下代码会以递归方式删除“music”目录及其包含的所有文件和目录:

window.requestFileSystem(window.TEMPORARY, 1024*1024, function(fs) {
  fs.root.getDirectory('/misc/../music', {}, function(dirEntry) {

    dirEntry.removeRecursively(function() {
      console.log('Directory removed.');
    }, errorHandler);

  }, errorHandler);
}, errorHandler);

复制、重命名和移动

FileEntry 和 DirectoryEntry 享有共同的操作。

复制条目

FileEntry 和 DirectoryEntry 均可使用 copyTo() 复制现有条目。该方法会自动以递归方式复制文件夹。

以下代码示例会将“me.png”文件从一个目录复制到另一个目录:

function copy(cwd, src, dest) {
  cwd.getFile(src, {}, function(fileEntry) {

    cwd.getDirectory(dest, {}, function(dirEntry) {
      fileEntry.copyTo(dirEntry);
    }, errorHandler);

  }, errorHandler);
}

window.requestFileSystem(window.TEMPORARY, 1024*1024, function(fs) {
  copy(fs.root, '/folder1/me.png', 'folder2/mypics/');
}, errorHandler);

移动或重命名条目

FileEntry 和 DirectoryEntry 的 moveTo() 方法可让您移动或重命名文件或目录。其第一个参数是文件要移动到的目标父目录,其第二个参数是文件可选的新名称。如未提供新名称,系统将使用文件的原名称。

以下示例将“me.png”重命名为“you.png”,但并不移动该文件:

function rename(cwd, src, newName) {
  cwd.getFile(src, {}, function(fileEntry) {
    fileEntry.moveTo(cwd, newName);
  }, errorHandler);
}

window.requestFileSystem(window.TEMPORARY, 1024*1024, function(fs) {
  rename(fs.root, 'me.png', 'you.png');
}, errorHandler);

以下示例将“me.png”(位于根目录中)移动到名为“newfolder”的文件夹。

function move(src, dirName) {
  fs.root.getFile(src, {}, function(fileEntry) {

    fs.root.getDirectory(dirName, {}, function(dirEntry) {
      fileEntry.moveTo(dirEntry);
    }, errorHandler);

  }, errorHandler);
}

window.requestFileSystem(window.TEMPORARY, 1024*1024, function(fs) {
  move('/me.png', 'newfolder/');
}, errorHandler);

filesystem: 网址

FileSystem API 使用新的网址机制,(即 filesystem:),可用于填充 src 或href 属性。例如,如果您要显示某幅图片且拥有相应的 fileEntry,您可以调用toURL() 获取该文件的 filesystem: 网址:

var img = document.createElement('img');
img.src = fileEntry.toURL(); // filesystem:http://example.com/temporary/myfile.png
document.body.appendChild(img);

另外,如果您已具备 filesystem: 网址,可使用 resolveLocalFileSystemURL()找回 fileEntry

window.resolveLocalFileSystemURL = window.resolveLocalFileSystemURL ||
                                   window.webkitResolveLocalFileSystemURL;

var url = 'filesystem:http://example.com/temporary/myfile.png';
window.resolveLocalFileSystemURL(url, function(fileEntry) {
  ...
});

将所有内容汇总到一起

基本示例

该演示列出了文件系统中的文件/文件夹。

src="http://playground.html5rocks.com/?mode=frame&hu=180&hl=160#filesystem_apis" style="border-style: none; width: 660px; height: 460px;">

HTML5 终端

该 Shell 会提取 FileSystem API,以复制 UNIX 文件系统中的一些常用操作(例如cdmkdirrmopen 和 cat)。要添加文件,可将文件从桌面拖放到下方的终端。

id="terminal-iframe" src="http://www.html5rocks.com/zh/tutorials/file/filesystem/terminal.html" style="display: block; border: 1px solid black; width: 660px; height: 300px;">

使用案例

HTML5 中提供了若干个存储选项,但 FileSystem 的目标在于满足没能从数据库获得很好服务的客户端存储使用案例。这些应用一般处理大型二进制 Blob 和/或与浏览器外部环境的应用共享数据。

该规范列出了几个使用案例:

  1. 持久型上传器
    • 选中要上传的文件或目录后,系统会将文件复制到本地沙盒并批次上传。
    • 即使发生浏览器崩溃、网络中断等状况,也可在之后重新开始上传。
  2. 视频游戏、音乐或其他具有大量媒体资产的应用
    • 下载一个或几个大的压缩包,然后将其本地解压缩到目录结构。
    • 任何操作系统均可使用相同的下载模式。
    • 对预先抓取即将使用的资产的功能进行后台管理,因此转到下一个游戏级别或激活新功能无需等待下载过程。
    • 直接读取文件或将本地 URI 移交至图片标记、视频标记、WebGL 资产加载器等,从而直接通过本地缓存使用这些资产。
    • 文件可使用任意二进制格式。
    • 在服务器端,压缩后的压缩包通常远远小于单独压缩的文件合集。另外,在其他条件相同的情况下,1 个压缩包涉及的搜索将小于 1000 个小文件。
  3. 可使用离线访问权限或本地存储的高速音频/照片编辑器
    • 数据 Blob 很可能是可读写的超大文件。
    • 可能需要向文件写入局部内容(例如,仅覆盖 ID3/EXIF 标记)。
    • 通过创建目录来整理项目文件这一功能非常有用。
    • 修改后的文件应可供客户端应用 [iTunes、Picasa] 访问。
  4. 离线视频观看者
    • 下载大文件(1 GB 以上)以供今后观看。
    • 需要有效的搜索和流式传输。
    • 必须能够将 URI 移交至视频标记。
    • 应该允许访问部分下载的文件,例如即使您在登机前尚未完成下载,也可以观看 DVD 的第一集。
    • 应该能够在下载期间抽取单个剧集,然后将该剧集单独移交至视频标记。
  5. 离线网络右键客户端
    • 下载并本地存储附件
    • 缓存用户选择的附件以供今后上传。
    • 需要能够引用缓存的附件和图片缩略图以供展示和上传。
    • 应该能够像与服务器交谈一样触发 UA 的下载管理器。
    • 应该能够将带附件的电子邮件视作由多个部分组成的邮件进行上传,而不是通过 XHR 一次发送一个文件。

参考规范

150

Next steps

Share

    Google+

Subscribe

Enjoyed this article? Grab the RSS feedand stay up-to-date.

Except as otherwise noted, the content of this page is licensed under the Creative Commons Attribution 3.0 License, and code samples are licensed under the Apache 2.0 License.

Logo

CSDN联合极客时间,共同打造面向开发者的精品内容学习社区,助力成长!

更多推荐