如何處理不同檔案系統

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。但如果您知道檔案系統保留大小寫,則在檢測檔案重新命名或檔案系統區分大小寫時,您應該將 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é的目錄(帶有字節序列<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的錯誤。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 時間戳具有非常粗糙的解析,例如某些 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。