unisonでディレクトリを双方向同期する

この記事は、Infocom Advent Calendar 2022 4日目の記事です。

qiita.com

PCをオフライン環境で使いたいときには必要なファイルをコピーして持っていくことになり、作業中に何らかの更新が発生したら作業後はコピー元に戻しますね。
gitで管理すればいいものもあるしソースはそうしてますけど、パワポExcelとかまあいろんな雑多なファイルをgitに全部入れるわけにもいきません。
とはいえ毎回手動でコピーして戻すとなるとミスもするし、毎回毎回手作業でやるのはうんざりするので自動化を考えました。

  • シェルスクリプト/バッチファイルでコピーコマンドを使う
    • 全部コピーするので単純に遅い
    • 戻すのは手作業
  • rsync
    • 一方向しかできない
    • 両方に修正があると詰む
    • 結局、戻すのは手作業
  • unison
    • 両方の修正を検出してそれなりにいい感じにやってくれる。これだ!

unison

どうやら結構古くからあるUnix系のOSSツールのようです。
私は1995年頃からUnix系OSを使っているのですが知りませんでした。
LinuxMacだけじゃなくWindows用のバイナリもあったのはうれしい。
コンフリクトを検出できる。
SSHでも同期できる。
rsyncのような部分転送になっているということでコピーが高速。ファイル数が多ければそれなりに時間はかかりますが、普段使いの感じだと十分早いなと思います。
お手軽。これだいじ。

www.cis.upenn.edu

alliance.seas.upenn.edu

  • インストール

Macだとbrewのcaskがありました。

$ brew install unison
Warning: Treating unison as a formula. For the cask, use homebrew/cask/unison
🍺  /usr/local/Cellar/unison/2.53.0: 9 files, 3.9MB

Windowsはscoopで入れてます。

> scoop install unison
Installing 'unison' (2.53.0) [64bit] from main bucket
unison-v2.53.0+ocaml-4.12.1+x86_64.windows.zip (29.8 MB) [====================================================] 100%
Checking hash of unison-v2.53.0+ocaml-4.12.1+x86_64.windows.zip ... ok.
Extracting unison-v2.53.0+ocaml-4.12.1+x86_64.windows.zip ... done.
Linking ~\scoop\apps\unison\current => ~\scoop\apps\unison\2.53.0
Creating shim for 'unison'.
Creating shim for 'unison-fsmonitor'.
'unison' (2.53.0) was installed successfully!
Notes
-----
Compiled with same OCaml compiler version 4.12.1

main bucketにあったのでbucket追加しなくても普通に入ると思います。

使ってみる

ということで使ってみます。

  • コマンドで実行

unison root1 root2 で起動します。

$ tree foo bar
foo
├── aaa
│   └── hogehoge.pptx
└── hoge.txt
bar  [error opening dir]

$ unison foo bar
Unison 2.52.1 (ocaml 4.12.0): Contacting server...
Looking for changes
Warning: No archive files were found for these roots, whose canonical names are:
    /Users/gozu/Documents/foo
    /Users/gozu/Documents/bar

Reconciling changes

foo            bar
dir      ---->              [f]

Proceed with propagating updates? [] y
Propagating updates

[BGN] Copying  from /Users/gozu/Documents/foo to /Users/gozu/Documents/bar
[END] Copying

$ tree foo bar
foo
├── aaa
│   └── hogehoge.pptx
└── hoge.txt
bar
├── aaa
│   └── hogehoge.pptx
└── hoge.txt

初回なのでまるっとコピーされました。
相手のディレクトリが無ければ単純に複製ができます。

  • bar/hoge.txt を修正して、同期してみる
$ unison foo bar

foo            bar
         <---- new file   .hoge.txt.un~  [f]
         <---- changed    hoge.txt  [f]
         <---- new file   hoge.txt~  [f]

Proceed with propagating updates? [] y
Propagating updates

[BGN] Copying .hoge.txt.un~ from /Users/gozu/Documents/bar to /Users/gozu/Documents/foo
[END] Copying .hoge.txt.un~
[BGN] Updating file hoge.txt from /Users/gozu/Documents/bar to /Users/gozu/Documents/foo
[END] Updating file hoge.txt
[BGN] Copying hoge.txt~ from /Users/gozu/Documents/bar to /Users/gozu/Documents/foo
[END] Copying hoge.txt~

barのファイルがfooへ同期されています。
が、vimの管理ファイルまで同期されてますね。これは設定で除外できます。

  • 双方で同じファイルを更新してみる
$ unison foo bar

foo            bar
changed  <-?-> changed    hoge.txt  [] d

diff -u '/Users/gozu/Documents/bar/hoge.txt' '/Users/gozu/Documents/foo/hoge.txt'

--- /Users/gozu/Documents/bar/hoge.txt  2022-12-02 20:07:40.000000000 +0900
+++ /Users/gozu/Documents/foo/hoge.txt  2022-12-02 20:07:45.000000000 +0900
@@ -1,3 +1 @@
 hello unison.
-hello unison.
-hello unison.

changed  <-?-> changed    hoge.txt  [] x
foo          : changed file       modified on 2022-12-02 at 20:07:45  size 14        rw-r--r--
bar          : changed file       modified on 2022-12-02 at 20:07:40  size 42        rw-r--r--
changed  ====> changed    hoge.txt  [] >

Proceed with propagating updates? [] y
Propagating updates

[BGN] Updating file hoge.txt from /Users/gozu/Documents/foo to /Users/gozu/Documents/bar
[END] Updating file hoge.txt

修正したファイルがテキストファイルなのでdiffを取ることができました。
あとはファイルの日付やサイズを確認したり。
どちらを採用するか > で指定しています。
インタラクティブなコマンドは

atmarkit.itmedia.co.jp

このくらいでなんとかなるかな。

よく使う設定を定義できる

確かにいいんですが、これだとあんまり便利になってませんね。
いらないファイルを除外したり、新しいファイルを採用して欲しいし、いちいち指定せずとも全部自動でやって欲しい。
なので、設定ファイルを書いて自動実行できるようにします。
Macだと ~/.unison ディレクトリを作って、拡張子がprfのファイルを作ります。
Windowsでは C:\Users\アカウント名\.unison です)
default.prf は定義名を指定せずにunisonを実行したときに使われます。
unison 定義名 とすると ~/.unison/定義名.prf が使われます。

私の設定ファイルは以下のようになっています。

includeで読み込むための基本的な設定(common.prf)をしておくと、コピーする対象ごとに設定ファイルを作ることができ、最小の設定ファイルにできます。

$ cat ~/.unison/default.prf
# Unison preferences file
# https://88171.net/unison-manual-ja
# https://www.seas.upenn.edu/~bcpierce/unison/download/releases/stable/unison-manual.html

# 自動実行
batch = true

# ownerも同期する
owner = true

# タイムスタンプをコピーする(ディレクトリはできない)
times = true

# permissionを設定しない
perms = 0

# chmodしない perms=0と組み合わせる
dontchmod = true

# 新しいファイルを優先
prefer = newer

# 新規作成ファイルはユーザに同期如何を問わない
auto = true

# ファイル更新日時による更新有無の判定(Windowsでは更新日時が変わらないことがあるのでたまにfalseにするとよいらしい)
fastcheck = true

# エラー以外の出力停止をしない
silent = false

# 無視するディレクトリやファイル名を指定する
ignore = Name .DS_Store
ignore = Name Icon?
ignore = Name {.*.swp,*.*~}
ignore = Name {*.gsheet,*.gslides,*.gdoc}
$ cat ~/.unison/foo.prf
# Include the contents of the file common
include common

# 同期する対象のルートパスの定義
root = /Users/gozu/Documents/foo/
root = /Volumes/bar/foo/

# バックアップ
backup = Name *
maxbackups = 5
backupprefix = $VERSION.
backupdir = /Volumes/nas/backup/unison/foo

実行は、 unison foo と打つだけです。
こうして設定を分けておく事で、重要性に応じてバックアップの世代数や取るファイルの種類、ディレクトリを変えることができます。

設定のオプションやサンプルが色々載っているので、

https://www.seas.upenn.edu/~bcpierce/unison/download/releases/stable/unison-manual.html

を、見てやってます。DeepLを使うと驚くほどわかりやすく訳してくれます。

使い道

MacWindowsNASなどプラットフォームを越えて個人的なディレクトリを同期するのにはとても便利です。
ファイルを消すときはやっぱり注意が必要。どっちを消しても消える事になるので、rsyncの--deleteオプションより危険な事もあると思う。 更新する人が不特定多数なところで使うのちょっと躊躇しますね。
でも、ログやバックアップがいい感じに取れるので運用次第で使えるのかも?

双方向で同期出来て普通のコピーより早いし差分更新でマルチプラットフォームとなればもっと使われてもいいのになーと思ってこのブログを書きながら何となくググっていたらdocker-syncというツールで使われているというのが分かった。コンテナの中のファイルとホスト側のファイルを同期させるもののようです。
なるほど。いかにもなユースケースですね。