Go の os.Rename をファイルの移動に使うとエラーになることがあるで


この記事は [ACCESS Advent Calendar 2017](https://qiita.com/advent-calendar/2017/access)、16日目の記事です。
どうも、Go言語大好き @pankona です。

os.Rename の使い方に誤りがあって日頃の行いを疑われた話をします。

# 長い話を短くすると

Go 言語の os.Rename をファイル移動目的に使うと、場合によってはエラーになるで。具体的に言うと移動元と移動先が違うパーティションだとあかんで。

# 何が起きたのかと言うと

いったんテンポラリ領域にとあるファイルを作成し、無事作成できたのを確認した後、所定の位置にファイルを移動する、という処理を、**特にファイル移動について os.Rename を用いて実装した。**

雰囲気的には以下のような感じ。
[go-playground はこちら](https://play.golang.org/p/7j1en6tHYK)

```go
package main

import (
	"bytes"
	"fmt"
	"io"
	"io/ioutil"
	"os"
)

// テンポラリファイルに今日の天気を吐き出してから os.Rename で移動を試みる関数
func createWeather() error {
	// テンポラリファイルの作成
	tmpfile, err := ioutil.TempFile(os.TempDir(), "tmp_")
	if err != nil {
		return err
	}
	tmpfilePath := tmpfile.Name()

	// ここで天気を書き込む
	// (途中エラーが起きたら tmpfile を消してエラーを返す)
	_, err = io.Copy(tmpfile, bytes.NewReader([]byte("it's sunny day today")))
	if err != nil {
		_ = tmpfile.Close()
		_ = os.Remove(tmpfilePath)
		return err
	}

	// 書き終わったら Close
	err = tmpfile.Close()
	if err != nil {
		return err
	}

	// 無事に tmpfile が作成できたので所定の位置に移動
	err = os.Rename(tmpfilePath, "weather.txt")
	if err != nil {
		_ = os.Remove(tmpfilePath)
		return err
	}
	return nil
}

func main() {
	err := createWeather()
	if err != nil {
		fmt.Println(err.Error())
		os.Exit(1)
	}
	fmt.Println("success!")
	os.Exit(0)
}
```

# エラーが起きて疑われた日頃の行い

* 各エンジニアの環境でのテストが通る、Jenkins の CI もエラー吐かない。
* 何なら上記の go-playground 版もエラーにはならない。
* 当該コードは自分が書いたものではなく、コードレビューの一貫でとりあえず checkout してテスト流してみるか、という感じで手元でやったらテストがコケたという展開。「テスト通らんのだけど？」と各位にヒアリングしてみたものの再現している各位はおらず。**なんかよくわからんけどおれっちの環境でだけエラー起きて上手く動かない...！**

日頃の行いが悪いだとか OS をクリーンインストールしてみればだとか言われつつ、いやいや、そんなことない、特に日頃の行いは大丈夫と信じ、原因調査に乗り出したのであった。

ちなみに、エラーの文言は以下のようなもの。

```bash
$ go run main.go
rename /tmp/tmp_207083713 ./weather.txt: invalid cross-device link
exit status 1
```

invalid cross-device link...？

# 原因

os.Rename はパーティションをまたぐようなファイルの移動に用いることはできない。

参考:
[ASCII.jp: Goならわかるシステムプログラミング ― 第10回 ファイルシステムと、その上のGo言語の関数たち（1）](http://ascii.jp/elem/000/001/423/1423022/)
* ページ中段あたりの「ファイルの削除・移動・リネーム」の項を参照。

`ioutil.TempFile` は、`$TMPDIR` (環境変数) に指定がない場合、 Unix/Linux 上においては `/tmp` 以下にファイルを作成しようとする。
一方、上記スニペットにおいてファイルの移動先は `weather.txt` としており、これはつまりカレントディレクトリへの移動が指示されている。

だので、上記例では `/tmp` に作ったファイルをカレントディレクトリに移動せよ、という動作を指示していることになる。一見動きそうな上の例も、`/tmp` とカレントディレクトリが別パーティションな環境である場合、`invalid cross-device link` のエラーに遭遇する始末となる。

エラーメッセージからは想像しにくい原因かと思われるので、お気をつけあれ！

(ちなみに、パーティションをまたいだ mv みたいなのは、Go に限らず出来ないと思われるので留意されたしである。)

# 余談 1. テスト、CI で発覚せず、おれっちのところでだけ発覚した理由

チームメンバー各位の開発環境、ないし Jenkins の CI 環境は、テンポラリ領域と移動先が同じパーティションな構成だったので問題が起こらなかった。
たぶん、各位の開発環境ないし Jenkins が稼働している環境には、Ubuntu が用られているような気がする。別に Ubuntu が悪いというわけではないのだが、一説によると何も考えずに Ubuntu を愚直エンター連打インストールすると、一個のパーティションに諸々が展開されるような構成になる、とのことで、だとしたら各位の環境で件の問題が発生しないのもさもありなん、と思わないでもない。

おれっちはたまたま Manjaro Linux を使って仕事にあたっており、各位と Distribution が異なっていた。そんなことも相まってか、たまたまテンポラリ領域とその他の領域が別パーティションに設定されており、本件が発覚したという寸法である。

# 余談 2. 本件での気づき

* ばっちりテスト書いてるつもりでもバグるときはバグるね、怖いね...。
* Go で書けばそもそもポータビリティ高いけれども、とはいえ色んな環境でテストするの大事。
  * 特に件のコードは IoT 機器的なものの上で動かす想定だったこともあり、色んな環境で動かすにつれていずれ発覚するものと思われるが、何にせよ早めに見つかって良かった。
* 自分の日頃の行いには問題がなかった...！

---

明日の [ACCESS Advent Calendar 2017](https://qiita.com/advent-calendar/2017/access) は @aTakaakiSeki の出番です。要チェケラ！