fix(backup): retirer users.sqlite du suivi git, logging, test restore CLI, garde snapshot (#89)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Colin Maudry
2026-06-24 17:45:40 +02:00
parent 02df6c10a0
commit ac3ba89c0a
7 changed files with 42 additions and 5 deletions
+6
View File
@@ -12,3 +12,9 @@ build
**/decp.duckdb.tmp **/decp.duckdb.tmp
**/decp.duckdb.lock **/decp.duckdb.lock
**/schema.cache.json **/schema.cache.json
# Runtime databases (never commit)
users.sqlite
*.sqlite
!tests/*.sqlite
!tests/**/*.sqlite
+11 -3
View File
@@ -1,4 +1,5 @@
import argparse import argparse
import logging
import os import os
import sys import sys
from datetime import datetime, timezone from datetime import datetime, timezone
@@ -7,6 +8,8 @@ from src.backup import service
from src.backup.config import load_config from src.backup.config import load_config
from src.backup.storage import S3Storage from src.backup.storage import S3Storage
logger = logging.getLogger(__name__)
def main(argv=None, env=None, storage=None) -> int: def main(argv=None, env=None, storage=None) -> int:
env = env if env is not None else os.environ 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) now = datetime.now(timezone.utc)
if args.cmd == "backup": if args.cmd == "backup":
key = service.run_backup(config, storage, now) try:
print(f"sauvegarde créée : {key}") key = service.run_backup(config, storage, now)
return 0 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": if args.cmd == "list":
backups = service.list_backups(config, storage) backups = service.list_backups(config, storage)
+5
View File
@@ -1,3 +1,4 @@
import logging
import os import os
import tempfile import tempfile
from datetime import datetime from datetime import datetime
@@ -8,6 +9,8 @@ from src.backup.config import BackupConfig
from src.backup.rotation import select_retained from src.backup.rotation import select_retained
from src.backup.storage import Storage from src.backup.storage import Storage
logger = logging.getLogger(__name__)
def _dated_keys(storage: Storage, prefix: str) -> list[tuple[str, datetime]]: def _dated_keys(storage: Storage, prefix: str) -> list[tuple[str, datetime]]:
dated: 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: for k, ts in dated:
if ts not in retained: if ts not in retained:
storage.delete(k) storage.delete(k)
logger.info("Sauvegarde créée : %s", key)
return 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()) backup_copy.write_bytes(db_path.read_bytes())
os.replace(tmp_path, db_path) os.replace(tmp_path, db_path)
except Exception: except Exception:
logger.error("Échec de la restauration depuis %s", key, exc_info=True)
tmp_path.unlink(missing_ok=True) tmp_path.unlink(missing_ok=True)
raise raise
return backup_copy return backup_copy
+2
View File
@@ -5,6 +5,8 @@ from pathlib import Path
def make_snapshot(db_path: Path) -> bytes: 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: with tempfile.NamedTemporaryFile(suffix=".sqlite", delete=False) as tmp:
tmp_path = Path(tmp.name) tmp_path = Path(tmp.name)
try: try:
+7 -2
View File
@@ -14,8 +14,13 @@ class Storage(Protocol):
class S3Storage: class S3Storage:
def __init__( 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._bucket = bucket
self._client = boto3.client( self._client = boto3.client(
"s3", "s3",
+11
View File
@@ -33,3 +33,14 @@ def test_backup_then_list(tmp_path, capsys):
out = capsys.readouterr().out out = capsys.readouterr().out
assert "users-" in out assert "users-" in out
assert len(storage.objects) == 1 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
BIN
View File
Binary file not shown.