Commit Graph
10 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
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 ee1df4cb21 Update tests 2026-02-04 19:31:29 -08:00
Claude 7c59bbc90b Fix high-priority bugs and add comprehensive test coverage
This commit addresses 8 high-priority issues identified in code analysis.

Fixes #3
Fixes #4
Fixes #5
Fixes #7
Fixes #10
Fixes #19
Fixes #20
Fixes #21

## Critical Bug Fixes

1. **Race condition in lock file creation (#3)**
   - Changed to atomic file creation using os.O_CREAT | os.O_EXCL
   - Prevents two processes from both acquiring the lock
   - Location: curateipsum/backup.py:110-115

2. **Invalid lock file error handling (#4)**
   - Added try/except for corrupted/empty lock files
   - Gracefully removes corrupted locks and retries
   - Location: curateipsum/backup.py:121-133

3. **SIGKILL vs SIGTERM issue (#5)**
   - Now sends SIGTERM first for graceful shutdown
   - Waits 5 seconds before escalating to SIGKILL
   - Allows previous process to clean up resources
   - Location: curateipsum/backup.py:146-156

4. **Wrong stat object for permissions (#7)**
   - Fixed bug where dst_stat was used instead of src_stat
   - Permissions are now correctly updated during rsync
   - Location: curateipsum/fs.py:371

5. **os.chown() fails for non-root users (#10)**
   - Wrapped all os.chown() calls in try/except blocks
   - Logs debug message instead of crashing
   - Allows backups to succeed for non-root users
   - Locations: curateipsum/fs.py:217-221, 228-231, 383-387, 469-472

## Comprehensive Test Coverage

6. **Lock file tests (#19)**
   - Added TestBackupLock class with 7 test cases
   - Tests: creation, concurrent prevention, stale locks, corruption
   - Location: tests/test_backups.py:228-330

7. **Filesystem operation tests (#20)**
   - Added tests/test_fs_extended.py with 6 test classes
   - Tests: copy_file, copy_direntry, rsync, hardlink_dir, scantree, rm_direntry
   - 20+ test cases covering normal and edge cases
   - Location: tests/test_fs_extended.py

8. **Integration tests (#21)**
   - Added tests/test_integration.py with 2 test classes
   - Tests full backup workflow end-to-end
   - Tests: incremental backups, hardlinks, delta dirs, cleanup, recovery
   - 14 test cases covering complete backup lifecycle
   - Location: tests/test_integration.py

## Test Results
All 68 tests pass successfully:
- 11 original backup cleanup tests
- 7 new lock file tests
- 16 original fs tests
- 20 new fs extended tests
- 14 new integration tests

## Impact
These fixes address critical bugs that could cause:
- Data corruption from concurrent backups
- Incomplete cleanup from forced process termination
- Permission sync failures
- Tool unusability for non-root users

The comprehensive test coverage ensures these bugs are caught early
and provides confidence for future refactoring.
2026-02-03 22:09:28 -08:00
snegov 62fcfe5881 Add running backup lock and marker of finished backup 2023-05-06 23:06:15 -07:00
snegov 9cf99f78b4 Add version support 2021-11-15 01:09:11 +03:00
snegov 84546243cb Add setup.py 2021-11-13 07:39:23 +03:00
snegov c64955362a Add backups cleanup 2021-11-12 10:38:28 +03:00