Mac の Go で birth time を取得する

[Gopher道場 Advent Calendar 2018](https://qiita.com/advent-calendar/2018/gopherdojo) の 21 日目のエントリです。
昨日は [yuuyamad](https://qiita.com/yuuyamad) さんによる『[今から学ぶgRPCの基礎 - Qiita](https://qiita.com/yuuyamad/items/bbbd1782150d7958ff11)』でした。そして、[塊魂アンコール](https://ec.nintendo.com/JP/ja/titles/70010000014833)の発売日でした。4 時間転がしました。

# はじめに

普段写真を管理する際に『ファイルを birth time でリネームする』といったことをしておりまして、この処理を今までは Ruby で行っていたのですが、今回ネタ作りのため Go で書き直してみました。( [hioki-daichi/birthtime-rename](https://github.com/hioki-daichi/birthtime-rename) )

その過程で得た birth time 周りの知見をつらつらと書いていこうと思います。

# そもそも birth time とは

birth time とは、inode ( [inode - Wikipedia](https://ja.wikipedia.org/wiki/Inode) ) が生成された時刻のことです。

例えば以下のようなファイル `foo` が存在する場合に

```shell-session
$ cat <<EOF > foo
foo
EOF
```

Mac では `stat -f %B <file>` で指定したファイルの birth time をエポック秒で表示できます。

```shell-session
$ stat -f %B foo
1545216600
```

また、`%B` 以外に指定可能な時刻情報の format としては以下の `%a`, `%m`, `%c` が存在します。

| format | 説明 |
|---|---|
| %a | 最終参照時刻 |
| %m | 最終ファイル更新時刻 |
| %c | 最終 inode 更新時刻 |

# 時刻情報を追ってみる

先程のファイル `foo` を使い、これらの時刻情報が具体的に何を行った時に更新されるのか、いくつか試して探ってみましょう。
※ 確認用として以下の Ruby のコードを使用します。内容としては `stat -f '%a %m %c %B' foo` の結果をわかりやすく表示しているだけです。

```rb
pp [:access, :modify, :change, :birth].zip(`stat -f '%a %m %c %B' foo`.chomp.split.map(&:to_i).map(&Time.method(:at)))
```

さて、ファイル作成直後は `:access`, `:modify`, `:change`, `:birth` のすべてで(ほぼ)同じ時刻が表示されています。

```shell-session
$ ruby -rpp -e "pp [:access, :modify, :change, :birth].zip(\`stat -f '%a %m %c %B' foo\`.chomp.split.map(&:to_i).map(&Time.method(:at)))"
[[:access, 2018-12-19 19:50:01 +0900],
 [:modify, 2018-12-19 19:50:00 +0900],
 [:change, 2018-12-19 19:50:00 +0900],
 [:birth, 2018-12-19 19:50:00 +0900]]
```

`cat` を実行すると `:access` の時刻が更新されました。

```shell-session
$ cat foo
foo

$ ruby -rpp -e "pp [:access, :modify, :change, :birth].zip(\`stat -f '%a %m %c %B' foo\`.chomp.split.map(&:to_i).map(&Time.method(:at)))"
[[:access, 2018-12-19 19:51:16 +0900],
 [:modify, 2018-12-19 19:50:00 +0900],
 [:change, 2018-12-19 19:50:00 +0900],
 [:birth, 2018-12-19 19:50:00 +0900]]
```

`chmod` を実行すると `:access` と `:change` の時刻が更新されました。

```shell-session
$ chmod 600 foo

$ ruby -rpp -e "pp [:access, :modify, :change, :birth].zip(\`stat -f '%a %m %c %B' foo\`.chomp.split.map(&:to_i).map(&Time.method(:at)))"
[[:access, 2018-12-19 19:51:35 +0900],
 [:modify, 2018-12-19 19:50:00 +0900],
 [:change, 2018-12-19 19:51:35 +0900],
 [:birth, 2018-12-19 19:50:00 +0900]]
```

ファイルに追記すると `:access`, `:modify`, `:change` の時刻が更新されました。

```shell-session
$ echo bar >> foo

$ ruby -rpp -e "pp [:access, :modify, :change, :birth].zip(\`stat -f '%a %m %c %B' foo\`.chomp.split.map(&:to_i).map(&Time.method(:at)))"
[[:access, 2018-12-19 19:52:10 +0900],
 [:modify, 2018-12-19 19:52:09 +0900],
 [:change, 2018-12-19 19:52:09 +0900],
 [:birth, 2018-12-19 19:50:00 +0900]]
```

Vim で保存すると `:access`, `:modify`, `:change`, `:birth` すべての時刻が・・・

```shell-session
$ # Neovim でファイル foo を開いた後すぐに保存して閉じています
$ nvim -c ":wq" foo

$ ruby -rpp -e "pp [:access, :modify, :change, :birth].zip(\`stat -f '%a %m %c %B' foo\`.chomp.split.map(&:to_i).map(&Time.method(:at)))"
[[:access, 2018-12-19 19:53:17 +0900],
 [:modify, 2018-12-19 19:53:17 +0900],
 [:change, 2018-12-19 19:53:17 +0900],
 [:birth, 2018-12-19 19:53:17 +0900]]
```

一見更新されたように確認用コードの都合上見えてしまうのですがそうではなく、実はファイル自体が変わっていまして

```shell-session
$ # -i オプションで inode 番号が表示されます。
$ ls -i foo
82280706 foo

$ nvim -c ":wq" foo

$ ls -i foo
82280774 foo
```

Vim 側で `set backup` と `set backupcopy=no` が設定されているとこの挙動になります。

試しに `~/.vimrc` に `set backup` と `set backupcopy=no` のみ設定した状態の Vim で同様の手順を行ってみると、以下のように `foo` が `foo~` にリネームされていることがわかります。

```shell-session
$ rm foo*; touch foo; ls -li foo*
82280992 -rw-r--r--  1 daichi  staff  0 12 19 19:59 foo

$ echo "set backupcopy=no\nset backup" > ~/.vimrc; \vim -c ":wq" foo; ls -li foo*
82281006 -rw-r--r--  1 daichi  staff  0 12 19 19:59 foo
82280992 -rw-r--r--  1 daichi  staff  0 12 19 19:59 foo~
```

`backupcopy` のヘルプは以下です。`set backupcopy=no` だと書き込んだ内容は新しいファイル `foo` に書き出されるようです。

> https://vim-jp.org/vimdoc-ja/options.html#'backupcopy'
>
```
'backupcopy' 'bkc'      文字列  (UnixでのViの既定値: "yes"、それ以外: "auto")
...(省略)...
        "yes"   先にファイルのコピーを作ってバックアップにして、更新した内容は
                元のファイルに上書きする
        "no"    先に元のファイルをリネームしてバックアップにして、更新した内容
                は新しいファイルに書き出す
...(省略)...
```

以上、ファイルの時刻情報が更新される様子でした。

# Mac 以外では・・ ?

Mac 以外でも birth time が取れるのか、手元に DSM という Synology 社の NAS 用 OS 『DiskStation Manager』 (Version: 6.2) があるためこちらを使って調べてみます。

DSM 上で `stat` を実行してみたところ、肝心の birth time は取得できませんでした。

```shell-session
root@ds216j:~# touch foo
root@ds216j:~# stat foo
  File: ‘foo’
  Size: 0               Blocks: 0          IO Block: 4096   regular empty file
Device: 900h/2304d      Inode: 27204       Links: 1
Access: (0644/-rw-r--r--)  Uid: (    0/    root)   Gid: (    0/    root)
Access: 2018-12-19 20:06:03.228951761 +0900
Modify: 2018-12-19 20:06:03.228951761 +0900
Change: 2018-12-19 20:06:03.228951761 +0900
 Birth: -
```

代わりに `debugfs` を使うことで `crtime` を取得することはできました。`crtime` というのはファイルの作成時刻だそうです。

```shell-session
root@ds216j:~# ls -i foo
27204 foo

root@ds216j:~# df -T .
Filesystem     Type 1K-blocks   Used Available Use% Mounted on
/dev/md0       ext4   2385528 877428   1389316  39% /

root@ds216j:~# debugfs -R "stat <27204>" /dev/md0
debugfs 1.42.6 (21-Sep-2012)
Inode: 27204   Type: regular    Mode:  0644   Flags: 0x80000
Generation: 3079282565    Version: 0x00000000:00000001
User:     0   Group:     0   Size: 0
File ACL: 0    Directory ACL: 0
Links: 1   Blockcount: 0
Fragment:  Address: 0    Number: 0    Size: 0
 ctime: 0x5c1a261b:36961b44 -- Wed Dec 19 20:06:03 2018
 atime: 0x5c1a261b:36961b44 -- Wed Dec 19 20:06:03 2018
 mtime: 0x5c1a261b:36961b44 -- Wed Dec 19 20:06:03 2018
crtime: 0x5c1a261b:0da586d1 -- Wed Dec 19 20:06:03 2018
Size of extra inode fields: 36
Extended attributes stored in inode body:
  archive_version = "52 25 01 00 01 00 00 00 " (8)
EXTENTS:
```

# Go でどうやるの

本題です。まず結論からですが、Mac の Go では以下のようにして birth time を取得できます。

```shell-session
$ gore -autoimport
gore version 0.3.0  :help for help

gore> fi, _err := os.Stat("foo")
&os.fileStat{name:"foo", size:0, mode:0x1a4, modTime:time.Time{wall:0x0, ext:63680814540, loc:(*time.Location)(0x1165d00)}, sys:syscall.Stat_t{Dev:16777220, Mode:0x81a4, Nlink:0x1, Ino:0x4e78498, Uid:0x1f5, Gid:0x14, Rdev:0, Pad_cgo_0:[4]uint8{0x0, 0x0, 0x0, 0x0}, Atimespec:syscall.Timespec{Sec:1545217740, Nsec:0}, Mtimespec:syscall.Timespec{Sec:1545217740, Nsec:0}, Ctimespec:syscall.Timespec{Sec:1545217740, Nsec:0}, Birthtimespec:syscall.Timespec{Sec:1545217740, Nsec:0}, Size:0, Blocks:0, Blksize:4096, Flags:0x0, Gen:0x0, Lspare:0, Qspare:[2]int64{0, 0}}}
<nil>

gore> fi.Sys().(*syscall.Stat_t).Birthtimespec.Sec
1545217740
```

では、詳細を見ていきます。まずは上記で一行になっている `os.Stat` の結果を整形して、時刻に関係ありそうな箇所だけ抜き出してみると、

```go
&os.fileStat{
// ...(省略)...
  sys:syscall.Stat_t{
// ...(省略)...
    Atimespec:     syscall.Timespec{Sec:1545217740, Nsec:0},
    Mtimespec:     syscall.Timespec{Sec:1545217740, Nsec:0},
    Ctimespec:     syscall.Timespec{Sec:1545217740, Nsec:0},
    Birthtimespec: syscall.Timespec{Sec:1545217740, Nsec:0},
// ...(省略)...
	}
}
```

`Birthtimespec` というそれらしいフィールドが存在することがわかりました。

ちなみに Ubuntu:16.04 では birth time 関連のフィールドは存在しませんでした。また、`Atimespec` に対応するフィールドが `Atim` になっていたりとフィールド名が若干異なっていました。

```go
//
// Ubuntu:16.04 での様子
//
&os.fileStat{
// ...(省略)...
  sys:syscall.Stat_t{
// ...(省略)...
    Atim:syscall.Timespec{Sec:1545217952, Nsec:0},
    Mtim:syscall.Timespec{Sec:1545217952, Nsec:0},
    Ctim:syscall.Timespec{Sec:1545217952, Nsec:0},
// ...(省略)...
  }
}
```

さて、Mac に戻ります。

`Birthtimespec` にアクセスするにはまずは `sys` にアクセスできなければなりません。

`sys` へは `os` パッケージの `Sys()` 関数でアクセスできますが、

> https://github.com/golang/go/blob/go1.11.4/src/os/types_unix.go#L27
>
> ```go
func (fs *fileStat) Sys() interface{}   { return &fs.sys }
```

上記の通り `Sys()` は `interface{}` を返すため、以下のようなキャストが必要です。(参考: [go1.11.4/src/syscall/ztypes_darwin_amd64.go#L65-L85](https://github.com/golang/go/blob/go1.11.4/src/syscall/ztypes_darwin_amd64.go#L65-L85))

```go
fi.Sys().(*syscall.Stat_t)
```

キャスト後は単純にフィールドにアクセスしていけば目的の birth time が取得できます :confetti_ball:

```go
gore> fi.Sys().(*syscall.Stat_t).Birthtimespec
syscall.Timespec{Sec:1545217740, Nsec:0}

gore> fi.Sys().(*syscall.Stat_t).Birthtimespec.Sec
1545217740
```

ちなみに、今回取得したエポック秒の birth time を `time.Time` に変換し、

```go
gore> ts := fi.Sys().(*syscall.Stat_t).Birthtimespec
syscall.Timespec{Sec:1545217740, Nsec:0}

gore> t := time.Unix(ts.Sec, ts.Nsec)
time.Time{wall:0x0, ext:63680814540, loc:(*time.Location)(0x1165d00)}
```

さらに `Format` すると以下のような人間に優しい文字列に変換できたりします。

```go
gore> t.Format("2006-01-02-15-04-05")
"2018-12-19-20-09-00"
```

# まとめ

いかがでしたでしょうか ? 自分にとっては初めて知ったことが多く楽しかったです。そうでなかった方はごめんなさい。

近況ですが、『[#3 Gopher道場](https://mercari.connpass.com/event/95886/)』を卒業してからはあんまり Go に触れられておらず『[Go言語による並行処理](https://www.oreilly.co.jp/books/9784873118468/)』は買って積んだままという残念な状態です。。が、同じ 3 期生の方から勧められた『[Docker/Kubernetes 実践コンテナ開発入門](https://gihyo.jp/book/2018/978-4-297-10033-9)』は『[【課外学習 II】Gopher道場](https://mercari.connpass.com/event/103766/)』で触れたおかげもあってか継続できています。年内、いや、平成の内には読み終わりたい。。(追記: なんとか年内に読み終わりました。すごくためになりました。)

そして今回久しぶりに Go のコードを書いたのですが、やはり [vim-go](https://github.com/fatih/vim-go) が最高で、特に `:GoCoverage` が最高です。緑にしたくなるモチベーションが無限に湧きます。

明日は [po3rin](https://qiita.com/po3rin) さんのエントリになります。po3rin さん、明日のエントリで今年の Advent Calendar 全体で 5 件目になるんですかね。やばいですね。お楽しみに !