feat(backup): CLI backup/list/restore (#89)
Implement CLI interface for the backup module with three subcommands: - backup: create a backup and apply rotation - list: list available backups - restore: restore a specific backup Also add __main__.py to enable 'python -m src.backup'. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,5 @@
|
|||||||
|
import sys
|
||||||
|
|
||||||
|
from src.backup.cli import main
|
||||||
|
|
||||||
|
sys.exit(main())
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import argparse
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
|
from src.backup import service
|
||||||
|
from src.backup.config import load_config
|
||||||
|
from src.backup.storage import S3Storage
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv=None, env=None, storage=None) -> int:
|
||||||
|
env = env if env is not None else os.environ
|
||||||
|
parser = argparse.ArgumentParser(prog="python -m src.backup")
|
||||||
|
sub = parser.add_subparsers(dest="cmd", required=True)
|
||||||
|
sub.add_parser("backup", help="Créer une sauvegarde et appliquer la rotation")
|
||||||
|
sub.add_parser("list", help="Lister les sauvegardes disponibles")
|
||||||
|
p_restore = sub.add_parser("restore", help="Restaurer une sauvegarde")
|
||||||
|
p_restore.add_argument("key", help="Clé S3 de la sauvegarde à restaurer")
|
||||||
|
args = parser.parse_args(argv)
|
||||||
|
|
||||||
|
config = load_config(env)
|
||||||
|
if storage is None:
|
||||||
|
storage = S3Storage.from_config(config)
|
||||||
|
now = datetime.now(timezone.utc)
|
||||||
|
|
||||||
|
if args.cmd == "backup":
|
||||||
|
key = service.run_backup(config, storage, now)
|
||||||
|
print(f"sauvegarde créée : {key}")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
if args.cmd == "list":
|
||||||
|
backups = service.list_backups(config, storage)
|
||||||
|
if not backups:
|
||||||
|
print("(aucune sauvegarde)")
|
||||||
|
return 0
|
||||||
|
for key, ts in backups:
|
||||||
|
print(f"{ts.isoformat()} {key}")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
if args.cmd == "restore":
|
||||||
|
print("⚠ Arrêtez d'abord le service : systemctl stop decpinfo")
|
||||||
|
backup_copy = service.restore(config, storage, args.key, now)
|
||||||
|
print(f"restauré depuis : {args.key}")
|
||||||
|
print(f"copie de secours de l'ancienne base : {backup_copy}")
|
||||||
|
print("Redémarrez ensuite le service : systemctl start decpinfo")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
return 1
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__": # pragma: no cover
|
||||||
|
sys.exit(main())
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import sqlite3
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from cryptography.fernet import Fernet
|
||||||
|
|
||||||
|
from src.backup import cli
|
||||||
|
from tests.backup.fakes import FakeStorage
|
||||||
|
|
||||||
|
|
||||||
|
def _env(tmp_path: Path) -> dict:
|
||||||
|
db = tmp_path / "users.sqlite"
|
||||||
|
conn = sqlite3.connect(str(db))
|
||||||
|
conn.execute("CREATE TABLE t (val TEXT)")
|
||||||
|
conn.execute("INSERT INTO t VALUES ('v1')")
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
return {
|
||||||
|
"USERS_DB_PATH": str(db),
|
||||||
|
"S3_BUCKET": "b",
|
||||||
|
"S3_BACKUP_PREFIX": "backups",
|
||||||
|
"S3_ENDPOINT_URL": "",
|
||||||
|
"S3_ACCESS_KEY_ID": "AK",
|
||||||
|
"S3_SECRET_ACCESS_KEY": "SK",
|
||||||
|
"BACKUP_ENCRYPTION_KEY": Fernet.generate_key().decode(),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_backup_then_list(tmp_path, capsys):
|
||||||
|
env = _env(tmp_path)
|
||||||
|
storage = FakeStorage()
|
||||||
|
assert cli.main(["backup"], env=env, storage=storage) == 0
|
||||||
|
assert cli.main(["list"], env=env, storage=storage) == 0
|
||||||
|
out = capsys.readouterr().out
|
||||||
|
assert "users-" in out
|
||||||
|
assert len(storage.objects) == 1
|
||||||
Reference in New Issue
Block a user