Commit Graph
70 Commits
Author SHA1 Message Date
a987180653 Replace PID-file process killing with a real fcntl.flock() lock (#6)
* Replace PID-file process killing with a real fcntl.flock() lock

set_backups_lock()/release_backups_lock() now use an exclusive
fcntl.flock() on the lock file's inode instead of trusting the PID
written inside it. The kernel releases the lock automatically on
normal exit, on any signal, and on a crash - no PID liveness check or
staleness heuristic is needed, and there is nothing left in this code
path that sends a signal to another process.

- The open file descriptor is held for the full operation in a
  module-level _held_locks map keyed by the backups directory, and
  release_backups_lock() only ever releases a descriptor this process
  itself acquired, never one it infers from the lock file's content.
- PID and start time are still written into the lock file, but purely
  as diagnostics for whoever reads it (e.g. in a warning log line);
  they play no role in acquisition or release decisions anymore.
- --force no longer sends SIGTERM/SIGKILL to the previous holder. It
  now blocks on the same flock() until that process releases the lock
  on its own (exit, signal, or crash all count).
- release_backups_lock() intentionally stops unlinking the lock file:
  since flock() locks an inode rather than a path, unlinking while
  another process is blocked opening the same path would let that
  process (or a third one) acquire an independent lock on a fresh
  inode at the same path, defeating mutual exclusion.

Rewrites TestBackupLock to match: a real second file descriptor with
its own flock() stands in for "another process" in the fast unit
tests, and a new subprocess-based test proves end-to-end that only one
process can hold the lock and that killing the holder with SIGKILL
frees it for the next acquirer without our code ever calling os.kill.

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

* Address Copilot review feedback on flock-based lock

- Make the fcntl import optional (try/except ImportError -> None), so
  importing curateipsum.backup no longer crashes on Windows before
  cli.py's own "Not supported platform" check gets a chance to run.
- Guard set_backups_lock() against being called twice for a directory
  this process already holds: it now fails fast instead of silently
  overwriting the tracked fd in _held_locks, which would leak the
  original descriptor and leave the lock held even after
  release_backups_lock() closes only the newest one.
- Wrap the post-acquire metadata write (ftruncate/write/fsync) in a
  try/except: a failure there now releases the flock and closes the fd
  before returning False, instead of leaving the lock held with
  no caller aware of it.
- Cap _read_lock_metadata()'s read and strip newlines before logging,
  so a stale or forged lock file can't inject bogus log lines or force
  an unbounded read.

Adds a regression test for the double-acquire guard.

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 22:00:21 +00:00
a48eaf8117 Make snapshot creation transactional (#5)
* Make snapshot creation transactional

Build each snapshot in a private .incomplete-<name> staging directory
instead of mutating the final backup path directly. Any failure while
building the snapshot (hardlink failure, rsync error, or a single
Actions.ERROR entry) is now fatal: the staging directory is discarded
and BackupFailedError is raised, so a failed or interrupted backup can
never silently look like a success.

On success, the completion marker is written and the directory fsync'd
before an atomic os.rename() into the final snapshot name, so a crash
can never leave a partially-populated directory under a real snapshot
name.

Also:
- cleanup_stale_staging_dirs() removes abandoned staging directories
  left by a crashed run before a new backup starts.
- _reserve_snapshot_name() avoids same-second snapshot name collisions.
- cli.py releases the backup lock in finally and returns a nonzero
  exit code when BackupFailedError is raised.

Updates the error-recovery tests that previously asserted failures
were swallowed silently to assert BackupFailedError is raised instead,
matching the new fail-loud contract.

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

* Address Copilot review feedback on transactional snapshots

- Remove unused uuid import.
- _reserve_snapshot_name() also checks for a collision with an existing
  .incomplete-<name> staging directory, not just the final snapshot
  name.
- _fsync_dir() opens with O_DIRECTORY where available.
- Fsync the completion marker file itself, and fsync the parent
  backups_dir after the rename, so the marker and the rename survive a
  crash/power loss.
- Re-raise an already-BackupFailedError unchanged (preserving its
  traceback) instead of wrapping it in a new instance; only
  fs.BackupCreationError gets wrapped.
- Wrap the initial os.mkdir(staging.path) failure in BackupFailedError
  so every failure path uses the documented exception.

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 21:30:39 +00:00
snegov 48ebb7f9ae Remove requirements-dev.txt in favor of uv dependency management
Dependencies are now managed via pyproject.toml and installed with
'uv sync --all-extras --dev'. This aligns with the workflow update.
2026-02-05 22:37:33 -08:00
snegov 96f063d877 Update Python version requirement to 3.8 or higher 2026-02-06 04:51:30 +00:00
snegov 79b56d770f Update github workflow to use uv 2026-02-05 20:44:03 -08:00
snegov 7c54bf3823 Fix PseudoDirEntry follow_symlinks handling and add documentation
- Fix follow_symlinks parameter being ignored in is_dir(), is_file()
- Change from realpath() to abspath() to preserve symlinks
- Add separate caches for stat() and lstat() results
- Remove incorrect follow_symlinks param from is_symlink()
- Add comprehensive docstring explaining purpose and design

When follow_symlinks=False, methods now correctly return False for
symlinks instead of following them. Previously all symlinks were
resolved, breaking symlink-aware backup operations.

Fixes #8
2026-02-04 23:00:06 -08:00
snegov 52b4fef814 Add error recovery tests for backup failures
Adds 7 new tests in TestErrorRecovery class covering:
- Hardlink failure cleanup (incomplete backup removal)
- Rsync failure cleanup
- Detection of incomplete backups without marker files
- Lock handling after backup failures
- Permission error handling during cleanup

Tests verify that failed backups are properly cleaned up and don't interfere
with subsequent backup operations or lock management.
2026-02-04 22:21:02 -08:00
snegov 84c8425422 Add tests for external rsync and hardlink tools
Tests --external-rsync and --external-hardlink flags with conditional
execution based on tool availability. Verifies external tools correctly
sync files and preserve hardlinks for unchanged files.
2026-02-04 22:14:04 -08:00
snegov e9a65a9b88 Add tests for rsync edge cases
Covers real-world scenarios: broken symlinks, unicode/spaces in filenames,
empty files, and deeply nested paths. All edge cases handled correctly.
2026-02-04 22:00:55 -08:00
snegov 95614b6eec Add metadata preservation tests for copy_direntry
Verify that timestamps, permissions, and ownership are correctly
preserved when copying files, directories, and symlinks. Tests account
for filesystem behavior where reading a file updates its atime by
capturing source timestamps before the copy operation.
2026-02-04 21:56:25 -08:00
snegov 3214b48ae8 Add tests for ISO week calendar boundary edge cases
Tests cover week 53 year transitions, same week numbers across years,
and late December dates in week 1 of next year. Discovered bug #33
where weekly retention compares only week number without ISO year,
causing backups 1 year apart to be incorrectly deduplicated.
2026-02-04 21:49:23 -08:00
snegov f0894584d4 Add tests for force lock behavior with signal handling
Tests verify --force flag properly handles running processes:
- SIGTERM success (process terminates gracefully)
- SIGKILL escalation (process survives SIGTERM)
- Kill failure handling (permission errors)
2026-02-04 21:03:46 -08:00
snegov 06583f7e1a Add test for combined retention policy interaction
Add test_combined_retention_policies to verify all 5 retention policies
(keep_all, keep_daily, keep_weekly, keep_monthly, keep_yearly) work
correctly together. Test validates precedence rules where later retention
tiers can override earlier ones (e.g., monthly overrides weekly, yearly
overrides monthly) and documents threshold inclusive behavior.

Test uses 36 backups spanning 2017-2021 with expected 21 backups kept
based on policy interaction.
2026-02-04 20:58:06 -08:00
snegov fb9dcbd392 Refactor: split rsync tests into separate module
Move all rsync-related tests (44 tests) from test_fs.py to new
test_rsync.py module for better organization:
- TestRsync (14 tests)
- TestRsyncBasic (5 tests)
- TestParseRsyncOutput (17 tests)
- TestRsyncExt (8 tests)

Extract shared helper functions to conftest.py to eliminate duplication:
- create_file(), create_dir(), relpath()
- check_identical_file()
- common_fs_dirs fixture

This improves test organization and maintainability by grouping related
tests and removing code duplication between test modules.
2026-02-04 20:23:51 -08:00
snegov dc9bc2e5e4 Add comprehensive CLI tests
Implements 26 tests across 6 test classes covering all CLI functionality:
- Argument parsing and flag handling
- Platform validation (Linux/macOS only)
- External tool availability checks (rsync, cp, gcp)
- Directory validation
- Lock acquisition and release
- Backup execution flow

Brings CLI module from zero coverage to complete test coverage.
Total test count: 99 → 127 tests (all passing).
2026-02-04 19:40:59 -08:00
snegov fe48010fc9 Add comprehensive tests for rsync output parsing
Implements TestParseRsyncOutput class with 17 tests covering all code
paths in _parse_rsync_output() function. Tests cover deletions, file/
directory/symlink creation, rewrites, metadata updates (permissions,
ownership, timestamps), and error handling for invalid input.
2026-02-04 19:36:24 -08:00
snegov ee1df4cb21 Update tests 2026-02-04 19:31:29 -08:00
snegov 5dc9235992 Fix potential NameError in copy_file exception handler
Initialize file descriptors to None before opening to prevent
UnboundLocalError in the finally block if file opening fails.

Changes:
- Initialize fin = None and fout = None at function start
- Check if descriptors are not None before closing
- Remove UnboundLocalError from exception handling (no longer needed)

This improves code quality by eliminating the need to catch an error
that indicates a logic bug. The existing tests verify correct behavior.

Fixes #6
2026-02-03 22:20:52 -08:00
snegov 32ce113608 Migrate to pyproject.toml with setuptools_scm
Replace custom version management with setuptools_scm to eliminate
source file modifications during installation. Version is now derived
from git tags at build time without writing to tracked files.

Changes:
- Add pyproject.toml with full project metadata
- Simplify setup.py to minimal backwards-compatible shim
- Remove curateipsum/_version.py from version control
- Add _version.py to .gitignore (auto-generated at build time)
- Use PEP 440 compliant version strings

Fixes #18
2026-02-03 21:27:49 -08:00
snegov 6fd6051642 Merge pull request #4 from snegov/claude/fix-github-actions-python-versions-011CUyaqGCtZoDZVxANHBu2n
Fix GitHub Actions Python version testing
2025-11-09 20:55:42 -08:00
snegov 6d6038b027 Remove backup markers from previous backups 2023-05-07 22:00:05 -07:00
snegov 5746a9bc82 Update github actions 2023-05-07 15:33:45 -07:00
snegov 45c2480778 Fix spent time calculation 2023-05-06 23:20:21 -07:00
snegov 62fcfe5881 Add running backup lock and marker of finished backup 2023-05-06 23:06:15 -07:00
snegov 775234a433 Fix incorrect logging 2022-08-28 19:42:00 +04:00
snegov ae95856a58 Fix symlinks issue in nest_hardlink function 2021-11-16 00:00:19 +03:00
snegov 2631b78d4d Handle socket files 2021-11-15 23:12:51 +03:00
snegov 04692be42d Minor fix in printing version info 2021-11-15 10:28:48 +03:00
snegov e7774a5e5a Create run_tests.yml 2021-11-15 10:28:10 +03:00
snegov 9cf99f78b4 Add version support 2021-11-15 01:09:11 +03:00
snegov a927f3e2d3 Minor fixes 2021-11-13 07:57:11 +03:00
snegov 84546243cb Add setup.py 2021-11-13 07:39:23 +03:00
snegov 990faf9a43 Add license 2021-11-12 20:50:32 +03:00
snegov 0c30501b9c Update debug logging 2021-11-12 11:33:53 +03:00
snegov 1cc5271c29 Add lock for backups directory 2021-11-12 11:30:11 +03:00
snegov c64955362a Add backups cleanup 2021-11-12 10:38:28 +03:00
snegov d521ef3c89 Do not create backup if no changes were found 2021-11-09 22:51:49 +03:00
snegov dae1398f49 Do not create _delta dir on first run 2021-11-09 22:50:38 +03:00
snegov 7395268e97 Add processing of changed files 2021-11-09 00:43:18 +03:00
snegov 1ed1032f42 Add nested dir hardlink test 2021-11-02 23:45:11 +03:00
snegov 2dec011ca9 Fix setting symlink attributes in linux 2021-11-02 22:35:23 +03:00
snegov 7daa8d74b3 Add --external-hardlink parameter 2021-10-24 21:39:36 +03:00
snegov f01d24f1bd Fix copying symlinks in Linux 2021-10-23 21:07:14 +03:00
snegov 1b6badf375 Check for rsync before using --external-rsync option 2021-10-23 00:36:39 +03:00
snegov da228dfb99 Code style 2021-10-23 00:22:04 +03:00
snegov 05ecd09f57 Rename copy_file function 2021-10-22 17:30:45 +03:00
snegov 14552db4c8 Add --external-rsync option 2021-08-13 17:06:56 +03:00
snegov eaf2b3c0ea Fix setting directories' mtime in rsync 2021-06-22 07:36:50 +03:00
snegov 036d241585 Fix setting directory mtime in hardlink_dir 2021-06-22 06:56:26 +03:00
snegov 5e042a73c1 Refactoring 2021-06-21 10:25:13 +03:00
snegov 874f2de9ff Enable rsync in initiate_backup 2021-06-21 10:24:53 +03:00
snegov 3620c5c137 Add spent time info 2021-06-21 10:22:37 +03:00
snegov bddf311d99 Remove created backup if fail 2021-06-21 10:21:14 +03:00
snegov 126ae85388 Add dry-run option 2021-06-21 08:27:18 +03:00
snegov 27b781a534 Improve _hardlink_dir_ext 2021-06-20 19:40:04 +03:00
snegov 21b854e92d Fix setting mtime for directories in recursive_hardlinking 2021-06-20 19:15:51 +03:00
snegov 862c20ed24 Add rsync support 2021-06-20 18:40:03 +03:00
snegov 17826c6978 Update hardlink tests 2021-06-16 19:42:32 +03:00
snegov 11c8197dba Add tests for fs.hardlink_dir() 2021-06-15 23:05:09 +03:00
snegov 170b73834b Add hardlinking with cp -al 2021-06-10 22:19:26 +03:00
snegov a867dadb75 Unfinished custom rsync 2021-06-10 19:33:08 +03:00
snegov 4d156c8867 Rework hardlink_dir, create initiate_backup 2021-06-10 19:16:23 +03:00
snegov ce32dd7317 Create per-module loggers 2021-05-28 00:41:32 +03:00
snegov d67550cb38 Add rsync func, logging and cli parser 2021-05-27 23:10:55 +03:00
snegov 50d2168b15 hardlink symlinks instead of recreating them 2019-02-11 10:48:38 +03:00
snegov 3c15dd0373 add symlink support to recursive hardlink 2019-01-16 23:02:35 +03:00
snegov a1e4f00a3b add .idea to .gitignore 2019-01-16 22:32:20 +03:00
snegov 235d54fec9 fix: exit on incorrect agruments amount 2019-01-15 22:44:52 +03:00
snegov 3c8c6953f2 add recursive hardlink 2019-01-10 22:58:40 +03:00
snegov 8590ad6a93 Initial commit 2019-01-10 22:46:08 +03:00