Ships every completed backup off-box over WebDAV via rclone, verified by exact byte-size match before local retention or schedule rotation ever touches it. Fixes prune_orphans to skip confirmed-offloaded backups whose local copy was already purged (it previously hard-deleted their DB row, discarding the only pointer to the remote copy). Installs rclone in the Docker image and gitignores the container's rclone.conf location, which lives inside the bind-mounted repo root and holds live credentials. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XjW4qocnaJxhugUi5ca8Wo
68 lines
2.2 KiB
Python
68 lines
2.2 KiB
Python
# retoor <retoor@molodetz.nl>
|
|
|
|
import tempfile
|
|
from pathlib import Path
|
|
|
|
from devplacepy.services.backup import store
|
|
|
|
|
|
def _make_backup(target, *, offloaded, missing_local=False, schedule_uid=""):
|
|
job_uid = f"job-{store.generate_uid()}"
|
|
uid = store.create_backup(
|
|
target=target, created_by="test", job_uid=job_uid, schedule_uid=schedule_uid
|
|
)
|
|
path = Path(tempfile.gettempdir()) / f"backup-store-test-{uid}.tar.gz"
|
|
if not missing_local:
|
|
path.write_bytes(b"x" * 10)
|
|
store.finalize_backup(
|
|
uid,
|
|
filename=path.name,
|
|
local_path=str(path),
|
|
stats={"bytes_out": 10, "bytes_in": 10, "file_count": 1, "dir_count": 0, "sha256": "abc"},
|
|
)
|
|
if offloaded:
|
|
store.mark_remote_uploaded(uid, f"remote:{target}/{path.name}")
|
|
return uid, path
|
|
|
|
|
|
def test_prune_orphans_spares_offloaded_backups_with_purged_local_copies(local_db):
|
|
offloaded_uid, _ = _make_backup("database", offloaded=True)
|
|
store.mark_local_purged(offloaded_uid)
|
|
orphan_uid, _ = _make_backup("database", offloaded=False, missing_local=True)
|
|
|
|
try:
|
|
removed = store.prune_orphans()
|
|
|
|
remaining = {row["uid"] for row in store.list_backups(limit=1000)}
|
|
assert offloaded_uid in remaining
|
|
assert orphan_uid not in remaining
|
|
assert removed >= 1
|
|
finally:
|
|
store.delete_backup(offloaded_uid)
|
|
|
|
|
|
def test_rotate_schedule_never_removes_a_backup_without_a_confirmed_remote_copy(local_db):
|
|
schedule_uid = store.create_schedule(
|
|
name="test-rotate",
|
|
target="database",
|
|
kind="interval",
|
|
every_seconds=86400,
|
|
cron="",
|
|
keep_last=1,
|
|
created_by="test",
|
|
next_run_at="2026-01-01T00:00:00",
|
|
)
|
|
older_uid, _ = _make_backup("database", offloaded=False, schedule_uid=schedule_uid)
|
|
newer_uid, _ = _make_backup("database", offloaded=True, schedule_uid=schedule_uid)
|
|
|
|
try:
|
|
removed = store.rotate_schedule(schedule_uid, keep_last=1)
|
|
|
|
remaining = {row["uid"] for row in store.list_backups(limit=1000)}
|
|
assert removed == 0
|
|
assert older_uid in remaining
|
|
assert newer_uid in remaining
|
|
finally:
|
|
store.delete_backup(older_uid)
|
|
store.delete_backup(newer_uid)
|