From ac3ba89c0a1b870a1846d460a2c744c87afce78a Mon Sep 17 00:00:00 2001 From: Colin Maudry Date: Wed, 24 Jun 2026 17:45:40 +0200 Subject: [PATCH] fix(backup): retirer users.sqlite du suivi git, logging, test restore CLI, garde snapshot (#89) Co-Authored-By: Claude Sonnet 4.6 --- .gitignore | 6 ++++++ src/backup/cli.py | 14 +++++++++++--- src/backup/service.py | 5 +++++ src/backup/snapshot.py | 2 ++ src/backup/storage.py | 9 +++++++-- tests/backup/test_cli.py | 11 +++++++++++ users.sqlite | Bin 36864 -> 0 bytes 7 files changed, 42 insertions(+), 5 deletions(-) delete mode 100644 users.sqlite 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 ef8453627776ac62dd06d3a3d451733e0f85718f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 36864 zcmeI)Z%^As90zba&=4pgeK0B7Gf(OyPzyTy?6VJPq6|!j2BARM+D%h*zCUU-AT$ZA z?K9}3y^+0%zJWd2-bnYf(jM;OghD8Enp&w{`?cgG_WA7leeNzwY~5{dt~I;bGp+WX z=z7$-;S>ta9nW(dr(oA{yPhti_Cs$yUEI?TZ+I^_b4S0HD}Ots>kH1*@0CBMj;6kt z{H^@w^@YhF-;fVU5P$##AOHafKmY;|_z(q7J|7#a#x)|tyl>M7 zxAOxIJG$LyDsQz`Pw%E%UTvf9)ppj_`lUHdOjt9GBQ={ZDWA|1Rwwb2tWV=5P$##AOL}nNZ_PcC^_lLW@&w09zPu|Imb^Y zOU`V6M{Bpae@C`<_gZsZEgyDd`^iCfo(3^X=978<=KAj1lkA7@(=^;U{4Q$k-ejCH zB6%QV!UYRi5GE?)LdCtUFs6ZFJcvXnwbT(;8B@$pqzFQ-Rbm1X8bY*;BBN=*WI{4+ z6pfO^&oWA>O0v*|0hO6ABTln`a4Ae^FD8SSYJ1s$1Zo);KbhnM4GU zucVDy2`Y{D1rLHK;IX6$Q<_AEk&F;&lqJ%>W-Q^T^nz%2c8_H%VyxhzUXrvr(Q8-e)%6Im$y@C6@*NdisQI)^6p6Q+Z)uus{F; z5P$##AOHafKmY;|fB*y_@Bs^WBdIIdz) z9*PD52tWV=5P$##AOHafKmY;|fWUtj7?8#yrX=FR&PG5O6G_yYfMD{~sQc*r zlmihZ{IXH^r&AI{lqUW3<1x2ik}M)5jCmY}c0j+RTJq2+Ul2Qu0FN0-jIu>x(l8lw zk|<$Rz?3AJ3Z;=jq{_o3IUV1h`2OHZuhR3$5zf-XK}q}1|DQXR=as{EtBtck00Izz z00bZa0SG_<0uX=z1U_+rbkyF4c-xy0g=Kq|@wdK#(5s3YpLl&Bd