安全に一気にファイルの内容を書き換える

ファイルを書き換えるときに、単純に open, write, close だと、そのファイルの性格にもよりますが、問題になるケースがあります。

  • 書き込み処理中に異常終了しちゃった

例えば

open my $fh, '>', '/foo/bar';
print $fh gen_header();
print $fh gen_section_1();
print $fh gen_section_2(); # ← die
print $fh "# EOF\n";
close $fh

な処理で、get_section_2 が異常終了してしまうと、その後書き込まれるはずのものが書き込まれずファイルの内容が中途半端になってしまいます。(まぁでもこのケースはエラー処理をちゃんとやれば回避できますね)

  • 書いている途中で読まれちゃった

サーバ名一覧のような定義ファイルや、毎回読まれるタイプの設定ファイルの場合、生成プログラムが書いている最中で、だれかに(中途半端な状態で)読まれちゃう可能性もあります。

open my $fh, '>', '/foo/bar';
for (@totemo_takusanno_data) {
  print $fh $_; # ← まだ読んじゃだめー
}
close $fh

これらの問題を回避するのは特に難しいことではなくて、おおざっぱにいうと、とりあえずテンポラリのファイルに書いておいて、書き終わったらお目当てのファイル名にrename(2)する、でOKです。

OKなんですが、毎回毎回書くのが面倒なので、裏でそういったお仕事をしてくれる便利Perlモジュールを書きました。

使い方は IO::File と同じ感じで:

use IO::File::AtomicChange;

my $fh = IO::File::AtomicChange->new("/foo/bar", "w");
$fh->print("# create new file\n");
$fh->print("foo\n");
$fh->print("bar\n");
$fh->close; # MUST CALL close EXPLICITLY

な感じでファイルに書き込むと、実は裏ではテンポラリのファイルに書き込んで、$fh->close したタイミングでrename(2)してくれます。

あとオマケ機能で、backup_dir を指定した場合は、オリジナルファイルのバックアップコピーもとってくれます。

例えば、

my $fh = IO::File::AtomicChange->new("/foo/bar", "a",
                                     { backup_dir => "/var/backup/" });

だと、変更前のファイルを、/var/backup/bar_YYYY-MM-DD_HHMMSS_PID に(パーミッション、mtimeを保持しつつ)コピーします。



ちなみになんでこんなモジュールを作ったかというと、9/10(木)、9/11(金)に開催されるYAPC::Asia 2009トークのためにシステム管理用のコマンドを作っている際に必要になったのでした。

イベントドリブンですね(^q^