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:
@@ -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
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Binary file not shown.
Reference in New Issue
Block a user