TECHSCORE Advent Calendar 2015 の 8 日目の記事です。
fuse でオレオレファイルシステムを作ってみた
Unix 系 OS では、fuse というモジュールを使って、オレオレファイルシステムを作成することができます。 今回は、fuse と locate コマンドで作ってみたファイルシステムをネタに、どんな感じで開発できるか紹介します。
この内容に興味のない方は、最後の世界で一番かわいい猫の画像を好きなだけ眺めていってください。
検索用ファイルシステム
GUI のファイラーでは、検索をすると結果のファイルがずらずらと出てきて通常のディレクトリと同じような操作ができる、そういったものがよくあります。 しかし、locate などで検索しても、単にファイルパスが表示されるだけです。 そこで、(閲覧だけになりますが)似たようなことをするために、locate コマンドの結果を通常のディレクトリのように見ることができるファイルシステムを作ることにしました。 設定ファイルなどを CLI のファイラーの ranger などで、こんな感じ↓で次々に見たりすることが主な目的です。
(systemd の service ファイル検索して見ています)
仕組み
アクセスするパスのディレクトリ名を locate に渡すクエリとして、そのディレクトリにアクセスすると locate の検索結果が出るという仕組みにしました。
具体例
/locate がこのファイルシステムのマウント先で、pacman.log を検索したいとします。 この場合、以下の様にアクセスします。
1 2 3 4 |
$ ls /locate/pacman.log/ home_-maomao_-.neocomplcache_-buffer__cache_-=+var=+log=+pacman.log home_-maomao_-.neocomplcache_-keyword__patterns_-=+var=+log=+pacman.log var_-log_-pacman.log |
/locate/pacman.log/ ディレクトリに var_-log_-pacman.log などのエントリが存在するように見える寸法です。 ちなみに、エントリ名はフルパスのスラッシュを _- といった風に適当に加工したものです。 デコードすることで、フルパスが得られるというわけです。
これらのエントリは、シンボリックリンクなので、このリンクにアクセスすることで、元のファイルなどにアクセスできます。
1 2 3 4 5 6 7 8 9 10 11 |
$ head /locate/pacman.log/var_-log_-pacman.log [2011-11-01 06:16] installed filesystem (2011.08-1) [2011-11-01 06:16] installed util-linux (2.19.1-3) [2011-11-01 06:16] installed libusb (1.0.8-1) [2011-11-01 06:16] installed libusb-compat (0.1.3-1) [2011-11-01 06:16] installed pcre (8.13-1) [2011-11-01 06:16] installed glib2 (2.28.8-1) [2011-11-01 06:16] installed module-init-tools (3.16-1) [2011-11-01 06:16] installed pciutils (3.1.7-4) [2011-11-01 06:16] installed udev (173-3) [2011-11-01 06:16] installed device-mapper (2.02.87-1) |
こうやってパスを打ち込む分には特にメリットがありませんが、上のスクリーンショットの様なファイラーでは、ちょっと便利というわけです。
どう実装するか
fuse での実装は、簡単です。 ファイルシステム上の機能(ファイル/ディレクトリの読み込み等々)を、関数を書くことで実装していくだけです。 不要な機能については、その機能は無いと返すだけで良いので、実質省略できます。
また、色々な言語のバインディング もあるので、大抵の言語で容易に実装できるようです。
そういうわけで(?)、今回は Haskell で実装しました。
実際の実装
実際に書いたコードを抜粋しつつ、軽く説明していきます。
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 |
dirStat :: FuseContext -> FileStat dirStat ctx = FileStat { statEntryType = Directory , statFileMode = foldr1 unionFileModes modes , statLinkCount = 2 , statFileOwner = fuseCtxUserID ctx , statFileGroup = fuseCtxGroupID ctx , statSpecialDeviceID = 0 , statFileSize = 4096 , statBlocks = 1 , statAccessTime = 0 , statModificationTime = 0 , statStatusChangeTime = 0 } where modes = [ownerReadMode, groupReadMode] linkStat :: FilePath -> IO FileStat linkStat path = stat <$> getFileStatus path where stat status = FileStat { statEntryType = SymbolicLink , statFileMode = fileMode status , statLinkCount = linkCount status , statFileOwner = fileOwner status , statFileGroup = fileGroup status , statSpecialDeviceID = specialDeviceID status , statFileSize = fileSize status , statBlocks = 1 -- fromIntegral (fileSize status `div` 1024) , statAccessTime = accessTime status , statModificationTime = modificationTime status , statStatusChangeTime = statusChangeTime status } |
ディレクトリとシンボリックリンクの情報を生成します。 リンクのほうは、getFileStatus でリンク元ファイルの情報を取得して、転記しています。 後述の関数で、これらを返すようにします。
1 2 3 4 5 6 |
lfsOps :: Semaphore -> Cache -> FuseOperations Unit lfsOps sem cache = defaultFuseOps { fuseGetFileStat = lfsGetFileStat , fuseOpenDirectory = lfsOpenDirectory , fuseReadDirectory = lfsReadDirectory sem cache , fuseReadSymbolicLink = lfsReadSymbolicLink , fuseGetFileSystemStats = lfsGetFileSystemStats } |
FuseOperations が、要となるデータ型です。 fuseGetFileStat などが各機能で、これら自前の関数をバインドしてやります。 (引数の Semaphore と Cache は locate 結果のキャッシュに関するもので、fuse と直接関係はありません)
あとは、それぞれの機能を個別に実装していきます。 lfs〜というのがそれです。
ファイルの情報を取得する機能
1 2 3 4 5 6 |
lfsGetFileStat :: FilePath -> IO (Either Errno FileStat) lfsGetFileStat = procPath r (const r) qp . unescape where r = Right . dirStat <$> getFuseContext qp fp = do exist linkStat fp else return $ Left eNOENT |
locate 結果のファイルを取得しに来たときだけ、シンボリックの情報を生成して返しています。 他は、固定のディレクトリの情報を返しています。
procPath は、引数のパスが ルート/クエリ/結果のパス かで処理を分ける関数です。
ディレトクリを開くことが出来るか確認する機能
1 2 |
lfsOpenDirectory :: FilePath -> IO Errno lfsOpenDirectory _ = return eOK |
本来であれば、パスを見て判断するのでしょうが、アクセスできないディレクトリは提供しないので、無条件で成功を返しています。
ディレクトリのエントリを取得する機能
1 2 3 4 5 6 7 8 9 |
lfsReadDirectory :: Semaphore -> Cache -> FilePath -> IO (Either Errno [(FilePath, FileStat)]) lfsReadDirectory sem cache = procPath r q qp where r = Right . dots <$> getFuseContext q query = do ctx forM ls entry qp _ = return $ Left eNOENT dots ctx = [(".", dirStat ctx), ("..", dirStat ctx)] entry path = (escape path,) <$> linkStat path |
ディレクトリ名をクエリとして locate を実行し、シンボリックリンクとして返しています。 "/" などのアクセス時は、とりあえず失敗(存在しない = eNOENT)を返しています。
シンボリックリンクの解決機能
1 2 3 4 5 |
lfsReadSymbolicLink :: FilePath -> IO (Either Errno FilePath) lfsReadSymbolicLink = return . procPath r (const r) qp . unescape where r = Left eNOENT qp = Right . fixString |
要するに readlink するものです。 指定のパスのリンクのリンク先を返します。 locate 結果のシンボリックリンクのリンク先を返しています。 これも、リンクでないはずのパスについては、失敗を返しています。
fixString は、リンク先のパスにマルチバイト文字が入っていると文字化けするという問題(バグ?)への workaround です。 本来は必要ないのだと思います。 後でちゃんと調べたいですね。
ファイルシステムの情報取得機能
1 2 3 4 5 6 7 8 9 10 |
lfsGetFileSystemStats :: String -> IO (Either Errno FileSystemStats) lfsGetFileSystemStats _ = return $ Right $ FileSystemStats { fsStatBlockSize = 512 , fsStatBlockCount = 1 , fsStatBlocksFree = 1 , fsStatBlocksAvailable = 1 , fsStatFileCount = 5 , fsStatFilesFree = 10 , fsStatMaxNameLength = 4096 } |
とりあえず、サンプルと同じ値を返しています。 ファイル名が長くなりがちなので、fsStatMaxNameLength だけ、長めにしています。
ソースコード
全体のソースコードは、こちら (github.com) に用意しました。 fuse の開発用パッケージ、haskell のコンパイラの ghc と cabal があれば、make 一発でビルドできると思います。 (cabal sandbox 上でビルドします)
使いかた
オプションにマウント先を渡してやれば、それだけでバックグラウンドで動作します。 実行ファイルは、 dist/build/locatefs/locatefs に生成されていると思います。
1 |
$ ./locatefs /locate |
問題点
なぜか偶に Segfault で落ちたり、妙なことになるようです。 なにかご存知のかたがいましたら、ぜひ御教示ください! 条件もまだわからず、謎です。
また、ファイル名のエスケープ方法も、もう少し見やすいものにしたいですね。
世界で一番かわいい猫
さて、ここまで読んでくれてお疲れさまでした。 あとは、世界で一番かわいいウチの猫の写真をお楽しみください。