Files
rar-autopass/tools/backup.py
Claude Agent 6fb92466cb feat(T6/T7): supervizare worker (healthcheck+autoheal) + backup online + cheie partajata
T6 — worker supravegheat:
- app/worker/healthcheck.py: probe pe heartbeat-ul din DB (beat invechit -> exit 1).
  Prinde worker-ul agatat (proces viu, beat inghetat) pe care restart:always nu-l
  vede. Cablat ca healthcheck pe serviciul worker in compose.
- sidecar autoheal: restarteaza efectiv containerul unhealthy (compose simplu doar
  marcheaza, nu restarteaza la unhealthy).

T7 — deploy:
- tools/backup.py: backup ONLINE via Connection.backup (WAL nu se copiaza sigur cu
  cp); --keep N roteste snapshot-urile.
- .env.example documenteaza env-urile; volum persistent numit deja in compose.

Fix critic (split api/worker in 2 containere): AUTOPASS_CREDS_KEY trebuie PARTAJATA
api<->worker, altfel worker nu decripteaza creds-urile criptate de API -> submission
blocate. Acum impusa in compose (${...:?} -> fail explicit daca lipseste).
.gitignore: exceptie !.env.example.

5 teste noi (tests/test_deploy.py). 100 pass total.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 20:20:18 +00:00

80 lines
2.6 KiB
Python

#!/usr/bin/env python3
"""Backup online SQLite (T7) — copie consistenta a bazei pe volum persistent.
SQLite in WAL NU se copiaza sigur cu `cp` (WAL-ul poate avea tranzactii necheckpoint-ate).
Folosim API-ul `Connection.backup` (online, consistent, fara oprirea worker-ului).
Utilizare:
python -m tools.backup # -> <db_dir>/backups/autopass-YYYYMMDD-HHMMSS.db
python -m tools.backup --out /path/snap.db # destinatie explicita
python -m tools.backup --keep 14 # pastreaza ultimele 14, sterge restul
Recomandat: rulat dintr-un cron pe gazda (ex. zilnic), tinta pe volum/montaj separat.
"""
from __future__ import annotations
import argparse
import sqlite3
import sys
from datetime import datetime, timezone
from pathlib import Path
from app.config import get_settings
def backup_db(src: Path, dest: Path) -> Path:
"""Backup online (consistent) din `src` in `dest`. Intoarce `dest`."""
dest.parent.mkdir(parents=True, exist_ok=True)
src_conn = sqlite3.connect(src)
try:
dst_conn = sqlite3.connect(dest)
try:
src_conn.backup(dst_conn)
finally:
dst_conn.close()
finally:
src_conn.close()
return dest
def prune(backup_dir: Path, keep: int) -> list[Path]:
"""Pastreaza cele mai noi `keep` snapshot-uri (dupa nume = timestamp), sterge restul."""
snaps = sorted(backup_dir.glob("autopass-*.db"), reverse=True)
removed = []
for old in snaps[keep:]:
old.unlink(missing_ok=True)
removed.append(old)
return removed
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(description="Backup online SQLite gateway RAR AUTOPASS")
parser.add_argument("--out", type=Path, default=None, help="destinatie explicita (default: <db_dir>/backups/)")
parser.add_argument("--keep", type=int, default=0, help="pastreaza ultimele N snapshot-uri (0 = nu sterge)")
args = parser.parse_args(argv)
src = get_settings().db_path
if not src.exists():
print(f"eroare: baza {src} nu exista", file=sys.stderr)
return 1
if args.out is not None:
dest = args.out
else:
stamp = datetime.now(timezone.utc).strftime("%Y%m%d-%H%M%S")
dest = src.parent / "backups" / f"autopass-{stamp}.db"
backup_db(src, dest)
print(f"backup -> {dest} ({dest.stat().st_size} bytes)")
if args.keep > 0:
removed = prune(dest.parent, args.keep)
if removed:
print(f"sterse {len(removed)} snapshot-uri vechi (pastrez {args.keep})")
return 0
if __name__ == "__main__":
sys.exit(main())