Rustで学ぶFUSE (2) ファイルの書き込み

# ReadWrite
RustでFUSEを利用したファイルシステムを作成する記事の2回目です。
最新のソースコードは[こちら](https://github.com/narumatt/sqlitefs)にあります。

## 記事一覧
[Rustで学ぶFUSE (1) リードオンリーなファイルシステムの実装](https://qiita.com/narumatt/items/2dfc3aedf3aafd459e81)
**Rustで学ぶFUSE (2) ファイルの書き込み**
[Rustで学ぶFUSE (3) ファイル作成と削除](https://qiita.com/narumatt/items/ad2d9227710dbeb600be)
[Rustで学ぶFUSE (4) ディレクトリ作成と削除](https://qiita.com/narumatt/items/bec85dd5218a85a089b8)
[Rustで学ぶFUSE (5) 名前の変更と移動](https://qiita.com/narumatt/items/293a1c1f30b1aa9a8270)

## 概要
[前回](https://qiita.com/narumatt/items/2dfc3aedf3aafd459e81)は、ファイルの読み込みができるファイルシステムを作成しました。
今回は、それに加えてファイルの書き込みができるようにします。

必要なのは以下の関数です。
ファイルサイズの変更を受け付ける必要があるので、今回の目的である `write` だけでなく、ファイルのメタデータを変更する `setattr` も実装する必要があります。

```rust
fn write(&mut self, _req: &Request<'_>, _ino: u64, _fh: u64, _offset: i64, _data: &[u8], _flags: u32, reply: ReplyWrite) {
    ...
}

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) {
    ...
}
```

なお、以下では実装する関数と同名のシステムコールと区別をつけるために、 システムコールは `write(2)` のように、末尾に`(2)`を付けた表記をします。

## DB関数
今回追加したDB側の関数は以下になります。

```rust
    /// inodeのメタデータを更新する。ファイルサイズが縮小する場合はtruncateをtrueにする
    fn update_inode(&self, attr: DBFileAttr, truncate: bool) -> Result<(), Error>;

    /// 1ブロック(4096byte)分のデータを書き込む
    fn write_data(&self, inode:u32, block: u32, data: &[u8], size: u32) -> Result<(), Error>;
```

## write

```rust
fn write(
    &mut self,
    _req: &Request<'_>,
    ino: u64,
    fh: u64,
    offset: i64,
    data: &[u8],
    flags: u32,
    reply: ReplyWrite
);
```

ファイルにデータを書き込むための関数です。
引数の `ino` で指定されたinode番号を持つファイルに、引数の `data` で渡ってきたデータを書き込みます。書き込み位置は引数の `offset`で指示されます。 

C言語で `write(2)` のようなシステムコールを使う場合はファイルオフセットが今どこを指しているかを意識する必要がありますが、FUSEではカーネルがオフセットの管理をしてくれていて、常に引数でオフセットが指示されるので、 `pwrite(2)` 相当の関数を一つ実装するだけで済むようになっています。

マウントオプション `direct_io` を使用していない場合(通常はこちら)、エラーを返す場合を除いて、`write` 関数は 渡されたデータのサイズと同じ数字を引数の`reply` に入れて返さないといけません。つまり、何らかの事情で渡されたデータをすべて書き込めなかった場合、エラーにする必要があります。
マウントオプション`direct_io`を使用している場合は、途中までしか書き込めなかった場合でも実際に書き込んだバイト数を返します。

引数の `fh` はファイルの `open` 時にファイルシステムが指定した値です。今回はまだ`open`を実装していないので、常に0になります。

引数の `flags` は書き込み時のフラグです。以下の2種類が設定される可能性があります。

1. FUSE_WRITE_CACHE :
    ページキャッシュからの書き込みです。引数の`fh` や、`req`が正しくない可能性があります。
2. FUSE_WRITE_LOCKOWNER: `lock_owner` 引数が正しい事を示しています…が、`fuse-rs` ではこの引数が実装されていません。

ちなみに、`cp`コマンドなどでファイルを上書きすると頭から後ろまで順番に`write` 関数が実行されますが、アプリケーションによってはファイルの一部分だけ更新する、という処理や、ばらばらの順番で書き込む、という処理はよく発生します。書き込みをキャッシュしたりしている場合は気をつけてください。

### O_APPEND
ファイルを `open` した時のフラグに `O_APPEND` が設定されている場合は適切に処理しなければなりません。まだ `open` を実装していませんが、説明しておきます。

ライトバックキャッシュが有効か無効かの場合で動作が異なります。マウントオプションに `-o writeback` がある場合、ライトバックキャッシュが有効になっています。

**ライトバックキャッシュが無効の時、**ファイルシステムは `O_APPEND` を検知して、全ての `write` の中で `offset` の値にかかわらずデータがファイル末尾に追記されるようにチェックします。
**ライトバックキャッシュが有効の時、** `offset` はカーネルが適切に設定してくれます。 `O_APPEND` は無視してください。

(今のところ)カーネルは`O_APPEND`が設定されていても `offset` をきちんと設定してくれるようです。なので、現状は `O_APPEND` は無視し、 `open` 実装時に対応します。
ただし、ネットワーク上のストレージを利用しているファイルシステムなどで複数のマシンから書き込みがあった場合、カーネルの認知しているファイル末尾と実際のファイル末尾がずれるので、問題が発生します。
こういった問題が発生しうるファイルシステムを作る場合は、`O_APPEND`に適切に対処する必要があります。

### ここまでのコード
```rust
fn write(&mut self, _req: &Request<'_>, ino: u64, _fh: u64, offset: i64, data: &[u8], flags: u32, reply: ReplyWrite) {
    let block_size = self.db.get_db_block_size();
    let ino = ino as u32;
    let size = data.len() as u32;
    let offset = offset as u32;
    let start_block = offset / block_size + 1;
    let end_block = (offset + size - 1) / block_size + 1;
    // 各ブロックに書き込む
    for i in start_block..=end_block {
        let mut block_data: Vec<u8> = Vec::with_capacity(block_size as usize);
        let b_start_index = if i == start_block {offset % block_size} else {0};
        let b_end_index = if i == end_block {(offset+size-1) % block_size +1} else {block_size};
        let data_offset = (i - start_block) * block_size;

        // 書き込みがブロック全体に及ばない場合、一度ブロックのデータを読み込んで隙間を埋める
        if (b_start_index != 0) || (b_end_index != block_size) {
            let mut data_pre = match self.db.get_data(ino, i, block_size) {
                Ok(n) => n,
                Err(err) => {reply.error(ENOENT); debug!("{}", err); return;}
            };
            if data_pre.len() < block_size as usize {
                data_pre.resize(block_size as usize, 0);
            }
            if b_start_index != 0 {
                block_data.extend_from_slice(&data_pre[0..b_start_index as usize]);
            }
            block_data.extend_from_slice(&data[data_offset as usize..(data_offset + b_end_index - b_start_index) as usize]);
            if b_end_index != block_size {
                block_data.extend_from_slice(&data_pre[b_end_index as usize..block_size as usize]);
            }
        } else {
            block_data.extend_from_slice(&data[data_offset as usize..(data_offset + block_size) as usize]);
        }
        // ここで書き込む。必要な場合はwrite_data内でファイルサイズの更新が行われる
        match self.db.write_data(ino, i, &block_data, (i-1) * block_size + b_end_index) {
            Ok(n) => n,
            Err(err) => {reply.error(ENOENT); debug!("{}", err); return;}
        }
    }
    // 書き込み後のサイズをreplyに入れる
    reply.written(size);
}
```

### マウントオプション
`main.rs` のReadOnlyのマウントオプションを削除します。

```rust
let options = ["-o", "fsname=sqlitefs"]
    .iter()
    .map(|o| o.as_ref())
    .collect::<Vec<&OsStr>>();
```

### 実行結果
現状ではファイルの新規作成ができないので、前回作成したデータベースファイルを使って、 `hello.txt` の末尾にデータを追記します。

- コマンド

```bash

# hello.txtに追記する
$ echo "append" >> ~/mount/hello.txt
# 追記された内容の確認
$ cat ~/mount/hello.txt
Hello world!
append
```

- FUSEログ

`write` 関数が呼ばれている事が分かります。

```
[2019-10-28T11:52:08Z DEBUG fuse::request] INIT(2)   kernel: ABI 7.31, flags 0x3fffffb, max readahead 131072
[2019-10-28T11:52:08Z DEBUG fuse::request] INIT(2) response: ABI 7.8, flags 0x1, max readahead 131072, max write 16777216
[2019-10-28T11:52:14Z DEBUG fuse::request] LOOKUP(4) parent 0x0000000000000001, name "hello.txt"
[2019-10-28T11:52:14Z DEBUG fuse::request] OPEN(6) ino 0x0000000000000002, flags 0x8401
[2019-10-28T11:52:14Z DEBUG fuse::request] FLUSH(8) ino 0x0000000000000002, fh 0, lock owner 9742156966771960265
[2019-10-28T11:52:14Z DEBUG fuse::request] GETXATTR(10) ino 0x0000000000000002, name "security.capability", size 0
[2019-10-28T11:52:14Z DEBUG fuse::request] WRITE(12) ino 0x0000000000000002, fh 0, offset 13, size 7, flags 0x0
[2019-10-28T11:52:14Z DEBUG fuse::request] RELEASE(14) ino 0x0000000000000002, fh 0, flags 0x8401, release flags 0x0, lock owner 0
```


## read, write時のメタデータの更新
一般的にファイルを `read` した時にはメタデータの `atime` を更新します。
また、 `write` した時には、 `size` (必要な場合) `mtime` `ctime` の3つを更新します。
DB関数側でこれらを更新できるようにしておきます。

今回はメタデータもデータもデータベース上にあり、元々パフォーマンスが悪い事が予想されるのであまり意識していませんが、メタデータの更新にある程度コストがかかる場合、ファイルサイズとタイムスタンプはメモリ上にキャッシュした方がいいです。
キャッシュを書き込むのは `flush` が呼ばれたタイミングです。

マウントオプションで `-o noatime` が指定された場合、 `atime` の更新は行いません。後々マウントオプションを追加する時に対応します。

### ファイルサイズ
`write` 時に書き込まれたデータの末尾が、既存のファイルサイズより大きくなる場合は、ファイルサイズを更新する必要があります。
また、書き込みのオフセットに現在のファイルサイズより大きい値が指定された場合、ファイルに何も書かれていない穴ができます。このエリアのデータが読まれた場合、ファイルシステムは0(NULLバイト)の列を返す必要があります。0埋めしたデータを書き込むか、そもそもその部分のデータを作らずに、読み込み時に検出して0のデータを返すようにします。
後者の方法はスパースファイルといい、見かけ上のファイルサイズより実ファイルサイズを小さくする事ができます。

ちなみに、ファイルのブロックをばらばらの順番で書き込む、というのはよくある処理なので、0バイト目から順番に書き込まれていく事は期待しないでください。特に `write` 実行時にファイルサイズを変更する場合は、現在のファイルサイズと比較して小さくならないように気をつけてください。  

### タイムスタンプ
各関数とどのタイムスタンプを更新すべきかの対応表です。
[Linuxプログラミングインターフェース](https://www.oreilly.co.jp/books/9784873115856/)のシステムコールとタイムスタンプの対応表を参考に、FUSEの関数にマップしました。
左側の `a, m, c` が操作対象のファイルまたはディレクトリのタイムスタンプ、右側の `a, m, c` は親ディレクトリのタイムスタンプです。

|関数名|a|m|c|親a|親m|親c|備考|
|---|---|---|---|---|---|---|---|
|setattr| | |o| | | | |
|setattr(*)| |o|o| | | | * ファイルサイズが変わる場合 |
|link| | |o| |o|o| |
|mkdir|o|o|o| |o|o| |
|mknod|o|o|o| |o|o| |
|create|o|o|o| |o|o| |
|read|o| | | | | | |
|readdir|o| | | | | | |
|setxattr| | |o| | | | |
|removexattr| | |o| | | | |
|rename| | |o| |o|o|移動前/移動後の両方の親ディレクトリを変更|
|rmdir| | | | |o|o| |
|symlink|o|o|o| |o|o|リンク自体のタイムスタンプで、リンク先は変更しない|
|unlink| | |o| |o|o|参照カウントが2以上でinode自体が消えない場合、ファイルのctimeを更新|
|write| |o|o| | | | |

## setattr

```rust
fn setattr(
    &mut self,
    _req: &Request<'_>,
    ino: u64, // 更新対象のinode番号
    mode: Option<u32>, // アクセス権
    uid: Option<u32>, // ファイル所有者のUID
    gid: Option<u32>, // ファイル所有グループのUID
    size: Option<u64>, // ファイルサイズ
    atime: Option<Timespec>, // 最終アクセス時刻
    mtime: Option<Timespec>, // 最終更新時刻
    _fh: Option<u64>,
    crtime: Option<Timespec>, // mac用
    chgtime: Option<Timespec>, // mac用
    bkuptime: Option<Timespec>, // mac用
    flags: Option<u32>, // mac用
    reply: ReplyAttr
);
```

`write` は実装しましたが、このままでは追記しかできません。テキストエディタでファイルを開いて編集したりすると問題が起こります。多くのプログラムでは、ファイルの更新は一度ディスク上のファイルの内容を消して、メモリ上のデータをすべて書き込む…といった処理をしているからです。
ファイルを丸ごと更新するために、ファイルサイズを0にする(truncateに相当) 処理を実装します。

fuse-rsでは、 `setattr` を実装する事でファイルサイズの変更が可能になります。今回は、ついでに `setattr` で変更できる全てのメタデータを変更できるようにします。

一般的なファイルのメタデータを変更するシステムコールのほかに、 `truncate(2)` でファイルサイズを変更する時と、 `open(2)` で `O_TRUNC` を指定した時も、この関数が呼ばれます。

`setattr` は各引数に `Option` 型で値が指定されるので、中身がある場合はその値で更新していきます。引数の`reply` に返事として入れる値は、更新後のメタデータです。

なお、 `ctime` は 現在のfuse-rsがプロトコルのバージョンの問題で未対応なので、引数には入っていません。基本的に `ctime` は自由に設定する事ができず、 `setattr` を実行すると現在時刻になるはずなので、問題はありません。

`open` 時に `O_TRUNC` を指定した場合のように、ファイルサイズに0が指定された場合は既存のデータを全て破棄すればいいですが、`truncate(2)` で元のファイルサイズより小さい0以外の値が指定された場合、残すべきデータは残しつついらないデータがきちんと破棄されるように気をつけてください。
また、元のファイルサイズより大きい値が指定された場合、間のデータが0(\0)で埋められるようにしてください。

残りの引数としてmacOS用に `chgtime` と `bkuptime` が引数にありますが、今回はスルーします。設定しなくてもとりあえず動作はします。

```rust
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
) {
    // 現在のメタデータを取得
    let mut attr = match self.db.get_inode(ino as u32) {
        Ok(n) => n,
        Err(err) => {reply.error(ENOENT); debug!("{}", err); return;}
    };
    // ファイルサイズの変更チェック(小さくなる場合、不要なデータを破棄するため)
    let old_size = attr.size;
    // 引数で上書き
    if let Some(n) = mode {attr.perm = n as u16};
    if let Some(n) = uid {attr.uid = n};
    if let Some(n) = gid {attr.gid = n}; 
    if let Some(n) = size {attr.size = n as u32};
    if let Some(n) = atime {attr.atime = datetime_from_timespec(&n)};
    if let Some(n) = mtime {attr.mtime = datetime_from_timespec(&n)};
    if let Some(n) = crtime {attr.crtime = datetime_from_timespec(&n)};
    if let Some(n) = flags {attr.flags = n};
    // 更新
    match self.db.update_inode(attr, old_size > attr.size) {
        Ok(_n) => (),
        Err(err) => {reply.error(ENOENT); debug!("{}", err); return;}
    };
    // 更新後のメタデータをreplyに入れる
    reply.attr(&ONE_SEC, &attr.get_file_attr());
}
```

実行結果は以下のようになります。
`helllo.txt` を新しい内容で上書きしています。

```bash

# hello.txt を上書きする
$ echo "Update hello world" > ~/mount/hello.txt
# 内容の確認
$ cat ~/mount/hello.txt
Update hello world
```

FUSEのログです。`write`の前に、 `setattr` が呼ばれている事が分かります。

```
[2019-10-28T12:08:10Z DEBUG fuse::request] INIT(2)   kernel: ABI 7.31, flags 0x3fffffb, max readahead 131072
[2019-10-28T12:08:10Z DEBUG fuse::request] INIT(2) response: ABI 7.8, flags 0x1, max readahead 131072, max write 16777216
[2019-10-28T12:08:37Z DEBUG fuse::request] LOOKUP(4) parent 0x0000000000000001, name "hello.txt"
[2019-10-28T12:08:37Z DEBUG fuse::request] OPEN(6) ino 0x0000000000000002, flags 0x8001
[2019-10-28T12:08:37Z DEBUG fuse::request] GETXATTR(8) ino 0x0000000000000002, name "security.capability", size 0
[2019-10-28T12:08:37Z DEBUG fuse::request] SETATTR(10) ino 0x0000000000000002, valid 0x208
[2019-10-28T12:08:37Z DEBUG fuse::request] FLUSH(12) ino 0x0000000000000002, fh 0, lock owner 17171727478964840688
[2019-10-28T12:08:37Z DEBUG fuse::request] WRITE(14) ino 0x0000000000000002, fh 0, offset 0, size 19, flags 0x0
[2019-10-28T12:08:37Z DEBUG fuse::request] RELEASE(16) ino 0x0000000000000002, fh 0, flags 0x8001, release flags 0x0, lock owner 0

```

## まとめ
これでファイルの書き込み(追記、上書き)ができるようになりました。
`write`は特にデータの破損やパフォーマンスの低下が起こりやすい関数なので、仕様通りに動作するか注意してください。

余談ですが、通常のファイルシステムであれば、ファイルの固定長ヘッダーをちょこちょこ変更する、という処理はファイルが何GBあろうと低コストで行えるので画像ファイルなどでよく行われますが、FUSEでネットワーク上のストレージを利用している場合、ファイル全てを更新する方法しかなくてパフォーマンスでストレスがマッハになる事があります。

ここまでのコードは [github](https://github.com/narumatt/sqlitefs/tree/readwrite)に置いてあります。
次回は、ファイルの作成と削除を実装します。

**次の記事**
[Rustで学ぶFUSE (3) ファイル作成と削除](https://qiita.com/narumatt/items/ad2d9227710dbeb600be)