一時ファイルの利用

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

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

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

たとえば,必ず /tmp/secret.tmp という一時ファイルを作成するプログラムがあったとしよう。 プログラム起動者以外でも,そのファイル名でアクセスすれば 読める危険がある。また仮に所有者以外が読み書きできないよう ファイル属性を変更していたとしても,同名のファイルを 他人が作っておくことで,プログラムの実行の邪魔をしたりできる。

また,他人から次のようないたずらを仕掛けられる可能性もある。

ln -s ~user/.zshrc /tmp/secret.tmp

/tmp/secret.tmp ファイルがない状態であれば, 誰でも /tmp ディレクトリにこのシンボリックリンクを作れる。 この状態で本人(user)がプログラムを起動すると大事なファイル(この例では ~/.zshrc) を上書きして重要ファイルを失うことに繋がる。

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

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

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

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

一時ファイル

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

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

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

一時ファイルのファイル名が必要な場合は fp.path で得られる。また一時ファイルをオープンした状態で fp.delete とすると即座にファイルを消して他のプロセスがアクセスする可能性を 消すことができる。 「ファイルの消えるタイミング」で 述べたように,オープンしたままであれば元のプログラムからは引続き アクセスは可能である。

一時ディレクトリ

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

このときの流れは

  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"	# 一時ディレクトリ名を決める
if test(?d, tmpdir) 		# (1) 既存か確認
  abort "一時dir作成失敗"	# 既に存在したら異常終了
end
# (2)
Dir.mkdir(tmpdir)		# なければ一時ディレクトリを作成し
# (3)
File.chmod(0600, tmpdir)	# すぐにchmodする

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

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

tmpdir.rb

#!/usr/bin/env ruby
# -*- coding: utf-8 -*-

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

このプログラムでは begin ... ensure ... end 構文を使っている。 最初のブロックで異常事態が起きようと必ず ensure から end までのブロックの処理を行なう。 実際に実行してみて一時ファイルが残らないことを確認せよ。

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

mktmpdir.rb

#!/usr/bin/env ruby
# -*- coding: utf-8 -*-

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