Skip to content

Latest commit

 

History

16 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

isync — verified folder and drive mirroring for macOS

A fast, free command-line backup tool for Mac that mirrors a folder or an entire volume onto another drive, copies only what changed, and proves the result: every copied file is read back from the destination and SHA-256-checked before the tool says SYNCED. A modern alternative to rsync -a on macOS — preserves Finder tags, extended attributes, resource forks, permissions and nanosecond timestamps on APFS — built in Swift with zero dependencies.

platform language dependencies tests license

Install

Requires Xcode or the Command Line Tools (xcode-select --install). Nothing else — no Homebrew, no packages.

git clone https://github.com/arashkashi/iSyncBackup-.git
cd iSyncBackup-
PREFIX=$HOME/.local make install      # → ~/.local/bin/isync, no sudo
isync --version

~/.local/bin must be on your PATH (add export PATH="$HOME/.local/bin:$PATH" to ~/.zshrc if it is not). Prefer a system-wide install? sudo make install → /usr/local/bin/isync.

To upgrade later: git pull && PREFIX=$HOME/.local make install.

Quick start

Mirror a folder onto an external drive, then keep it up to date:

# 1. first copy — creates the destination folder if its parent exists
isync ~/Pictures /Volumes/BackupDrive/Pictures

# 2. later: preview what a rerun would do (changes nothing)
isync -n --delete ~/Pictures /Volumes/BackupDrive/Pictures

# 3. rerun for real. Unchanged files are not even read; only differences are copied.
isync --delete ~/Pictures /Volumes/BackupDrive/Pictures

The last line of every run is the verdict. If it does not say ✔ SYNCED, the backup is not complete, and the lines above it say why.

Common use cases

Back up a folder (never deletes anything)

isync ~/Documents /Volumes/BackupDrive/Documents

New and changed files are copied and verified. Files you deleted from ~/Documents stay in the backup; the summary tells you how many ("N extra items remain in the destination").

Keep an exact mirror (deletes what you deleted)

isync --delete ~/Documents /Volumes/BackupDrive/Documents

--delete shows what would be removed, then does all the copying first, and only then asks:

  1,022 item(s) (21.9 MB) exist only in the destination:
    MyProjects/old-app/node_modules/   612 items inside, 12.1 MB
    tmp/export-2024/                   300 items inside, 8.0 MB
    Misc/notes-draft.txt               14 KB
  You will be asked whether to delete them once copying and verification are done.
  …
  Delete them?  [y] yes   [n] no — keep them   [l] list every path  (default n):

You can walk away during the copy. n (or just Enter) keeps the items and the run still completes. --yes answers yes without asking (for scripts). It refuses to delete more than 25 % of the destination unless you add --force.

Preview before doing anything

isync -n --delete SRC DST        # -n = dry run: prints every action, changes nothing

Back up an entire volume

isync -x --delete /Volumes/Archive /Volumes/ArchiveBackup/Archive

-x stays on the source volume (does not descend into other mounted volumes). macOS housekeeping (.Spotlight-V100, .fseventsd, .Trashes, .DS_Store, …) is skipped automatically. If you see "Operation not permitted" on folders like ~/Library, give your terminal Full Disk Access in System Settings → Privacy & Security.

Skip caches and junk

Put a .isyncignore file in the source root (one glob per line, # comments):

.build/
node_modules/
.venv/
__pycache__/
DerivedData/
*.tmp

Name patterns match anywhere in the tree; patterns containing / are relative to the root; a trailing / means directories only. --exclude PATTERN does the same from the command line. On a developer's disk these few lines typically remove more than half of all files.

Check that the backup is really intact (audit)

isync --compare hash ~/Documents /Volumes/BackupDrive/Documents

Reads every byte of every file on both sides, compares SHA-256, and repairs whatever differs. This is the only mode that catches a backup drive silently corrupting a file (bit rot). Run it now and then — it is fast on SSDs (about 300 MB/s on small files, disk-bound on large ones).

First big copy onto a spinning hard drive

Hard drives hate per-file seeks. For the initial copy of hundreds of thousands of files onto one, copy in bulk first and let the audit read everything back sequentially afterwards:

isync -x --delete --no-fsync --no-verify SRC DST
isync -x --compare hash SRC DST

Also keep -j 2 on a hard drive (parallel readers only add seeks), exclude caches (above), and add the backup volume to System Settings → Spotlight → Search Privacy so indexing does not compete for the disk.

Schedule it

Exit code 0 means synced; anything else means look. A cron or launchd line:

isync -q --delete --yes --report ~/backup-report.json /Volumes/Archive /Volumes/Backup/Archive \
  || osascript -e 'display notification "Backup needs attention" with title "isync"'

--report writes a JSON file with every count, every error and the verdict — the auditable record.

Get a second opinion, without trusting isync

scripts/verify-independent.sh SRC DST --hash

Audits a mirror using only find, stat, readlink, xattr, shasum and diff — no isync code at all.

Reading the result

Last line Meaning Exit
✔ SYNCED — … (all copies verified by SHA-256; …) Done. The parenthesis states exactly what was checked. 0
● DRY RUN — N action(s) would be performed Nothing was touched. 0
▲ SYNCED WITH WARNINGS — N file(s) changed while being copied Something was writing to the source mid-run. Run again. 3
✖ NOT SYNCED — N error(s) Everything else was synced; the listed files were not. 2
✖ Refusing: … A safety rail stopped it before doing anything (e.g. source drive not mounted). 1
✖ INTERRUPTED Ctrl-C. Completed files are complete; the next run picks up the rest. 130

While it runs you see the phase, a progress bar, throughput, ETA, the files each worker is on, and errors the moment they happen.

All options

isync [options] <source> <destination>

  -n, --dry-run             Show what would happen; change nothing (hashes are still computed).
      --delete              Remove items from destination that no longer exist in source.
      --force               Skip the safety guard that refuses to delete >25% of the destination.
  -y, --yes                 Delete without asking (the prompt shows the list; "no" keeps the
                            items and syncs the rest; no terminal counts as "no").
      --compare quick|hash  quick (default): size + mtime + permissions, hashing only when in doubt.
                            hash: SHA-256 every file on both sides (full audit; reads everything).
      --no-verify           Skip re-reading each copied file to confirm the bytes on disk.
      --no-fsync            Skip per-file fsync (faster on many small files; less crash-safe).
      --exclude PATTERN     Glob to skip (repeatable). Name match if no "/", else relative path.
      --no-default-excludes Also copy .DS_Store, .Spotlight-V100, .fseventsd, .Trashes, … .
  -x, --one-file-system     Do not descend into other mounted volumes.
  -j, --jobs N              Parallel file workers (default: cores, max 8).
      --mtime-window SEC    Treat mtimes this close as equal (default: 0 APFS→APFS, 1 with HFS+,
                            2 FAT/exFAT/network).
      --report FILE         Write a JSON report of the run.
  -v, --verbose             Print one line per action.
  -q, --quiet               Only print the final verdict and errors.
      --no-color            Disable colors (also honours NO_COLOR).
  -h, --help / --version

Who this is for

  • You keep photos, video, music or project archives on an external drive and want a second drive that is an exact copy — with evidence, not hope.
  • You back up one Mac volume to another (a Thunderbolt/USB SSD, a second internal volume) from the terminal or a scheduled job, and you want clear exit codes instead of a GUI.
  • Your rsync -a script quietly changed behaviour. macOS now ships Apple's openrsync, which drops extended attributes and Finder tags unless you add -E, and truncates timestamps to whole seconds. See isync vs rsync for measurements.
  • You worry about bit rot / silent corruption on backup drives and want an audit mode that reads every byte on both sides and repairs what differs.
  • You want a free, scriptable alternative to Carbon Copy Cloner or ChronoSync for plain folder mirroring, and you are comfortable with a command line.

Not for: two-way sync, cloud storage, remote machines over SSH (use rsync there), or versioned history with "go back to last Tuesday" (that is Time Machine's job). Direction is always source → destination.


Details

What "synced" means — the trust model

The final line of every run is a verdict. Its wording states exactly what was checked.

Mode How a file on both sides is judged unchanged What is proven when the run says SYNCED
--compare quick (default) same size and identical mtime (nanosecond-exact on APFS→APFS) and same permissions/flags. If only the mtime differs, both sides are SHA-256 hashed before deciding. Every file that was copied was re-read from the destination disk (page cache bypassed) and its SHA-256 matched what was read from the source. Files judged unchanged were not read — that judgement rests on size+mtime.
--compare hash SHA-256 of both sides, always. Every file on both sides was read and compared. This is the audit mode.
--no-verify as quick Copies were not re-read. Fastest; use when the destination is a trustworthy local disk and speed matters more.

Honest limits:

  • Quick mode is a heuristic. A file whose content changed while its size and mtime stayed identical (a deliberately back-dated write, or bit rot on the destination) is invisible to it — the same is true of rsync, Time Machine and every other incremental tool. Run --compare hash periodically to catch it.
  • Verification proves the copy reached the drive, not that the drive will still return it in five years. For that, keep more than one backup.
  • Read-back bypasses the OS page cache (F_NOCACHE), so it reads what the filesystem stored, but a drive's own write cache can still be between you and the platters/cells. A final F_FULLFSYNC on the destination asks the drive to flush that cache before the verdict is printed.

How it works

  1. Scan source and destination in parallel (readdir + lstat, symlinks never followed).

  2. Plan — a pure diff producing ordered actions: removals for type changes (file↔dir↔symlink), mkdirs, file work, deletions (children before parents), directory metadata (deepest first). Big files are scheduled first so all workers stay busy to the end.

  3. Execute file work on a pool of workers (--jobs, default = cores, max 8), in two passes:

    • Pass 1 — copy. Stream source → temp file in the destination directory, hashing the bytes on the way → atomic rename() over the target. The file now has the right content but a fresh mtime and 0600 permissions — deliberately not yet "stamped".
    • Pass 2 — verify and stamp. Walk the copied files in the order they were written (sequential on spinning disks): fsync → re-read bypassing the page cache → compare SHA-256 → apply permissions, flags, ACLs, xattrs (copyfile(3): Finder tags, resource forks, quarantine) → stamp the mtime that was scanned, not the source's current one.

    A crash or Ctrl-C at any point leaves either the old file, the complete new file with a fresh mtime (so the next run re-checks it), or a stray .isync-tmp-* — never a partial file under the real name, and never a file that merely looks up to date. If verification fails, the bad destination file is deleted so the next run cannot mistake it for a good copy. If the source was modified during the run, the backup keeps the timestamp of the content it actually holds, the run ends with a warning (exit 3), and the next run picks up the new version.

  4. Deletions (only with --delete, and only after you have answered the question).

  5. Verdict + optional JSON report (--report run.json) listing every error and every count.

Names are matched the way the volume matches them: on case-insensitive APFS/HFS+ (the default), Readme.md and README.MD are the same file, so no phantom copy/delete pairs.

Safety rails

  • Nothing is ever deleted from the destination without --delete.
  • --delete shows you exactly what would be removed (whole directories collapsed to one line with their item count; l lists every path) and asks after all copying and verification is done, so a slow answer never holds up the real work. "No" keeps the items and finishes the run. --yes skips the question; no terminal on stdin counts as "no".
  • --delete refuses to remove more than 25 % of the destination unless you pass --force — the classic "source disk wasn't mounted, backup got wiped" accident cannot happen by default.
  • --delete is also refused when the source scan had errors, since "missing from the source" cannot be trusted then.
  • Source inside destination → refused. Destination inside source → excluded from the scan.
  • Errors (unreadable files, permission denied…) never abort the run; everything else is synced, every error is listed, the verdict is NOT SYNCED and the exit code is 2.
  • Ctrl-C finishes the current chunk, removes temp files, and reports what was completed.
  • Sockets, FIFOs and device nodes are reported as skipped, never copied.

Exclusions

Default: .DS_Store .Spotlight-V100 .fseventsd .Trashes .TemporaryItems .DocumentRevisions-V100 and a few more volume-housekeeping items (--no-default-excludes to keep them). The same rules apply to both sides, so an excluded item in the destination is never deleted either. Add your own with --exclude PATTERN or a .isyncignore file in the source root (see Skip caches and junk). -x / --one-file-system stops at mount points.

isync vs rsync on macOS

macOS 26 ships Apple's openrsync, not the upstream rsync 3.x. Measured on the same 62,490-entry / 1.9 GB tree, APFS→APFS, Apple silicon:

openrsync -aE isync (fsync + read-back verify)
first copy 37.4 s 22.7 s
no-op rerun 7.3 s 1.5 s
full-content audit of both sides 82 s (-aEc, MD4 checksums, single-threaded) 6.0 s (--compare hash, SHA-256, 8 workers)
xattrs / tags / resource forks only with -E; silently dropped with plain -a always
mtime truncated to whole seconds nanosecond-exact
copy verified by re-reading the destination no yes
atomic temp+rename yes yes, plus per-file fsync
refuses to wipe the destination only via --max-delete yes, by default
machine-readable report no JSON

The two tools' outputs were cross-checked with diff -rq and found identical. rsync remains the right choice when the far end is a remote machine over SSH; isync is for local disks and mounted volumes where you want certainty and speed.

FAQ

How do I mirror an external drive to another drive on macOS from the terminal?

isync -x --delete /Volumes/Archive /Volumes/ArchiveBackup/Archive

-x stays on the source volume, --delete makes the copy a true mirror and asks before removing anything. Re-run the same command whenever you like; unchanged files are not even read, so a rerun of a 60,000-file tree takes about a second and a half.

Does it preserve Finder tags, extended attributes, resource forks, ACLs and permissions?

Yes, always, via copyfile(3) — the same mechanism Finder uses. Modification times are preserved to the nanosecond on APFS. Symlinks are copied as symlinks, never followed.

Is --delete safe?

It is opt-in. It lists what would go, does all the copying first, and only then asks; "no" keeps the items and the run still completes. It refuses outright to delete more than 25 % of the destination (the "source drive was not mounted" accident) unless you pass --force. Type changes (a file replaced by a folder) are handled without --delete, since they are updates, not removals.

Can it detect bit rot or silent corruption on my backup drive?

isync --compare hash reads every byte of every file on both sides and repairs any mismatch. Quick mode — like rsync and Time Machine — trusts size + modification time for files it did not copy, so run the hash audit periodically.

Why not Time Machine, Carbon Copy Cloner or rsync?

Different jobs. Time Machine keeps versioned history of your boot volume; isync keeps one drive an exact mirror of another. CCC and ChronoSync are excellent GUI tools; isync is free, scriptable and prints a verdict you can check in a cron job. Apple's bundled openrsync is compared above; upstream rsync remains the right tool for remote machines.

Does it work with exFAT drives or a NAS/SMB share?

It detects those filesystems and relaxes the comparison (2-second timestamp window, permissions not compared) so every run does not "fix" things they cannot store. It has been tested extensively on APFS→APFS; treat other targets as supported-but-less-tested and run --compare hash after the first sync.

It is slow on my external hard drive — what should I do?

See First big copy onto a spinning hard drive: exclude caches, bulk-copy with --no-fsync --no-verify then audit with --compare hash, use -j 2, and keep Spotlight off the backup volume.

Testing

  • swift test — 32 unit tests: the planner (case folding, mtime windows, type conflicts, delete ordering, flag masking, ignore rules), the two-pass copy (verification failure removes the copy, scanned mtime is stamped even if the source changed, cancellation leaves no temp files) and the executor on real temporary trees (deletion question asked only after copying, decline keeps items, approval deletes children first).
  • scripts/smoke-test.sh — 72 end-to-end checks on a fixture tree: xattrs, modes, exact mtimes, symlinks (relative, dangling, retargeted), unicode names, empty files/dirs, FIFOs, immutable files, same-size edits, touch-only changes, type changes in both directions, extraneous items with and without --delete, silent-corruption detection in audit mode, unreadable files, every safety refusal, destination-inside-source with --delete, .isyncignore.

make test and make smoke run them.

Roadmap

  • --trash: move deleted items into .isync/trash/<timestamp>/ instead of unlinking.
  • Hard-link preservation (nlink > 1 is already recorded per entry).
  • Persistent manifest in the destination for bit-rot detection without re-reading the source.
  • A SwiftUI front end over ISyncCore (the engine is a separate library target for this reason).

License

MIT — see LICENSE.

About

Verified folder & drive mirroring for macOS from the terminal. Copies only what changed, preserves Finder tags/xattrs/permissions, re-reads every copy with SHA-256 before saying SYNCED. A zero-dependency Swift alternative to rsync -a on the Mac.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages