JavaScript 操作文件系统
参考
- The File System Access API: simplifying access to local files - Chrome Developers
- text-editor 一个用于演示 File System Access API 的文本编辑器 Web 应用的源码
- File System Access API - MDN
File System Access API 文件系统访问接口让 Web 应用拥有读写、管理本地设备或线上的文件系统的能力,实现与原生应用类似的功能。
注意
大部分功能仅在 secure context 安全上下文,例如使用 HTTPS 协议的网站中可用。
使用该系列的 API 的核心概念是一个名为 FileSystemHandle
句柄的类 class
该类的实例表示文件系统中的一个条目(可能是一个文件或一个目录,以下用 handle
表示),它具有以下属性和方法:
说明
对于文件系统中的同一个 entry 条目,可以有多个句柄与之对应
handle.kind
(只读)属性:该属性值可能是file
或directory
,句柄所关联的条目的类型。
如果关联的是文件,则该属性值就是file
;如果关联的是目录,则该属性值就是directory
handle.name
(只读)属性:该属性值是一个字符串,句柄所关联的条目的名称handle.isSameEntry(anotherHandle)
方法:返回一个布尔值,以判断两个句柄是否关联到文件系统中的同一个条目handle.queryPermission(permissionDescriptor)
(异步)方法:查看该句柄是否已获取授权。
可选参数permissionDescriptor
是一个对象,用以设置需要查询该句柄的哪一种权限- 如果是参数是
{ mode: 'read' }
则只是查看句柄是否具有读取权限 - 如果参数是
{ mode: 'readwrite'}
则查看句柄是否同时具有读写权限
返回一个对象,其属性state
用以描述该句柄指定权限的授权状态,属性值可以是以下三个值之一:granted
已经获取相应的权限denied
相应的权限被拒绝prompt
需要询问用户是否赋予相应的权限,这时候应该调用handle.requestPermission(permissionDescriptor)
方法,以弹出一个对话框获取用户授权。
提示
关于句柄的权限相关内容,可以查看本文的 保存句柄与获取授权 部分
- 如果是参数是
handle.requestPermission(permissionDescriptor)
(异步)方法:为句柄请求特定的权限
可选参数permissionDescriptor
是一个对象,用以设置需要为句柄请求哪一种权限- 如果是参数是
{ mode: 'read' }
则只是为句柄请求读取权限 - 如果参数是
{ mode: 'readwrite'}
则为句柄同时请求读写权限
返回一个 Promise 如果 resolve 就会得到一个对象,其中state
属性可以是以下三个值之一,以表示句柄的相应权限信息:granted
已经获取相应的权限denied
相应的权限请求被拒绝prompt
需要询问用户是否赋予相应的权限
- 如果是参数是
handle.remove(options)
方法:从文件系统种删除该句柄所关联的条目
可选参数options
是一个对象,其属性recursive
是一个布尔值,以表示是否执行递归删除(如果删除的条目是一个非空文件夹时,是否同时删除文件夹里的内容)说明
该方法可以移除文件系统中与句柄所关联的条目,但是该方法依然在实验中,在某些浏览器版本中可能不可用。
作为 fallback 替代方法,可以先获取目标条目所在的(父)目录的句柄
FileSystemDirectoryHandle
再调用directoryHandle.removeEntry(targetEntryName)
来移除目标条目注意
如果
recursive
属性没有设置为true
(默认值为false
),且要删除的条目是一个非空文件夹,则会抛出InvalidModificationError
错误
句柄 FileSystemHandle 类有两个子类:
FileSystemFileHandle
文件句柄:表示一个文件条目 file entry说明
基于文件句柄,还可以创建一个名为
FileSystemSyncAccessHandle
同步访问的文件句柄,用于实现高性能的同步写入。但是该句柄只能在 Web Worker 中使用,且只适用于 origin private file system
FileSystemDirectoryHandle
目录句柄:表示一个文件夹条目 directory entry
TypeScript
因为这个 API 是暴露在 window
对象下的,但是依然在开发中的,所以 VS Code 的内置 TypeScript 插件可能并不能识别,需要在项目中安装相应的类型包
文件句柄
FileSystemFileHandle
该子类的实例表示文件系统中的一个文件。
文件句柄除了继承了 FileSystemHandle
句柄的属性和方法,还具有以下一些特有的方法:
fileHandle.getFile()
(异步)方法:获取该句柄所关联的文件对象File
对象 实际上是一个 Blob 对象(数据以二进制格式表示),可以调用 Blob 对象的一些方法将二进制的数据转换为其他格式提示
blob 对象有一些实用的方法可用于读取其二进制内容,转换为特定的格式,如
text()
、slice()
、stream()
、arrayBuffer()
注意
通过方法
fileHandle.getFile()
所获得的一个文件对象,如果在此期间文件被更改了,那么该对象就变得不可读/访问,则需要重新调用fileHandle.getFile()
方法来获取新的文件对象fileHandle.createSyncAccessHandle()
(异步)方法:创建一个FileSystemSyncAccessHandle
同步访问的文件句柄,具体可参考本文的 同步写入 部分fileHandle.createWritable(options)
(异步)方法:创建一个FileSystemWritableFileStream
写入流(它是 WritableStream 的子类(将流数据写入目标),该子类添加了一些额外的方法以便用于操作文件系统中的单个文件),以便将更改写入到句柄的关联文件中
可选参数options
是一个对象,其属性keepExistingData
是一个布尔值,用以设置写入方式。- 如果参数是
{ keepExistingData: true}
则保留原有的内容,以追加的方式写入内容 - 如果参数是
{ keepExistingData: false}
(默认值)则以覆盖的方式写入内容
说明
当调用
fileHandle.createWritable()
方法时,浏览器首先会查看是否已经获得了对于指定文件的写入权限。如果没有获得权限则会弹出一个 prompt 提示框以询问用户是否允许授权,如果没有获得权限就会抛出DOMException
错误。只有调用了方法
writableStream.close()
将写入流关闭时,更改的内容才会真的写入到文件中。实际上写入流将更改写入到一个临时文件中,当写入流关闭时,再用临时文件去替代句柄所关联的真实文件
FileSystemWritableFileStream 该类的实例具有以下方法,以便设置如何将内容写入到文件中:- 方法
writableFileStream.write(content)
将内容写入到文件中当前光标所在的位置处 - 方法
writableFileStream.seek(position)
更新光标位置,以字节 byte 为单位 - 方法
writableFileStream.truncate(size)
将写入流中的内容截短为指定的字节 bytes 长度
其中方法writableStream.write()
可以传入一个对象,包含配置参数和要写入的数据,以实现其他两个方法的作用js// just pass in the data (no options) writableStream.write(data); // writes the data to the stream from the determined position writableStream.write({ type: "write", position, data }); // updates the current file cursor offset to the position specified writableStream.write({ type: "seek", position }); // resizes the file to be size bytes long writableStream.write({ type: "truncate", size });
- 如果参数是
注意
如果要使用以上列出的 file handle 文件句柄的相应的方法,请先使用 fileHandle.queryPermission(permissionDescriptor)
确保句柄有相应的权限
如果页面刷新重载,而且浏览器的其他标签页中没有同源的,那么之前所获取的文件句柄的权限将会失效,需要通过 fileHandle.requestPermission(permissionDescriptor)
重新获取
目录句柄
FileSystemDirectoryHandle
该子类的实例表示文件系统中的一个目录。
目录句柄除了继承了 FileSystemHandle
句柄的属性和方法,还具有以下一些特有的方法,可用于查看或操作目录中的内容:
directoryHandle.entries()
(异步)方法:返回一个可迭代对象(列表),其元素是一个二维数组,以[key, value]
键值对的格式保存着该目录中的(直接)子条目,包括(直接)子目录和文件。jsconst dirHandle = await window.showDirectoryPicker() for await (const [key, value] of dirHandle.entries()) { console.log({ key, value }) }
返回的条目称为 FileSystemDirectoryEntry,该对象具有一些实用的方法directoryHandle.resolve(possibleDescendantEntry)
(异步)方法:返回一个数组或null
如果返回的是一个数组,则里面的元素是字符串,是从directoryHandle
所对应的(父)目录,到给定的后代 entry 条目possibleDescendantEntry
所「经历」的目录的名称,所以该数组记录了从父目录到目标子条目的路径,而且数组的最后一个元素就是该 entry 条目。
如果没有找到该路径,则返回null
。directoryHandle.getFileHandle(fileName[, options])
(异步)方法:返回该目录下的一个指定名称fileName
的文件的句柄
其中第二个参数options
是可选参数,它是一个对象,具有create
属性,该属性值是一个布尔值,如果设置为true
则会在未找到指定名称的文件时,新建一个相应的文件directoryHandle.getDirectoryHandle(dirName[, options])
(异步)方法:返回该目录下的一个指定名称dirName
的子目录的句柄
其中第二个参数options
是可选参数,它是一个对象,具有create
属性,该属性值是一个布尔值,如果设置为true
则会在未找到指定名称的子目录时,新建一个相应的子目录directoryHandle.removeEntry(entryName[, options])
(异步)方法:移除指定名称的条目(文件或子目录)
其中第二个参数options
是可选参数,它是一个对象,具有recursive
属性。该属性是一个布尔值,如果设置为true
则会递归地删除指定子目录里的所有内容注意
如果不将属性
recursive
设置为true
,而所需删除的子目录中有内容,则会抛出InvalidModificationError
错误
推荐
在使用目录句柄时可能需要同时访问其中的多个条目,因为 File System Access API 的大部分方法都是异步的,并不推荐 使用 async-await 代码模式,更推荐使用 Promise.all()
同时进行操作
获取句柄
句柄可以通过很多方法获得,一般从以下这三个(异步)方法开始入手:
window.showOpenFilePicker()
以读取read
模式获取文件句柄window.showSaveFilePicker()
以读写(可保存)readwrite
模式获取文件句柄window.showDirectoryPicker()
获取目录句柄
调用这些方法就会弹出一个文件/目录选择窗口 file or directory picker,让用户在文件系统中选择需要操作的条目,并将操作权限授予给当前的 web 应用/页面。
这个交互比较繁琐,但这是为了确保用户在网络上的安全,Web 应用需要获取用户主动授权确认后,才可以访问文件系统的内容。
提示
除了调用以上的方法通过弹出的对话框来选择文件/目录来获取句柄,还可以将 FileSystemHandle 与 Drag-N-Drop 交互相结合,通过方法 DataTransferItem.getAsFileSystemHandle()
获取相应的句柄。
还可以将已「安装」的 Web 应用(PWA 应用)注册为特定类型的文件的处理/打开程序
以下是一些常见的使用场景和样例代码
读取文件
调用(异步)方法 window.showOpenFilePicker(pickerOpts)
会弹出一个文件选择对话框,称为 file picker 文件选择器,让用户选择一个文件(以读取模式来打开它)。
该方法返回的是 Promise,如果 resolve 就会得到一个文件句柄
可选参数 pickerOpts
用于设置文件选择器的行为,它是一个对象,支持设置以下的属性:
id
属性:一个字符串,作为该文件句柄的唯一标识符,用于区分记录文件选择对话框打开时最后位于哪一个目录(而不必总是打开默认的目录)startIn
属性:可以是 directory handle 目录句柄,也可以是一个表示系统的内置目录的字符串,用于设置打开对话框时默认位于哪一个目录。提示
各系统都常见的一些内置目录如下:
desktop
桌面documents
文档downloads
下载music
音乐pictures
图片videos
视频
multiple
属性:一个布尔值,用于设置文件选择器是否支持选择多文件。
默认值为false
,所以文件选择器的默认行为只支持选择单个文件。excludeAcceptAllOption
属性:一个布尔值,默认值为false
。在弹出的文件选择对话框,可选文件格式的下拉菜单中,默认会包含一个「允许所有格式」的选项,以便用户可以选择任何格式的文件。
如果将该属性设置为true
则下拉菜单中就没有这个选项,相当于强制用户只能选择预期格式的文件types
属性:一个数组,列出了允许选择的文件格式。数组的每一个元素是一个对象,具有以下两个属性:description
(可选)属性:对于该文件类型的可选说明accept
属性:一个对象,用键值对来表示文件格式,键名是一个 MIME 类型,值则是一个数组,其中每个元素都是表示文件格式/拓展名的字符串
// 一个 file picker 文件选择器选项的示例
const pickerOpts = {
types: [
{
description: 'Images',
accept: {
'image/*': ['.png', '.gif', '.jpeg', '.jpg']
}
},
],
excludeAcceptAllOption: true,
multiple: false
};
该方法的返回值是一个 Promise,如果 resolve 就会得到一个包含文件句柄的数组(如果打开的是一个文件,那么数组中就只有一个元素)
let fileHandle;
// 假设 btnOpenFile 是指页面上的一个按钮
btnOpenFile.addEventListener('click', async () => {
// 该方法是一个异步函数
// 返回值是一个数组,因为默认只允许选择一个文件
// 所以通过数组解构,提取出其中的唯一元素
const fileHandlesArr = await window.showOpenFilePicker();
console.log(fileHandlesArr);
// 获取第一个文件句柄
fileHandle = fileHandlesArr[0]
console.log(fileHandle);
});
获取了文件句柄后就可以调用相关方法以获取文件内容,例如假设用户选择打开的是一个 hello.txt
的文本文件,通过以下代码查看文本文件的具体内容:
// 通过 getFile() 方法获取文件句柄所关联的文件
// 返回一个 File 对象,它是一个 Blob 对象
const file = await fileHandle.getFile()
// 使用 Blob 对象的 text() 方法将二进制的数据解析转换为 UTF-8 格式的字符串
const content = await file.text()
console.log(content); // 将字符串内容打印到控制台
写入文件
调用(异步)方法 window.showSaveFilePicker(options)
会弹出一个文件选择对话框,让用户选择一个已存在的文件,或创建一个新文件(需要输入新文件的名称),以读写/保存模式来打开
可选参数 options
用于设置 file picker 文件选择器 的行为,它是一个对象,支持设置以下的属性:
startIn
属性:可以是 directory handle 目录句柄,也可以是一个表示系统的内置目录的字符串,用于设置打开对话框时默认位于哪一个目录。提示
各系统都常见的一些内置目录如下:
desktop
桌面documents
文档downloads
下载music
音乐pictures
图片videos
视频
id
属性:一个字符串,作为该文件句柄的唯一标识符,用于区分记录文件选择对话框打开时,最后位于哪一个目录。excludeAcceptAllOption
属性:与方法window.showOpenFilePicker(pickerOpts)
选项中的属性作用一样suggestedName
属性:一个字符串,作为新建文件的建议/默认名称types
属性:与方法window.showOpenFilePicker(pickerOpts)
选项中的属性作用一样
该方法是一个异步函数,返回值是一个 file handle 文件句柄,获取了支持读写/保存模式的 file handle 文件句柄后,就可以调用其中的相关方法将内容写入到文件中:
// fileHandle is an instance of FileSystemFileHandle..
async function writeFile(fileHandle, contents) {
// 创建一个 FileSystemWritableFileStream 写入流
const writable = await fileHandle.createWritable();
// 将给定的内容 content 写入到文件中
await writable.write(contents);
// 将写入流关闭,此时才会将内容写入磁盘
await writable.close();
}
注意
如果选择的是一个已存在的文件,且以上方法都采用默认参数,则在保存文件时,会将文件原有的内容覆盖掉
内容并不会真正写入磁盘,直至调用了 writableStream.close()
方法将写入流关闭
操作目录内容
调用(异步)方法 window.showDirectoryPicker(options)
会弹出一个目录选择对话框,让用户选择一个目录,以打开该目录
说明
默认情况下,目录句柄可以对该目录里的文件具有读取权限,而没有写入权限。如果需要对该目录里所有文件具有读写权限,需要在在参数 options
中进行配置,将属性 mode
的值设置为 readwrite
可选参数 options
用于设置 directory picker 目录选择器 的行为,它是一个对象,支持设置以下的属性:
id
属性:目录句柄的标识符,以便浏览器进行记录区分不同的目录。
如果新建一个目录句柄,其id
与已存在的目录句柄相同,则新建的句柄将会打开同一个目录mode
属性:一个字符串,表示目录句柄的权限,该属性值可以是read
表示对于该目录里所有文件只具有读取权限;如果是readwrite
表示对该目录里所有文件具有读写权限startIn
属性:可以是 directory handle 目录句柄,也可以是一个表示系统的内置目录的字符串,用于设置打开对话框时默认位于哪一个目录。提示
各系统都常见的一些内置目录如下:
desktop
桌面documents
文档downloads
下载music
音乐pictures
图片videos
视频
该方法是一个异步函数,返回值是一个 FileSystemDirectoryHandle 对象,即一个 directory handle 目录句柄
常见的一个场景是遍历目录中的(直接)条目(文件和子目录)
btn.addEventListener('click', async () => {
// 获取 directory handle 目录句柄
const directoryHandle = await window.showDirectoryPicker();
// directoryHandle.values() 获取目录中的内容,包含(直接)子目录和文件
for await (const entry of directoryHandle.values()) {
// 列出每个条目的类型 entry.kind 和名称 entry.name
// 类型包含 directory 目录和 file 文件
// 名称就是文件夹名称或文件名称
console.log(entry.kind, entry.name);
}
});
如果要遍历目录中所有的文件(包括嵌套较深的文件)需要使用递归的方法
async function* getFilesRecursively (entry) {
if (entry.kind === 'file') {
const file = await entry.getFile();
if (file !== null) {
file.relativePath = getRelativePath(entry);
yield file;
}
} else if (entry.kind === 'directory') {
for await (const handle of entry.values()) {
yield* getFilesRecursively(handle);
}
}
}
for await (const fileHandle of getFilesRecursively(directoryHandle)) {
console.log(fileHandle);
}
另一个常见的场景是获取/创建一个文件/子目录的操作句柄
// 在当前目录下,打开或创建一个名为 `My Documents` 的子目录,并返回它的目录句柄
const newDirectoryHandle = await existingDirectoryHandle.getDirectoryHandle('My Documents', {
create: true,
});
// 在当前目录下,打开或创建一个名为 `My Notes.txt` 的文件,并返回它的文件句柄
const newFileHandle = await newDirectoryHandle.getFileHandle('My Notes.txt', { create: true });
// 以上两个操作都将 create 属性设置为 true,所以在当前目录下没有找到给定名称的条目时,就会创建一个新的
重命名与移到
文件句柄和目录句柄都支持 move
方法,而且可以传递相应的参数,在移动文件或目录的同时实现重命名
// 传递一个字符串,重命名文件
await fileHandle.move('new_name');
// 传递一个目录手柄,将文件移动到指定的目录
await fileHandle.move(directoryHandle);
// 依次传递一个目录手柄和字符串,将文件移动到指定的目录,并同时重命名文件
await fileHandle.move(directoryHandle, 'newer_name');
该方法还是实验阶段,可以在该页面查看浏览器是否支持该方法。
注意
FileSystemHandle.move()
该方法目前只在 origin private file system,OPFS,域名私有文件系统中其作用,这和浏览器的其他存储方式一致(例如 Cookie、LocalStorage、SessionStorage、IndexedDB 等,都是域名专属私有的,无法与其他域名的页面共享)
对于其他文件系统的支持计划进度可以关注这个页面
保存句柄与获取授权
将所得的 file handle 文件句柄或 directory handle 目录句柄保存起来以便之后使用是一个好习惯。
因为它们支持 serializable 序列化,所以可以将它们保存到数据库中(例如浏览器的 IndexedDB 数据库,💡 推荐使用一些框架,如 idb-keyval、Dexie.js,来操作 IndexedDB 数据库),以便下一次使用。
注意
但是由于目前无法将句柄授权在会话之间持久化,所以在每次关闭网页再重新打开时,都需要重复询问用户授权,以便该句柄获得相关的权限。
文件句柄或目录句柄的权限查询与获取:
- 调用(异步)方法
fileHandle.queryPermission(permissionDescriptor)
:查看文件句柄是否已经获得授权,返回一个(只读)字符串以表示句柄当前的权限状态,有以下的的可能值;granted
表示句柄已获取相应的权限denied
表示句柄的相应权限请求被拒绝prompt
表示句柄需要用户授权(一般从数据库,例如 IndexedDB 中取回的句柄的权限状态就是prompt
),此时应该调用fileHandle.requestPermission()
方法
- 调用(异步)方法
fileHandle.requestPermission(permissionDescriptor)
:弹出一个对话框,询问用户是否为文件句柄授予相应的权限
其中(可选)参数 permissionDescriptor
是一个对象,用于描述句柄所需的权限,该对象只有一个属性 mode
,而属性值只能是 read
或 readwrite
分别表示只读权限或读写权限(💡 推荐为句柄请求 readwrite
读写权限,这样在之后如果要将内容写入到文件时,就不必再次去询问用户以获得授权)
这个不太友好的交互方式是否在日后会得到改善,可以关注跟踪这个页面的报道
同步写入
文件句柄有一个方法 fileHandle.createSyncAccessHandle()
可实现高性能的同步写入。
该方法本身是一个异步方法,返回的 Promise 如果 resolve 就会获得一个 FileSystemSyncAccessHandle
句柄,它会为关联的文件设置独占锁,调用该句柄的所提供的相关方法来操作文件:
fileSyncAccessHandle.read()
读取文件fileSyncAccessHandle.truncate()
截短文件到指定的大小(以 bytes 字节为单位)fileSyncAccessHandle.write()
将指定缓冲区的内容写入到文件fileSyncAccessHandle.flush()
手动将通过write
方法对文件所做的任何更改保存到磁盘(调用完write
方法后,也可以不调用该方法,让操作系统进行自动处理)fileSyncAccessHandle.getSize()
获取文件的大小(以 bytes 字节为单位)fileSyncAccessHandle.close()
关闭该同步文件访问句柄,并释放文件的独占锁
说明
这个方法最大的特点是可以实现在页面输入内容的同时,写入到文件系统的文件内容中。
而其他方法一般都是异步的,如 fileHandle.createWritable()
方法,只有在关闭写入流时,才真正将内容写入到磁盘中
注意
这个方法只能用于 Web Workers 中