A fast, interactive file mirror tool for local backups —
.gitignore-aware, multithreaded, and confirmation-first.
mirroir (French for mirror) syncs one or more source directories to destination directories. Before touching a single file, it builds a full diff plan and asks you to confirm. Only new or changed files are copied; files deleted from the source are removed from the destination too — a true mirror, not just a copy.
- Interactive dry-run — always shows a per-folder summary before making any changes
.gitignore-aware — automatically skips files and folders your.gitignoreexcludes- Multithreaded — copies files in parallel (16 worker threads) for fast external drive performance
- Smart diffing — only acts on files that have actually changed (size + mtime comparison)
- Multi-job config — back up as many source/destination pairs as you like from one
config.json - Fails soft — a job that errors out never takes the remaining jobs down with it
- Mass-deletion guard — warns before wiping a destination whose source only looks available
- Script-friendly — honest error counts and a meaningful exit code
- Clean progress bars — powered by tqdm
- mise — manages Python and uv per-project
- Everything else (Python 3.12, uv) is installed automatically by mise
- Clone the repository:
git clone https://github.com/your-username/mirroir.git
cd mirroir- Install tools and dependencies:
mise install # installs Python 3.12 + uv (scoped to this project)
mise exec -- uv sync # creates .venv and installs tqdm + pathspec- Copy the example config and edit it:
cp config.json.example config.json- Open
config.jsonand define your backup jobs (see Configuration below).
config.json holds a list of named jobs. Each job needs a source and a destination path.
{
"jobs": [
{
"name": "My Project",
"source": "/home/user/Documents/MyProject",
"destination": "/mnt/backup/MyProject"
},
{
"name": "My Photos",
"source": "/home/user/Pictures",
"destination": "/mnt/backup/Pictures"
}
]
}Paths can be absolute paths on any OS. On Windows, use forward slashes or escaped backslashes:
"source": "C:/Users/YourName/Documents/MyProject"
config.jsonis listed in.gitignore— your paths won't be committed to version control.
mirroir looks for the config in this order, and prints the path it settled on as the first line of every run:
- the path you pass as an argument
config.jsonnext tomirroir.pyconfig.jsonin the current working directory
The script's own folder deliberately beats the working directory. Desktop shortcuts and scheduled tasks often start in some unrelated folder, and a stray config.json sitting there would otherwise take over silently.
| Key | Default | What it does |
|---|---|---|
delete_warn_threshold |
0.5 |
Warn when a plan deletes at least this share of the destination. Set to a value above 1 to switch the warning off. |
{
"delete_warn_threshold": 0.8,
"jobs": [ ... ]
}uv run mirroir # uses the config next to the script
uv run mirroir /path/to/other.json # or point it at a specific configmirroir processes each job in sequence:
- Analyze — scans source and destination, builds a diff plan
- Summarize — prints a breakdown of what will be copied/deleted, grouped by top-level folder
- Confirm — asks
y/nbefore touching anything - Execute — runs the sync with a live progress bar
Example output:
================================================================================
========================= INTERACTIVE BACKUP (MIRROR) ==========================
================================================================================
Config: /path/to/mirroir/config.json
---> My Project <---
SUMMARY (top-level folders):
--------------------------------------------------------------------------------
/
3 to update
assets/
5 to update
2 to delete
old_build/
folder will be completely removed incl. contents
--------------------------------------------------------------------------------
Proceed with these changes? (y/n): y
🚀 Running backup : 100%|██████████| 13/13 [00:00<00:00, 1083.18action/s]
✅ Job completed successfully!
================================================================================
============================= All jobs completed! ==============================
================================================================================
Done: 1 Skipped: 0 With errors: 0
| Step | What happens |
|---|---|
| Scan source | Walks the source tree, skipping anything matched by .gitignore |
| Scan destination | Walks the destination tree to find stale files and folders |
| Build plan | Compares files by size and modification time; only flags genuinely changed files |
| Confirm | Shows the plan and waits for your y/n |
| Copy (parallel) | Up to 16 threads copy new/changed files simultaneously |
| Delete (parallel) | Stale files are removed in parallel |
| Prune dirs (sequential) | Empty or removed directories are cleaned up safely in order |
A backup tool is only useful if a bad file, a locked folder or a stray keystroke can't derail the whole run.
One job never breaks the others. Declining a job, an unreadable file, a missing source folder or an unexpected crash all end that single job. The run moves on to the next one and reports what happened at the end.
Read-only files are handled. Git marks everything under .git/objects/ read-only, and Windows refuses to delete such files. mirroir clears the flag and retries instead of failing.
Mass deletions are flagged. If a plan would delete most of the destination, mirroir says so before the prompt. This almost always means the source only looks present — a reassigned drive letter, a disconnected network share, an unsynced cloud folder — and not that you really want the backup emptied.
Ctrl+C cancels the current job, then asks whether to continue with the rest. Queued copies are dropped immediately rather than draining first.
Errors are counted, not glossed over. A job with failures reports ⚠️ Job completed with N error(s) instead of a green checkmark, and every run ends with a tally:
Done: 3 Skipped: 1 With errors: 1
| Code | Meaning |
|---|---|
0 |
Every job finished cleanly, or you declined them |
1 |
At least one job reported errors, or the run was aborted |
Jobs you decline with n, and jobs whose source folder is missing, count as skipped and leave the exit code at 0.
- External drives — the 16-thread default is tuned for USB/SSD drives. For network shares you may see diminishing returns.
- Large repos — if your source has a
.gitignore, mirroir respects it automatically. No extra setup needed. - Dry run only — answer
nat the prompt to see the plan without running it. - Debugging a failing job — set
MIRROIR_DEBUG=1to get a full traceback instead of a one-line error. - Which config am I actually running? — check the
Config:line at the top of the output before blaming the tool.
MIT