Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
Help us understand the problem. What is going on with this article?

Mac の Go で birth time を取得する

More than 1 year has passed since last update.

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 が設定されているとこの挙動になります。

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

$ 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 は取得できませんでした。

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)

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

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

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 件目になるんですかね。やばいですね。お楽しみに !

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away