ネットワーク越しの共有に大量のファイルを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を実行ユーザーにする