Phase 2.1.1 : durcissement avant exposition publique
Fail-fast sur la configuration. Une limite de debit illisible (10/hours, 10/banana, -1/hour) arrete desormais le demarrage avec un message nommant la variable, au lieu de desactiver silencieusement la protection : seules les valeurs 0, off, none, disabled et vide desactivent volontairement. Meme traitement pour MAX_SHARED_MAPS, MAX_SHARE_SIZE_MB, DEFAULT_SHARE_EXPIRY_DAYS et chaque entree de TRUSTED_PROXY_CIDRS. X-Forwarded-For n'est plus cru sur la seule foi de TRUST_PROXY_HEADERS : la connexion doit aussi provenir d'un reseau declare dans TRUSTED_PROXY_CIDRS (ipaddress, IPv4/IPv6/CIDR). Activer la confiance sans declarer de reseau est refuse au demarrage, faute de quoi la configuration semblerait active sans l'etre. L'adresse retenue est normalisee : les differentes ecritures d'une meme IPv6 ne donnent plus des compteurs distincts. MAX_SHARED_MAPS : comptage et insertion serialises par un verrou de processus, pour que deux creations simultanees ne franchissent pas ensemble la derniere place. Documente comme valable pour un service applicatif unique. Ajoute APP_BIND pour ne pas publier 8080 sur toutes les interfaces en production, une option backup --keep pour la retention, et documente Nginx, Traefik en override, cron et systemd. Corrige un bug de rendu du build local : mon edition avait reecrit docker-entrypoint.sh en CRLF, rendant le shebang introuvable dans le conteneur. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -224,6 +224,36 @@ def _sqlite_backup(source: Path, destination: Path) -> None:
|
||||
origin.backup(copy)
|
||||
|
||||
|
||||
def _apply_retention(destination: Path, keep: int) -> list[Path]:
|
||||
"""Ne garde que les `keep` sauvegardes les plus recentes du meme dossier.
|
||||
|
||||
Le filtre est volontairement etroit : meme dossier, meme extension, et le
|
||||
fichier qui vient d'etre ecrit est toujours conserve. Une commande de
|
||||
sauvegarde ne doit pas devenir un effaceur de fichiers surprise.
|
||||
"""
|
||||
if keep <= 0:
|
||||
return []
|
||||
|
||||
others = sorted(
|
||||
(
|
||||
path
|
||||
for path in destination.parent.glob(f"*{destination.suffix}")
|
||||
if path.is_file() and path != destination
|
||||
),
|
||||
key=lambda path: path.stat().st_mtime,
|
||||
reverse=True,
|
||||
)
|
||||
# La sauvegarde qui vient d'etre ecrite compte comme la plus recente, et
|
||||
# comme l'une des `keep` conservees : on ne peut donc pas finir avec keep+1.
|
||||
ordered = [destination, *others]
|
||||
|
||||
removed = []
|
||||
for path in ordered[keep:]:
|
||||
path.unlink()
|
||||
removed.append(path)
|
||||
return removed
|
||||
|
||||
|
||||
def cmd_backup(args: argparse.Namespace) -> int:
|
||||
source = database_path()
|
||||
if source is None or not source.exists():
|
||||
@@ -237,6 +267,9 @@ def cmd_backup(args: argparse.Namespace) -> int:
|
||||
|
||||
_sqlite_backup(source, destination)
|
||||
print(f"Sauvegarde ecrite : {destination} ({_format_size(destination.stat().st_size)})")
|
||||
|
||||
for removed in _apply_retention(destination, args.keep):
|
||||
print(f"Ancienne sauvegarde supprimee : {removed.name}")
|
||||
return 0
|
||||
|
||||
|
||||
@@ -315,6 +348,12 @@ def build_parser() -> argparse.ArgumentParser:
|
||||
backup = sub.add_parser("backup", help="Sauvegarder la base pendant que l'app tourne")
|
||||
backup.add_argument("destination", help="Chemin du fichier de sauvegarde")
|
||||
backup.add_argument("--yes", action="store_true", help="Ecraser un fichier existant")
|
||||
backup.add_argument(
|
||||
"--keep",
|
||||
type=int,
|
||||
default=0,
|
||||
help="Ne garder que les N sauvegardes les plus recentes du dossier (0 = tout garder)",
|
||||
)
|
||||
backup.set_defaults(func=cmd_backup)
|
||||
|
||||
restore = sub.add_parser("restore", help="Restaurer une sauvegarde (prudent)")
|
||||
|
||||
Reference in New Issue
Block a user