Skip to content

異なるファイルシステムの扱い方

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

ファイルシステムの動作

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

process.platformからファイルシステムの動作を推測することに注意してください。例えば、プログラムがDarwin上で実行されているからといって、ユーザーがケースセンシティブなファイルシステム(HFSX)を使用している可能性があるため、ケースインセンシティブなファイルシステム(HFS+)で動作していると仮定しないでください。同様に、プログラムがLinux上で実行されているからといって、特定の外部ドライブ、USB、またはネットワークドライブではUnixパーミッションとiノードをサポートするファイルシステムで動作していると仮定しないでください。

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

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

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

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

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

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

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

スーパーセットアプローチの採用

サポートする各プラットフォームを最大限に活用するために、スーパーセットアプローチを採用します。例えば、ポータブルバックアッププログラムは、Windowsシステム間でbtimes(ファイルまたはフォルダの作成時刻)を正しく同期する必要があり、Linuxシステムではbtimesがサポートされていなくても、btimesを破壊したり変更したりしてはなりません。同じポータブルバックアッププログラムは、Linuxシステム間でUnixパーミッションを正しく同期する必要があり、WindowsシステムではUnixパーミッションがサポートされていなくても、Unixパーミッションを破壊したり変更したりしてはなりません。

異なるファイルシステムを処理するには、プログラムをより高度なファイルシステムのように動作させます。大文字と小文字の区別、大文字と小文字の保持、Unicode形式の区別、Unicode形式の保持、Unixパーミッション、高解像度ナノ秒タイムスタンプ、拡張属性など、可能なすべての機能のスーパーセットをサポートします。

プログラムで大文字と小文字の保持を実装すれば、大文字と小文字を区別しないファイルシステムと対話する必要がある場合、いつでも大文字と小文字を区別しない機能を実装できます。しかし、プログラムで大文字と小文字の保持を省略した場合、大文字と小文字を保持するファイルシステムと安全に対話することはできません。Unicode形式の保持とタイムスタンプ解像度の保持についても同様です。

ファイルシステムから小文字と大文字が混在するファイル名が提供された場合は、指定された正確な大文字と小文字でファイル名を保持します。ファイルシステムからUnicode形式が混在しているファイル名、またはNFCまたはNFD(またはNFKCまたはNFKD)が提供された場合は、指定された正確なバイトシーケンスでファイル名を保持します。ファイルシステムからミリ秒単位のタイムスタンプが提供された場合は、ミリ秒単位の解像度でタイムスタンプを保持します。

下位レベルのファイルシステムを使用する場合は、プログラムが実行されているファイルシステムの動作に必要な比較関数を使用して、適切にダウンサンプリングできます。ファイルシステムがUnixパーミッションをサポートしていないことがわかっている場合は、書き込んだUnixパーミッションと同じUnixパーミッションを読み取ることが期待できません。ファイルシステムが大文字と小文字を保持しないことがわかっている場合は、プログラムがabcを作成したときに、ディレクトリ一覧にABCが表示される可能性があることを考慮する必要があります。しかし、ファイルシステムが大文字と小文字を保持することがわかっている場合は、ファイル名の変更を検出する場合、またはファイルシステムで大文字と小文字が区別される場合は、ABCabcを異なるファイル名と見なす必要があります。

大文字小文字の保持

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形式を区別するファイルシステム(例:NFCとNFDの両方、または混合Unicode形式を保持するNTFSとほとんどのLinuxファイルシステム)で作業している場合、Unicode形式を区別しない比較関数を使用しないでください。ナノ秒精度のタイムスタンプ解像度のファイルシステムで作業している場合、2秒精度のタイムスタンプで比較しないでください。

比較関数のわずかな違いに備える

比較関数がファイルシステムのものと一致していることを確認してください(可能であれば、ファイルシステムを調査して、実際にどのように比較されるかを確認します)。たとえば、大文字小文字の区別は、単純なtoLowerCase()比較よりも複雑です。実際、toUpperCase()は通常toLowerCase()よりも優れています(特定の外国語の文字を異なる方法で処理するためです)。しかし、さらに良いのは、ファイルシステムを調査することです。すべてのファイルシステムには独自の文字列比較テーブルが組み込まれているためです。

例として、AppleのHFS+はファイル名をNFD形式に正規化しますが、このNFD形式は実際には現在のNFD形式の古いバージョンであり、最新のUnicode標準のNFD形式とはわずかに異なる場合があります。HFS+のNFDが常にUnicode NFDとまったく同じであると期待しないでください。