ネットワーク越しの共有に大量のファイルをrsyncで転送していると、接続が切れて途中でエラー終了する場合がある。rsyncを実行し直せば、転送済みのファイルはスキップされて、続きから再開できる。実行し直す作業は、シェルスクリプトのループで自動化できる。
#!/bin/bash
# usage: rsync-retry.sh <src> <dst> <log-file>
src=$1
dst=$2
log=$3
max=10
wait_sec=${WAIT_SEC:-30}
opts=(-rlt --partial --timeout=60)
for ((i = 1; i <= max; i++)); do
echo "=== $(date '+%F %T') attempt $i/$max" >> "$log"
rsync "${opts[@]}" "$src" "$dst" >> "$log" 2>&1
rc=$?
echo "=== rsync exit code: $rc" >> "$log"
# 24: some source files vanished during the transfer; retrying does not help
if [[ $rc -eq 0 || $rc -eq 24 ]]; then
echo "=== DONE" >> "$log"
exit 0
fi
sleep "$wait_sec"
done
echo "=== GAVE UP after $max attempts" >> "$log"
exit 1
転送元と転送先、ログファイルを引数に指定して実行する。
$ ./rsync-retry.sh /path/to/src/ /path/to/dst/ rsync.log
実行例はrsync 3.5.1、macOS 27.0.1のもの。Ubuntu 24.04(rsync 3.2.7)でも、同じ結果になった。
スクリプトの仕組み
再実行しても二重に転送されない
rsyncは、転送先に同じファイルがあれば、既定では更新時刻とサイズを比較して、同じならスキップする。転送が途中で止まっても、再実行すれば、転送済みのファイルは転送されない。
途中まで転送されたファイルは、--partialを付けると、転送先に残される。再実行したときに、残った部分の続きから転送される。サイズの大きいファイルを転送するときは、--partialがないと、毎回先頭から転送し直す。
--timeoutで通信の停止を検出する
--timeout=60は、60秒間データの送受信がなければ、rsyncを終了する。接続が切れたまま応答を待ち続ける状態を、エラー終了(終了コード30)に変えて、リトライにつなげる。
終了コードで成功と失敗を判定する
rsyncの終了コードを$?で受け取って、リトライするかを判定する。
| 終了コード | 意味 | スクリプトの扱い |
|---|---|---|
| 0 | 成功 | 終了する |
| 24 | 転送中にソースのファイルが消えた | 成功として終了する |
| 11 | ファイルI/Oのエラー | リトライする |
| 23 | 一部のファイルを転送できなかった | リトライする |
| 30 | データの送受信がタイムアウトした | リトライする |
終了コード24は、転送中にファイルが削除されたときに出力される。名前を読み取れないファイルなど、ソースに恒久的な原因がある場合は、何度実行しても24になる。リトライしても結果が変わらないため、成功として扱う。
恒久的なエラーは試行回数で打ち切る
読み取り権限のないファイルがあると、rsyncは終了コード23で終了する。何度実行しても同じエラーになる。
$ chmod 000 src/secret.txt
$ WAIT_SEC=0 ./rsync-retry.sh src/ dst/ rsync.log
$ echo $?
1
$ grep '^===' rsync.log | tail -3
=== 2026-10-30 07:48:06 attempt 10/10
=== rsync exit code: 23
=== GAVE UP after 10 attempts
maxで試行回数を決めておくと、恒久的なエラーでも、無限にループせずに終了する。終了コードが1のときは、ログを確認する。
ログにはエラーだけを出力する
スクリプトは、rsyncの標準出力と標準エラー出力を、ログに追記している。-vや--progressを付けていないため、ファイル名や進捗は出力されない。長時間の転送でも、ログが大きくならない。エラーが発生したときだけ、内容が追記される。
接続が切れる状況を再現して確認する
転送中にサーバーを停止して、リトライを確認する。150MBのファイルを、転送速度を制限したrsyncでSMB共有に転送している最中に、サーバーを再起動した。
$ cat rsync.log
=== 2026-10-30 07:48:22 attempt 1/10
rsync: [receiver] write failed on "/mnt/dst/big/big.bin": Input/output error (5)
rsync: [receiver] stat "/mnt/dst/big/.big.bin.g21SQd" failed: Socket is not connected (57)
rsync: [receiver] rename "/mnt/dst/big/.big.bin.g21SQd" -> "big.bin": Socket is not connected (57)
rsync error: error in file IO (code 11) at receiver.c(660) [receiver=3.5.1]
rsync: [sender] write error: Broken pipe (32)
=== rsync exit code: 11
=== 2026-10-30 07:48:34 attempt 2/10
=== rsync exit code: 0
=== DONE
1回目は終了コード11で失敗した。待機後の2回目で転送が完了した。cmpで転送元と転送先のファイルを比較すると、内容は一致した。
失敗するたびにマウントし直す
転送先のマウントが外れる場合は、リトライの前にマウントし直す。次のスクリプトは、SMB共有をゲストでマウントして、失敗したらアンマウントしてから再試行する。
#!/bin/bash
# usage: rsync-retry-mount.sh <src> <dst-subdir> <log-file>
src=$1
mnt=$HOME/mnt/share
dst=$mnt/$2
log=$3
max=10
opts=(-rlt --partial --timeout=60)
is_mounted() {
mount | grep -q " on $mnt "
}
ensure_mounted() {
is_mounted && return 0
echo "not mounted, mounting" >> "$log"
mkdir -p "$mnt"
mount_smbfs -N '//GUEST:@nas.local/share' "$mnt" >> "$log" 2>&1
is_mounted
}
for ((i = 1; i <= max; i++)); do
echo "=== $(date '+%F %T') attempt $i/$max" >> "$log"
if ! ensure_mounted; then
echo "=== GAVE UP: could not mount" >> "$log"
exit 1
fi
rsync "${opts[@]}" "$src" "$dst" >> "$log" 2>&1
rc=$?
echo "=== rsync exit code: $rc" >> "$log"
if [[ $rc -eq 0 || $rc -eq 24 ]]; then
echo "=== DONE" >> "$log"
exit 0
fi
# reset the SMB session before the next attempt
diskutil unmount "$mnt" >> "$log" 2>&1
sleep 30
done
echo "=== GAVE UP after $max attempts" >> "$log"
exit 1
試行の最初に、マウントされているかをmountの出力で確認して、マウントされていなければmount_smbfsでマウントする。失敗したときは、diskutil unmountでアンマウントして、次の試行でマウントし直す。サーバーの停止から回復すると、2回目の試行でマウントし直して転送を完了できた。
参考: 【mount_smbfs】SMB共有をダイアログなしでゲストマウントする (-N)
スリープを防いで実行する
ノートPCで長時間実行する場合は、caffeinateでスリープを防ぐ。
$ caffeinate -dimsu ./rsync-retry.sh /path/to/src/ /path/to/dst/ rsync.log
参考: 【caffeinate】macOSのスリープを一時的に抑止する
参考: rsync -aコマンドでもownerとgroupを実行ユーザーにする
