#!/bin/sh # Sauvegarde applicative de la base Postgres, en complément du snapshot OVH. # Lecture seule vis-à-vis de la base : pg_dump prend un snapshot MVCC, il ne verrouille rien. set -eu CONTAINER=${CONTAINER:-myim_postgres} DB=${POSTGRES_DATABASE:-my_info_mate} DB_USER=${POSTGRES_USER:-mym} OUT=${OUT:-./backups} STAMP=$(date -u +%Y%m%dT%H%M%SZ) FILE="$OUT/${DB}_${STAMP}.dump" mkdir -p "$OUT" # Les embeddings sont régénérables depuis le contenu CMS : on garde le schéma, pas les vecteurs. docker exec "$CONTAINER" pg_dump \ --username="$DB_USER" --dbname="$DB" \ --format=custom --compress=6 --no-owner --no-privileges \ --exclude-table-data='public."ContentEmbeddings"' \ > "$FILE" # Un dump tronqué ne doit jamais entrer dans la rotation. if ! docker exec -i "$CONTAINER" pg_restore --list < "$FILE" > /dev/null; then echo "Dump illisible, supprimé : $FILE" >&2 rm -f "$FILE" exit 1 fi echo "$FILE ($(du -h "$FILE" | cut -f1))" # Copie hors du VPS. C'est cette étape qui fait la différence entre un fichier # et une sauvegarde : un dump posé sur la machine qui héberge la base disparaît # avec elle. BACKUP_DEST vide = rien n'est protégé, le script le dit. if [ -z "${BACKUP_DEST:-}" ]; then echo "BACKUP_DEST vide : le dump reste sur cette machine, il ne protege de rien." >&2 else # Un remote rclone s'ecrit "nom:chemin", sans double slash — d'ou le test sur # le prefixe de chemin et non sur "://", qui ne matchait aucun remote. case "$BACKUP_DEST" in /*|./*|../*) mkdir -p "$BACKUP_DEST" && cp "$FILE" "$BACKUP_DEST/" ;; *:*) rclone copy --immutable "$FILE" "$BACKUP_DEST" ;; *) mkdir -p "$BACKUP_DEST" && cp "$FILE" "$BACKUP_DEST/" ;; esac echo "copie -> $BACKUP_DEST" fi # Rétention : 7 quotidiens. Les hebdo/mensuels sont gérés côté stockage objet # par une lifecycle rule, pas ici — le VPS ne doit pas être le gardien de l'historique. ls -1t "$OUT"/${DB}_*.dump 2>/dev/null | tail -n +8 | while read -r old; do echo "rotation: $old" rm -f "$old" done