如何處理不同的檔案系統
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.js 的錯誤。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.js 的錯誤。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.js 的錯誤。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 完全相同。