如何使用不同的文件系统
Node.js 公开了许多文件系统功能。但并非所有文件系统都相同。以下是一些建议的最佳实践,以便在使用不同的文件系统时保持代码简单安全。
文件系统行为
在使用文件系统之前,您需要了解它的行为。不同的文件系统行为不同,并且具有或多或少的特性:区分大小写、不区分大小写、保留大小写、Unicode 形式保留、时间戳分辨率、扩展属性、inode、Unix 权限、备用数据流等。
不要从 process.platform
推断文件系统行为。例如,不要假设因为您的程序正在 Darwin 上运行,所以您正在使用不区分大小写文件系统 (HFS+),因为用户可能正在使用区分大小写文件系统 (HFSX)。同样,不要假设因为您的程序正在 Linux 上运行,所以您正在使用支持 Unix 权限和 inode 的文件系统,因为您可能在不支持这些功能的特定外部驱动器、USB 或网络驱动器上。
操作系统可能无法轻松推断文件系统行为,但并非没有办法。与其维护一个包含所有已知文件系统和行为的列表(这将永远不完整),不如探测文件系统以查看它的实际行为。某些易于探测的功能的存在或不存在,通常足以推断其他更难探测的功能的行为。
请记住,一些用户可能在工作树中的不同路径上挂载了不同的文件系统。
避免使用最低公分母方法
您可能很想让您的程序像最低公分母文件系统一样,将所有文件名规范化为大写,将所有文件名规范化为 NFC Unicode 形式,并将所有文件时间戳规范化为 1 秒分辨率。这将是最小的公分母方法。
不要这样做。您只能安全地与在各个方面都具有完全相同的最低公分母特征的文件系统交互。您将无法以用户期望的方式使用更高级的文件系统,并且您会遇到文件名或时间戳冲突。您肯定会通过一系列复杂的依赖事件丢失和损坏用户数据,并且您会创建难以解决甚至无法解决的错误。
当您以后需要支持只有 2 秒或 24 小时时间戳分辨率的文件系统时会发生什么?当 Unicode 标准升级以包含略有不同的规范化算法(过去曾发生过)时会发生什么?
最低公分母方法倾向于尝试通过仅使用“可移植”系统调用来创建可移植程序。这会导致程序泄漏,实际上不可移植。
采用超集方法
通过采用超集方法,充分利用您支持的每个平台。例如,一个可移植的备份程序应该在 Windows 系统之间正确同步 btimes(文件或文件夹的创建时间),并且不应该破坏或更改 btimes,即使 btimes 在 Linux 系统上不受支持。同一个可移植备份程序应该在 Linux 系统之间正确同步 Unix 权限,并且不应该破坏或更改 Unix 权限,即使 Unix 权限在 Windows 系统上不受支持。
通过让您的程序像更高级的文件系统一样来处理不同的文件系统。支持所有可能功能的超集:区分大小写、保留大小写、Unicode 形式敏感性、Unicode 形式保留、Unix 权限、高分辨率纳秒时间戳、扩展属性等。
一旦您的程序中有了大小写保留,您就可以在需要与不区分大小写文件系统交互时始终实现不区分大小写。但是,如果您在程序中放弃大小写保留,则无法安全地与保留大小写文件系统交互。对于 Unicode 形式保留和时间戳分辨率保留也是如此。
如果文件系统为您提供了一个混合大小写字母的文件名,那么请保留该文件名的大小写。如果文件系统为您提供了一个混合 Unicode 形式或 NFC 或 NFD(或 NFKC 或 NFKD)的文件名,那么请保留该文件名的大小写。如果文件系统为您提供了一个毫秒时间戳,那么请保留该时间戳的毫秒分辨率。
当您使用较低级别的文件系统时,您可以始终进行适当的下采样,并根据您的程序运行的文件系统的行为使用比较函数。如果您知道文件系统不支持 Unix 权限,那么您不应该期望读取与您写入的相同 Unix 权限。如果您知道文件系统不保留大小写,那么您应该准备好看到目录列表中的 ABC
,即使您的程序创建的是 abc
。但是,如果您知道文件系统保留大小写,那么在检测文件重命名或文件系统区分大小写时,您应该将 ABC
视为与 abc
不同的文件名。
大小写保留
您可能会创建一个名为 test/abc
的目录,并惊讶地发现有时 fs.readdir('test')
返回 ['ABC']
。这不是 Node 的错误。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 的错误。Node.js 返回文件系统存储的文件名,并非所有文件系统都支持 Unicode 形式保留。
例如,HFS+ 会将所有文件名规范化为几乎总是与 NFD 形式相同的形式。不要期望 HFS+ 的行为与 NTFS 或 EXT4 相同,反之亦然。不要尝试通过规范化来永久更改数据,因为这是一种泄漏的抽象,用于掩盖文件系统之间 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 时间戳(尤其是 atime 时间戳)具有非常粗略的分辨率,例如某些 FAT 文件系统为 24 小时。
不要通过规范化来破坏文件名和时间戳
文件名和时间戳是用户数据。就像你永远不会自动将用户文件数据全部转换为大写或将CRLF
规范化为LF
行尾一样,你也不应该通过大小写/Unicode形式/时间戳规范化来更改、干扰或破坏文件名或时间戳。规范化应该只用于比较,绝不用于更改数据。
规范化实际上是一种有损的哈希码。你可以用它来测试某些类型的等价性(例如,几个字符串看起来相同,即使它们有不同的字节序列),但你永远不能用它来代替实际数据。你的程序应该按原样传递文件名和时间戳数据。
你的程序可以在NFC(或任何它喜欢的Unicode形式组合)中创建新数据,或者使用小写或大写文件名,或者使用2秒分辨率的时间戳,但你的程序不应该通过强制大小写/Unicode形式/时间戳规范化来破坏现有的用户数据。相反,采用超集方法,在你的程序中保留大小写、Unicode形式和时间戳分辨率。这样,你就可以安全地与执行相同操作的文件系统进行交互。
正确使用规范化比较函数
确保你正确地使用大小写/Unicode形式/时间戳比较函数。如果你正在使用区分大小写的文件系统,不要使用不区分大小写的文件名比较函数。如果你正在使用区分Unicode形式的文件系统(例如NTFS和大多数保留NFC和NFD或混合Unicode形式的Linux文件系统),不要使用不区分Unicode形式的比较函数。如果你正在使用纳秒时间戳分辨率的文件系统,不要以2秒分辨率比较时间戳。
准备好比较函数的细微差异
注意你的比较函数要与文件系统的比较函数匹配(或者如果可能的话探测文件系统以查看它将如何实际比较)。例如,不区分大小写比简单的toLowerCase()
比较更复杂。事实上,toUpperCase()
通常比toLowerCase()
更好(因为它对某些外语字符的处理方式不同)。但更好的方法是探测文件系统,因为每个文件系统都有自己的内置大小写比较表。
例如,苹果的 HFS+ 将文件名规范化为 NFD 形式,但这种 NFD 形式实际上是当前 NFD 形式的旧版本,有时可能与最新的 Unicode 标准的 NFD 形式略有不同。不要期望 HFS+ NFD 一直与 Unicode NFD 完全相同。