個人でWebサイトやアプリを作っています(屋号は sesebox です)。
アプリの開発では、APIの鍵(外部サービスに接続するための秘密の文字列)や証明書などを、1つのフォルダにまとめて置いています。このフォルダは gitに乗せていません(公開されると困るので、.gitignore で外しています)。
ある日、このフォルダの中身が全部消えました。しかも同じ日に2回です。どちらも消すつもりはなく、「リンクを外しただけ」のつもりでした。Windowsで同じことをしないように、何が起きたかを書いておきます。
そもそも何をしていたか
同じリポジトリで作業を並行して進めるために、git worktree(1つのリポジトリから、作業用のフォルダを別にもう1つ作れる機能)を使っていました。
ただ、gitに乗せていない鍵のフォルダは、新しく作った作業フォルダには入ってきません。そこで、ジャンクション(Windowsのフォルダ版ショートカットのようなもの)で、本体の鍵フォルダを作業フォルダからつないでいました。
mklink /J "作業フォルダ\.company\secrets" "本体\.company\secrets"
こうすると、作業フォルダから見ても鍵が使えます。鍵の置き場は1か所のままなので、入れ替えたときも全部に反映されて便利です。
1回目:Remove-Item -Recurse
作業中に「このリンク、つなぎ方がおかしいな」と思い、作り直すために PowerShell(Windows PowerShell 5.1)でリンクを消しました。
Remove-Item $d -Recurse -Force # ← $d はジャンクション
リンクだけでなく、つないだ先(本体の鍵フォルダ)の中身が全部消えました。
Remove-Item -Recurse は、フォルダの中身をたどりながら消していくコマンドです。ジャンクションに対して使うと、リンクの向こう側の本物のファイルまでたどって消してしまいます。ショートカットを消したつもりが、元のファイルを消していた、という状態です。
しかも Remove-Item はごみ箱を通りません。gitにも乗せていないので、どこにも残っていませんでした。証明書の中には、発行したときに一度しかダウンロードできないものもありました。
2回目:git worktree remove
1回目のあと、鍵を集め直して、毎日フォルダの外に控え(バックアップ)を取るスクリプトを作りました。
そして同じ日に、作業が終わった作業フォルダを片づけました。
git worktree remove --force 作業フォルダ
また本体の鍵フォルダが空になりました。 今度はPowerShellではなくgitのコマンドです。作業フォルダを消すときに、中にあったジャンクションの先までたどって消していました。
このときは、その日に作ったばかりの控えから戻せて、5分ほどで元どおりになりました。
原因は「コマンド」ではなく「リンクという作り」だった
1回目のあと、最初は「Remove-Item -Recurse に気をつければいい」と考えていました。でも2回目は別のコマンドで同じことが起きました。
つまり、問題はコマンドではなく、「片方を消すと、もう片方も消える」という作りそのものでした。消すコマンドはほかにもたくさんあるので、1つずつ気をつけるのでは防げません。
直したこと
1. リンクをやめて、コピーにした
作業フォルダには、鍵フォルダをコピーで置くようにしました。
| リンク(やめた) | コピー(いま) | |
|---|---|---|
| 鍵の置き場 | 1か所 | 作業フォルダの数だけ増える |
| 作業フォルダを消したとき | 本体も消える | 本体は無傷 |
| 鍵を入れ替えたとき | 全部に反映される | 作業フォルダは古いまま(作り直せば最新) |
鍵が複数の場所に置かれるのは気持ちよくないですが、作業フォルダは終わったらすぐ消す運用にして受け止めています。コピーにしたあと、同じ git worktree remove --force を試して、本体の鍵が無傷なことも確かめました。
2. リンクを消すときは、リンクだけを外すコマンドを使う
どうしてもジャンクションを消すときは、先に「それがリンクかどうか」を確かめます。
$item = Get-Item $path -Force
if ($item.LinkType) {
cmd /c rmdir "$path" # リンクだけ外す(中はたどらない)
} else {
Remove-Item $path -Recurse -Force # 本物のフォルダのときだけ
}
cmd の rmdir(/s を付けない形)は、ジャンクションそのものだけを外して、中身はたどりません。
3. gitに乗せないものは、別の場所に控える
いちばん根っこの原因は、gitに乗せない=どこにも控えが無いことでした。乗せない判断そのものは正しいので、リポジトリの外のフォルダに、日付つきで毎日コピーするスクリプトを作りました。
- 置き場はリポジトリの外(今回の事故はリポジトリの中で起きたので、外なら巻きこまれない)
- OneDriveなどの同期フォルダは使わない(同期は「消した」ことまで同期してしまう)
- 中身が空っぽのときは控えない(壊れた状態の控えで、古い正しい控えを押し出さないため)
2回目の事故は、この控えがあったから5分で戻せました。
まとめ
- Windows PowerShell 5.1 の
Remove-Item -Recurseをジャンクションに使うと、つないだ先の中身まで消える -
git worktree remove --forceでも、作業フォルダの中のジャンクションの先が消えた - 「このコマンドに気をつける」では防げない。リンクでつなぐ作りをやめるか、リンクだけ外すコマンド(
cmd /c rmdir)を使う - gitに乗せないファイルは、リポジトリの外に控えを取っておく
(PowerShell 7 ではリンクの扱いが変わっているという情報もありますが、今回は試していません)
ほかに作っているものは sesebox にまとめています。