RustでFUSE


これはRust Advent Calendar 2017 11日目の記事です。
今回はRustを用いてファイルシステムを作ることができる[rust-fuse](https://github.com/zargony/rust-fuse)を紹介します。

## FUSE（Filesystem in Userspace）とは？

FUSEは、通常カーネル空間で提供するファイルシステムの機能をユーザ空間上で定義し動作させることができるものです。
ユーザ空間とカーネル空間の橋渡しをするインターフェースとカーネル内での処理を提供しています。実装者はカーネルのコードをいじることなくファイルシステムを作成できます。

リファレンス実装は[libfuse](https://github.com/libfuse/libfuse)として提供されています。
FUSEを使ったファイルシステムとしては、[SSHFS](https://github.com/libfuse/sshfs)や[GlusterFS](https://www.gluster.org/)、[gitfs](https://www.presslabs.com/gitfs/)などがあります。

## rust-fuse

[rust-fuse](https://github.com/zargony/rust-fuse)はRustでFUSEを実装できることを目的としたcrateで、単純なlibfuseのバインディングではなくRustに最適化した形で機能を提供しています。

rust-fuseの内部実装について日本語で説明されているページが以下にあります。
興味ある方は見てみるとよいのではないでしょうか。

* http://akiradeveloper.hatenadiary.com/entry/2015/01/12/142641
* http://akiradeveloper.hatenadiary.com/entry/2015/01/12/161501

ちなみに現時点でのrust-fuseの安定版はFUSE2.6（FUSE Kernel ABI 7.8）ベースとなっており、Linuxでは2.9.1、macOS(OSXFUSE)では3.0（FUSE Kernel ABI 7.19）への対応をおこなっている最中のようです。

https://github.com/zargony/rust-fuse/pull/97

## 最小構成で動かしてみる

前置きが長くなりましたが、手始めに何もしないファイルシステムを作ってみましょう。

```TOML:Cargo.toml
[dependencies]
fuse = "0.3"
env_logger = "0.3"
```

[env_logger](https://crates.io/crates/env_logger)は必須ではありませんが、rust-fuseにもloggerでログが埋め込まれているので、`env_logger::init()`して環境変数`RUST_LOG`に`debug`あたりを渡しておくと動作確認が楽になります。

```Rust:src/main.rs
extern crate env_logger;
extern crate fuse;

use std::env;
use fuse::Filesystem;

struct EmptyFS;

impl Filesystem for EmptyFS {}

fn main() {
    env_logger::init().expect("fail logger init");
    let mountpoint = env::args_os().nth(1).expect("usage: emptyfs MOUNTPOINT");
    fuse::mount(EmptyFS, &mountpoint, &[]).expect("fail mount()");
}
```
何もない`fuse::Filesystem`トレイトを定義します。

`fsroot`ディレクトリをマウントポイントにして起動してみます。
なお、動作環境はArch Linuxを使っています。

```shell-session
$ cargo build --release
$ mkdir fsroot
$ RUST_LOG=debug ./target/release/emptyfs fsroot
INFO:fuse::session: Mounting fsroot
DEBUG:fuse::request: INIT(1)   kernel: ABI 7.26, flags 0x1ffffb, max readahead 131072
DEBUG:fuse::request: INIT(1) response: ABI 7.8, flags 0x1, max readahead 131072, max write 16777216
```

アンマウントは`fusermount`コマンドで行えます。

```shell-session
$ fusermount -u fsroot
```

```shell-session
$ RUST_LOG=debug ./target/release/emptyfs fsroot
    :
INFO:fuse::session: Unmounted /vagrant/emptyfs/fsroot
$
```

## 初期処理と終了処理
`init()`と`destroy()`を定義することで、初期処理と終了処理を行うことができます。

```Rust:src/main.rs
use std::os::raw::c_int;
use fuse::{Filesystem, Request};

struct EmptyFS;

impl Filesystem for EmptyFS {
    fn init(&mut self, _req: &Request) -> Result<(), c_int> {
        println!("my init");
        Ok(())
    }

    fn destroy(&mut self, _req: &Request) {
        println!("my destroy");
    }
}
```

「できます」と書きつつ、初期処理はうまく動いたのですが、`destroy()`はアンマウントしたりプロセス終了しただけでは呼ばれませんでした。
トリガをご存知の方教えてください。


## メモリFS

FUSEでできることを知るために、もう少し大きな例を見ていきましょう。
ここでは、FUSEプロセス(メモリ)上だけでファイル管理するメモリFSもどきを作ってみます。

ちなみに、[こちらのリポジトリ](https://github.com/hhatto/rust-snippets/tree/master/fuse)に今回のメモリFSもどきのコードを置いていますので、興味ある方はみてみてください。

### ディレクトリ参照
まずはルートディレクトリを`ls`コマンドで参照できるようにしてみましょう。

ファイルシステム用の構造体として`MemFS`を定義します。
iノード番号をキーに、 親のiノード番号とパス名、rust-fuseでファイル属性を定義する`fuse::FileAttr`にアクセスできるようにしておきます。（`inodes`）

```Rust:src/main.rs
struct MemFS {
    // <ino, (parent_ino, path, fileattr)>
    inodes: HashMap<u64, (u64, String, FileAttr)>,
}

impl Filesystem for MemFS {
    fn getattr(&mut self, _req: &Request, ino: u64, reply: ReplyAttr) {
        for (&inode, f) in self.inodes.iter() {
            if ino == inode {
                reply.attr(&TTL, &f.2);
                return;
            }
        }

        // 何も返すものがない場合はlibc::ENOENTを返しておく
        reply.error(ENOENT);
    }

    fn readdir(&mut self, _req: &Request, _ino: u64, _fh: u64, offset: i64, mut reply: ReplyDirectory) {
        // offset?
        // see also: https://github.com/libfuse/libfuse/blob/master/include/fuse.h#L535
        // readdirには2つのモードがあるらしく、offsetが 0 or non-0 でモードが分かれる。
        // ここではoffset==0の時のみ処理させる
        if offset == 0 {
            // 今は固定でカレントディレクトリと親ディレクトリの情報のみを返しておく
            reply.add(1, 0, FileType::Directory, ".");
            reply.add(2, 1, FileType::Directory, "..");
        }
        reply.ok();
    }
}
```

該当iノード番号のファイル属性を取得するために`getattr()`コールバックが呼ばれるのと、
ディレクトリ読み出しするために`readdir()`コールバックが呼ばれるので、それぞれ定義しておきます。

rust-fuseではコールバックに対する応答(`ReplyXX`)でファイルシステム上の情報を返すようになっています。
`getattr()`では`ReplyAttr.attr()`で該当ファイルのファイル属性を返します。
`readdir()`では`ReplyDirectory.add()`で該当エントリの情報を追加し`ReplyDirectory.ok()`で情報を確定します。

`reply.attr()`の第1引数TTLは`getattr()`の結果をキャッシュする時間を指定します。後ほど説明する`lookup()`にも同様の結果をキャッシュする仕組みがあります。

`MemFS`ではマウント前にルートディレクトリ用のデータを作成してからマウントします。

```Rust:src/main.rs
fn file_create(ino: u64, size: u64, ftype: FileType) -> FileAttr {
    let t = time::now().to_timespec();
    FileAttr {
        ino: ino,
        size: size,
        blocks: 0,
        atime: t,
        mtime: t,
        ctime: t,
        crtime: t,
        kind: ftype,
        perm: match ftype {
            FileType::Directory => 0o755,
            _ => 0o644,
        },
        nlink: 2,
        uid: 501,
        gid: 20,
        rdev: 0,
        flags: 0
    }
}

fn main() {
    env_logger::init().expect("fail logger init");
    let mountpoint = env::args_os().nth(1).expect("usage: memfs MOUNTPOINT");
    let mut inodes = HashMap::new();
    // ルートディレクトリを作っておく
    inodes.insert(0, ("/".to_string(), file_create(1, 0, FileType::Directory)));
    // マウント
    fuse::mount(MemFS{inodes: inodes}, &mountpoint, &[]).expect("fail mount()");
}
```

```shell-session
$ RUST_LOG=debug ./target/release/memfs fsroot
INFO:fuse::session: Mounting fsroot
DEBUG:fuse::request: INIT(1)   kernel: ABI 7.26, flags 0x1ffffb, max readahead 131072
DEBUG:fuse::request: INIT(1) response: ABI 7.8, flags 0x1, max readahead 131072, max write 16777216
```

`ls`コマンドを実行し参照できるか試してみます。

最初の何もしないEmptyFSの場合は

```shell-session
$ ls -al fsroot
ls: cannot access 'fsroot': Function not implemented
```

となってしまいますが、MemFSの場合は

```shell-session
$ ls -a fsroot
total 0
drwxr-xr-x 2     501 utmp      0 Dec 11 00:05 .
drwxr-xr-x 1 vagrant vagrant 306 Dec 10 22:49 ..
```
となり、無事ディレクトリが読み出せました。

### ファイル作成

次に`touch`コマンドを実行し、ファイルを作成できるようにしてみましょう。

ファイル作成時は、ファイルの存在をチェックする`lookup()`コールバックと、ファイル作成する`create()`コールバック、ファイル属性を設定する`setattr()`コールバックが呼ばれるのでこれらを定義しておきます。

```Rust:src/main.rs
impl Filesystem for MemFS {
    fn lookup(&mut self, _req: &Request, parent: u64, name: &OsStr, reply: ReplyEntry) {
        for (_, f) in self.inodes.iter() {
            if f.0 == parent && name.to_str().unwrap() == f.1.as_str() {
                reply.entry(&TTL, &f.2, 0);
                return;
            }
        }
        reply.error(ENOENT);
    }

    fn create(&mut self, _req: &Request, parent: u64, name: &OsStr, _mode: u32, _flag: u32, reply: ReplyCreate) {
        let inode = time::now().to_timespec().sec as u64;
        let f = file_create(inode, 0, FileType::RegularFile);
        self.inodes.insert(inode, (parent, name.to_str().unwrap().to_string(), f));
        reply.created(&TTL, &f, 0, 0, 0);
    }

    fn setattr(&mut self, _req: &Request, ino: u64, _mode: Option<u32>, _uid: Option<u32>,
               _gid: Option<u32>, _size: Option<u64>, _atime: Option<Timespec>,
               _mtime: Option<Timespec>, _fh: Option<u64>, _crtime: Option<Timespec>,
               _chgtime: Option<Timespec>, _bkuptime: Option<Timespec>, _flags: Option<u32>,
               reply: ReplyAttr) {
        match self.inodes.get(&ino) {
            Some(f) => reply.attr(&TTL, &f.2),
            None => reply.error(EACCES),
        }
    }
}
```

```shell-session
$ touch fsroot/foo  # 変更前
touch: cannot touch 'fsroot/foo': Function not implemented
$

$ touch fsroot/foo  # 変更後
$
```
ファイル作成ができましたと言いたいところですが、このままだと`ls`コマンドでファイル自体を確認することができません。
`readdir()`を変更して作成したファイルが見えるようにしてみます。

```Rust:src/main.rs
impl Filesystem for MemFS {
    fn readdir(&mut self, _req: &Request, ino: u64, _fh: u64, offset: i64, mut reply: ReplyDirectory) {
        if offset > 0 {
            reply.ok();
            return;
        }

        reply.add(1, 0, FileType::Directory, ".");
        reply.add(2, 1, FileType::Directory, "..");
        let mut reply_add_offset = 2;
        for (_, f) in self.inodes.iter() {
            if ino == f.0 {
                let attr = f.2;
                let name = &f.1;
                reply.add(attr.ino, reply_add_offset, attr.kind, name);
                reply_add_offset += 1;
            }
        }
        reply.ok();
    }
}
```

```shell-session
$ touch fsroot/foo
$ ls fsroot -al
total 0
drwxr-xr-x 2     501 utmp      0 Dec 11 13:35 .
drwxr-xr-x 1 vagrant vagrant 306 Dec 11 02:49 ..
-rw-r--r-- 1     501 utmp      0 Dec 11 13:35 foo
```

無事、作成したファイルが参照できました。

### ファイルの読み書き

続いて、ファイルの読み書きをできるようにしてみます。
`echo`コマンドリダイレクトでファイル書き込み、そのファイルを`cat`コマンドで読み出してみます。

```shell-session
$ echo "hogehoge" > fsroot/bar
-bash: echo: write error: Function not implemented
$ cat fsroot/bar
cat: fsroot/bar: Function not implemented
```

ファイル内のデータを格納するエリアとしてMemFSに`datas`メンバを追加します。
iノード番号をキーにデータを保持します。雑に`String`にしています。

```Rust:src/main.rs
struct MemFS {
    // <ino, (parent_ino, path, fileattr)>
    inodes: HashMap<u64, (u64, String, FileAttr)>,

    // <ino, file_data>
    datas: HashMap<u64, String>,
}
```

ファイル読み書き時のコールバック関数は`write()`と`read()`です。
以下のように定義します。

```Rust:src/main.rs
impl Filesystem for MemFS {

    fn write(&mut self, _req: &Request, ino: u64, _fh: u64, _offset: i64, data: &[u8], _flags: u32, reply: ReplyWrite) {
        // 書き込み対象のデータ長を保持
        let length: usize = data.len();

        // Stringにしてdatasに格納する
        let x = String::from_utf8(data.to_vec()).expect("fail to-string");
        self.datas.insert(ino, x);
        if let Some(f) = self.inodes.get_mut(&ino) {
            let parent_ino = f.0;
            let name = f.1.clone();
            *f = (parent_ino, name, file_create(ino, length as u64, FileType::RegularFile));
        }

        // 書き込みサイズを伝える
        reply.written(length as u32);
    }

    fn read(&mut self, _req: &Request, ino: u64, _fh: u64, _offset: i64, _size: u32, reply: ReplyData) {
        match self.datas.get(&ino) {
            Some(x) => reply.data(x.as_bytes()),    // 読み出しデータ内容を伝える
            None => reply.error(EACCES),            // 雑にlibc::EACCES返す
        }
    }
}
```

実行結果は以下の通りです。

```shell-session
$ echo "hogehoge" > fsroot/bar
$ cat fsroot/bar
hogehoge
$ ls -la fsroot/
total 0
drwxr-xr-x 2     501 utmp      0 Dec 11 20:55 .
drwxr-xr-x 1 vagrant vagrant 306 Dec 11 02:49 ..
-rw-r--r-- 1     501 utmp      9 Dec 11 20:55 bar
$ ls -la fsroot/bar
-rw-r--r-- 1 501 utmp 9 Dec 11 20:55 fsroot/bar
$
```

ファイルを読み書きできました。

## その他のコールバック関数

今回紹介しなかった主なコールバック関数を紹介します。

| コールバック関数 | 概要                                    |
|:-----------------|:----------------------------------------|
| symlink()        | シンボリックリンク作成時(ln -s等)       |
| link()           | ハードリンク(ln等)                      |
| unlink()         | 削除(rm等)                              |
| mkdir()          | ディレクトリ作成(mkdir等)               |
| rmdir()          | ディレクトリ削除(rmdir等)               |
| rename()         | リネーム(mv等)                          |

詳しくは[rust-fuseのドキュメント](https://docs.rs/fuse/0.3.1/fuse/trait.Filesystem.html)を参照するとよいでしょう。

## Rustによるファイルシステム実装あれこれ

RustによるFUSEなプロジェクトは以下のようなものがあります。

* [anowell/netfuse](https://github.com/anowell/netfuse) rust-fuseを使ったネットワークバックエンドなファイルシステム
* [kahing/catfs](https://github.com/kahing/catfs) 適度にキャッシュしてくれるファイルシステム？
* [wfraser/fuse-mt](https://github.com/wfraser/fuse-mt) マルチスレッドなrust-fuse

## おわりに

かなり早足でしたがRustでFUSEをさわる手順を説明しました。
みなさんもRustで自分のファイルシステムを実装してみてはいかがでしょうか？