乐趣区

Node.js 指南(使用不同的文件系统)

使用不同的文件系统
Node 公开了文件系统的许多功能,但并非所有文件系统都相似,以下是建议的最佳实践,以便在使用不同的文件系统时保持代码简单和安全。
文件系统行为
在使用文件系统之前,你需要知道它的行为方式,不同的文件系统表现不同,并且具有比其他或多或少的功能:区分大小写、不区分大小写、大小写保留、Unicode 形式保留、时间戳解析、扩展属性、inode、Unix 权限、备用数据流等。
警惕从 process.platform 推断文件系统行为,例如,不要假设因为你的程序在 Darwin 上运行,因此你正在处理不区分大小写的文件系统(HFS+),因为用户可能正在使用区分大小写的文件系统(HFSX)。类似地,不要假设因为你的程序在 Linux 上运行,因此你正在处理支持 Unix 权限和 inode 的文件系统,因为你可能位于特定的外部驱动器、USB 或网络驱动器上。
操作系统可能不容易推断文件系统行为,但并不会丢失所有内容,你可以探测文件系统以查看它的实际行为,而不是保留每个已知文件系统和行为的列表(总是不完整),某些易于探测的特征的存在或缺失,往往足以推断其他更难探测的特征的行为。
请记住,某些用户可能在工作树中的各种路径上安装了不同的文件系统。
避免使用最低公分母方法
你可能想让你的程序像最低公分母文件系统一样,通过将所有文件名规范化为大写,将所有文件名规范化为 NFC Unicode 格式,并将所有文件时间戳标准化为 1 秒分辨率,这是最小公分母的方法。
不要这样做,你只能安全地与文件系统进行交互,该文件系统在各个方面具有完全相同的最小公分母特征,你将无法以用户期望的方式使用更高级的文件系统,并且你将遇到文件名或时间戳冲突,你肯定会通过一系列复杂的相关事件来丢失和损坏用户数据,并且你将创建即使不是不可能解决也很困难的 bug。
当你以后需要支持仅具有 2 秒或 24 小时时间戳分辨率的文件系统时会发生什么?当 Unicode 标准进展到包括稍微不同的规范化算法时(如过去发生的那样)会发生什么?
最小公分母方法倾向于尝试仅使用“可移植”系统调用来创建可移植程序,这会导致程序出现漏洞,而且实际上是不可移植的。
采用超集方法
通过采用超集方法充分利用你支持的每个平台,例如,一个可移植备份程序应该在 Windows 系统之间正确地同步 btimes(文件或文件夹的创建时间),并且不应该销毁或更改 btimes,即使 Linux 系统不支持 btimes。相同的可移植备份程序应该在 Linux 系统之间正确同步 Unix 权限,并且不应该销毁或更改 Unix 权限,即使在 Windows 系统上不支持 Unix 权限。
通过使程序像更高级的文件系统一样处理不同的文件系统,支持所有可能功能的超集:大小写敏感、大小写保留、Unicode 形式敏感、Unicode 形式保留、Unix 权限、高分辨率纳秒时间戳、扩展属性等。
在程序中保留大小写后,如果需要与不区分大小写的文件系统进行交互,则可以始终实现大小写不敏感。但是,如果你放弃了程序中的大小写保留,你就无法安全地与保留大小写的文件系统进行交互,对于 Unicode 形式保留和时间戳分辨率保留也是如此。
如果文件系统为你提供小写和大写混合的文件名,则将文件名保留在给定的确切大小写中,如果文件系统为你提供混合 Unicode 格式或 NFC 或 NFD(或 NFKC 或 NFKD)的文件名,则将文件名保留在给定的确切字节序列中,如果文件系统为你提供毫秒时间戳,则保持时间戳以毫秒为单位。
当你使用较小的文件系统时,你可以始终适当地进行下采样,使用运行程序的文件系统的行为所需的比较函数。如果你知道文件系统不支持 Unix 权限,那么你不应该期望读取你编写的相同 Unix 权限。如果你知道文件系统不保留大小写,那么你应该准备在程序创建 abc 时在目录列表中看到 ABC。但是,如果你知道文件系统确实保留了大小写,那么在检测文件重命名或文件系统区分大小写时,你应该将 ABC 视为与 abc 不同的文件名。
大小写保留
你可以创建一个名为 test/abc 的目录,有时会惊奇地发现 fs.readdir(‘test’) 返回 [‘ABC’],这不是 Node 中的 bug,Node 返回文件系统存储它的文件名,并非所有文件系统都支持大小写保留,某些文件系统将所有文件名转换为大写(或小写)。
Unicode 形式保留
大小写保留和 Unicode 形式保留是类似的概念,要理解为什么应该保留 Unicode 形式,请确保首先理解为什么要保留大小写,如果正确理解,Unicode 形式保留就一样简单。
Unicode 可以使用几个不同的字节序列对相同的字符进行编码,几个字符串可能看起来相同,但具有不同的字节序列。使用 UTF- 8 字符串时,请注意你的期望与 Unicode 的工作方式一致。正如你不希望所有 UTF- 8 字符编码为单个字节一样,你不应期望几个在人眼看起来相同的 UTF- 8 字符串具有相同的字节表示,这可能是你可以拥有 ASCII 而不是 UTF- 8 的期望。
你可以创建一个名为 test/café的目录(NFC Unicode 形式,字节序列 <63 61 66 c3 a9> 并且 string.length === 5)并且有时你会惊讶地发现 fs.readdir(‘test’) 返回 [‘café’](NFD Unicode 形式,字节序列 <63 61 66 65 cc 81> 并且 string.length === 6),这不是 Node 中的 bug。Node 返回文件系统存储时的文件名,并非所有文件系统都支持 Unicode 形式保留。
例如,HFS+ 会将所有文件名规范化为几乎总是与 NFD 形式相同的形式,不要指望 HFS+ 的行为与 NTFS 或 EXT4 相同,反之亦然。不要试图通过规范化永久地更改数据作为掩盖文件系统之间 Unicode 差异的漏洞抽象,这会产生问题而不解决任何问题,相反,保留 Unicode 形似并仅使用规范化作为比较函数。
Unicode 形式不敏感
Unicode 形式不敏感和 Unicode 形式保留是两种不同的文件系统行为,经常互相误解。正如在存储和传输文件名时将文件名永久规范化为大写一样,有时不正确地实现了大小写不敏感,因此,在存储和传输文件名时,通过将文件名永久规范化为某种 Unicode 格式(在 HFS+ 的情况下为 NFD),有时会错误地实现 Unicode 格式不敏感性。通过使用 Unicode 规范化进行比较,可以并且更好地实现 Unicode 形式不敏感而不牺牲 Unicode 形式保留。
比较不同的 Unicode 形式
Node 提供 string.normalize(‘NFC’ / ‘NFD’),你可以使用它将 UTF- 8 字符串规范化为 NFC 或 NFD,你永远不应该存储此函数的输出,而只是将其用作比较函数的一部分,以测试两个 UTF- 8 字符串对于用户是否看起来相同。
你可以使用 string1.normalize(‘NFC’) === string2.normalize(‘NFC’) 或 string1.normalize(‘NFD’) === string2.normalize(‘NFD’) 作为比较函数,你使用哪种形式并不重要。
规范化很快但你可能希望使用缓存作为比较函数的输入,以避免多次规范化相同的字符串,如果该字符串不在缓存中,则对其进行规范化并对其进行缓存,注意不要存储或保留缓存,只能将其用作缓存。
请注意,使用 normalize() 要求你的 Node 版本包含 ICU(否则 normalize() 将返回原始字符串),如果你从网站下载最新版本的 Node,那么它将包括 ICU。
时间戳分辨率
你可以将文件的 mtime(修改时间)设置为 1444291759414(毫秒分辨率),并有时惊讶地发现 fs.stat 将新 mtime 返回为 1444291759000(1 秒分辨率)或 1444291758000(2 秒分辨率),这不是 Node 中的 bug。Node 返回文件系统存储它的时间戳,并非所有文件系统都支持纳秒、毫秒或 1 秒时间戳分辨率。有些文件系统甚至对 atime 时间戳的分辨率非常粗糙,例如,对于一些 FAT 文件系统,分辨率为 24 小时。
不要通过规范化来破坏文件名和时间戳
文件名和时间戳是用户数据,正如你永远不会自动重写用户文件数据以使数据大写或将 CRLF 规范化为 LF 行结束一样,因此你不应该通过大小写 /Unicode 格式 / 时间戳规范化来更改、干扰或损坏文件名或时间戳,规范化只应用于比较,绝不能用于改变数据。
规范化实际上是有损哈希码,你可以使用它来测试某些类型的等价性(例如,即使它们具有不同的字节序列,几个字符串看起来相同)但你永远不能将它用作实际数据的替代品,你的程序应按原样传递文件名和时间戳数据。
你的程序可以在 NFC 中创建新数据(或者以其喜欢的任何 Unicode 形式组合)或使用小写或大写文件名,或者使用 2 秒的分辨率时间戳,但是你的程序不应该通过强加大小写 /Unicode 形式 / 时间戳规范化来破坏现有的用户数据。相反,采用超集方法并在程序中保留大小写、Unicode 格式和时间戳分辨率,这样,你就可以安全地与执行相同操作的文件系统进行交互。
适当地使用标准化比较功能
确保正确使用大小写 /Unicode 形式 / 时间戳比较功能,如果你正在处理区分大小写的文件系统,请不要使用不区分大小写的文件名比较函数。如果你正在使用 Unicode 形式敏感文件系统(例如 NTFS 和大多数保留 NFC 和 NFD 或混合 Unicode 形式的 Linux 文件系统),请不要使用 Unicode 形式不敏感的比较函数。如果你正在使用纳秒时间戳分辨率文件系统,请不要以 2 秒分辨率比较时间戳。
为比较功能的微小差异做好准备
请注意你的比较函数与文件系统的比较函数匹配(或者如果可能的话探测文件系统以查看它实际比较的方式),例如,不区分大小写比简单的 toLowerCase() 比较复杂,事实上,toUpperCase() 通常比 toLowerCase() 更好(因为它以不同的方式处理某些外语字符)。但更好的方法是探测文件系统,因为每个文件系统都有自己的大小写比较表。
例如,Apple 的 HFS+ 将文件名规范化为 NFD 格式,但这种 NFD 格式实际上是当前 NFD 格式的旧版本,有时可能与最新的 Unicode 标准的 NFD 格式略有不同,不要指望 HFS+ NFD 始终与 Unicode NFD 完全相同。

上一篇:HTTP 事务的剖析

退出移动版