Gopher道場 Advent Calendar 2018 の 21 日目のエントリです。
昨日は yuuyamad さんによる『今から学ぶgRPCの基礎 - Qiita』でした。そして、塊魂アンコールの発売日でした。4 時間転がしました。
はじめに
普段写真を管理する際に『ファイルを birth time でリネームする』といったことをしておりまして、この処理を今までは Ruby で行っていたのですが、今回ネタ作りのため Go で書き直してみました。( hioki-daichi/birthtime-rename )
その過程で得た birth time 周りの知見をつらつらと書いていこうと思います。
そもそも birth time とは
birth time とは、inode ( inode - Wikipedia ) が生成された時刻のことです。
例えば以下のようなファイル foo
が存在する場合に
$ cat <<EOF > foo
foo
EOF
Mac では stat -f %B <file>
で指定したファイルの birth time をエポック秒で表示できます。
$ stat -f %B foo
1545216600
また、%B
以外に指定可能な時刻情報の format としては以下の %a
, %m
, %c
が存在します。
format | 説明 |
---|---|
%a | 最終参照時刻 |
%m | 最終ファイル更新時刻 |
%c | 最終 inode 更新時刻 |
時刻情報を追ってみる
先程のファイル foo
を使い、これらの時刻情報が具体的に何を行った時に更新されるのか、いくつか試して探ってみましょう。
※ 確認用として以下の Ruby のコードを使用します。内容としては stat -f '%a %m %c %B' foo
の結果をわかりやすく表示しているだけです。
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
のすべてで(ほぼ)同じ時刻が表示されています。
$ 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
の時刻が更新されました。
$ 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
の時刻が更新されました。
$ 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
の時刻が更新されました。
$ 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
すべての時刻が・・・
$ # 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]]
一見更新されたように確認用コードの都合上見えてしまうのですがそうではなく、実はファイル自体が変わっていまして
$ # -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~
にリネームされていることがわかります。
$ 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
に書き出されるようです。
'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 は取得できませんでした。
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
というのはファイルの作成時刻だそうです。
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 を取得できます。
$ 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
の結果を整形して、時刻に関係ありそうな箇所だけ抜き出してみると、
&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
になっていたりとフィールド名が若干異なっていました。
//
// 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
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 が取得できます
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
に変換し、
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
すると以下のような人間に優しい文字列に変換できたりします。
gore> t.Format("2006-01-02-15-04-05")
"2018-12-19-20-09-00"
まとめ
いかがでしたでしょうか ? 自分にとっては初めて知ったことが多く楽しかったです。そうでなかった方はごめんなさい。
近況ですが、『#3 Gopher道場』を卒業してからはあんまり Go に触れられておらず『Go言語による並行処理』は買って積んだままという残念な状態です。。が、同じ 3 期生の方から勧められた『Docker/Kubernetes 実践コンテナ開発入門』は『【課外学習 II】Gopher道場』で触れたおかげもあってか継続できています。年内、いや、平成の内には読み終わりたい。。(追記: なんとか年内に読み終わりました。すごくためになりました。)
そして今回久しぶりに Go のコードを書いたのですが、やはり vim-go が最高で、特に :GoCoverage
が最高です。緑にしたくなるモチベーションが無限に湧きます。
明日は po3rin さんのエントリになります。po3rin さん、明日のエントリで今年の Advent Calendar 全体で 5 件目になるんですかね。やばいですね。お楽しみに !