简介:
我常常想,如果网络应用能够读取和写入文件与目录,将会非常方便。从离线转移到在线后,应用变得更加复杂,而文件系统方面的 API 的缺乏也一直阻碍着网络前进。存储二进制数据或与其进行交互不应局限于桌面。令人欣慰的是,由于 FileSystem API 的出现,这一现状终于得到了改变。有了 FileSystem API,网络应用就可以创建、读取、导航用户本地文件系统中的沙盒部分以及向其中写入数据。
API 被分为以下不同的主题:
- 读取和处理文件:File/Blob、FileList、FileReader
- 创建和写入:BlobBuilder、FileWriter
- 目录和文件系统访问:DirectoryReader、FileEntry/DirectoryEntry、LocalFileSystem
- 浏览器支持与存储限制
在写这篇文章时,只有 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 包含标准文件系统中会有的属性类型(name、isFile...)和方法(remove、moveTo、copyTo...)。
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) { ... });
将所有内容汇总到一起
基本示例
该演示列出了文件系统中的文件/文件夹。
HTML5 终端
该 Shell 会提取 FileSystem API,以复制 UNIX 文件系统中的一些常用操作(例如 cd、mkdir、rm、open 和 cat)。要添加文件,可将文件从桌面拖放到下方的终端。
使用案例-
HTML5 中提供了若干个存储选项,但 FileSystem 的目标在于满足没能从数据库获得很好服务的客户端存储使用案例。这些应用一般处理大型二进制 Blob 和/或与浏览器外部环境的应用共享数据。
该规范列出了几个使用案例: -
持久型上传器
选中要上传的文件或目录后,系统会将文件复制到本地沙盒并批次上传。
即使发生浏览器崩溃、网络中断等状况,也可在之后重新开始上传。 -
视频游戏、音乐或其他具有大量媒体资产的应用
下载一个或几个大的压缩包,然后将其本地解压缩到目录结构。
任何操作系统均可使用相同的下载模式。
对预先抓取即将使用的资产的功能进行后台管理,因此转到下一个游戏级别或激活新功能无需等待下载过程。
直接读取文件或将本地 URI 移交至图片标记、视频标记、WebGL 资产加载器等,从而直接通过本地缓存使用这些资产。
文件可使用任意二进制格式。
在服务器端,压缩后的压缩包通常远远小于单独压缩的文件合集。另外,在其他条件相同的情况下,1 个压缩包涉及的搜索将小于 1000 个小文件。 -
可使用离线访问权限或本地存储的高速音频/照片编辑器
数据 Blob 很可能是可读写的超大文件。
可能需要向文件写入局部内容(例如,仅覆盖 ID3/EXIF 标记)。
通过创建目录来整理项目文件这一功能非常有用。
修改后的文件应可供客户端应用 [iTunes、Picasa] 访问。 -
离线视频观看者
下载大文件(1 GB 以上)以供今后观看。
需要有效的搜索和流式传输。
必须能够将 URI 移交至视频标记。
应该允许访问部分下载的文件,例如即使您在登机前尚未完成下载,也可以观看 DVD 的第一集。
应该能够在下载期间抽取单个剧集,然后将该剧集单独移交至视频标记。 - 离线网络右键客户端
下载并本地存储附件
缓存用户选择的附件以供今后上传。
需要能够引用缓存的附件和图片缩略图以供展示和上传。
应该能够像与服务器交谈一样触发 UA 的下载管理器。
应该能够将带附件的电子邮件视作由多个部分组成的邮件进行上传,而不是通过 XHR 一次发送一个文件。
参考规范