cc61aed10c Preserve symlinks and old-snapshot immutability (#9)
* Preserve symlinks and old-snapshot immutability

Two real bugs found while implementing this item, both in fs.py:

- copy_direntry() checked entry.is_dir() (default follow_symlinks=True)
  before entry.is_symlink(). A symlink pointing at a directory was
  misclassified as a directory and an empty real directory was created
  in its place, silently destroying the symlink and losing its target
  entirely. Fixed by checking is_symlink() first, and is_dir() with
  follow_symlinks=False.
- rsync()'s permission/ownership-sync branch called os.chmod/os.chown
  on dst_entry.path unconditionally. Two problems: (1) for a symlink
  whose target already matched, this had no follow_symlinks=False
  guard and could rewrite an external symlink target's permissions;
  (2) for a regular file, that path may still be hardlinked to the
  same file in the previous, completed snapshot (hardlink_dir() links
  unchanged files in at the start of every backup) - mutating it in
  place silently corrupted every older snapshot sharing the inode.
  Fixed by skipping symlinks entirely (nothing to sync once their
  target matches) and adding _break_shared_hardlink(), a copy-on-write
  step that gives a file its own inode before any in-place metadata
  change.

Also fixed the dir-mtime-restore loop's is_dir() call (same
default-follow_symlinks issue, lower severity since its own operations
were already lstat-based/no-follow).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* Address Copilot review: preserve ownership/timestamps when breaking hardlinks, fix test fd leak

- _break_shared_hardlink() now explicitly restores the original file's
  uid/gid and atime/mtime on the new private copy before it replaces
  the shared one. copy_file() only carries over mode at create time, so
  without this a caller that only meant to update permissions would
  silently also reset ownership/mtime as a side effect of the
  copy-on-write break itself. Also cleans up the temp file on failure
  and uses a less collision-prone temp name.
- Fixed a file descriptor leak in the new hardlink-immutability test
  (bare open(...).read() without a context manager)
- Strengthened the same test to assert mtime survives the break

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

---------

Co-authored-by: Maxim Shulga <maxim.shulga@alludo.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-03 23:08:24 +00:00
2021-11-12 20:50:32 +03:00

cura-te-ipsum

cura-te-ipsum is a space-efficient incremental backup utility for Linux and macOS that uses hardlinks to minimize storage usage while maintaining complete directory snapshots.

Similar to Time Machine or rsnapshot, cura-te-ipsum creates backups that appear as complete directory trees but intelligently share unchanged files between snapshots, dramatically reducing storage requirements.

Features

  • Space-Efficient Incremental Backups: Creates full directory snapshots using hardlinks, unchanged files share inodes with previous backups
  • Intelligent Retention Policies: Automatic cleanup with configurable grandfather-father-son rotation (daily/weekly/monthly/yearly)
  • Pure Python Implementation: No external dependencies required for basic operation (optional rsync support available)
  • Delta Tracking: Automatically identifies and tracks changed files between backups
  • Backup Integrity: Lock files and completion markers prevent concurrent runs and identify incomplete backups
  • Safe Operations: Dry-run mode to preview changes before execution
  • Cross-Platform: Supports both Linux and macOS

Installation

From Source

git clone https://github.com/snegov/cura-te-ipsum.git
cd cura-te-ipsum
pip install .

Requirements

  • Python 3.8 or higher
  • Linux or macOS operating system
  • Optional: rsync and GNU cp for alternative implementation modes

Usage

Basic Backup

cura-te-ipsum -b /path/to/backups /path/to/source

This creates a timestamped backup in /path/to/backups/YYYY-MM-DD_HH-MM-SS/.

Multiple Sources

cura-te-ipsum -b /backups /home/user/documents /home/user/photos

Command-Line Options

cura-te-ipsum -b BACKUPS_DIR SOURCE [SOURCE ...]

Required Arguments:
  -b BACKUPS_DIR        Directory where backups will be stored
  SOURCE                One or more directories to backup

Optional Arguments:
  -n, --dry-run         Preview changes without creating backup
  -f, --force           Force run even if previous backup is in progress
  -v, --verbose         Enable debug logging
  --external-rsync      Use external rsync instead of Python implementation
  --external-hardlink   Use cp/gcp command for hardlinking

Examples

Dry run to preview changes:

cura-te-ipsum -b /backups /home/user/data --dry-run

Verbose output for debugging:

cura-te-ipsum -b /backups /home/user/data --verbose

Using external rsync:

cura-te-ipsum -b /backups /home/user/data --external-rsync

How It Works

cura-te-ipsum creates complete directory snapshots, but files that haven't changed between backups share the same inode (hardlinked). This means:

  • Each backup appears as a complete, browseable directory tree
  • Only changed or new files consume additional disk space
  • Deleting old backups doesn't affect other snapshots until the last reference is removed

Backup Process

  1. Lock Acquisition: Creates .backups_lock to prevent concurrent operations
  2. Hardlink Creation: Hardlinks all files from the most recent backup
  3. Rsync Sync: Syncs source directories to the new backup, updating changed files
  4. Delta Tracking: Copies changed/new files to .backup_delta directory
  5. Completion Marker: Creates .backup_finished marker file
  6. Cleanup: Removes old backups based on retention policy
  7. Lock Release: Removes lock file

Retention Policy

Default retention (configurable in code):

  • 7 days: Keep all backups
  • 30 days: Keep one backup per day
  • 52 weeks: Keep one backup per week
  • 12 months: Keep one backup per month
  • 5+ years: Keep one backup per year

The cleanup process never deletes the only remaining backup.

Backup Structure

backups/
  2025-01-15_10-30-00/          # backup snapshot
    .backup_finished            # completion marker
    .backup_delta/              # changed files in this backup
    [your backed up files]      # complete directory tree
  2025-01-16_10-30-00/
    .backup_finished
    .backup_delta/
    [your backed up files]
  .backups_lock                 # lock file (only during backup)

Development

Running Tests

pip install -r requirements-dev.txt
pytest

CI/CD

GitHub Actions automatically runs tests on Python 3.6 through 3.11 for every push and pull request.

Author

Maks Snegov (snegov@spqr.link)

Project Status

Development Status: Pre-Alpha

This project is actively maintained and used in production for personal backups, but the API and configuration options may change in future releases.

S
Description
No description provided
Readme GPL-3.0
286 KiB
0 Stars 1 Watchers 0 Forks
Languages
Python 100%