Files
colibre/docs/superpowers/specs/2026-06-24-sauvegarde-base-utilisateurs-s3-design.md
T
2026-06-24 16:51:45 +02:00

6.9 KiB
Raw Blame History

Sauvegarde de la base utilisateurs sur S3 — conception

Date : 2026-06-24 Statut : conception validée, prête pour le plan d'implémentation

Contexte et objectif

users.sqlite contient les comptes utilisateurs (src/auth/db.py) et les tokens API (src/api/tokens_db.py) — un seul fichier SQLite, ~36 Ko aujourd'hui, attendu jusqu'à ~500 utilisateurs × quelques centaines de lignes. C'est la seule donnée non reproductible du projet (la base DECP en Parquet/DuckDB est régénérable, donc hors périmètre).

L'app tourne sur un VPS unique (systemd + gunicorn, /var/www/<APP_NAME>). On veut prévenir toute perte de données par des sauvegardes régulières vers un stockage compatible S3 (non-Amazon), avec rotation multi-paliers et un mécanisme de restauration.

Décisions validées

  • Déclencheur : timer systemd sur le VPS (indépendant de gunicorn, robuste aux redémarrages, pas de duplication entre workers).
  • Chiffrement : les sauvegardes sont chiffrées côté application avant l'envoi (données personnelles : emails, hash de mots de passe, tokens).
  • Périmètre : le seul fichier users.sqlite (chemin USERS_DB_PATH).

Vue d'ensemble

Un nouveau module src/backup/ expose une CLI à trois sous-commandes : backup, list, restore. Le timer systemd lance backup toutes les heures. Chaque exécution :

  1. produit un snapshot cohérent de users.sqlite (API de backup en ligne de SQLite, sûre même en cas d'écriture concurrente) ;
  2. le compresse (gzip) puis le chiffre ;
  3. l'envoie sur le S3 sous une clé horodatée ;
  4. applique la rotation : recalcule l'ensemble à conserver et supprime les obsolètes.

Schéma de rétention

Union de quatre paliers — une sauvegarde est conservée si elle qualifie pour au moins un palier :

Palier Granularité Horizon ~ Nb gardés
Horaire 1 par heure 12 dernières heures ~12
Bi-quotidien 1 par tranche de 12 h 72 dernières heures ~6
Quotidien 1 par jour 21 derniers jours ~21
Mensuel 1 par mois calendaire 12 derniers mois ~12

Soit ~51 fichiers au régime permanent, chacun de quelques dizaines de Ko → stockage négligeable.

Algorithme — fonction pure

select_retained(timestamps: list[datetime], now: datetime) -> set[datetime] :

  • Pour chaque palier à période fixe (horaire, bi-quotidien, quotidien) : regrouper les horodatages par tranche (floor((now - t) / période)), et pour chaque tranche comprise dans l'horizon, garder le plus récent de la tranche.
  • Pour le palier mensuel : regrouper par mois calendaire ((année, mois)) car les mois ont des durées variables ; garder le plus récent de chaque mois sur les 12 derniers mois.
  • Le résultat est l'union des ensembles retenus de tous les paliers.
  • L'ensemble à supprimer = tous les horodatages présents ensemble retenu.

Aucune I/O dans cette fonction : elle prend la liste des horodatages (extraits des clés S3) et now, et renvoie quoi garder. Testée unitairement de façon exhaustive (chevauchements de paliers, bascules d'horizon, mois variables, ensemble vide).

Modules (src/backup/)

Chaque module a une responsabilité unique et une interface explicite, testable isolément.

  • rotation.pyselect_retained (fonction pure, pas d'I/O). Cœur logique, tests unitaires nombreux.
  • snapshot.py — produit un snapshot SQLite cohérent via l'API de backup en ligne (sqlite3.connect(src).backup(dest)) vers un fichier temporaire, puis gzip. Renvoie le chemin du fichier compressé.
  • crypto.py — chiffrement/déchiffrement symétrique authentifié (Fernet, lib cryptography). Clé lue depuis BACKUP_ENCRYPTION_KEY. Round-trip testé.
  • storage.py — wrapper S3 (upload, list, download, delete) via boto3 avec endpoint_url (compatible tout fournisseur S3 non-Amazon). Configuration lue depuis l'env.
  • cli.pyargparse : backup, list, restore. Point d'entrée appelé par le timer systemd et par l'opérateur pour la restauration.

Nommage des objets S3

Clé : <S3_BACKUP_PREFIX>/users-<YYYYMMDDTHHMMSSZ>.sqlite.gz.enc, horodatage UTC ISO 8601 compact, triable lexicographiquement. La rotation extrait l'horodatage depuis la clé.

Procédure de sauvegarde (backup)

  1. Snapshot cohérent de users.sqlite → fichier temporaire.
  2. gzip.
  3. Chiffrement Fernet.
  4. Upload sur S3 sous la clé horodatée.
  5. Rotation : lister les clés sous le préfixe, parser les horodatages, calculer l'ensemble retenu via select_retained, supprimer le reste.
  6. Nettoyage des fichiers temporaires (y compris en cas d'erreur).

Journalisation de chaque étape (succès/échec, clés uploadées/supprimées) pour suivi dans les logs systemd.

Procédure de restauration (restore, manuelle, avec garde-fous)

  • list → affiche les sauvegardes disponibles (clé, date, taille), triées.
  • restore <clé> :
    1. télécharge l'objet ;
    2. déchiffre puis décompresse vers un fichier temporaire ;
    3. vérifie l'intégrité SQLite (PRAGMA integrity_check) — refuse de restaurer si KO ;
    4. fait une copie de secours de la base courante (users.sqlite.bak-<ts>) ;
    5. remplace de façon atomique (os.replace).
  • Le script avertit d'arrêter le service (systemctl stop <APP_NAME>) avant la restauration et de le redémarrer après.

La restauration est volontairement manuelle : un outil de reprise sur sinistre ne doit jamais restaurer automatiquement.

Déploiement et configuration

  • Unités systemd dans deploy/ :
    • decpinfo-backup.service (type oneshot, exécute python -m src.backup backup) ;
    • decpinfo-backup.timer (OnCalendar=hourly, Persistent=true pour rattraper un créneau manqué après un redémarrage).
  • Nouvelles variables dans .template.env :
    • S3_ENDPOINT_URL, S3_BUCKET, S3_ACCESS_KEY_ID, S3_SECRET_ACCESS_KEY, S3_BACKUP_PREFIX, BACKUP_ENCRYPTION_KEY.
  • Nouvelles dépendances : boto3, cryptography.
  • Gestion de la clé de chiffrement : BACKUP_ENCRYPTION_KEY doit être sauvegardée hors du serveur (gestionnaire de secrets / coffre). Si elle est perdue, les sauvegardes sont irrécupérables.

Tests

  • Unitaires : select_retained (chevauchements, horizons, mois variables, vide) ; round-trip crypto ; nommage/parsing des clés.
  • Intégration : snapshot d'une base SQLite réelle → vérifie une base valide ; cycle complet backup → list → restore sur un faux backend S3 (mock boto3 ou moto), vérifie l'égalité du contenu restauré et la copie de secours créée.

Hors périmètre

  • Sauvegarde de la base DECP (régénérable).
  • Restauration automatique / orchestration multi-serveurs.
  • Réplication temps réel.