Skip to content

如何处理不同的文件系统

Node.js 公开了文件系统的许多功能。但并非所有文件系统都相同。以下是一些建议的最佳实践,以便在处理不同的文件系统时保持代码的简单性和安全性。

文件系统行为

在使用文件系统之前,您需要了解它的行为方式。不同的文件系统行为不同,并且具有或多或少的功能:区分大小写、不区分大小写、保留大小写、Unicode 形式保留、时间戳精度、扩展属性、inodes、Unix 权限、备用数据流等等。

注意不要从 process.platform 推断文件系统行为。例如,不要因为您的程序运行在 Darwin 上就认为您正在使用不区分大小写的文件系统 (HFS+),因为用户可能正在使用区分大小写的文件系统 (HFSX)。同样,不要因为您的程序运行在 Linux 上就认为您正在使用支持 Unix 权限和 inodes 的文件系统,因为您可能正在使用不支持这些特性的特定外部驱动器、USB 或网络驱动器。

操作系统可能不容易推断文件系统的行为,但并非一切都丢失了。您可以探测文件系统以查看它的实际行为,而不是保留每个已知文件系统和行为的列表(这始终是不完整的)。某些容易探测的特性是否存在,通常足以推断其他更难探测的特性的行为。

请记住,某些用户可能在工作树中的各种路径下挂载了不同的文件系统。

避免最低公分母方法

您可能会试图让您的程序表现得像最低公分母文件系统一样,通过将所有文件名规范化为大写,将所有文件名规范化为 NFC Unicode 形式,以及将所有文件时间戳规范化为例如 1 秒分辨率。这将是最低公分母方法。

不要这样做。您只能与在各个方面都具有完全相同的最低公分母特性的文件系统安全地交互。您将无法以用户期望的方式使用更高级的文件系统,并且您会遇到文件名或时间戳冲突。您很可能会通过一系列复杂的依赖事件丢失和损坏用户数据,并且您会创建难以解决甚至无法解决的错误。

当您以后需要支持一个只有 2 秒或 24 小时时间戳分辨率的文件系统时会发生什么?当 Unicode 标准发展到包含稍微不同的规范化算法时会发生什么(就像过去发生的那样)?

最低公分母方法倾向于尝试仅使用“可移植”的系统调用来创建可移植程序。这导致程序具有漏洞,实际上并不可移植。

采用超集方法

通过采用超集方法,充分利用你所支持的每个平台。例如,一个可移植的备份程序应该在 Windows 系统之间正确同步 btimes(文件或文件夹的创建时间),即使 Linux 系统不支持 btimes,也不应该破坏或更改 btimes。 同样的,这个可移植的备份程序应该在 Linux 系统之间正确同步 Unix 权限,即使 Windows 系统不支持 Unix 权限,也不应该破坏或更改 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 字符串具有相同的字节表示形式。 这可能是您对 ASCll 的期望,但不是对 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.js 返回的文件名是文件系统存储的原始文件名,并非所有文件系统都支持 Unicode 形式保留。 例如,HFS+ 会将所有文件名标准化为几乎与 NFD 形式相同的形式。 不要期望 HFS+ 的行为与 NTFS 或 EXT 4 相同,反之亦然。 不要试图通过规范化永久更改数据,以此作为一种泄漏的抽象来掩盖文件系统之间的 Unicode 差异。 这会产生问题而不会解决任何问题。 而是保留 Unicode 形式,并将规范化仅用作比较函数。

Unicode 形式不敏感性

Unicode 形式不敏感性和 Unicode 形式保持性是两种不同的文件系统行为,常常被混淆。正如大小写不敏感性有时被错误地实现为在存储和传输文件名时永久地将文件名规范化为大写一样,Unicode 形式不敏感性有时也被错误地实现为在存储和传输文件名时永久地将文件名规范化为特定的 Unicode 形式(在 HFS+ 的情况下为 NFD)。通过仅将 Unicode 规范化用于比较,可以在不牺牲 Unicode 形式保持性的情况下实现 Unicode 形式不敏感性,这是一种更好且可行的方法。

比较不同的 Unicode 形式

Node.js 提供了 string.normalize ('NFC' / 'NFD'),您可以使用它将 UTF-8 字符串规范化为 NFC 或 NFD。您永远不应该存储此函数的输出,而只能将其用作比较函数的一部分,以测试两个 UTF-8 字符串对用户而言是否看起来相同。您可以使用 string1.normalize('NFC')=== string2.normalize('NFC')string1.normalize('NFD')=== string2.normalize('NFD') 作为您的比较函数。使用哪种形式并不重要。

规范化速度很快,但您可能希望使用缓存作为比较函数的输入,以避免多次规范化相同的字符串。如果字符串不在缓存中,则对其进行规范化并缓存。注意不要存储或持久化缓存,仅将其用作缓存。

请注意,使用 normalize () 需要您的 Node.js 版本包含 ICU(否则 normalize () 将仅返回原始字符串)。如果您从网站下载最新版本的 Node.js,则它将包含 ICU。

时间戳精度

您可能会将文件的 mtime(修改时间)设置为 1444291759414(毫秒级精度),但有时会惊讶地发现 fs.stat 返回的新 mtime 为 1444291759000(1 秒级精度)或 1444291758000(2 秒级精度)。这不是 Node 中的错误。Node.js 按文件系统的存储方式返回时间戳,并非所有文件系统都支持纳秒、毫秒或 1 秒级的时间戳精度。某些文件系统甚至对 atime 时间戳具有非常粗略的精度,例如,某些 FAT 文件系统为 24 小时。

不要通过标准化破坏文件名和时间戳

文件名和时间戳是用户数据。正如你永远不会自动将用户文件数据重写为大写字母或将 CRLF 标准化为 LF 换行符一样,你也不应该通过大小写/ Unicode 形式/时间戳标准化来更改、干扰或破坏文件名或时间戳。标准化应该只用于比较,绝不能用于更改数据。

标准化实际上是一种有损哈希码。你可以使用它来测试某些类型的等价性(例如,几个字符串看起来是否相同,即使它们具有不同的字节序列),但你永远不能用它来代替实际数据。你的程序应该按原样传递文件名和时间戳数据。

你的程序可以使用 NFC(或它喜欢的任何 Unicode 形式)、小写或大写文件名或具有 2 秒分辨率的时间戳来创建新数据,但你的程序不应通过强制大小写/ Unicode 形式/时间戳标准化来破坏现有用户数据。相反,应采用超集方法,并在你的程序中保留大小写、Unicode 形式和时间戳分辨率。这样,你将能够安全地与执行相同操作的文件系统进行交互。

适当使用标准化比较函数

确保你适当地使用大小写/ Unicode 形式/时间戳比较函数。如果你在区分大小写的文件系统上工作,请不要使用不区分大小写的文件名比较函数。如果你在对 Unicode 形式敏感的文件系统上工作(例如,NTFS 和大多数 Linux 文件系统,它们同时保留 NFC 和 NFD 或混合 Unicode 形式),请不要使用对 Unicode 形式不敏感的比较函数。如果你在纳秒时间戳分辨率文件系统上工作,请不要以 2 秒分辨率比较时间戳。

为比较函数的细微差异做好准备

请注意,你的比较函数与文件系统的比较函数相匹配(或者如果可能,探测文件系统以查看它实际上将如何比较)。例如,不区分大小写比简单的 toLowerCase() 比较更复杂。实际上,toUpperCase() 通常比 toLowerCase() 更好(因为它以不同的方式处理某些外语字符)。但更好的方法是探测文件系统,因为每个文件系统都有其内置的大小写比较表。

例如,Apple 的 HFS+ 将文件名标准化为 NFD 形式,但此 NFD 形式实际上是当前 NFD 形式的旧版本,有时可能与最新 Unicode 标准的 NFD 形式略有不同。不要期望 HFS+ NFD 始终与 Unicode NFD 完全相同。