4

使用不同的文件系统

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事务的剖析
下一篇:流中的背压

博弈
2.5k 声望1.5k 粉丝

态度决定一切