TIME WAIT BLOG.
#Software Engineering August 14, 2026 10 MIN READ

rsync Two Large Directories: Resume Interrupted Copies and Verify Them

Don’t stuff two trees into one rsync. Dry-run first, copy serially with partial files and logs, then verify. A 100% progress bar is not permission to wipe the source disk.

rsync Two Large Directories: Resume Interrupted Copies and Verify Them

The usual failure when moving two large directories is not “forgot a flag.” It is stuffing both paths into one command, or wiping the source disk because the terminal printed 100%.

Safer: map each source to its own destination, --dry-run first, keep partial files and a log on the real run, then do a read-only compare. An empty compare only means size, mtime, and the attributes you asked for match. Content equality needs --checksum.

This is a verified rewrite of KnightLi’s rsync guide. Flags and exit codes were checked against the rsync manual and upstream errcode.h.

The job:

/mnt/disk1/photos/   → /mnt/disk2/photos/
/mnt/disk1/archive/  → /mnt/disk2/archive/

Don’t put both trees in one command

The last positional argument is the destination; everything before it is a source:

rsync -a /source/a/ /source/b/ /target/

That merges both sources into /target/. It does not mean a → target/a and b → target/b. Two destinations means two runs, or a serial script.

Separate runs also give you independent logs and exit statuses, and sequential I/O on a spinning disk. If both sources really must land in one tree, watch for colliding relative paths—two 2026/report.pdf files and the later run wins.

Confirm the mount first

A dangerous failure: the destination disk never mounted, but the path still exists. rsync then writes into a normal directory on the root filesystem until / fills up.

lsblk -o NAME,SIZE,FSTYPE,UUID,MOUNTPOINTS
findmnt -T /mnt/disk1/photos
findmnt -T /mnt/disk2/photos
findmnt -T /mnt/disk1/archive
findmnt -T /mnt/disk2/archive
df -hT /mnt/disk1 /mnt/disk2
readlink -f "$SRC1" "$DST1" "$SRC2" "$DST2"

Check: expected devices, enough space, not read-only, not two names for the same filesystem, destination not inside the source. Do a tiny write test and delete only the file you just created.

Trailing slashes change the layout

rsync -a /source/ /target/
rsync -a /source  /target/

The first copies contents of /source/. The second creates target/source/. For two already-matching trees, put / on both sides. Use --dry-run --itemize-changes before the first real run.

A command you can actually run

SRC1=/mnt/disk1/photos
DST1=/mnt/disk2/photos
SRC2=/mnt/disk1/archive
DST2=/mnt/disk2/archive

sudo mkdir -p "$DST1" "$DST2"

Dry-run:

sudo rsync -aHAXn \
  --numeric-ids \
  --itemize-changes \
  "$SRC1/" \
  "$DST1/"

If paths and the change list look right, drop -n. Skip --delete on the first migration so extra files on the destination stay until you have verified structure and content.

sudo rsync -aHAX \
  --numeric-ids \
  --partial \
  --partial-dir=.rsync-partial \
  --info=progress2,stats2 \
  --log-file=/var/log/rsync-photos.log \
  "$SRC1/" \
  "$DST1/"

Repeat for the second pair with its own log. Run them serially; two jobs on the same HDD mostly fight over the actuator.

What the flags actually do

Base set: -aHAX.

FlagRoleCaveat
-aArchive: recurse and keep common attrsDoes not include hard links, ACLs, xattrs
-HHard linksLots of them cost RAM
-APOSIX ACLsDestination must support ACLs
-XExtended attributesDestination must support xattr
--numeric-idsKeep numeric UID/GIDRight for Linux disk moves
--partialKeep unfinished filesNext run continues
--partial-dir=.rsync-partialPark partials in a dirDon’t delete until verify is done
--info=progress2Whole-job progressBetter than --progress for huge trees
--log-fileDetailed logLog directory must be writable

-a is -rlptgoD—the manual is explicit. FAT, exFAT, and some network filesystems cannot keep owners, ACLs, or xattrs. More flags are not automatically safer. Check both filesystems with findmnt -no FSTYPE -T .... Drop -HAX if the data never had those features.

What “incremental” copies

By default rsync uses size and mtime. On a rerun: new files copy, changed files update, unchanged files skip, destination-only files stay (without --delete), interrupted files resume.

This is not a snapshot. If the source keeps changing, the destination can mix timestamps. Photo libraries can get a second incremental pass; databases, VM images, and live app data should be quiesced or snapshotted first.

--itemize-changes in short:

Unexpected top-level names usually mean a missing trailing /. Mass retransfers usually mean timestamp precision, mount options, or files still being rewritten.

Long jobs belong in tmux

Terabytes take hours. Don’t rely on an SSH window:

tmux new -s rsync-copy
# Ctrl+b, then d to detach
tmux attach -t rsync-copy

In a wrapper script, set -Eeuo pipefail so the second tree never runs after the first failed. Rerunning the same command is resume; you rarely need a special “continue” flag if --partial / --partial-dir were on.

Then echo $?. 0 means this run reported no errors—not that every byte was checksummed. Exit codes match upstream errcode.h:

CodeMeaning
11File I/O error
12Protocol data-stream error
23Partial transfer
24Source files vanished
30Send/receive timeout

IO error, Permission denied, failed, or vanished file in the log still matter even if the last line shows total bytes.

Three verification layers

Layer 1 — dry-run again with --delete (still -n, so nothing is deleted):

sudo rsync -aHAXn \
  --numeric-ids \
  --delete \
  --itemize-changes \
  "$SRC1/" \
  "$DST1/"

Empty output usually means nothing to add or update, no extra dest paths, and requested attrs match. If you only see .rsync-partial/, finish or inspect leftovers. *deleting means extra dest files—decide before dropping -n.

Layer 2 — add -c (--checksum). rsync reads both sides and uses a checksum to decide whether to transfer. With -n it only reports. This is rsync’s transfer-decision checksum (negotiated; often MD5 or xxHash), not a SHA-256 audit list. On multi-TB trees it rereads both disks; don’t hammer a dying drive.

Layer 3 — compare file counts and logical byte sums with find -xdev. Do not treat du -sh as proof: sparse files, compression, hard links, and block size all diverge.

For an auditable content list, SHA-256 both trees by relative path and diff. That covers regular-file content and paths only—not permissions, ACLs, hard links, or xattrs. Pair it with rsync -aHAXnc for a full Linux move.

Leave --delete for last

--delete mirrors the destination to the source. Dry-run first: paths not swapped, mount online, every *deleting expected, nothing unique on dest you still need, another recoverable copy exists. --backup --backup-dir=... if you want deleted files recoverable; keep that dir outside the source tree.

NAS junk (#recycle/, @eaDir/, .Trash-*/) can be excluded. Quote '#recycle/' or the shell eats # as a comment. Use the same excludes on copy and verify.

Local disk to local disk usually does not need -z. Photos, video, and archives won’t compress; you just burn CPU. On a live server, nice / ionice; over the network, --bwlimit (modern rsync accepts suffixes like 80M).

Typical failures

When you may delete the source

Not before: both rsyncs exited 0, logs have no unresolved I/O / permission / space errors, ordinary dry-runs are clean, important data passed -nc or independent SHA-256, and key files open from the destination. A restore drill is better.

Copy finished, verify passed, and restore works are three different stages. 100% on the progress bar is not permission to unmount the source.

/related_artifacts

Mechanical Sympathy: Software for Silicon Reality
#Software Engineering Aug 02, 2024

Mechanical Sympathy: Software for Silicon Reality

A practical playbook for recovering performance lost in modern abstraction stacks.

read full log arrow_right_alt
Monoliths in 2024: Principled Simplicity
#Software Engineering Jul 15, 2024

Monoliths in 2024: Principled Simplicity

Why many teams ship faster with a disciplined monolith than early microservices.

read full log arrow_right_alt
Zero-Cost Abstractions: Borrow Checker Deep Dive
#Software Engineering May 12, 2024

Zero-Cost Abstractions: Borrow Checker Deep Dive

How Rust guarantees memory safety without GC while keeping predictable performance.

read full log arrow_right_alt