Skip to content

如何处理不同的文件系统

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

文件系统行为

在您可以使用文件系统之前,您需要了解它的行为方式。不同的文件系统行为不同,并且特性或多或少有所差异:大小写敏感性、大小写不敏感性、大小写保持、Unicode 形式保持、时间戳分辨率、扩展属性、inode、Unix 权限、备用数据流等等。

要警惕从 process.platform 推断文件系统行为。例如,不要假设因为您的程序正在 Darwin 上运行,您就在使用大小写不敏感的文件系统 (HFS+),因为用户可能正在使用大小写敏感的文件系统 (HFSX)。同样,不要假设因为您的程序正在 Linux 上运行,您就在使用支持 Unix 权限和 inode 的文件系统,因为您可能在一个特定的外部驱动器、USB 或网络驱动器上,而这些驱动器不支持这些功能。

操作系统可能无法轻松推断文件系统行为,但这并非完全没有办法。与其保留每个已知文件系统及其行为的列表(这总是会不完整),不如探测文件系统以查看其实际行为。某些易于探测的特性是否存在,通常足以推断更难以探测的其它特性的行为。

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

避免最低公分母方法

你可能会倾向于让你的程序像最低公分母文件系统一样工作,将所有文件名规范化为大写,将所有文件名规范化为NFC Unicode形式,并将所有文件时间戳规范化为例如1秒精度。这将是最低公分母方法。

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

当你以后需要支持只有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。但是,如果您知道文件系统保留大小写,那么在检测文件重命名或文件系统区分大小写时,您应该认为 ABCabc 是不同的文件名。

大小写保持

你可能会创建一个名为 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.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 的 bug。Node.js 返回文件系统存储的时间戳,并非所有文件系统都支持纳秒、毫秒或 1 秒的时间戳精度。某些文件系统甚至对 atime 时间戳的精度非常粗略,例如某些 FAT 文件系统为 24 小时。

不要通过规范化破坏文件名和时间戳

文件名和时间戳是用户数据。就像您永远不会自动将用户文件数据重写为大写数据或将 CRLF 规范化为 LF 行尾一样,您也不应该通过大小写/Unicode 形式/时间戳规范化来更改、干扰或破坏文件名或时间戳。规范化应该只用于比较,永远不要用于更改数据。

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

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

正确使用规范化比较函数

确保您正确使用大小写/Unicode形式/时间戳比较函数。如果您正在处理大小写敏感的文件系统,请不要使用大小写不敏感的文件名比较函数。如果您正在处理Unicode形式敏感的文件系统(例如,保留NFC和NFD或混合Unicode形式的NTFS和大多数Linux文件系统),请不要使用Unicode形式不敏感的比较函数。如果您正在处理纳秒级时间戳分辨率的文件系统,请不要以2秒分辨率比较时间戳。

准备好应对比较函数的细微差异

请注意,您的比较函数应与文件系统的比较函数匹配(如果可能,请探测文件系统以查看其实际的比较方式)。例如,大小写不敏感比简单的toLowerCase()比较要复杂得多。事实上,toUpperCase()通常比toLowerCase()更好(因为它对某些外语字符的处理方式不同)。但更好的方法是探测文件系统,因为每个文件系统都有自己的内置大小写比较表。

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