2020-05-22

【Node.js】fsでファイルの存在確認をするコードが非推奨な理由

ファイル処理をするとどうしても対象のファイルがすでに存在するかどうかチェックする必要がある。しかしfs.stat(), fs.access()は公式では非推奨のコードだ。

Article Image

はじめに

5秒足ファイルを作成するプログラムを作っていて「あれ上書きするときのファイルの存在確認って...?」と思ったのでまとめることにした。初心者向け記事ではあるが、上級者でも忘れている人がいそうな内容だ。

結論

そもそもファイルの存在確認はopenopenSyncなどのコードがその機能を内包している。

あえて分けてコードを実装した場合、タイミングによっては別のプロセスによる割り込みでファイル内容が変化したり、いざ書き込もうと思った時に開けない可能性がある。

ただし、非推奨であるだけであり禁止ではない。ケースによっては利用したい場合もある。

ファイルの存在確認をするコード

主に2つのコードがある。

  • fs.stat()
  • fs.access()

ここでは詳しく説明しない。

補足:fs.existsは古いNode.js用で現在廃止されたコードなので使用してはいけない。

ファイルが途中で変更される可能性とは

例えばまず上記のコードで上書き対象のファイルが存在することを確認する。

その上でなにか書き出し対象のデータを内部で処理し、いざ対象のファイルに書き込もうと思った時にすでに別のプロセスから読み込まれており書き込みできない、というケースが考えられる。

これは公式の見解であり非推奨とされている。原文でほぼ同じことが述べられている。

Using fs.access() to check for the accessibility of a file before calling fs.open(), fs.readFile() or fs.writeFile() is not recommended. Doing so introduces a race condition, since other processes may change the file's state between the two calls.

そんなシビアに割り込まれる?

軽いアプリ&独自のファイルを読み書きであればそもそも他のプロセスが割り込んでくる可能性は低い。

私個人としてはそれほど厳しく考える必要はないと思っているが、けっこう簡単にファイルをロックできるのでここからしっかり読んでいただければと思う。

ファイル存在確認を「しない」方法とは

たとえばこんなサンプルを作った。ファイル確認をせずいきなり開いている。

const ws = fs.createWriteStream('A.txt') //ファイルを書き込む準備

setInterval(() => { //2秒に1回"ok"を表示するコード
    console.log("ok")
}, 2000)

え?

え?である。このプログラムはファイルを呼び出したが書き込みも読み込みもしていない。

Streamオブジェクトに関しては他に譲る。一括に書き込むのではなくどんどん追記していくコードと思えば良い。

このプログラムを起動するとどうなるか

実は永遠に"ok"が表示されるだけではない。内部では次の2つの現象が起こる。

  1. 初めから A.txt の中にデータが入っていた場合、全て消去されファイルだけが残る
  2. プログラム実行中はメモ帳等でA.txtを開くことは出来るが上書きできない

これは結構面白い状態だ。初心者だとこれもまた「え?」となりそうだ。

つまり、どういうこと?

まずcreateWriteStreamにはオプションが指定されておらずデフォルトでは

「ファイルが存在した場合、完全にデリートした上で上書き。そうでなければ新規

という動作をする(後述のflagsのデフォルトがwのため)

これによりA.txtは残るが内容が消去されてしまうという状態になる。この動作を変更したい場合はオプション(Flags)で色々選べる。オプションは少し後で解説する。

さて、重要なのは**ファイルを開いたままsetIntervalが動き続ける(プログラムは終了されていない)**という点だ。

つまり一度プログラムの中で開いた状態が維持されていればファイルは「他のプログラムからは上書き不可能」になる(メモ帳などからa.txt上書きしようとしても弾かれる)

これが存在確認が不要である理由

つまりプログラムを実行した冒頭でファイルを開いてしまえば、プログラムを終了しない限りファイルは割り込まれることがない。

この仕様を利用してプログラムの冒頭でとりあえずファイルを開いておきましょう、というのがファイルを存在確認するコードが非推奨である理由だ。

では次のコードはどうなる?

const test = () => {
    const ws = fs.createWriteStream('A.txt')
}

test()

setInterval(() => {
    console.log("ok")
}, 2000)

ファンクションの中でfsがファイル操作をしているが、上のプログラムと同様でsetIntervalによってプログラムは終了されない。

これは別のメモ帳から上書きできるだろうか?

ぱっと見で定数 wstest()の中だけ。つまりtest()を抜ければファイルはすぐに上書きできてしまうのではないだろうか。

実は上書きできない

これは意外かもしれない。

一度開かれたA.txtはプログラムを終了するまで別のプロセスから書き込みができないようになっているのだ。

これであらためてファイルをわざわざファイルを存在確認しなくても、とりあえず冒頭で開いてしまえば良いという事が分かる。

※ただしこれは公式ドキュメントで読んだわけでなく私の実験によるものであり仕様が変更される可能性はある。

注意点:削除はできる

これは非常に恐ろしい。

上記の方法でファイルをロックすることにより別のプロセスから割り込んで上書きされることがなくなるが、ファイルごと消去は受け付けるらしい。

例えば

  1. 貴方の作ったメインの読み書きプログラムが実行されているとする。冒頭でfsを呼んでおりファイルAがロックされている(他のプロセスからは書き込みできない)
  2. このプロセスによりAにはどんどんデータが入っていく
  3. 別プロセスがファイルAを削除する
  4. メインのプログラムがいくら書き込もうとAが生成されることはなく無駄な処理がはしるだけになる

という現象が起こりかねない。

残念ながら現状私はこの解決策を知らない。解決策があれば追記していく。

とにかく「削除」には注意してほしい。

ファイルの存在確認が「必要」なケース

ではなぜファイルの存在確認がしたいのか。

ケース①読み込むファイルが「なければ」処理を中止したい

このケースはよくありそうだ。

fs.open('./A.txt', 'r', (err, fd) => {
    if (err) throw err //ファイルが無い時にerrに文字列が入る
 }

先程の読み込みのオプションで'r'(読み込み限定)を指定してやればファイルが無かった時にコールバック1個めの引数errになにか文字列が入ってくるのでハンドルしてやればよい。

ってなに?

読み込みや書き込みモードを指定するオプションである。公式ではflagsと呼ばれている。指定しない場合はデフォルトが設定される(メソッドによって異なると思われる)

  • r(読み込みだけ)やr+(読み込み+書き込み)はファイルが存在しなければエラーが出る。空のファイルが新規作成されることもない
  • w書き込みモード。はファイルが存在してもしなくてもエラーが出ず、空のファイルが新規作成される
  • wx書き込みモードで開くが、ファイルが存在していればエラーを返す
  • w+wと基本同じ動作だが加えて読み込みもできる。
  • wx+基本はw+と同じで読み書きのモード。しかしファイルが存在していればエラーを返す。

複雑...

このあたりは正直オプションを全て把握しておかないとエラーが出るのか出ないのか、ファイルが出力されるのかしないのかで間違えそうで不親切には思う。

このためどうしてもファイル確認のコードを作ってしまいがちだが、初心者ならそれも悪くないと私は考えている。

ケース②上書き確認のダイアログを出したい

これもよくありそうだ。

もしくはファイルが存在したら処理を中止したい、というケースでもある。

今回はダイアログは出さないがその前処理を考える。

fs.open('./A.txt', 'r', (err, fd) => {
    if (err) {
        console.log("ファイルが存在しない時の処理")
    }
    if(fd){
        console.log("ファイルが存在する時の処理")
    }
})

少し複雑。

rモードでfs.open。読み込み専用モードなので

  • ファイルが存在**「しなければ」**エラーなのでerrにエラー文字列が入る

  • ファイルが存在**「すれば」**fdに数字が入ってくる。fdはファイルディスクリプタと呼ばれる内部での識別子のようなもの。readwriteで使うがちょっと複雑なので割愛。

このあたりでハンドリングすればよいかと考えている。

※正し上記コードは「非同期」であるのに注意。同期はopenSyncがあるがコールバックが使用できないのでtry...catchでファイルの存在を判定する。

とりあえずプログラム冒頭で上記のコードを実行してやれば明示的にcloseが無い限りプログラム内では開かれっぱなし(つまり他のプロセスから割り込んで書き込みされない)状態になるので、好きな箇所で別のfs.createWriteStreamなどを呼んでもよいのではと考えている。

微妙?

実は私もfsに関してはそれほど詳しくない。

非常にできることが複雑であるため正直なところこれだけで本が1冊かけそうなボリュームが有る。

そのため上記の方法でファイルをハンドリングするのが非推奨である可能性もある。

もしなにか情報があれば教えていただければと思う。

最後に

ファイル関係の操作は非常に複雑でたくさんあるメソッドの一部しか紹介しなかった。

今回はあくまでも**「ファイルの存在確認専用のコードは他で置き換えましょう」**ということだけ理解していただければ良いと思う。

このあたりはtipsを思いつくたびに小さな記事で出していけたらと思う。



この記事のタグ

この記事をシェア


謎の技術研究部 (謎技研)