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>
80 lines
2.9 KiB
Python
80 lines
2.9 KiB
Python
"""Application des migrations Alembic au demarrage.
|
|
|
|
Deux situations coexistent et doivent toutes deux fonctionner sans
|
|
intervention :
|
|
|
|
* **installation neuve** : la base est vide, toutes les revisions s'appliquent ;
|
|
* **base existante** creee avant Alembic par `create_all` : la table est deja
|
|
la, mais `alembic_version` n'existe pas. On estampille alors la revision
|
|
initiale (aucune modification de donnees) avant de derouler la suite.
|
|
|
|
Aucune operation destructive n'est effectuee : la seule migration de cette
|
|
phase ajoute une colonne nullable.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
from pathlib import Path
|
|
|
|
from alembic import command
|
|
from alembic.config import Config
|
|
from sqlalchemy import inspect
|
|
|
|
from app.config import get_settings
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
BACKEND_ROOT = Path(__file__).resolve().parents[2]
|
|
ALEMBIC_INI = BACKEND_ROOT / "alembic.ini"
|
|
INITIAL_REVISION = "0001_initial_schema"
|
|
MAIN_TABLE = "shared_maps"
|
|
|
|
|
|
def build_config(database_url: str | None = None, *, configure_logger: bool = False) -> Config:
|
|
config = Config(str(ALEMBIC_INI))
|
|
config.set_main_option("script_location", str(BACKEND_ROOT / "migrations"))
|
|
config.set_main_option("sqlalchemy.url", database_url or get_settings().database_url)
|
|
# Les migrations tournent dans le processus applicatif : elles ne doivent pas
|
|
# reconfigurer les logs deja en place.
|
|
config.attributes["configure_logger"] = configure_logger
|
|
return config
|
|
|
|
|
|
def run_migrations(engine=None, database_url: str | None = None) -> None: # type: ignore[no-untyped-def]
|
|
# Import tardif : creer le moteur lit la configuration. En le faisant ici
|
|
# plutot qu'a l'import du module, une configuration invalide remonte comme
|
|
# une erreur attrapable, avec un message d'exploitation et sans traceback.
|
|
from app.db.session import engine as default_engine
|
|
|
|
target = engine or default_engine
|
|
config = build_config(database_url)
|
|
|
|
with target.connect() as connection:
|
|
tables = set(inspect(connection).get_table_names())
|
|
|
|
legacy = MAIN_TABLE in tables and "alembic_version" not in tables
|
|
if legacy:
|
|
# Base d'avant Alembic : on declare l'etat initial deja atteint plutot
|
|
# que de rejouer une creation de table qui echouerait.
|
|
logger.info("Base existante detectee : estampillage de %s", INITIAL_REVISION)
|
|
command.stamp(config, INITIAL_REVISION)
|
|
|
|
command.upgrade(config, "head")
|
|
logger.info("Migrations appliquees (head)")
|
|
|
|
|
|
if __name__ == "__main__": # pragma: no cover - appele par l'entrypoint Docker
|
|
import sys
|
|
|
|
from app.config import ConfigurationError
|
|
|
|
logging.basicConfig(level="INFO", format="%(levelname)-8s | %(message)s", stream=sys.stdout)
|
|
try:
|
|
run_migrations()
|
|
except ConfigurationError as error:
|
|
# Message d'exploitation, pas de traceback : c'est le fichier .env qu'il
|
|
# faut corriger, pas le code.
|
|
print(str(error), file=sys.stderr)
|
|
raise SystemExit(2) from None
|