5秒足ファイルを作成するプログラムを作っていて「あれ上書きするときのファイルの存在確認って...?」と思ったのでまとめることにした。初心者向け記事ではあるが、上級者でも忘れている人がいそうな内容だ。
そもそもファイルの存在確認はopen
やopenSync
などのコードがその機能を内包している。
あえて分けてコードを実装した場合、タイミングによっては別のプロセスによる割り込みでファイル内容が変化したり、いざ書き込もうと思った時に開けない可能性がある。
ただし、非推奨であるだけであり禁止ではない。ケースによっては利用したい場合もある。
主に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つの現象が起こる。
A.txt
の中にデータが入っていた場合、全て消去されファイルだけが残る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
によってプログラムは終了されない。
これは別のメモ帳から上書きできるだろうか?
ぱっと見で定数 ws
は test()
の中だけ。つまりtest()
を抜ければファイルはすぐに上書きできてしまうのではないだろうか。
これは意外かもしれない。
一度開かれたA.txt
はプログラムを終了するまで別のプロセスから書き込みができないようになっているのだ。
これであらためてファイルをわざわざファイルを存在確認しなくても、とりあえず冒頭で開いてしまえば良いという事が分かる。
※ただしこれは公式ドキュメントで読んだわけでなく私の実験によるものであり仕様が変更される可能性はある。
これは非常に恐ろしい。
上記の方法でファイルをロックすることにより別のプロセスから割り込んで上書きされることがなくなるが、ファイルごと消去は受け付けるらしい。
例えば
fs
を呼んでおりファイルA
がロックされている(他のプロセスからは書き込みできない)A
にはどんどんデータが入っていくA
を削除するA
が生成されることはなく無駄な処理がはしるだけになるという現象が起こりかねない。
残念ながら現状私はこの解決策を知らない。解決策があれば追記していく。
とにかく「削除」には注意してほしい。
ではなぜファイルの存在確認がしたいのか。
このケースはよくありそうだ。
fs.open('./A.txt', 'r', (err, fd) => {
if (err) throw err //ファイルが無い時にerrに文字列が入る
}
先程の読み込みのオプションで'r'
(読み込み限定)を指定してやればファイルが無かった時にコールバック1個めの引数err
になにか文字列が入ってくるのでハンドルしてやればよい。
r
ってなに?読み込みや書き込みモードを指定するオプションである。公式では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
はファイルディスクリプタと呼ばれる内部での識別子のようなもの。read
やwrite
で使うがちょっと複雑なので割愛。
このあたりでハンドリングすればよいかと考えている。
※正し上記コードは「非同期」であるのに注意。同期はopenSync
があるがコールバックが使用できないのでtry...catch
でファイルの存在を判定する。
とりあえずプログラム冒頭で上記のコードを実行してやれば明示的にclose
が無い限りプログラム内では開かれっぱなし(つまり他のプロセスから割り込んで書き込みされない)状態になるので、好きな箇所で別のfs.createWriteStream
などを呼んでもよいのではと考えている。
実は私もfs
に関してはそれほど詳しくない。
非常にできることが複雑であるため正直なところこれだけで本が1冊かけそうなボリュームが有る。
そのため上記の方法でファイルをハンドリングするのが非推奨である可能性もある。
もしなにか情報があれば教えていただければと思う。
ファイル関係の操作は非常に複雑でたくさんあるメソッドの一部しか紹介しなかった。
今回はあくまでも**「ファイルの存在確認専用のコードは他で置き換えましょう」**ということだけ理解していただければ良いと思う。
このあたりはtips
を思いつくたびに小さな記事で出していけたらと思う。