Skip to content

さまざまなファイルシステムとの連携方法

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が表示される可能性があることを覚悟する必要があります。しかし、ファイルシステムが大文字と小文字を保持することがわかっている場合は、ファイル名の変更を検出する場合や、ファイルシステムが大文字と小文字を区別する場合は、ABCabcとは異なるファイル名と見なす必要があります。

大文字小文字の保持

test /abcというディレクトリを作成した際に、fs.readdir('test')['ABC']を返すことがあることに驚くかもしれません。これはNodeのバグではありません。Nodeは、ファイルシステムが格納しているファイル名をそのまま返し、すべてのファイルシステムが大文字小文字の保持をサポートしているわけではありません。一部のファイルシステムは、すべてのファイル名を大文字(または小文字)に変換します。

Unicode形式の保持

大文字小文字の保持とUnicode形式の保持は似た概念です。Unicode形式を保持すべき理由を理解するためには、まず大文字小文字を保持すべき理由を理解していることを確認してください。Unicode形式の保持も、正しく理解すれば同じように簡単です。Unicodeは、複数の異なるバイトシーケンスを使用して同じ文字をエンコードできます。複数の文字列が同じように見えても、異なるバイトシーケンスを持つことがあります。UTF-8文字列を扱う場合、あなたの期待がUnicodeの仕組みと一致しているか注意してください。すべてのUTF-8文字が単一のバイトにエンコードされることを期待しないように、人間の目には同じように見える複数のUTF-8文字列が同じバイト表現を持つことを期待すべきではありません。これはASCllに対しては期待できるかもしれませんが、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やEXT 4と同じように動作することを期待しないでください。また、ファイルシステム間のUnicodeの違いをごまかすために、正規化を通じてデータを永続的に変更しようとしないでください。これは何も解決せずに問題を引き起こすでしょう。むしろ、Unicode形式を保持し、正規化を比較関数としてのみ使用してください。

Unicode フォームの非依存性

Unicode フォームの非依存性と Unicode フォームの保持は、しばしば混同される異なるファイルシステムの挙動です。大文字・小文字の区別をしないことが、ファイル名を保存および送信する際にファイル名を永続的に大文字に正規化することによって誤って実装されたことがあるように、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 のバグではありません。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と完全に同じであるとは期待しないでください。