diff --git a/.gitignore b/.gitignore index 2bff50a..10dc959 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,9 @@ build **/decp.duckdb.tmp **/decp.duckdb.lock **/schema.cache.json + +# Runtime databases (never commit) +users.sqlite +*.sqlite +!tests/*.sqlite +!tests/**/*.sqlite diff --git a/src/backup/cli.py b/src/backup/cli.py index c7b4a75..4046dd2 100644 --- a/src/backup/cli.py +++ b/src/backup/cli.py @@ -1,4 +1,5 @@ import argparse +import logging import os import sys from datetime import datetime, timezone @@ -7,6 +8,8 @@ from src.backup import service from src.backup.config import load_config from src.backup.storage import S3Storage +logger = logging.getLogger(__name__) + def main(argv=None, env=None, storage=None) -> int: env = env if env is not None else os.environ @@ -24,9 +27,14 @@ def main(argv=None, env=None, storage=None) -> int: now = datetime.now(timezone.utc) if args.cmd == "backup": - key = service.run_backup(config, storage, now) - print(f"sauvegarde créée : {key}") - return 0 + try: + key = service.run_backup(config, storage, now) + print(f"sauvegarde créée : {key}") + return 0 + except Exception as exc: + logger.error("Échec de la sauvegarde : %s", exc, exc_info=True) + print(f"Erreur : {exc}", file=sys.stderr) + return 1 if args.cmd == "list": backups = service.list_backups(config, storage) diff --git a/src/backup/service.py b/src/backup/service.py index 08ee401..371faca 100644 --- a/src/backup/service.py +++ b/src/backup/service.py @@ -1,3 +1,4 @@ +import logging import os import tempfile from datetime import datetime @@ -8,6 +9,8 @@ from src.backup.config import BackupConfig from src.backup.rotation import select_retained from src.backup.storage import Storage +logger = logging.getLogger(__name__) + def _dated_keys(storage: Storage, prefix: str) -> list[tuple[str, datetime]]: dated: list[tuple[str, datetime]] = [] @@ -30,6 +33,7 @@ def run_backup(config: BackupConfig, storage: Storage, now: datetime) -> str: for k, ts in dated: if ts not in retained: storage.delete(k) + logger.info("Sauvegarde créée : %s", key) return key @@ -57,6 +61,7 @@ def restore(config: BackupConfig, storage: Storage, key: str, now: datetime) -> backup_copy.write_bytes(db_path.read_bytes()) os.replace(tmp_path, db_path) except Exception: + logger.error("Échec de la restauration depuis %s", key, exc_info=True) tmp_path.unlink(missing_ok=True) raise return backup_copy diff --git a/src/backup/snapshot.py b/src/backup/snapshot.py index eb3660e..e6dbc9d 100644 --- a/src/backup/snapshot.py +++ b/src/backup/snapshot.py @@ -5,6 +5,8 @@ from pathlib import Path def make_snapshot(db_path: Path) -> bytes: + if not db_path.exists(): + raise FileNotFoundError(f"Base de données introuvable : {db_path}") with tempfile.NamedTemporaryFile(suffix=".sqlite", delete=False) as tmp: tmp_path = Path(tmp.name) try: diff --git a/src/backup/storage.py b/src/backup/storage.py index 0e7b734..203cb6c 100644 --- a/src/backup/storage.py +++ b/src/backup/storage.py @@ -14,8 +14,13 @@ class Storage(Protocol): class S3Storage: def __init__( - self, bucket, endpoint_url, access_key, secret_key, region="us-east-1" - ): + self, + bucket: str, + endpoint_url: str | None, + access_key: str, + secret_key: str, + region: str = "us-east-1", + ) -> None: self._bucket = bucket self._client = boto3.client( "s3", diff --git a/tests/backup/test_cli.py b/tests/backup/test_cli.py index fa4db01..a324238 100644 --- a/tests/backup/test_cli.py +++ b/tests/backup/test_cli.py @@ -33,3 +33,14 @@ def test_backup_then_list(tmp_path, capsys): out = capsys.readouterr().out assert "users-" in out assert len(storage.objects) == 1 + + +def test_restore(tmp_path): + env = _env(tmp_path) + storage = FakeStorage() + # Create a backup first + assert cli.main(["backup"], env=env, storage=storage) == 0 + key = next(iter(storage.objects)) + # Restore it + ret = cli.main(["restore", key], env=env, storage=storage) + assert ret == 0 diff --git a/users.sqlite b/users.sqlite deleted file mode 100644 index ef84536..0000000 Binary files a/users.sqlite and /dev/null differ