Skip to content

Latest commit

 

History

1 Commit

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

restoreprobe

CI License: MIT Python

A backup you have never restored is a rumour.

Backup software reports that it ran. That is a different claim from the data can come back. restoreprobe answers the second one the only way it can be answered: it pulls files out of the backup for real and compares the bytes with the tree they were supposed to protect.

Everything below is something a green backup log will happily tell you is fine.


Install

pip install -e .          # from a clone

Python 3.10+. No dependencies.

Use it

restoreprobe check --source /srv/data --backup /mnt/backups/nightly.zip

Directories, .zip and .tar[.gz|.bz2|.xz] are supported. By default it restores a seeded sample of 50 files; --full restores everything.

What it tells you

The repository ships a demo whose backup is sabotaged in three quiet ways — each one the kind that a backup log calls a success. Build it and probe it:

$ python examples/make_demo.py
$ restoreprobe check --source examples/demo/live --backup examples/demo/nightly.zip --full --quiet

source  examples\demo\live  9 files
backup  examples\demo\nightly.zip  zip, 6 entries
newest  2026-08-27 03:44 UTC  (0 days old)

Restored every file and compared the bytes  (2.0 KB in 0.1s, seed 0)

1 × restored, but different from the live file
    db/dump.sql  restored 1880 bytes, the live file has 1720

1 × restored as an empty file
    notes/todo.md  restored 0 bytes, the live file has 59

3 × never backed up
    photos/beach.jpg  not present in the backup
    photos/city.jpg  not present in the backup
    photos/family.jpg  not present in the backup

Where the gaps are:
  photos/                      3/3 missing   ENTIRE FOLDER

coverage 66.7 %   ·   4 of 6 sampled files restored identically
NOT RESTORABLE — 5 defect(s) above.

Three different disasters, none of which shows up in an archive listing:

What the listing says What the restore says
photos/ isn't mentioned an exclude rule swallowed a whole folder
notes/todo.md — 0 bytes, fine the file came back empty
db/dump.sql — present, 1880 bytes those are yesterday's bytes

What it checks

Finding Meaning
missing in the live tree, absent from the backup
unrestorable listed by the backup, and the data will not come out — bit rot, a broken stream
mismatch restored, and the bytes are not the same
truncated restored as zero bytes while the live file is not empty
unsafe-path an entry that would be written outside the restore directory
extra in the backup, not in the live tree — reported, never a defect

Plus staleness (--max-age DAYS), because a perfect backup of a month ago is still a month of lost work.

unsafe-path is not a formality

An archive is untrusted input, even one you made yourself. An entry named ../../etc/cron.d/payload writes outside wherever you extract it — the classic Zip-Slip. restoreprobe refuses to extract such an entry, reports it, and fails the probe. A tool that says "let me restore this for you" must not be the thing that hands an attacker a write primitive.

Options

Flag Meaning
--source DIR the live tree the backup is supposed to protect
--backup PATH directory, .zip, or .tar[.gz|.bz2|.xz]
--sample N how many files to actually restore (default 50)
--full restore and compare every file — the only real proof
--seed N reproduce an earlier probe exactly
--max-age DAYS fail if the newest file in the backup is older than this
--exclude GLOB skip these paths in the source; repeatable
--receipt PATH write a dated JSON record of the probe
--show-extra also list entries that exist only in the backup

restoreprobe inspect --backup PATH describes a backup on its own, with no source to compare against.

Exit codes

Code Meaning
0 restorable — every sampled file came back byte-identical
1 defects found, or the backup is stale
2 misuse
3 the backup could not be opened at all

Nightly, in cron

restoreprobe check \
  --source /srv/data \
  --backup /mnt/backups/nightly.tar.gz \
  --sample 100 --seed "$(date +%j)" \
  --max-age 2 \
  --receipt "/var/log/restoreprobe/$(date +%F).json" \
  || mail -s "backup probe FAILED" ops@example.com

A different seed each day walks a different hundred files, so a month of cheap probes covers far more of the tree than one expensive one. The receipts pile up as dated, hash-bearing evidence that somebody actually looked — and because the seed is in each one, anybody can re-run exactly that probe.

Design notes

Sampling limits what is restored, never what is counted. Coverage — is a file in the backup at all? — is answered from the listing, for every file, on every run. So a cheap nightly probe that restores 50 files still notices that an entire folder has vanished. Only the byte-for-byte comparison is sampled.

The sample is a shuffle, then a cut. Taking "the first 50" alphabetically would probe the same corner of the tree every single night and never touch the rest. It is seeded so a probe is reproducible.

Sampling is never dressed up as proof. A sampled run says so in the output: evidence about the sample, not a proof about the whole backup. Only --full drops that line.

Symlinks are skipped, not followed. Following them compares the same bytes twice, and with a loop it never finishes. More importantly, a backup that stored the link and one that stored the target are different backups, and silently equating them would hide the difference.

A bug found while building this

Zip archives store MS-DOS timestamps in the local time of the machine that wrote them, with no zone attached. Reading them as UTC — as the first version did — dates every archive written east of Greenwich into the future, and a backup dated in the future can never be reported as stale. The age check would have been silently dead for most of the world. It has a regression test now: test_zip_timestamps_are_read_as_local_time_not_utc.

Honest limits

  • It verifies data, not your restore procedure. Byte-identical files are necessary and not sufficient: permissions, ownership, extended attributes, hard links, sparseness and database consistency are all out of scope. A green probe means the bytes are there.
  • It compares against the live tree as it is right now. A file legitimately edited since the backup ran will show as a mismatch. That is honest, not a false positive — but on a busy tree, expect it.
  • Encrypted and deduplicating backups need their own tooling. For restic, borg or kopia, use their check --read-data, which knows how to verify their internal structure. restoreprobe is for the enormous number of backups that are, in the end, a folder or an archive.
  • A --sample run is evidence, not proof. It says so itself.

License

MIT — see LICENSE.

About

A backup you have never restored is a rumour. Restore a sample for real and compare the bytes.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages