portalfsとFUSEで独自ファイルシステムを実装しよう

以下のテキストは、執筆時当時の情報を元に書いたものであり、 現在の情勢にそぐわないことを含む場合があるので注意されたい。 また、テキストは最終提出原稿で校正を経る前のものなので、実際にOSM 本誌に記載されたものとは異なる。誤字脱字等そのままである。

致命的な誤り以外は加筆修正等は行なわないので情報の鮮度に気をつけつつ 利用して欲しい。

目次


======================================================================
Part4: portalfsとFUSEで独自ファイルシステムを実装しよう
======================================================================


■
■独自ファイルシステムを構築しよう
■

圧縮ファイル、リモートファイル、参照する時刻によって変化する文字列、どん
なものでも、コマンド起動やシェルスクリプトでねらったものを取得できる方法
が分かっていればそれをファイルシステムとして機能させることができる。
Part4 portalfs と FUSEの力を借り独自のファイル取り出しスキーマを持つファ
イルシステムを作っていこう。いきなりフルスペックのファイルシステムに挑む
のはたいへんなので、難易度で分けた次の3つのコースを考えた。順を追って実験
していけば、未来の kernel hacker になれる…かもしれない:-)

	* 梅コース(portalfs+シェルスクリプト)
	  シェルスクリプトでportalfsのファイル取り出しを組み立て。読み取
	  り専用、または書き出し専用のファイルのみだがとても簡単で意外に
	  便利。

	* 竹コース(portalfs+Cの微修正+Rubyスクリプト)
	  portalfsの既存のソースを参考に、新しいルールを作成。ディレクト
	  リの一覧を取ったりすることはできないがファイルの読み書きはでき
	  る。Cの基本的な知識があればすぐにできる。

	* 松コース(FUSE+Rubyスクリプト)
	  FUSE付属のサンプルソースを参考に、ほぼ完全なファイルシステムを
	  実装する。(f)open()、readdir()、(f)stat()を利用したことがある、
	  または利用の仕方が分かる程度の知識があれば難なくできる。

■
■梅コース - portalfs+シェルスクリプトで構築
■

●どんな機能を持ったファイルシステムになるか

WebサーバとHTMLファイルの関係を考えてみよう。普通のHTMLファイルは静的な
コンテンツを提供する。動的なものを提供したい場合、通常はCGIやSSI、PHPな
どの機構を利用する。しかしそれらは、Webサーバプログラムに実装されている
からこそできることで、Webサーバを介さずにダイレクトにファイルを見てもプ
ログラムソースが見えるだけだったりするし、FTPサーバで利用することはでき
ない。

portalfsとシェルスクリプトを組み合わせると、たとえるならCGIファイルを開い
たような効果をWebサーバとは無関係な場所で得ることができる。ここでは、特定
の名前のファイルを開くと適切な処理を施した結果が得られる、というようなファ
イルシステムを作ろう。これはFreeBSD/NetBSDともに標準添付の portalfs 設定
ファイルで実験することができる。

FreeBSDの場合は /usr/share/examples/portal に、NetBSDの場合は
/usr/share/examples/mount_portal にサンプルの portal.conf がある。まずは
これを利用してportalfsを /p にマウントしよう。

	# cp /usr/share/examples/*portal/portal.conf /etc
	# mount_portalfs /etc/portal.conf /p

●つねに「今日の運勢」を読み出せるファイルを作る

運勢に限らず、なんでもよい。コマンドを打ったら日によって違う結果が返って
来るものが面白いだろう。今回たまたま見付けたにすぎないが、
「今日の運勢」
http://www.ageun.com/daily/furtune.html
がテキスト形式で運勢占いを得るのに適しているのでこれを使ってみよう【註 ち】。
---[註 ち]------------------------------------------------------------
もちろん、別のWebでもよいし、ネットに繋がずに試すなら
/usr/games/foretune コマンドを使うとよい。
----------------------------------------------------------------------

牡羊座から魚座まで12ある星座に対応した01〜12の値を変数idに代入する形で
CGIを呼ぶとよいようだ。つまり、

牡羊座 http://www.ageun.com/cgi-bin/dairy2/index.cgi?id=01
牡牛座 http://www.ageun.com/cgi-bin/dairy2/index.cgi?id=02
	:
	:
魚座   http://www.ageun.com/cgi-bin/dairy2/index.cgi?id=12

というURLをテキスト形式で取得すると良さそうである。Webページをテキスト形
式で得るにはw3mコマンド【註 り】が便利だ。

---[表 り]------------------------------------------------------------
http://w3m.sourceforge.net/index.ja.html
FreeBSD ports は、/usr/ports/www/w3m-m17n-img
NetBSD pkgsrc は、/usr/pkgsrc/www/w3m-m17n で make install すれば
インストールされるはずである。
----------------------------------------------------------------------

これをふまえて、指定した星座の運勢を取得するシェルスクリプトを作成し、そ
れを自動的に呼ぶportalfs内のPATHを作成しよう。

●シェルスクリプトの作成

w3mを-dumpオプションつきで利用すれば簡単で、【リスト ぬ】のものでよいだ
ろう。これを /usr/local/bin/getzodf という名前で保存し実行属性を付けてお
くものとする。

---[リスト ぬ]--------------------------------------------------------
[ /usr/local/bin/getzodf ]

#!/bin/sh
PATH=/usr/local/bin:/usr/pkg/bin:$PATH
CGI=http://www.ageun.com/cgi-bin/dairy2/index.cgi
id=01
case "$1" in
  oh*|ar*)	id=01 ;;
  ou*|ta*)	id=02 ;;
  fu*|ge*)	id=03 ;;
  ka*|can*)	id=04 ;;
  sh*|si*|le*)	id=05 ;;
  ot*|v*)	id=06 ;;
  te*|li*)	id=07 ;;
  sas*|sc*)	id=08 ;;
  i*|sag*)	id=09 ;;
  y*|cap*)	id=10 ;;
  m*|aq*)	id=11 ;;
  u*|p*)	id=12 ;;
  [1-9]|[12][0-9]) id=$1 ;;
esac
w3m -dump "$CGI?id=$id" | colrm 1 14
----------------------------------------------------------------------

このスクリプトを使うと、第1引数に星座和名のローマ字か星座英名の小文字表
記を与えると対応する運勢がテキスト形式で取り出せる。

例:	% getzodf uoza			# 魚座の運勢

  * FreeBSD portalfs pipe ルールによる作成

    pipeルールは、起動するコマンド列を直接パス名に指定すればよい。つまり、
    魚座運勢出力に結びつけられたファイルは、
    /p/pipe"/usr/local/bin/getzodf uoza"
    というパス名となる。これに簡単にアクセスするために、ホームディレクト
    リで

	% ln -s /p/pipe"/usr/local/bin/getzodf uoza" .uoza

    などとしておけばよいだろう。

  * NetBSD portalfs rfilter ルールによる作成

    rfilterルールは、あらかじめportal.confに起動すべきコマンドを記述して
    おく。FreeBSDのpipeルールに比べて設定の手間は僅かにあるが、参照すると
    きに起動すべきコマンドラインをパス名に書かなくて済む。
    /etc/portal.conf に以下の行を追加する。

	/zodiac		rfilter /zodiac /usr/local/bin/getzodf %s

    修正した portal.conf で /p をマウントし直す。

	# umount /p
	# mount_portal /etc/portal.conf /p

    参照する場合は起動コマンド(ここではgetzodfスクリプト)への引数をサブ
    ディレクトリの名前とする。

	% cat /p/zodiac/uo

●他の応用例

上で紹介した例は、「別にそのままシェルスクリプトを起動すれば」と感じるか
もしれないが、これらの用途はなんらかのアプリケーションで通常はファイルを
指定するようなものに指定するものに動的な要素を加える場合に有効である。

たとえば、ランダムなJPGファイルを選んで標準出力に書き出すようなシェルスク
リプトを作成しておいて、それを呼び出すportalfs内ファイルをデスクトップ画
面の背景画像として指定しておけば固定ファイル名なのにランダムな画像が得ら
れる。あるいは、メイルの本文の末尾につける署名(signature)ファイルをランダ
ムに切り替える機能がメイルリーダにない場合でも、ランダムに署名を書き出す
シェルスクリプトに結びついたportalfs内ファイルを指定すれば簡単に動的に変
化する署名を実現できる。

アイデア次第で、既存のアプリケーションに便利で楽しい機能を追加できるだろ
う。


■
■竹コース portalfsに機能追加
■

まずは、FreeBSDのmount_potalfsのソースを見て欲しい。

-----------------------------------------------------------------ここから
# cd /usr/src/usr.sbin/mount_portalfs
# ls -l *.[ch]
-rw-r--r--  1 root  wheel  4911 May 11  2005 activate.c
-rw-r--r--  1 root  wheel  7259 Aug  7  2004 conf.c
-rw-r--r--  1 root  wheel  2543 Mar 11  2005 cred.c
-rw-r--r--  1 root  wheel  6440 Mar 11  2005 mount_portalfs.c
-rw-r--r--  1 root  wheel  1935 Aug  7  2004 pathnames.h
-rw-r--r--  1 root  wheel  3149 Mar 11  2005 portald.h
-rw-r--r--  1 root  wheel  2111 Mar 11  2005 pt_conf.c
-rw-r--r--  1 root  wheel  2081 Aug  7  2004 pt_exec.c
-rw-r--r--  1 root  wheel  2950 May 20  2005 pt_file.c
-rw-r--r--  1 root  wheel  5777 Mar 11  2005 pt_pipe.c
-rw-r--r--  1 root  wheel  4226 Aug  7  2004 pt_tcp.c
-rw-r--r--  1 root  wheel  6816 Aug  7  2004 pt_tcplisten.c
-----------------------------------------------------------------ここまで
(※NetBSDの場合は /usr/src/sbin/mount_portal)

portal.confに記述できるルール(fs, pipe, tcp, tcplisten)を処理するファイル
が別々のCプログラムファイルになっているのが分かる。これらは大体5000バイ
ト前後ととても短い。いくつかソースを見れば分かるが、これらはプロセスとの
パイプ、あるいはTCPソケットを開いて、そのファイル記述子を返しているだけ
のもので、やっていることはかなり単純だ。TCP通信プログラムなど書いたこと
がないとしても、

	FILE *fp;
	fp = fopen("file", "r");

程度が分かれば、あとは見様見まねでなんとかなってしまうのである。ここでは、
こんな目標を立ててやってみよう。

	* FreeBSDのportalfsに新しいルールを作る
	* とはいえゼロから作るのは難しいので、
	  NetBSDだけにあるrfilterルール(pt_filter.c)を移植してみる
	* rfilterは読み出ししかできないので、読み書き両方ができるように
	  してみる(rfilterとwfilterの合体版)

これができ上がると、

	* あるパス名を読み取り用に開くとそこからデータを取り出すことができる
	* 同じパス名を書き込み用に開くとそこにデータを保存できる

ようになる。たとえば、ftpサーバや、アーカイバの中のファイル、WebDAVの先、
RDBの中、など、読み書きできる可能性を持つ場所にあるファイルが普通のパス
名で普通のアプリケーションからアクセスできるようになる【註 る】。

---[註 る]------------------------------------------------------------
厳密にいうとファイルに対してread/write以外の操作(statなど)をするアプリケー
ションからは正常に扱えない場合がある。
----------------------------------------------------------------------

では、作業に移ろう。具体的手順としては

	1. NetBSDの pt_filter.c をFreeBSDのソースツリーにコピーする
	2. rfilter処理関数のほうだけ抽出(wfilter処理関数の方を消す)
	3. FreeBSDの pt_conf.c に rfilter 処理関数を登録する
	4. コンパイルしてみてエラーが出る箇所を潰していく

という風になる。順番にやってみよう。

【1】FreeBSDのソースにNetBSDのpt_filter.cをコピー

先にFreeBSDのソースが /usr/src に展開されていることを確認する【註 を】。
確認できたらホームディレクトリなど適当な作業ディレクトリで、NetBSDの
mount_portal のソースを取得しよう。

---[註 を]------------------------------------------------------------
sysinstallで、Configure → Distributions → src(Sources for everything)
を選んでインストールする。
----------------------------------------------------------------------

% cvs -d :pserver:anoncvs@sup.jp.netbsd.org:/cvs/cvsroot login
(パスワードには anoncvs と入力)

% cvs -d :pserver:anoncvs@sup.jp.netbsd.org:/cvs/cvsroot \
	co -r netbsd-3 src/sbin/mount_portal

% cd src/sbin/mount_portal

ここにある、pt_filter.c をFreeBSDソースの
/usr/src/usr.sbin/mount_portalfs にコピーする。

% su		(スーパーユーザになる)
# cp -i pt_filter.c /usr/src/usr.sbin/mount_portalfs

【2】rfilter関数だけ残してwfilterを消す

コピーした NetBSDのpt_filter.c ファイルには、rfilterルールとwfilterルール
両方の処理関数が定義されている。片方だけで十分練習になるので、wfilterルー
ルを処理する portal_wfilter() 関数を消して【註 わ】しまおう。

---[註 わ]------------------------------------------------------------
もちろん wfilter の関数の移植もしたい場合は残してもよい。ぜひチャレンジ
してみて欲しい。
----------------------------------------------------------------------

コピー先ディレクトリに移動し、以下のようにエディタを起動する。

# cd /usr/src/usr.sbin/mount_portalfs
# vi +/portal_wfilter/-1 pt_filter.c

  (int portal_wfilter関数定義の先頭にカーソルが来るはずなので
   確認したら dG とタイプして末尾まで消去する。)

これから作成するのは「読み書き両用フィルタ」なので、「rfilter」という単
語を、全て「rwfilter」に変えておこう。viで編集しているのなら、

	:%s/rfilter/rwfilter/w [Return]

で一括置換できる。

次に、「読み取り専用」になっている部分を「読み書き両用」に変える。
「/popen[Return]」とタイプし、popenで検索すると次のような行が見付かる。

	fp = popen(cmd, "r");

読み取り専用を意味する "r" を読み書き両用を意味する "w+" に変える。

	fp = popen(cmd, "w+");

以上全てよければ :wq [Return] で保存終了する。

【3】pt_conf.c に rfilter 処理関数を登録する

pt_conf.c の末尾を見ると、【リスト か】のような構造体があり、登録が容易
だと分かる。
---[リスト か]--------------------------------------------------------
provider providers[] = {
        { "exec",       portal_exec },
        { "file",       portal_file },
        { "pipe",       portal_pipe },
        { "tcp",        portal_tcp },
        { "tcplisten",  portal_tcplisten },
        { 0, 0 }
};
----------------------------------------------------------------------

tcplistenの定義の次の行に、rwfilter を追加しよう【リスト よ】。

---[リスト か]--------------------------------------------------------
【pt_conf.c の構造体宣言部】


provider providers[] = {
        { "exec",       portal_exec },
        { "file",       portal_file },
        { "pipe",       portal_pipe },
        { "tcp",        portal_tcp },
        { "tcplisten",  portal_tcplisten },
        { "rwfilter",   portal_rwfilter },
        { 0, 0 }
};
----------------------------------------------------------------------

この構造体で参照する関数のプロトタイプ宣言を行なっているのが portald.h
なので、そこに portal_rwfilter() のプロトタイプ宣言を追加する。これも、
tcplistenの行を真似して追加すればよい【リスト よ】。

---[リスト よ]--------------------------------------------------------
【portald.h追加部分】

extern int portal_rwfilter(struct portal_cred *,
                                char *key, char **v, int so, int *fdp);
----------------------------------------------------------------------

【4】コンパイルしてみてエラーが出る箇所を潰していく

Makefileに pt_filter.c を追加【リスト た】してコンパイルしてみよう。

---[リスト た]--------------------------------------------------------
【Makfile修正部分】

SRCS=   mount_portalfs.c activate.c conf.c cred.c getmntopts.c pt_conf.c \
        pt_exec.c pt_file.c pt_pipe.c pt_tcp.c pt_tcplisten.c pt_filter.c
		  	    	      	       		     ------------
							これを追加↑
----------------------------------------------------------------------

意外なほど、コンパイルが順調に進むが、最後に

----------------------------------------------------------------------ここから
pt_filter.o(.text+0x10): In function `portal_rwfilter':
: undefined reference to `lose_credentials'
*** Error code 1

Stop in /usr/src/usr.sbin/mount_portalfs.
----------------------------------------------------------------------ここまで

という未定義シンボルエラーで止まる。これは user_credential の扱いが
NetBSDとFreeBSDで違うためだが、FreeBSDにある pt_pipe.c を見るとどう直せば
よいか分かる。手順を説明すると少々長くなるので、この部分はパッチを示す
【リスト れ】ので参考に修正して欲しい(他のファイルのパッチも含めCD-ROMに
も収録した)。

---[リスト れ]--------------------------------------------------------
--- pt_filter.c.orig    Sat Aug 12 19:38:48 2006
+++ pt_filter.c Sat Aug 12 19:47:26 2006
@@ -91,13 +91,14 @@
        char   *path;
        FILE   *fp;
        int     error = 0;
+       struct portal_cred save_area;
 
        /* We don't use this parameter. */
        (void) kso;
 
-       error = lose_credentials(pcr);
-       if (error != 0)
-               return error;
+       /* Swap priviledges. */
+       if (set_user_credentials(pcr, &save_area) < 0)
+               return (errno);
 
 #ifdef DEBUG
        fprintf(stderr, "rwfilter:  Got key %s\n", key);
@@ -162,6 +163,9 @@
                        }
                }
        }
+       /* Re-establish our priviledges. */
+       if (restore_credentials(&save_area) < 0)
+               error = errno;
        if (error == 0)
                fdp[0] = fileno(fp);
        return (errno);
----------------------------------------------------------------------


以上で作業完了だ。make してインストールすれば私家版拡張portalfsが使える
ようになる。

# make all install

●新作rwfilterでZIPアーカイブファイルシステムを作ってみよう

Windows XPでのZIPアーカイブフォルダほどまで透過的にはいかないが、指定した
ファイルをパス名で開くだけで自動的にZIPアーカイブの中から対応するファイル
を取り出す(あるいは書き戻す)処理をしてくれるファイルシステムはrwfilterと
短いスクリプトをつかって構築することができる。

ここでは、

	/p/zip/foo/bar.zip/baz
	というパス名でopenしたファイルからデータを読み出すと
	/foo/bar.zip アーカイブの中の baz ファイルを読み出し、

	同じパス名でopenしたファイルにデータを書き出すと
	/foo/bar.zip アーカイブの中の baz ファイルに書き出す

ようなフィルタを作ってみよう。このためには、/etc/portal.conf で次のよう
に記述することになるだろう。

	zip/	rwfilter zip/	フィルタプログラム %s

フィルタプログラムとして呼び出されるプログラムは、引数として、開くべきファ
イルのフルパス名から /p/zip/ を除去したものを受け取る。たとえば、
/p/zip/foo/bar.zip/baz というパス名の場合は foo/bar.zip/baz がフィルタプ
ログラムに渡されることになる。

これをふまえるとフィルタプログラムでは、

	受け取った引数のディレクトリ部分(dirname)をZIPアーカイブ名として、
	ファイル名部分(basename)をアーカイブの中のファイル名として認識し、
	そのファイルをアーカイブから取り出す、またはそれに書き込む処理を
	自動的に行なう

という手順を取ればよいことが分かる。これを行なうプログラムをRubyで実装し
てみたのが【リスト そ】である。

---[リスト そ]--------------------------------------------------------
【/usr/local/bin/pzipio.rb】

#!/usr/local/bin/ruby

exit if Process.euid == 0	# rootでの使用は遠慮:)
target="/"+ARGV[0]		# 先頭に/を追加
zipname=File.dirname(target)
filename=File.basename(target)
ENV["PATH"] = "/bin:/usr/bin:/usr/local/bin"
TMPDIR=ENV["TMPDIR"] || "/tmp"
trial=10

r = IO.select([STDIN], [STDIN], nil, nil)
if r[0][0]		# STDINにデータが来ているなら
  tmpdir=nil		# 書き込み処理
  while trial > 0
    f=sprintf("%s/%s.%s.%d", TMPDIR, "pzipio", $$, trial-=1)
    begin		# 作業ディレクトリ内で実行
      Dir.mkdir(f, 0700)# raise ERROR if exists
      tmpdir=f
      break
    rescue
      next
    end
  end
  if tmpdir && test(?d, tmpdir) && test(?w, tmpdir)
    Dir.chdir(tmpdir){|dir|
      open(filename, "w") do |out| # STDINからのデータを全て
	out.print STDIN.readlines  # 一時ファイルに書き出す
      end
      system("zip #{zipname} #{filename} > /dev/null")
    }
    File.unlink(tmpdir+"/"+filename)
    Dir.rmdir(tmpdir)	# 一時ファイルとディレクトリの後始末
  end
else			# ZIPアーカイブからの読み込み
  if filename == ".list"	# .list というファイルなら一覧
    system("unzip -v #{zipname}")
  else				# それ以外なら中味を標準出力へ
    system("unzip -cqq #{zipname} #{filename}")
  end
end
----------------------------------------------------------------------

このスクリプトを /usr/local/bin/pzipio.rb という名前で保存し、
実行属性をつけてから /etc/portal.conf にこのスクリプトを登録しよう。


---[リスト つ]--------------------------------------------------------
【/etc/portal.confへ追加する1行】

	zip/	rwfilter zip/	/usr/local/bin/portal.conf %s
----------------------------------------------------------------------

/p をマウントし直す。

# umount /p
# mount_portalfs /etc/portal.conf /p


●rwfilter/ZIPアーカイブファイルシステムの実験

ZIPアーカイブファイルを作成して portalfs 経由で中のファイルに直接アクセス
できるか確かめてみよう。作成するファイルはどんなものでも構わないが、ここ
では簡単に今月のカレンダーファイルをZIPアーカイブに格納してみる。

----------------------------------------------------------------------ここから
% mkdir /tmp/ziptest
% cd /tmp/ziptest
% jcal
      2006年 長月 9月       
 日  月  火  水  木  金  土 
                      1   2 
( 3)  4   5   6   7   8   9 
(10) 11  12  13  14 (15) 16 
(17) 18  19  20  21  22 (23)
(24) 25  26  27  28  29  30 

% jcal > ca
% ls -lF
total 1
-rw-r--r--  1 yuuji  wheel  135 Aug 13 15:18 ca
% zip -m a.zip ca		# (-mオプションは元ファイルを消去する)
  adding: ca (deflated 42%)
% ls -lF
total 1
-rw-r--r--  1 yuuji  wheel  271 Aug 13 15:18 a.zip
% unzip -v a
Archive:  a.zip
 Length   Method    Size  Ratio   Date   Time   CRC-32    Name
--------  ------  ------- -----   ----   ----   ------    ----
     232  Defl:N      135  42%  08-13-06 15:18  75f95a44  ca
--------          -------  ---                            -------
     232              135  42%                            1 file
----------------------------------------------------------------------ここまで

「/tmp/ziptest/a.zip 内の ca ファイル」を、改良版portalfsによって
/p/zip/tmp/ziptest/a.zip/ca というパスで直接開けるようにした。試してみよう。

----------------------------------------------------------------------ここから
% cat /p/zip/tmp/ziptest/a.zip/ca
      2006年 長月 9月       
 日  月  火  水  木  金  土 
                      1   2 
( 3)  4   5   6   7  =8=  9 
(10) 11  12  13  14 (15) 16 
(17) 18  19  20  21  22 (23)
(24) 25  26  27  28  29  30 
----------------------------------------------------------------------ここまで

逆に、アーカイブ中のcaファイルを更新することもできる。

----------------------------------------------------------------------ここから
% uname -a > /p/zip/tmp/ziptest/a.zip/ca
(ファイル圧縮処理に少し時間がかかる)
% unzip -cqq a.zip ca
FreeBSD lead.yk.gentei.org 6.1-STABLE FreeBSD 6.1-STABLE #2: Tue Aug  1 14:56:51 JST 2006
yuuji@lead.yk.gentei.org:/usr/src/sys/i386/compile/VMWARE  i386
----------------------------------------------------------------------ここまで

●より完全なものにするには

今回作成した rzipio.rb では、アーカイブ中のファイルがディレクトリを含む場
合に処理できないし、絶対パスを含んだり相対パスで作業ディレクトリ以外に飛
び出してしまう場合などを考慮していない。また、ファイルの置き換えはできる
が追加はできない。これを直すには pt_filter.c とすり合わせをしながら少しず
つ修正していく必要があるだろう。もっとも、portalfsで作るのは、「気軽に簡
単に」作れる特別なファイルシステムなので、より厳格なものを作りたい場合は
次節で述べるFUSEで実装したほうがよいといえる。


■
■松コース FUSE用ファイルシステム作成(Linux)
■

FUSEのソースアーカイブ(fuse-2.5.3.tar.gz)中の、example/ ディレクトリには、
いくつか簡単なファイルシステム実装のCソースがある。その中の hello.c は、

	* マウントポイント以下に hello というファイルをただひとつ持つ
	* そのファイルの中味は「Hello World!」

というシンプルなものだが、ソースを見ることでFUSEベースのファイルシステム
を実装するための要件が理解できる。具体的には struct fuse_operations 構造
体にFUSEのAPIで定義されているいくつかの関数を登録し、それらを実際に作っ
ていけばよい。fuse.h の fuse_operations 構造体の定義を見れば、どのような
関数を定義すればよいかが分かる。

さて、このCソースを参考に独自ファイルシステムのプログラムを書くのはさほど
難しくはないのだが、竹コースほどの簡単な説明では書き切れないので、Cよりも
もっと簡単にできるFUSEのRubyバインディングを利用する方法で進めてみること
にした。これはRubyで簡単にファイルシステム制御メソッドを記述できるのでと
ても手軽だ。なお、このRubyバインディングの利用には、Ruby 1.8が必要なので、
最新版のRubyにアップデートするか、手動で1.8.4をインストールしておこう。

---[註 う]------------------------------------------------------------
パッケージでインストールする場合は ruby だけでなく、ruby-devel も必要で
ある。また fuse-devel もインストールしておく必要がある。
----------------------------------------------------------------------

FUSEのRubyバインディングは
http://rubyforge.org/projects/fusefs
から入手できる。このページの「ダウンロード」リンクより
fusefs-0.6.0.tar.gz を取得して作業ディレクトリにコピーし、以下のようにコ
ンパイルする。


% tar fusefs-0.6.0.tar.gz
% cd fusefs-0.6.0
% ruby setup.rb config
% ruby setup.rb setup
% sudo ruby setup.rb install

これらはFUSEFSライブラリとRubyが正常にインストールされていれば滞りなく完
了するはずだが、システムによっては「ruby setup.rb setup」のところで、コン
パイルエラーが出たり、実際にモジュールを使うときに libfuse.so が見付から
ないというエラーが出たりする場合がある。その場合は、「ruby setup.rb
config」した直後に ext/Makefile を以下のように修正する。

	% vi "+/^LIBS" Makefile
--------------------------------------------------------------修正部ここから
LIBS =  -lfuse  -ldl -lcrypt -lm   -lc -Wl,-R/usr/local/lib -L/usr/local/lib
     		     	     	      --------------------------------------
				これを追加↑
--------------------------------------------------------------修正部ここまで

この修正は libfuse.so が /usr/local/lib にインストールされている場合のも
のである。別の場所にインストールされている場合はそのディレクトリにする。

コンパイルとインストールがうまくいったかの確認は、sample/ ディレクトリに
あるRubyスクリプトで行なえる。

----------------------------------------------------------------------ここから
% cd sample
% mkdir hellotest
% ruby hello.rb hellotest &
% ls -lF hellotest
合計 0
-r--r--r--    1 yuuji    yuuji           0 Aug 13 08:18 hello.txt
% cat hellotest/hello.txt
Hello, World!
----------------------------------------------------------------------ここまで

うまくいったら hello.rb スクリプトを止めておく。

--------------------------------------------------------------ここから
% kill %ruby
--------------------------------------------------------------ここまで

●Ruby/fusefs による独自ファイルシステムの作成

	!!!注意!!!
	Ruby/fusefs 付属のAPI.txtにも書いてあるが、作成したfusefsモジュー
	ルからFUSEベースのファイルシステムにアクセスするとそこでFUSEがハン
	グする。そのような場合はrubyスクリプトを kill -KILL するしかなく、
	さらに fusermount -u で手動アンマウントする必要がある。


実用的な独自ファイルシステムを作りたいところだがずっと単純化し、とっかか
りになりそうなレベルで進めていこう。FUSE付属のhello.cが、かなり簡単なファ
イルシステムの実装なので、ここではそれよりもう少しだけ欲張ったファイルシ
ステムを作ってみよう。このようなファイルシステムはどうだろう。

	* 題して「一週間ファイルシステム」
	* マウントポイント以下のファイルは Sunday, Monday, ...,
	   Saturday の7つ
	* それらのファイルの中味はその曜日名を日本語に翻訳したもの

実行イメージは次のような感じだ。
----------------------------------------------------------------------ここから
% weekfs.rb ~/week &
% ls week
Friday  Monday  Saturday  Sunday  Thursday  Tuesday  Wednesday
% cat week/Friday
金曜日
----------------------------------------------------------------------ここまで

Ruby/fusefs によるファイルシステムを構築するときに定義すべきメソッドは、
Ruby/fusefs ソースアーカイブ中の sample/hello.rb と API.txt を見れば分かる。
ここでは、

	* ディレクトリに含まれるファイル一覧を返す contents メソッド
	* 指定したパスがファイルかどうかを検査する file? メソッド
	* 指定したパスの内容を返す read_file メソッド

のみ定義しよう。

* contentsメソッド
  Friday Monday Saturday Sunday Thursday Tuesday Wednesday
  の7つの文字列の配列を返せばよい。したがって、
  def contents(path)
    %w(Friday Monday Saturday Sunday Thursday Tuesday Wednesday)
  end
  となる。

* file?メソッド
  file?メソッドにはマウントポイントを起点とするフルパス名が渡される。つ
  まりたとえば、Sundayファイルの検査の場合は "/Sunday" が渡されることに
  なる。先頭のスラッシュ(/)を取った文字列が7つの単語のどれかにマッチすれ
  ばよい。したがって、
  def file?(path)
    path = path[1..-1]  # 最初の1文字を切り取る
    %w(Friday Monday Saturday Sunday Thursday Tuesday Wednesday).index(path)
  end
  とすればよい。indexメソッドは配列中に引数と一致する値があったときに、
  そのインデックス位置を整数で、なかったときにはnilを返すものである。

* read_fileメソッド
  存在する7つのファイルのいずれかであれば、それに対応した曜日名を返す。
  def read_file(path)
    jwday = %w(日曜日 月曜日 火曜日 水曜日 木曜日 金曜日 土曜日)
    ix = file?(path)	# マッチしていれば添字が返る
    jwday[ix] if ix	# もしマッチしているなら和名を返す
  end

以上をまとめよう。7つの曜日の単語を持つ配列を変数にいれておいた方がよい
ので、Rubyでクラスを生成するときに呼ばれる初期化メソッド initialize で、
変数を定義するように変えて完成したものが【リスト ね】である。

---[リスト ね]--------------------------------------------------------
#!/usr/bin/env ruby
# EUCコードで保存

require 'fusefs'

class WeekDir
  def initialize
    # 7つの単語の配列を作っておく
    @wday = %w(Sunday Monday Tuesday Wednesday Thursday Friday Saturday)
    @jwday = %w(日曜日 月曜日 火曜日 水曜日 木曜日 金曜日 土曜日)
  end
  def contents(path)
    @wday
  end

  def file?(path)
    path = path[1..-1]
    @wday.index(path)
  end

  def read_file(path)
    ix = file?(path)
    @jwday[ix] if ix
  end

  def size(path)
    c = read_file(path)
    c ? c.length : 0
  end
end

weekdir = WeekDir.new
FuseFS.set_root( weekdir )

# Mount under a directory given on the command line.
FuseFS.mount_under ARGV.shift
FuseFS.run
----------------------------------------------------------------------


●より実用的な実装

portalfsへの拡張機能のように、圧縮アーカイブをあたかも直接覗けるようなファ
イルシステムも作ることができる。FUSEではファイルやディレクトリに対するオ
ペレーションを一通り定義できるため、lsでファイル一覧を取ったりすることが
できる。実はFUSEにもそのようなモジュールがあるのではないかと思ったが、
それとおぼしき「unpackfs」http://www.nongnu.org/unpackfs/ は、テンポラリ
ディレクトリにどんどん圧縮アーカイブを展開していくという豪快な仕様のもの
だったので、いい気になってどんどん圧縮アーカイブを除いていくとディスクが
あふれてしまう。もしかしたら他にもあるのかもしれないが探すことができなかっ
た。

ということで、FUSE-APIを使ってディスクに展開せずに標準出力だけに書き出し
ていく圧縮アーカイブファイルシステム「acvfs」をRuby/fusefsで実装してみた。
Rubyの書きやすさのおかげで半日くらいで作ったものだが、なかなか使えるとい
う感触を得た。改造も容易なので、色々なアーカイバ対応やエラー処理の追加な
どを試みて欲しい。作成したスクリプトを【リスト な】に示す(本号CD-ROMにも
収録)。

---[表 な]------------------------------------------------------------
#!/usr/bin/env ruby
#
# acvfs.rb: Archive FileSystem Based on FUSE/Ruby
# (C)2006 by HIROSE, Yuuji [yuuji@yatex.org]
#
# Usage: acvfs.rb mountpoint
#
# This file system provides zip/tar.gz/tar.bz2 auto inspection.
#
# Requirements:
# http://fuse.sourceforge.net/ - Filesystem in Userspace
# http://rubyforge.org/projects/fusefs - Ruby binding of FUSE-FS
#
# Todo:
#  Support other archivers than arc, arj, rar, ... and so on.
#  But it might be easy.  Try it by yourself.
#
require 'fusefs'

class ArchiveDir
  def initialize
    @cachedacv = Hash.new
  end
  def zip?(path)
    /\.zip/i =~ path
  end
  def targz?(path)
    /\.(tar\.gz|tgz)/i =~ path
  end
  def tarbz2?(path)
    /\.(tar\.bz2|tbz)/i =~ path
  end
  def acv?(path)  # path is archived file or within it, or not
    zip?(path) || targz?(path) || tarbz2(path)
  end
  def acvfile?(path) # path IS archived file, or not
    /\.(zip|tar\.(gz|bz2))$/i =~ path
  end
  def listcmd(arc)
    # Return the array of [ListingCmd, Regexp, [$size, $name], EndRattern]
    if zip?(arc)
      ["| unzip -v \"#{arc}\" | tail +4",
        /(\d+) +(\d+)% +(\d+)-(\d+)-(\d+) +(\d\d):(\d\d) .* (\S+)$/,
        [1, 8],
        /^---/]
    elsif targz?(arc)||tarbz2?(arc)
      c = targz?(arc) ? "z" : "j"
      ["| tar v#{c}tf \"#{arc}\"",
        /(\d+) +(\d{4})-(\d+)-(\d+) +(\d\d):(\d\d):(\d\d) +(\S+)$/,
        [1, 8],
        /^$/]
    end
  end

  # cache all filenames, sizes and whether they are directory
  def acvinfo(acv)
    %r,(.*\.(zip|tar\.(gz|bz2)))(/(.*))?, =~ acv
    dir = $1
    prefix = $5
    if !@cachedacv[acv]
      @cachedacv[acv] = Hash.new
      listinfo = listcmd(dir)
      open(listinfo[0], "r") do |zls|
        while line = zls.gets
          case line
          when listinfo[3]
            break
          when listinfo[1]
            md = Regexp.last_match
            dirp = nil          # is_directory flag
            name = md[listinfo[2][1]]
            if prefix
              next unless name.index(prefix) == 0
              next if name == prefix
              name.sub!(prefix+"/", "") # strip prefix directory
            end
            next if name == ""     # skip prefix dir itself
            next if %r,/., =~ name # skip files in subdirectory
            if %r,/$, =~ name
              name.chop!
              dirp = true
            end
            @cachedacv[acv][name] = Hash.new
            @cachedacv[acv][name]['dir'] = dirp
            @cachedacv[acv][name]['size'] = md[listinfo[2][0]].to_i
            # p ["set", acv, name, dir, md[listinfo[2][0]].to_i, dirp]
          end
        end
      end
    end
    @cachedacv[acv]
  end
  # Return the list of files in directory: path
  def contents(path)
    mntpt=ARGV[0].gsub(%r|/+|, "/")
    if test(?d, path)
      Dir.entries(path).select{|f|
        n = (path+"/"+f).gsub(%r|/+|, "/")
        next if ARGV[0] == n
        test(?d, n) || test(?f, n) && acvfile?(f)
      }
    elsif acv?(path)
      acvinfo(path).keys
    end
  end

  def file?(path)
    test(?f, path) ||
      if acv?(path)
        acv = File.dirname(path)
        file = File.basename(path)
        acvinfo(acv).has_key?(file) && !acvinfo(acv)[file]['dir']
      end

  end
  def executable?(path)
    test(?d, path) ? test(x, path) : false # shoud be safer
  end
  def directory?(path)
    test(?d, path) || acvfile?(path) ||
      if acv?(path)
        acv = File.dirname(path)
        file = File.basename(path)
        acvinfo(acv)[file]['dir']
      end
  end
  def can_write?(path)
    false
  end

  def read_file(path)
    %r,(.*\.(tar\.(gz|bz2)|zip))(/(.*))?, =~ path
    acv, file = $1, $5
    if acv && file
      cmd = if zip?(acv)
              "unzip -cqq \"#{acv}\" \"#{file}\""
            elsif targz?(acv)
              "tar zxOf \"#{acv}\" \"#{file}\""
            elsif tarbz2?(acv)
              "tar jxOf \"#{acv}\" \"#{file}\""
            end
      IO.popen(cmd, "r").read
    else
      "Not an archived file\n"
    end
  end

  def size(path)
    acv = File.dirname(path)
    file = File.basename(path)
    if acv?(acv)
      acvinfo(acv)[file]['size']
    else
      test(?s, path)
    end
  end
end

adir = ArchiveDir.new
FuseFS.set_root( adir )

# Mount under a directory given on the command line.
FuseFS.mount_under ARGV[0]
FuseFS.run
----------------------------------------------------------------------

acvfs.rb でマウントしたディレクトリ以下は、ルートファイルシステム以下と同
じように見えるが、

	* 通常ファイルは見えない
	* ディレクトリはディレクトリとして見える
	* zipとtar.gzアーカイブはディレクトリとして見え、
	  その中に、圧縮されているファイル群があたかも展開されているよう
	  に見える

という風になっている。実際にはacvfs.rbが on demand でzipやtarコマンドを
起動してオンメモリで処理している。

acvfs.rb を利用して、/z にアーカイブ直視ファイルシステムをマウントする実
験をFedoraCore 5で行なった例を示す。

----------------------------------------------------------------------ここから
cb1{yuuji}% sudo mkdir /z
cb1{yuuji}% sudo chown yuuji /z
cb1{yuuji}% ./acvfs.rb /z &		(バックグラウンドで起動)

cb1{yuuji}% mount -t fuse
/dev/fuse on /z type fuse (rw,nosuid,nodev,user=yuuji)
cb1{yuuji}% ls -F /z
bin/   etc/   lost+found/  misc/  opt/   sbin/     sys/  var/
boot/  home/  media/       mnt/   proc/  selinux/  tmp/
dev/   lib/   memory/      net/   root/  srv/      usr/

(一見 / と同じように見える)

cb1{yuuji}% ls -F /z/home/yuuji/make
fusefs-0.6.0/  fusefs-0.6.0.tar.gz/  gmail/  python/

(fusefs-0.6.0.tar.gz がディレクトリのように見える)

cb1{yuuji}% ls -F /home/yuuji/make
acvfs.rb*  fusefs-0.6.0/  fusefs-0.6.0.tar.gz  gmail/  python/
(実際の ~/make にはディレクトリ3つと通常ファイル2つがある)

cb1{yuuji}% ls -F /z/home/yuuji/make/fusefs-0.6.0.tar.gz
fusefs-0.6.0/

cb1{yuuji}% ls -F /z/home/yuuji/make/fusefs-0.6.0.tar.gz/fusefs-0.6.0
API.txt    Changes.txt  README.txt  ext/      lib/     setup.rb
COPYRIGHT  Makefile     TODO        foo.yaml  sample/
----------------------------------------------------------------------ここまで

もちろんディレクトリブラウザからも閲覧可能だ。

---[図 ゐ]------------------------------------------------------------
%image acvfs-onFC5.png
----------------------------------------------------------------------
fusefs-0.6.0.tar.gz があたかもフォルダのように開ける。


■
■まとめ
■

OS全体はとても複雑だが、その一部、それもファイルシステムという根幹的な部
分を自分でカスタマイズできることが体感できただろうか。小さな部分をちょこっ
といじってみと案外簡単で、その次のステップへ、と繰り返しているうちにコミュ
ニティに貢献できるものを作れるようになっているだろう。ユニークな発想で誰
も思いつかなかったファイルシステムを考えてみてはいかがだろう。

---[表 の]------------------------------------------------------------
----------------------------------------------------------------------


yuuji@gentei.org
Fingerprint16 = FF F9 FF CC E0 FE 5C F7 19 97 28 24 EC 5D 39 BA
HIROSE Yuuji - ASTROLOGY / BIKE / EPO / GUEST BOOK / YaTeX [Tweet]