在 GitHub 上编辑

与不同的文件系统协同工作

Node.js 公开了许多文件系统的特性。但并不是所有的文件系统都相同。以下都是给你的最佳建议,它们能够让你的代码在不同的文件系统之间保持简单又安全。

文件系统的行为

在你开始与文件系统工作前,你需要知道它是如何工作的。不同的文件系统行为表现差异,而且相互之间有或多或少的特性:如大小写敏感、大小写不敏感、大小写保存、编码格式保存、时间戳解析、扩展特性、节点表、Unix 权限、备用数据流等。

你应该小心翼翼地通过 process.platform 推断文件系统类型。举一个例子,不要假设因为您的程序在 Darwin 系统上运行,因此您正在处理一个不区分大小写的文件系统(HFS+),因为用户可能使用一个区分大小写的文件系统(HFSX)。类似地,不要假设因为您的程序在 Linux 上运行,因此您正在处理支持 Unix 权限和 INODE 的文件系统,因为您可能在特定的外部驱动器上,USB 或网络驱动器不支持。

操作系统可能不容易推断文件系统行为,但都不会丢失。不用保存所有已知文件系统和行为的列表(这总是不完整的),您可以探测文件系统来查看它实际上是如何运行的。容易探测的某些特征的存在或不存在常常足以推断更难探测的其它特征的行为。

请记住,一些用户可能在工作树中的不同路径上安装了不同的文件系统。

避免最低公分母方法

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

不要这样做。您只能安全地与文件系统进行交互,该文件系统在各个方面具有完全相同的最低公分母特性。您将无法以用户期望的方式使用更高级的文件系统,并且您会遇到文件名或时间戳冲突。您肯定会通过一系列复杂的依赖事件造成用户数据丢失和损坏。你会不断给你自己制造缺陷,而且解决起来很困难。

当您稍后需要支持只有 2 秒或 24 小时时间戳分辨率的文件系统时会发生什么?当 Unicode 标准前进到包含稍微不同的归一化算法(如以前发生)时会发生什么?

最常用的分母方法倾向于通过只使用“便携”系统调用来创建一个可移植的程序。这导致了程序泄漏,而实际上不是便携式的。

采用超集方法

充分利用你支持的每一个平台,采用超集方法。例如,一个可移植的备份程序应该正确地在 Windows 系统之间同步文件或文件夹,即使在 Linux 系统上不支持 btime,也不应该破坏或更改它。相同的便携式备份程序应在 Linux 之间正确同步 Unix 权限系统且不应破坏或更改 Unix 权限,即便 Unix 权限在 Windows 系统上不被支持。

通过使程序像更高级的文件系统一样处理不同的文件系统。支持所有可能功能的超集: 区分大小写、保存大小写、Unicode 敏感编码、Unicode 格式保存、Unix 权限、高分辨率纳秒时间戳、扩展属性等。

一旦你在程序中拥有了大小写保存设置后,如果需要与不区分大小写的文件系统进行交互,则始终可以实现不区分大小写。但是如果您放弃程序中的大小写保存,则无法与拥有大小写保存的文件系统安全地进行交互。对于 Unicode 编码保存和时间戳解析保存也是如此。

如果文件系统为您提供小写和大写混合的文件名,请将文件名保留在给定的大小写状态下。如果文件系统为您提供混合 Unicode 编码或 NFC 或 NFD (或 NFKC 或 NFKD),然后将文件名保留为给定的精确字节序列。如果文件系统为您提供毫秒时间戳,则将时间戳保留为毫秒分辨率。

当您使用较小的文件系统时,您总是可以适当地向下采样,并根据运行程序的文件系统的行为要求进行比较。如果您知道文件系统不支持 Unix 权限,则不应期望读取与您编写的相同的 Unix 权限。如果您知道文件系统不保留大小写,那么当你的程序创建 ABC 时,你应该准备在目录列表中看到 ABC 。但是,如果您知道文件系统确实保留了大小写区分,那么当检测到文件名重命名或文件系统区分大小写时,您应该考虑将 ABCabc 作为不同的文件名进行区分。

大小写保留

您可以创建一个名为 test/abc 的目录,并惊讶地看到有时 fs.readdir('test') 返回 ['ABC'] 。这不是 Node.js 的 bug。Node.js 返回文件名,因为文件系统存储它;而不是所有文件系统 支持大小写保存。某些文件系统将所有文件名转换为大写(或小写)。

Unicode 编码格式保存

大小写保存和 Unicode 编码保存是相似的概念。要了解为什么要保留 Unicode 编码,请确保首先了解为什么应保留大小写。在正确理解时,Unicode 形式保存也同样简单。

Unicode 可以使用多个不同的字节序列对相同的字符进行编码。多个字符串可能看起来相同,但有不同的字节序列。使用 UTF-8 字符串时请注意,您的期望与 Unicode 的工作方式一致。正如您不希望所有 UTF-8 字符都编码到单个字节一样,您不应该期望用人的肉眼看上去是是一样的几个 UTF-8 字符串一定具有相同的字节表示形式。这可能只是一个期望,你可以有 ASCII,但不是 UTF-8。

您可以创建一个名为 test/café 的目录(带有字节序列的 <63 61 66 c3 a9>string.length === 5 的 NFC Unicode 编码)。然后你会惊讶地看到,有时 fs.readdir('test') 会返回 ['café'](带有字节序列的 <63 61 66 65 cc 81>string.length === 6 的 NFD Unicode 编码)。这不是 Node.js 的缺陷。Node.js 返回文件名因为文件系统存储它,而不是所有文件系统支持 Unicode 形式保存。

例如 HFS+ 将所有文件名正常化为一种格式,几乎总是与 NFD 格式相同。不要期望 HFS+ 与 NTFS 或 EXT4 的行为相同,反之亦然。不要尝试通过规范化来永久更改数据。这会对对文件系统间的 Unicode 差异进行了漏抽象总结。反而造成问题而不是解决它们。相反地,我们应该保留 Unicode 格式并仅使用正常化作为比较函数。

敏感的 Unicode 编码

Unicode 不敏感编码和 Unicode 编码保存是两种不同的文件系统行为,经常相互误解。正如在存储和传输文件名时将文件名永久规范化为大写时,有时会不正确地实现了不区分大小写;因此 Unicode 不敏感编码有时会被永久地错误地实现。在存储和传输文件名时,将文件名规范化为特定的 Unicode 形式(NFD 的情况下为 HFS+)。在不牺牲 Unicode 编码保存的情况下实现 Unicode 不敏感编码是可能的,而且最好使用 Unicode 规范化进行比较。

比较不同的 Unicode 编码

节点提供了 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.js 的 bug。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() 好(因为它处理某些外语字符的方式不同)。但是最好还是探测文件系统,因为每个文件系统都有自己的编码比较表。

例如,苹果的 HFS+ 将文件名规范化为 NFD 形式,但此 NFD 表单实际上是当前 NFD 表单的较旧版本,有时可能与最新的 Unicode 标准的 NFD 形式稍有不同。不要期望 HFS+ NFD 和 Unicode NFD 总是完全相同的。

回到页顶