fix: flush and unmount even when the sync is interrupted
Build and publish container / build (pull_request) Canceled after 2m10s

rsync itself is safe under interruption. It writes to a hidden temporary file
and renames it into place only once complete, and by default deletes any
partial file when interrupted -- verified both in the manual and by killing a
transfer and inspecting what was left, which was nothing. --partial is
deliberately absent and there is now a test asserting it stays that way. An
unclean kill can leave a hidden .track.mp3.XXXXXX behind; it is unplayable, it
is not in the source, and the next run's --delete removes it.

The script was not safe. Ctrl-C killed it before the sync and the unmount,
leaving a journal-less FAT filesystem holding dirty buffers -- which is the
exact corruption the script exists to prevent, arrived at by the most likely
route a person would take.

INT and TERM are now trapped. Both the normal path and the interrupt path call
the same finish function, so the flush and the unmount cannot drift apart, and
an interrupted run exits 130 rather than pretending to have succeeded.

The test is structural rather than timed. Reproducing a mid-transfer signal
needs a payload large enough to be slow, and a test that depends on winning a
race is a test that fails in CI for reasons that have nothing to do with the
code. The behaviour was verified by hand: SIGTERM mid-transfer gave exit 130,
the flush ran, and the destination held no short files and no leftover
temporaries.
This commit is contained in:
Emma Thorpe
2026-08-25 12:51:42 +01:00
parent d6ef70922c
commit 8228c81b5c
3 changed files with 78 additions and 13 deletions
+33 -13
View File
@@ -204,21 +204,41 @@ else
printf 'sync-to-ipod: %s files to copy\n' "$total" >&2
fi
# Flushing and unmounting is the whole reason this is a script, so it has to
# happen on the way out whichever way that is. Ctrl-C during a transfer would
# otherwise leave a FAT filesystem with dirty buffers and no journal, which is
# the corruption this exists to avoid.
finish() {
sync
if $unmount; then
device=$(findmnt -no SOURCE --target "$destination" 2>/dev/null || true)
if [ -n "$device" ]; then
printf 'sync-to-ipod: unmounting %s\n' "$device" >&2
if command -v udisksctl >/dev/null 2>&1; then
udisksctl unmount -b "$device" || umount -- "$destination" || true
else
umount -- "$destination" || true
fi
printf 'sync-to-ipod: safe to disconnect\n' >&2
fi
else
printf 'sync-to-ipod: still mounted; unmount before disconnecting\n' >&2
fi
}
interrupted() {
trap - INT TERM
printf '\nsync-to-ipod: interrupted -- rsync leaves no partial files, but the\n' >&2
printf 'sync-to-ipod: filesystem still needs flushing before you pull anything\n' >&2
finish
exit 130
}
trap interrupted INT TERM
rsync "${options[@]}" --out-format='%l %n' "$mirror/" "$destination/" |
python3 "$here/rsync_progress.py" --total "$total" --bytes "$total_bytes"
status=${PIPESTATUS[0]}
[ "$status" -eq 0 ] || die "rsync exited $status"
sync
if $unmount; then
device=$(findmnt -no SOURCE --target "$destination")
printf 'sync-to-ipod: unmounting %s\n' "$device" >&2
if command -v udisksctl >/dev/null 2>&1; then
udisksctl unmount -b "$device"
else
umount -- "$destination"
fi
printf 'sync-to-ipod: safe to disconnect\n' >&2
else
printf 'sync-to-ipod: still mounted; unmount before disconnecting\n' >&2
fi
finish