異なるファイルシステムと連携する方法

Node.js はファイルシステムの多くの機能を公開しています。ただし、すべてのファイルシステムが同じように動作するわけではありません。異なるファイルシステムで作業する場合に、コードをシンプルかつ安全に保つための推奨されるベストプラクティスを以下に示します。

ファイルシステムの動作

ファイルシステムを扱う前に、その動作を知る必要があります。ファイルシステムによって動作が異なり、機能の多寡も異なります。大文字と小文字の区別、大文字と小文字の区別なし、大文字と小文字の保持、Unicode形式の保持、タイムスタンプの解像度、拡張属性、inode、Unixパーミッション、代替データストリームなどです。

process.platform からファイルシステムの動作を推測するのは危険です。たとえば、プログラムが Darwin 上で実行されているからといって、大文字と小文字を区別しないファイルシステム (HFS+) で作業していると想定しないでください。ユーザーは大文字と小文字を区別するファイルシステム (HFSX) を使用している可能性があります。同様に、プログラムが Linux 上で実行されているからといって、Unix パーミッションと inode をサポートするファイルシステムで作業していると想定しないでください。特定の外部ドライブ、USB、またはネットワークドライブではサポートされていない可能性があります。

オペレーティングシステムではファイルシステムの動作を推測するのが難しい場合がありますが、すべてが失われるわけではありません。既知のすべてのファイルシステムと動作のリストを保持する代わりに(これは常に不完全になります)、ファイルシステムをプローブして実際の動作を確認できます。プローブしやすい特定の機能の有無は、プローブするのが難しい他の機能の動作を推測するのに十分な場合が多いためです。

一部のユーザーは、作業ツリーのさまざまなパスに異なるファイルシステムをマウントしている可能性があることに注意してください。

最小公倍数アプローチを避ける

すべてのファイル名を大文字に正規化し、すべてのファイル名を NFC Unicode 形式に正規化し、すべてのファイルのタイムスタンプを 1 秒の解像度に正規化することで、プログラムを最小公倍数ファイルシステムのように動作させたいと思うかもしれません。これは最小公倍数アプローチです。

これを行わないでください。すべての点でまったく同じ最小公倍数の特性を持つファイルシステムとのみ安全にやり取りできます。ユーザーが期待する方法でより高度なファイルシステムを扱うことができなくなり、ファイル名またはタイムスタンプの衝突が発生します。一連の複雑な依存イベントを通じてユーザーデータが失われたり破損したりする可能性が高く、解決が困難または不可能なバグが発生します。

後で 2 秒または 24 時間のタイムスタンプ解像度しかないファイルシステムをサポートする必要がある場合はどうなりますか?Unicode 標準が進化して、わずかに異なる正規化アルゴリズム(過去に発生したように)が含まれるようになった場合はどうなりますか?

最小公倍数アプローチは、「移植可能な」システムコールのみを使用して移植可能なプログラムを作成しようとします。これは、リークがあり、実際には移植性のないプログラムにつながります。

スーパーセットアプローチを採用する

スーパーセットアプローチを採用することで、サポートする各プラットフォームを最大限に活用します。たとえば、ポータブルバックアッププログラムは、Windows システム間で btime(ファイルまたはフォルダーの作成時刻)を正しく同期する必要があり、Linux システムでは btime がサポートされていない場合でも、btime を破棄または変更しないでください。同じポータブルバックアッププログラムは、Linux システム間で Unix パーミッションを正しく同期する必要があり、Windows システムでは Unix パーミッションがサポートされていない場合でも、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 形式の保存は altrettanto 簡単です。

Unicode は、同じ文字を複数の異なるバイトシーケンスを使用してエンコードできます。複数の文字列が同じように見えても、バイトシーケンスが異なる場合があります。UTF-8 文字列を扱う場合は、期待が Unicode の仕組みに一致していることに注意してください。すべての UTF-8 文字が 1 バイトにエンコードされるとは限らないのと同様に、人間の目に見える同じ 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 形式の保存は、しばしば混同される 2 つの異なるファイルシステムの動作です。大文字と小文字の区別なしが、ファイル名の保存と送信時にファイル名を大文字に永続的に正規化することによって誤って実装されることがあるのと同様に、Unicode 形式の非区別も、ファイル名の保存と送信時にファイル名を特定の Unicode 形式(HFS+ の場合は NFD)に永続的に正規化することによって誤って実装されることがあります。比較にのみ Unicode 正規化を使用することで、Unicode 形式の保存を犠牲にすることなく、Unicode 形式の非区別を実装することは可能であり、はるかに優れています。

異なる Unicode 形式の比較

Node.js は、UTF-8 文字列を NFC または NFD に正規化するために使用できる `string.normalize('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.js のバグではありません。Node.js は、ファイルシステムがタイムスタンプを保存する形式でタイムスタンプを返しますが、すべてのファイルシステムがナノ秒、ミリ秒、または 1 秒のタイムスタンプ解像度をサポートしているわけではありません。一部のファイルシステム、特に一部の FAT ファイルシステムでは、atime タイムスタンプの解像度が 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 とまったく同じであると期待しないでください。