様々なファイルシステムの扱い方
Node.js はファイルシステムの多くの機能を公開しています。しかし、すべてのファイルシステムが同じではありません。以下は、様々なファイルシステムを扱う際にコードをシンプルかつ安全に保つためのベストプラクティスです。
ファイルシステムの挙動
ファイルシステムを扱う前に、その挙動を知る必要があります。ファイルシステムによって挙動は異なり、機能も様々です:大文字と小文字の区別、区別しない、大文字と小文字の保持、Unicode 形式の保持、タイムスタンプの解像度、拡張属性、inode、Unix パーミッション、代替データストリームなど。
process.platform からファイルシステムの挙動を推測することには注意が必要です。例えば、プログラムが Darwin 上で実行されているからといって、大文字と小文字を区別しないファイルシステム(HFS+)で作業していると仮定してはいけません。ユーザーが大文字と小文字を区別するファイルシステム(HFSX)を使用している可能性があるからです。同様に、プログラムが Linux 上で実行されているからといって、Unix パーミッションや inode をサポートするファイルシステムで作業していると仮定してはいけません。特定の外部ドライブ、USB、またはそれらをサポートしないネットワークドライブ上である可能性があるからです。
オペレーティングシステムはファイルシステムの挙動を推測しやすくしてくれないかもしれませんが、すべてが失われたわけではありません。既知のすべてのファイルシステムとその挙動のリストを保持する(これは常に不完全になる)代わりに、ファイルシステムを調査して実際にどのように動作するかを確認できます。調査しやすい特定の機能の有無は、調査がより困難な他の機能の挙動を推測するのに十分なことが多いです。
一部のユーザーは、作業ツリー内の様々なパスに異なるファイルシステムをマウントしている可能性があることを覚えておいてください。
最小公分母アプローチを避ける
すべてのファイル名を大文字に正規化し、すべてのファイル名を NFC Unicode 形式に正規化し、すべてのファイルタイムスタンプを例えば 1 秒の解像度に正規化することで、プログラムを最小公分母のファイルシステムのように動作させたくなるかもしれません。これは最小公分母アプローチです。
これをしないでください。あらゆる点でまったく同じ最小公分母の特性を持つファイルシステムとのみ安全にやり取りできるだけです。より高度なファイルシステムをユーザーが期待する方法で扱うことができず、ファイル名やタイムスタンプの衝突に遭遇するでしょう。複雑な依存イベントの連鎖を通じて、ユーザーデータを確実に失い、破損させ、解決が困難または不可能なバグを生み出すことになります。
後で 2 秒または 24 時間のタイムスタンプ解像度しか持たないファイルシステムをサポートする必要が出てきたらどうなりますか?Unicode 標準が進んで、わずかに異なる正規化アルゴリズムが含まれるようになったらどうなりますか(過去に起こったように)?
最小公分母アプローチは、「ポータブルな」システムコールのみを使用してポータブルなプログラムを作成しようとする傾向があります。これは、実際にはポータブルではない、漏れのあるプログラムにつながります。
スーパーセットアプローチを採用する
スーパーセットアプローチを採用することで、サポートする各プラットフォームを最大限に活用してください。例えば、ポータブルなバックアッププログラムは、Windows システム間で btime(ファイルやフォルダーの作成時刻)を正しく同期し、btime が Linux システムでサポートされていなくても、btime を破壊したり変更したりすべきではありません。同じポータブルなバックアッププログラムは、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 文字が 1 バイトにエンコードされると期待しないのと同じように、人間の目には同じように見える複数の 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 形式の保持は、互いに混同されがちな 2 つの異なるファイルシステムの挙動です。大文字小文字の非区別が、ファイル名を格納・送信する際に永久に大文字に正規化することで誤って実装されることがあったように、Unicode 形式の非区別も、ファイル名を格納・送信する際に特定の Unicode 形式(HFS+ の場合は NFD)に永久に正規化することで誤って実装されることがあります。Unicode 正規化を比較にのみ使用することで、Unicode 形式の保持を犠牲にすることなく、Unicode 形式の非区別を実装することは可能であり、はるかに優れています。
異なる Unicode 形式の比較
Node.js は string.normalize('NFC' / 'NFD') を提供しており、これを使用して UTF-8 文字列を NFC または NFD のいずれかに正規化できます。この関数の出力を保存することは決してせず、2 つの 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() よりも優れていることが多いです(特定の外国語文字を異なる方法で処理するため)。しかし、さらに良いのは、すべてのファイルシステムに独自のケース比較テーブルが組み込まれているため、ファイルシステムを調査することです。
一例として、Apple の HFS+ はファイル名を NFD 形式に正規化しますが、この NFD 形式は実際には現在の NFD 形式の古いバージョンであり、最新の Unicode 標準の NFD 形式とはわずかに異なる場合があります。HFS+ の NFD が常に Unicode の NFD とまったく同じであると期待しないでください。