1. 目标
本规范记录了当文件和目录的层级结构被拖放到页面上或通过表单元素选择时,Web 浏览器向脚本提供的类型和操作,或通过等效的用户操作。
这在很大程度上基于早期的 [file-system-api] 草案,该草案在沙箱文件系统的上下文中定义了类似的类型,包括用于创建和修改文件和目录的操作,但尚未被 Web 浏览器广泛采用。
注意:本文档描述的 API 最初在谷歌 Chrome 中实现。其他浏览器(目前包括 Edge、Firefox 和 Safari)正在开始支持 Chrome API 和行为的子集。本文档的目的是指定通用的子集,以确保这些实现具有互操作性。
2. 概念
2.1. 名称和路径
一个 名称 是一个字符串,满足以下条件:
-
不包含 '/'(U+002F SOLIDUS)
-
不包含 NUL(U+0000)
-
不包含 '\'(U+005C REVERSE SOLIDUS)
-
不是 '.'(U+002E FULL STOP)
-
不是 '..'(U+002E FULL STOP, U+002E FULL STOP)
一个 路径段 是一个 名称、'.'(U+002E FULL STOP)或 '..'(U+002E FULL STOP, U+002E FULL STOP)。
一个 相对路径 是一个由一个或多个 路径段 通过 '/'(U+002F SOLIDUS)连接而成的字符串,且不以 '/'(U+002F SOLIDUS)开头。
一个 绝对路径 是一个由 '/'(U+002F SOLIDUS)后跟零个或多个通过 '/'(U+002F SOLIDUS)连接的 路径段 组成的字符串。
一个 有效路径 是一个 USVString
,它是一个 路径。
2.2. 文件和目录
一个 文件 由二进制数据和一个 名称 (一个非空的 名称)组成。
一个 目录 由一个 名称(一个 名称)和一个有序的成员列表组成。每个成员要么是一个 文件,要么是一个 目录。一个 目录 的每个成员必须具有不同的非空 名称。
一个 根目录 是一个 目录,它不是任何 目录 的成员。一个 根目录 的 名称 是空的。
一个 父目录 是一个 文件 或 目录 所属的 目录。一个 根目录 没有 父目录。
注意:在大多数情况下,用户选择的文件和目录将由 API 以包含在一个 虚拟根目录 中的方式呈现,该虚拟根目录在与 API 交互的实际本地文件系统中并不存在。
一个 文件系统 由一个 名称 和一个 根目录 组成,根目录是一个关联的 根目录。一个 名称 的 文件系统 是一个 USVString
,它由实现定义,但对于每个 文件系统 来说是唯一的。一个 根目录 仅与一个 文件系统 关联。
注意:实现可以通过为每个 文件系统 实例生成带有一些固定前缀和后缀字符串的 UUID 来生成一个 名称。使用 API 的作者被建议不要对名称的结构或内容做出假设。
2.3. 条目
一个 条目 要么是一个 文件条目,要么是一个 目录条目。
一个 条目 具有一个 名称(一个 名称)和一个 完整路径(一个 绝对路径)。
注意: 条目 是相对于一个 路径 和一个 根目录 定义的,以考虑到支持与 API 交互的本地文件系统在枚举目录内容等操作期间可能会被异步修改的事实。在这些情况下,对 条目 上暴露的操作将产生错误,因为 路径 不再引用相同的实体。
一个 文件系统 是一个 条目 所属的 文件系统,它与该 条目 的 根目录 关联。
3. 算法
要使用 abspath(一个 绝对路径)和 path(一个 绝对路径、一个 相对路径,或空字符串)来 解析相对路径, 执行以下步骤。它们返回一个 绝对路径。
-
如果 path 是一个 绝对路径,则返回 path。
-
让 abspath segments 是 严格分割 abspath 在 '/'(U+002F SOLIDUS)上的结果。
注意:第一个字符串将是空的。
-
让 path segments 是 严格分割 path 在 '/'(U+002F SOLIDUS)上的结果。
-
对于 path segments 中的每个 segment,根据 segment 进行切换:
- 空字符串
-
继续。
- '.'(U+002E FULL STOP)
-
继续。
- '..'(U+002E FULL STOP, U+002E FULL STOP)
-
移除 abspath segments 的最后一个成员,除非它是唯一的成员。
- 其他情况
-
将 segment 追加到 abspath segments。
-
返回由 '/'(U+002F SOLIDUS)连接的 abspath segments。
要使用 directory(一个 根目录)和 path(一个 绝对路径)来 评估路径, 执行以下步骤。它们返回一个 文件、目录,或失败。
4. File
接口
partial interface File {readonly attribute USVString webkitRelativePath ; };
webkitRelativePath
的 getter 步骤是返回 this 的 相对路径,如果未指定则返回空字符串。
5. HTML:表单
partial interface HTMLInputElement {attribute boolean ;
webkitdirectory readonly attribute FrozenArray <FileSystemEntry >; };
webkitEntries
当一个
input
元素的
type
属性处于 文件上传 状态时,本节中的规则适用。
webkitEntries
IDL 属性允许脚本访问元素所选的条目。获取时,如果 IDL
属性适用,则必须返回一个包含当前 所选文件(包括允许的目录)的 FileSystemEntry
对象的数组。如果 IDL 属性不适用,则必须返回 null。
webkitEntries
枚举条目:
< input id = a type = file multiple >
document. querySelector( '#a' ). addEventListener( 'change' , e=> { for ( const entryof e. target. webkitEntries) { handleEntry( entry); } });
webkitEntries
仅在拖放操作的结果中填充,而不是在点击元素时填充。我们是否应该修复此问题,使其始终被填充?
webkitdirectory
上指定了 HTMLInputElement
,
webkitEntries
不会被填充;必须使用 files
集合和 webkitRelativePath
属性来重建目录结构。我们是否应该修复此问题,使其始终被填充?
6. HTML:拖放
在 拖放操作 期间,文件 和 目录 项与 条目 关联。每个 条目 是一个独属于 根目录 的 拖放数据存储 的成员。
此外,每个 目录 项在 拖放数据存储项目列表 中表示为一种 类型 为 文件 的项。如果通过 getAsFile()
访问,则返回一个零长度的 File
。
注意:用户代理可以在拖放操作期间将任何层级数据表示为文件和目录。例如,在一个不直接向用户公开本地文件系统的设备上,如果为
"image/*"
指定了 accept
属性,则存储在关系数据库中的音频数据(具有用于专辑元数据的单独表和用于曲目的 Blob)可以在从媒体播放器应用程序拖动时作为目录和文件暴露给脚本。
partial interface DataTransferItem {FileSystemEntry ?webkitGetAsEntry (); };
webkitGetAsEntry()
方法步骤如下:
-
让 store 为 this 的
DataTransfer
对象的 拖放数据存储。 -
让 item 为 store 的 拖放数据存储项目列表 中代表 this 的项目。
-
如果 item 的 类型 不是 文件,则返回 null 并中止这些步骤。
-
返回一个新的
FileSystemEntry
对象,表示该 条目。
elem. addEventListener( 'dragover' , e=> { // 防止导航。 e. preventDefault(); }); elem. addEventListener( 'drop' , e=> { // 防止导航。 e. preventDefault(); // 处理所有项目。 for ( const entryof e. dataTransfer. items) { // kind 对于文件/目录条目将是 'file'。 if ( item. kind=== 'file' ) { const entry= item. webkitGetAsEntry(); handleEntry( entry); } } });
7. 文件和目录
callback =
ErrorCallback undefined (DOMException );
err
一个 ErrorCallback
函数用于可能异步返回错误的操作。
-
用于
readEntries()
成功和错误回调的 文件读取任务源 [FileAPI]。(Chromium 实现称其为TaskType::kFileReading
。) -
其他地方的 DOM 操作任务源 [HTML]。(Chromium 实现使用
TaskType::kMiscPlatformAPI
,它针对与TaskType::kDOMManipulation
相同的底层任务队列。)
7.1. FileSystemEntry
接口
[Exposed =Window ]interface {
FileSystemEntry readonly attribute boolean isFile ;readonly attribute boolean isDirectory ;readonly attribute USVString name ;readonly attribute USVString fullPath ;readonly attribute FileSystem filesystem ;undefined getParent (optional FileSystemEntryCallback ,
successCallback optional ErrorCallback ); };
errorCallback
一个 FileSystemEntry
关联一个 条目。
isFile
的 getter 步骤是如果 this 是一个 文件条目,则返回 true,否则返回 false。
isDirectory
的 getter 步骤是如果 this 是一个 目录条目,则返回 true,否则返回 false。
name
的 getter 步骤是返回 this 的 名称。
fullPath
的 getter 步骤是返回 this 的 完整路径。
filesystem
的 getter 步骤是返回 this 的 文件系统。
getParent(successCallback, errorCallback)
方法步骤如下:
-
并行执行以下步骤:
-
如果 item 失败,将一个任务排队以使用 « 一个新创建的“
NotFoundError
”DOMException
» 和“report
”调用 errorCallback(如果已提供),并中止这些步骤。 -
将一个任务排队以使用 « 一个与 entry 关联的新
FileSystemDirectoryEntry
对象 » 和“report
”调用 successCallback。
注意:如果文件在创建 FileSystemEntry
之后在磁盘上被修改,则可能会出错。
function handleEntry( entry) { console. log( 'name: ' + entry. name); console. log( 'path: ' + entry. fullPath); if ( entry. isFile) { console. log( '... is a file' ); } else if ( entry. isDirectory) { console. log( '... is a directory' ); } }
getParent()
适配为与 Promises [ECMA-262] 一起使用:
function getParentAsPromise( entry) { return new Promise(( resolve, reject) => { entry. getParent( resolve, reject); }); }
7.2. FileSystemDirectoryEntry
接口
[Exposed =Window ]interface :
FileSystemDirectoryEntry FileSystemEntry {FileSystemDirectoryReader createReader ();undefined getFile (optional USVString ?,
path optional FileSystemFlags = {},
options optional FileSystemEntryCallback ,
successCallback optional ErrorCallback );
errorCallback undefined getDirectory (optional USVString ?,
path optional FileSystemFlags = {},
options optional FileSystemEntryCallback ,
successCallback optional ErrorCallback ); };
errorCallback dictionary {
FileSystemFlags boolean =
create false ;boolean =
exclusive false ; };callback =
FileSystemEntryCallback undefined (FileSystemEntry );
entry
注意:虽然指定标志时没有有用的行为,但为了与现有实现兼容,create
成员及其相关行为已包含在内。同样,exclusive
成员未被明确引用,但如果传递一个带有 getter 的对象,绑定行为将在脚本中可观察到。
一个 FileSystemDirectoryEntry
的
关联 条目 是一个 目录条目。
createReader()
方法步骤是:
-
返回一个新创建的
FileSystemDirectoryReader
对象,该对象 关联到 目录条目。
getFile(path, options, successCallback, errorCallback)
方法的步骤如下:
-
并行执行以下步骤:
-
如果 path 是 undefined 或 null,则令 path 为空字符串。
-
如果 path 不是一个有效路径,将一个任务排队以使用 « 一个新创建的“
TypeMismatchError
”DOMException
» 和“report
”调用 errorCallback(如果已提供),并中止这些步骤。 -
如果 options 的
create
成员为 true,将一个任务排队以使用 « 一个新创建的“SecurityError
”DOMException
» 和“report
”调用 errorCallback(如果已提供),并中止这些步骤。 -
如果 item 失败,将一个任务排队以使用 « 一个新创建的“
NotFoundError
”DOMException
» 和“report
”调用 errorCallback(如果已提供),并中止这些步骤。 -
如果 item 不是一个文件,将一个任务排队以使用 « 一个新创建的“
TypeMismatchError
”DOMException
» 和“report
”调用 errorCallback(如果已提供),并中止这些步骤。 -
将一个任务排队以使用 « 一个与 entry 关联的新
FileSystemFileEntry
对象 » 和“report
”调用 successCallback(如果已提供)。
-
getDirectory(path, options, successCallback, errorCallback)
方法的步骤如下:
-
并行执行以下步骤:
-
如果 path 是 undefined 或 null,则令 path 为空字符串。
-
如果 path 不是一个有效路径,将一个任务排队以使用 « 一个新创建的“
TypeMismatchError
”DOMException
» 和“report
”调用 errorCallback(如果已提供),并中止这些步骤。 -
如果 options 的
create
成员为 true,将一个任务排队以使用 « 一个新创建的“SecurityError
”DOMException
» 和“report
”调用 errorCallback(如果已提供),并中止这些步骤。 -
如果 item 失败,将一个任务排队以使用 « 一个新创建的“
NotFoundError
”DOMException
» 和“report
”调用 errorCallback(如果已提供),并中止这些步骤。 -
如果 item 不是一个目录,调用 errorCallback(如果已提供)并附带 « 一个新创建的“
TypeMismatchError
”DOMException
» 和“report
”,并中止这些步骤。 -
将一个任务排队以使用 « 一个与 entry 关联的新
FileSystemDirectoryEntry
对象 » 和“report
”调用 successCallback(如果已提供)。
-
getFile()
和 getDirectory()
适配为与 Promises [ECMA-262] 一起使用:
function getFileAsPromise( entry, path) { return new Promise(( resolve, reject) => { entry. getFile( path, {}, resolve, reject); }); } function getDirectoryAsPromise( entry, path) { return new Promise(( resolve, reject) => { entry. getDirectory( path, {}, resolve, reject); }); }
7.3. FileSystemDirectoryReader
接口
[Exposed =Window ]interface {
FileSystemDirectoryReader undefined (
readEntries FileSystemEntriesCallback ,
successCallback optional ErrorCallback ); };
errorCallback callback =
FileSystemEntriesCallback undefined (sequence <FileSystemEntry >);
entries
FileSystemDirectoryReader
关联一个 条目(一个 目录条目),
一个 目录(初始为 null),
一个 读取标志(初始为 false),
一个 完成标志(初始为 false),
以及一个 读取器错误(初始为 null)。
readEntries(successCallback, errorCallback)
方法的步骤如下:
-
如果 this 的读取标志为 true,将一个任务排队以使用 « 一个新创建的“
InvalidStateError
”DOMException
» 和“report
”调用 errorCallback,并中止这些步骤。 -
如果 this 的读取器错误不为 null,将一个任务排队以使用 « 读取器错误 » 和“
report
”调用 errorCallback(如果已提供),并中止这些步骤。 -
如果 this 的完成标志为 true,将一个任务排队以使用一个空列表和“
report
”调用 successCallback,并中止这些步骤。 -
并行执行以下步骤:
-
令 entries 为 this 的目录中尚未由此
FileSystemDirectoryReader
生成的非零数量的条目(如果有)。 -
如果上一步失败(例如,目录被删除或权限被拒绝),则:
-
将一个任务排队以运行以下步骤:
const reader= dirEntry. createReader(); const doBatch= () => new Promise(( resolve, reject) => { // 读取一个批次。 reader. readEntries( entries=> { // 完成了吗? if ( entries. length=== 0 ) { return ; } // 处理这个批次。 entries. forEach( handleEntry); // 读取下一个批次。 doBatch(); }, error=> console. warn( error)); }; // 开始读取 doBatch();
FileSystemDirectoryReader
适配为与 Promises [ECMA-262] 一起使用:
function getEntriesAsPromise( dirEntry) { return new Promise(( resolve, reject) => { const result= []; const reader= dirEntry. createReader(); const doBatch= () => { reader. readEntries( entries=> { if ( entries. length> 0 ) { entries. forEach( e=> result. push( e)); doBatch(); } else { resolve( result); } }, reject); }; doBatch(); }); }
FileSystemDirectoryReader
适配为与 AsyncIterators [ECMA-262] 一起使用:
async function * getEntriesAsAsyncIterator( dirEntry) { const reader= dirEntry. createReader(); const getNextBatch= () => new Promise(( resolve, reject) => { reader. readEntries( resolve, reject); }); let entries; do { entries= await getNextBatch(); for ( const entryof entries) { yield entry; } } while ( entries. length> 0 ); }
这允许使用 for-await-of
进行有序的异步遍历目录树:
async function show( entry) { console. log( entry. fullPath); if ( entry. isDirectory) { for await ( const eof getEntriesAsAsyncIterator( entry)) { await show( e); } } }
7.4. FileSystemFileEntry
接口
[Exposed =Window ]interface :
FileSystemFileEntry FileSystemEntry {undefined file (FileCallback ,
successCallback optional ErrorCallback ); };
errorCallback callback =
FileCallback undefined (File );
file
A FileSystemFileEntry
的
关联 条目 是一个 文件条目。
file(successCallback, errorCallback)
方法的步骤如下:
-
并行执行以下步骤:
-
如果 item 失败,将一个任务排队以使用 « 一个新创建的“
NotFoundError
”DOMException
» 和“report
”调用 errorCallback(如果已提供),并中止这些步骤。 -
如果 item 是一个目录,将一个任务排队以使用 « 一个新创建的“
TypeMismatchError
”DOMException
» 和“report
”调用 errorCallback(如果已提供),并中止这些步骤。 -
将一个任务排队以使用 « 一个代表 item 的新
File
对象 » 和“report
”调用 successCallback。
FileReader
读取拖放文件的内容:
function readFileEntry( entry) { entry. file( file=> { const reader= new FileReader(); reader. readAsText( file); reader. onerror= error=> console. warn( error); reader. onload= () => { console. log( reader. result); }; }, error=> console. warn( error)); }
file()
适配到 Promises [ECMA-262] 使用的辅助函数:
function fileAsPromise( entry) { return new Promise(( resolve, reject) => { entry. file( resolve, reject); }); }
7.5. FileSystem
接口
[Exposed =Window ]interface {
FileSystem readonly attribute USVString name ;readonly attribute FileSystemDirectoryEntry root ; };
一个 FileSystem
的
关联 文件系统。
name
的 getter 步骤是返回 this 的 名称。
root
的 getter 步骤是返回一个与 this 的 根目录 关联的 FileSystemDirectoryEntry
对象。
8. 致谢
本规范在很大程度上基于 Eric Uhrhane 在 [file-system-api] 中的工作,
该工作引入了 FileSystemEntry
类型。
感谢 Tab Atkins, Jr. 创建和维护 Bikeshed,这是用于创建本文件的规范编写工具。
还要感谢 Ali Alabbas, Philip Jägenstedt, Marijn Kruisselbrink, Olli Pettay, 和 Kent Tamura 提供的建议、审查和其他反馈。