一時ファイルの利用

処理の途中で一時的なファイルを作成する必要が発生することがある。 たとえば,既にあるファイルの中味を直接書き換える場合や, プログラムで生成したテキストを別のプログラムに処理させたいときに, 別プログラムの処理対象がファイルのみだったりする場合は 一時ファイルが必要となる。ここでは,一時ファイルの作成と利用, 適切な後始末の重要性について説明する。

一時ファイルとセキュリティ

処理の都合上,一時的なファイルを作成する場合, そのファイルをどこに置くかが問題となる。Unix系のシステムでは 慣習的に /tmp ディレクトリが一時ファイル作成場所として 利用できるよう用意されている。ファイル名の衝突さえなければ 自由な名前でファイルを作成することができるが,そのときに ファイルの中味の漏洩に注意しなければならない。

たとえば,必ず /tmp/secret.tmp という一時ファイルを作成するプログラムがあった場合, プログラム起動者以外でも,そのファイル名でアクセスすれば 読める危険がある。また仮に所有者以外が読み書きできないよう ファイル属性を変更していたとしても,同名のファイルを 他人が作っておくことで,プログラムの実行の邪魔をしたりできる。 さらには,一時ファイルの名前のシンボリックリンクが存在し 重要なファイルを指し示していたとしたら,一時ファイルへの書き込みが 重要ファイルを壊すことに繋がる。

一時ファイルを作成・利用する場合には,以下の点に留意する必要がある。

  1. 作らずに済むなら作らない
  2. 予測可能なファイル名にしない。
  3. 他者がアクセスできないファイル属性とする。
  4. 不要になったら削除する。

1はとくに重要で,もし一時ファイルを作る目的が別プログラムに データを渡すことなら,そのプログラムにデータを標準入力から読む 機能があればそれを利用するように試みることが好ましい。

それでもなお一時ファイルを作成する場合は残りの3点に気をつけて行なう。 これらを確実に行なうには,一時ファイルを作成するメソッドを 正しい手順で利用する。

一時ファイル

Rubyでは Tempfile を利用することで, 一時ファイルの処理を確実に進めることができる。

require 'tempfile'
Tempfile.open(ファイルのベース名) {|fp|
  〜〜処理〜〜
  fp.close(true)
}

以上のような流れにより,ファイル属性が0600(-rw-------) のファイルが,"w+" モードで開かれる。また, close(true) により自動的に削除される。 仮に,エラーでスクリプトが異常終了した場合も一時ファイルは削除される。

一時ディレクトリ

複数の一時ファイル作成の必要や,ファイル名(の一部)を決まったものに したい場合は一時ファイルを格納する一時ディレクトリを作ればよい。

このときの流れは

  1. 競合する可能性の低い名前の一時ディレクトリを 他者にアクセスできないモードで作る。
  2. 作成したディレクトリにファイルを作り処理を済ます。
  3. 一時ディレクトリ以下のファイルを削除する。

とする。1に関し,他のプログラム,あるいは同時に起動された同一プログラム が使う一時ファイルやディレクトリと 名前が競合しないようにするための工夫として,一般的には プログラム自身の名前とプロセスID(pid)を組み合わせた名前を 利用することが多い。たとえば以下のような流れで決める。

myname = File.basename($0, ".rb")	# $0 はスクリプトの起動時のパス名
pid    = $$				# $$ はプロセスIDを持つ組み込み変数
tmpdir = "/tmp/#{myname}-#{pid}"

そうして決めたディレクトリを 作成する処理は,複数の処理に分けてはならない。たとえば,

pid = $$			# PIDは $$ で得られる
tmpdir = "/tmp/foo-#{pid}.d"
abort "一時ディレクトリ作成失敗" if test(?d, tmpdir)	# (1)
# (2)
Dir.mkdir(tmpdir)
# (3)
File.chmod(0600, tmpdir)

のようにすると,(1)のときにこれから作りたいディレクトリが ないことを確認できたとしても,(2)のタイミングで別プロセスが 同じ名前のディレクトリを作ってしまう可能性はある。 また仮に,(2)の次の行まで無事進めてディレクトリ作成が成功しても (3)のタイミングで別ユーザによる別プロセスがこのディレクトリをオープン (Dir.open)していれば, その後chmodで別ユーザの読み取り属性を落としたとしても, そのプロセスは引続きディレクトリの中味を読み続けられる。

このような危険の可能性を排除するため,一時ディレクトリ作成は モード設定と合わせて単一アクションで行なう。この点に配慮して 作成した例を示す。

tmpdir.rb

#!/usr/bin/env ruby

require 'tmpdir'              	# Dir.tmpdir に必要
tmp = Dir.tmpdir              	# テンポラリディレクトリを得る 既定値=/tmp
me = File.basename($0, ".rb")   # スクリプト自身のベース名
dname = nil                     # 一時ディレクトリ名保存用変数
0.upto(9) do |n|                # 10個の違うディレクトリ名で試行
  # ベース名とPIDに0〜9の番号をつけた名前とする
  dname = sprintf("%s/%s-%d.%d", tmp, me, $$, n)
  begin
    Dir.mkdir(dname, 0700)     	# 他ユーザにアクセスできない属性で
    break
  rescue                        # mkdirが失敗したら
    dname = nil                 # ディレクトリ名を消しておく
  end
end
if dname && test(?d, dname)     # mkdirに成功した場合
  begin
    # ここに一時ディレクトリを利用した処理を書く
    Dir.chdir(dname) do
      printf("%s で一時ファイル作成\n", dname)
      open("hoge", "w") do |a| a.puts("hoge") end
      print `ls -laF`
      printf("1秒sleepします。C-cで止めても%sは消えます。\n", dname)
      sleep 1
    end
  ensure
    # ここには後始末(一時ファイル消去)処理を書く
    # system "/bin/rm -rf '#{dname}'" # 外部コマンドrmに任る場合
    require 'fileutils'
    FileUtils.remove_entry_secure(dname, :force)
  end
else
  abort "#{tmp} 内に一時ディレクトリを作れませんでした.
環境変数 TMPDIR に書き込み可能なディレクトリを設定して再度起動して下さい。"
end

実際に実行してみて一時ファイルが残らないことを確認せよ。

なお,Ruby 1.8.7以降ではtmpdirライブラリによる Dir.mktmpdir メソッドを使うことで, 上記のような一時ディレクトリ作成・抹消処理ができる。 上記 tmpdir.rb と同等の処理を Dir.mktmpdir を用いて書き換えたものを示す。

mktmpdir.rb

#!/usr/bin/env ruby

require 'tmpdir'              	# Dir.mktmpdir に必要
begin
  Dir.mktmpdir do |dname|
    Dir.chdir(dname) do
      printf("%s で一時ファイル作成\n", dname)
      open("hoge", "w") do |a| a.puts("hoge") end
      print `ls -laF`
      printf("1秒sleepします。C-cで止めても%sは消えます。\n", dname)
      sleep 1
    end
  end
rescue
  abort "#{tmp} 内に一時ディレクトリを作れませんでした.
環境変数 TMPDIR に書き込み可能なディレクトリを設定して再度起動して下さい。"
end

本日の目次

yuuji@e.koeki-u.ac.jp