* Implement restore command
Add a restore subcommand alongside the existing backup command:
- curateipsum/restore.py: restore_snapshot() and verify_restored(),
reusing fs.scantree/copy_direntry/rm_direntry so metadata handling
(mode, ownership, timestamps, symlinks) matches backup exactly.
- backup.find_backup(): resolve a snapshot by name or latest.
- cli.py: argparse subcommands (backup/restore), with backup implied
when no subcommand is given so existing invocations keep working.
Restore supports dry-run (creates nothing), partial restore by
snapshot-relative path, an overwrite policy (never/always), a
destination-inside-snapshot boundary check, and manifest-checksum
verification of restored content. It takes the same backups_lock a
backup does, since retention could otherwise delete the snapshot
being restored out from under it.
Closes P1 'Provide a safe restoration workflow' in IMPROVEMENTS.md.
* Address Copilot review: symlink-escape boundary check, docstring fix
- restore_snapshot() and verify_restored() now canonicalize each
destination path and refuse to operate on it if it resolves outside
dest_dir - a symlink planted under dest_dir (by another process, or
left over from a prior run) could otherwise redirect a write, delete,
or read outside the intended destination.
- Fixed restore_snapshot()'s docstring: the skipped-paths list reports
what would be skipped on dry-run too, it isn't always empty.
* Address Copilot suppressed findings: stream plan_restore, restore-specific lock message
- plan_restore() is now a generator instead of building a full list in
memory, matching fs.scantree()'s streaming style everywhere else in
the codebase.
- restore CLI logs a restore-specific message on lock-acquire failure,
since set_backups_lock()'s own log line talks about a 'previous
backup', which read wrong for a restore.
* Fix PATH docs: snapshot-relative means source-basename-prefixed
README and CLI help examples for restore's PATH argument omitted the
source directory's basename, which backup.initiate_backup() always
prefixes onto snapshot-relative paths - the given examples would have
matched nothing in an actual snapshot.
---------
Co-authored-by: Maxim Shulga <maxim.shulga@alludo.com>