fix(sauvegardes): restauration testee en CI, copie de la base avant migration en tout-en-un

- La CI joue les commandes de sauvegarde et de restauration du README sur une
  vraie installation compose (base videe, medias effaces) et verifie comptes,
  arbres, personnes, migrations et fichiers (tools/docker-restore-check.sh).
- README : restauration avec --if-exists, sinon pg_restore echoue sur une
  base vide. Sauvegarde avant chaque mise a jour, dite clairement.
- Image tout-en-un : copie de la base dans /data/backups avant d'appliquer une
  migration (5 gardees), demarrage arrete si la copie echoue.
- backup.sh : l'empreinte d'une archive de medias gardee disparaissait avec la
  rotation des fichiers d'empreintes, elle est recalculee.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
IliesAllaliandClaude Opus 5.5 committed 2026-10-19 00:06:30 +02:00
1 parent 1504a1e124
commit 217d503e20
6 files changed
+84 -10

No files matched your search

+10
View File
@@ -182,6 +182,13 @@ jobs:
- name: Test de fumée par le port web
run: bash tools/docker-smoke.sh http://localhost:8080
# Les commandes de sauvegarde et de restauration du README, jouées pour de vrai
- name: Sauvegarde puis restauration
run: |
set -euo pipefail
bash tools/docker-restore-check.sh
bash tools/docker-smoke.sh http://localhost:8080
# Un second front qui joint l'API par le nom de son conteneur (API_UPSTREAM), comme sur Umbrel.
- name: Front avec une autre adresse d'API
run: |
@@ -233,6 +240,9 @@ jobs:
docker restart aio
bash tools/docker-smoke.sh http://localhost:8080
docker logs aio 2>&1 | grep -c "première installation" | grep -qx 1
# Base à jour au redémarrage : pas de sauvegarde avant migration, mais pg_dump marche dans l'image
[ -z "$(docker exec aio ls -A /data/backups 2>/dev/null)" ]
[ "$(docker exec aio runuser -u postgres -- pg_dump -Fc parenthese | wc -c)" -gt 1000 ]
- name: Journaux en cas d'échec
if: failure()
+2 -2
View File
@@ -99,7 +99,7 @@ docker run --rm -v parenthese_media:/data -v "$PWD":/backup alpine tar -czf /bac
La première commande sauvegarde la base. Elle contient les personnes, leurs liens, les comptes et les contributions, et elle sait à qui appartient chaque photo, vidéo ou enregistrement. La seconde sauvegarde les fichiers eux-mêmes. Il faut les deux. Restaurez la base seule et les souvenirs pointent vers des fichiers disparus. Restaurez l'archive seule et vous avez un dossier de fichiers rattachés à personne. L'export GEDCOM n'est pas une sauvegarde non plus. Il sert à emmener l'arbre vers un autre logiciel de généalogie et laisse de côté les médias et les annotations.
Adaptez `-U parenthese` et le nom de base si vous avez changé `POSTGRES_USER` ou `POSTGRES_DB`. Pour restaurer la base : `docker compose exec -T db pg_restore -U parenthese -d parenthese --clean < parenthese-db-AAAA-MM-JJ.dump`, puis décompressez l'archive des médias dans le volume avec la même commande `docker run`, `tar -xzf` à la place de `-czf`.
Adaptez `-U parenthese` et le nom de base si vous avez changé `POSTGRES_USER` ou `POSTGRES_DB`. Pour restaurer la base : `docker compose exec -T db pg_restore -U parenthese -d parenthese --clean --if-exists < parenthese-db-AAAA-MM-JJ.dump`, puis décompressez l'archive des médias dans le volume avec la même commande `docker run`, `tar -xzf` à la place de `-czf`.
Le script `deploy/backup.sh` fait la même chose (dump PostgreSQL, archive des médias, sommes de contrôle, rotation, envoi S3 optionnel) pour une installation sans Docker : il lit `backend/.env` et a besoin de `pg_dump` sur la machine. Ses réglages passent par les variables `BACKUP_ROOT`, `BACKUP_RETENTION_DAYS` et `BACKUP_S3_BUCKET`.
@@ -111,7 +111,7 @@ docker compose pull && docker compose up -d
`latest` suit la branche main. Pour rester sur une version, renseignez `PARENTHESE_VERSION` dans `.env` (par exemple `1.0`). Depuis un clone, `git pull && docker compose up -d --build` reconstruit depuis les sources.
Les migrations de base de données s'appliquent toutes seules au démarrage de l'API. Faites une sauvegarde avant, par habitude.
Les migrations de base de données s'appliquent toutes seules au démarrage de l'API, et une migration ne se défait pas. Lancez les deux commandes de sauvegarde ci-dessus avant chaque mise à jour. L'image tout-en-un le fait seule : avant d'appliquer une migration, elle copie la base dans `/data/backups` et garde les cinq dernières copies.
## Développer
+2 -2
View File
@@ -105,7 +105,7 @@ docker run --rm -v parenthese_media:/data -v "$PWD":/backup alpine tar -czf /bac
The first command saves the database. It holds the people, their links, the accounts and the contributions, plus the record of which photo, video or recording belongs to whom. The second saves the files themselves. You need both. Restore only the database and the memories point to files that are gone. Restore only the archive and you get a folder of files with no names attached. The GEDCOM export is not a backup either. It moves the tree to another genealogy program and leaves the media and the annotations behind.
Adjust `-U parenthese` and the database name if you changed `POSTGRES_USER` or `POSTGRES_DB`. To restore the database, run `docker compose exec -T db pg_restore -U parenthese -d parenthese --clean < parenthese-db-YYYY-MM-DD.dump`, then extract the media archive into the volume with the same `docker run` command, using `tar -xzf` instead of `-czf`.
Adjust `-U parenthese` and the database name if you changed `POSTGRES_USER` or `POSTGRES_DB`. To restore the database, run `docker compose exec -T db pg_restore -U parenthese -d parenthese --clean --if-exists < parenthese-db-YYYY-MM-DD.dump`, then extract the media archive into the volume with the same `docker run` command, using `tar -xzf` instead of `-czf`.
The `deploy/backup.sh` script does the same (PostgreSQL dump, media archive, checksums, rotation, optional S3 upload) for an installation without Docker. It reads `backend/.env` and needs `pg_dump` on the machine. It is configured through the `BACKUP_ROOT`, `BACKUP_RETENTION_DAYS` and `BACKUP_S3_BUCKET` variables.
@@ -117,7 +117,7 @@ docker compose pull && docker compose up -d
`latest` follows the main branch. To stay on a version, set `PARENTHESE_VERSION` in `.env` (for example `1.0`). From a clone, `git pull && docker compose up -d --build` builds from the source instead.
Database migrations apply on their own when the API starts. Make a backup first, as a habit.
Database migrations apply on their own when the API starts, and a migration cannot be undone. Run the two backup commands above before each update. The all-in-one image does it by itself: before applying a migration, it copies the database to `/data/backups` and keeps the last five copies.
## Development
+16
View File
@@ -78,6 +78,22 @@ stop_all() {
trap 'stop_all; exit 0' TERM INT
cd /app
# Migration en attente sur une base existante : copie de la base dans /data/backups avant de l'appliquer
# (une migration ne se défait pas). Les 5 dernières sont gardées. Si la copie échoue, le conteneur s'arrête.
if [ "$FIRST_START" = 0 ] && ! runuser -u node -- env PATH="$PATH" prisma migrate status >/dev/null 2>&1; then
mkdir -p "$DATA_DIR/backups"
chmod 700 "$DATA_DIR/backups"
dump="$DATA_DIR/backups/parenthese-avant-migration-$(date -u +%Y%m%dT%H%M%SZ).dump"
log "migration en attente : sauvegarde de la base dans $dump"
if ! (umask 077 && runuser -u postgres -- pg_dump -Fc "$DB_NAME" > "$dump"); then
rm -f "$dump"
log "la sauvegarde a échoué, migration annulée (place sur le disque ?)"
stop_all
exit 1
fi
ls -1t "$DATA_DIR"/backups/parenthese-avant-migration-*.dump | tail -n +6 | xargs -r rm -f
fi
log "migrations"
runuser -u node -- env PATH="$PATH" prisma migrate deploy
+3 -6
View File
@@ -98,12 +98,9 @@ echo "[3/5] Generate checksums..."
(
cd "$BACKUP_ROOT"
sha256sum "db/$(basename "$DB_BACKUP_FILE")"
if [ "$MEDIA_IS_NEW" = "1" ]; then
sha256sum "media/$(basename "$MEDIA_BACKUP_FILE")"
else
# empreinte déjà calculée le jour où l'archive a été faite
grep -h " media/$(basename "$MEDIA_BACKUP_FILE")" "$META_DIR"/genealogy-*.sha256 2>/dev/null | head -1 || true
fi
# Archive du jour ou archive gardée : son empreinte est recalculée, celle du jour où elle a été faite
# disparaît avec la rotation des fichiers d'empreintes (14 jours) alors que l'archive reste
sha256sum "media/$(basename "$MEDIA_BACKUP_FILE")"
) > "$CHECKSUM_FILE"
echo "[4/5] Optional offsite upload..."
+51
View File
@@ -0,0 +1,51 @@
#!/usr/bin/env bash
# Sauvegarde puis restauration d'une installation compose avec les commandes du README, et contrôle
# que rien ne s'est perdu : comptes, arbres, personnes, migrations et fichiers des médias.
#
# Usage : bash tools/docker-restore-check.sh (depuis le dossier du dépôt, installation démarrée)
# La CI le lance après le test de fumée, qui a déjà créé un compte et un arbre.
# Attention : la base et les médias sont effacés puis restaurés. Jamais sur une installation en service.
set -euo pipefail
cd "$(dirname "$0")/.."
DB_USER="${POSTGRES_USER:-parenthese}"
DB_NAME="${POSTGRES_DB:-parenthese}"
MEDIA_VOLUME="parenthese_media"
WORK="$(mktemp -d)"
trap 'rm -rf "$WORK"' EXIT
fail() { echo "ÉCHEC : $*" >&2; exit 1; }
sql() { docker compose exec -T db psql -U "$DB_USER" -d "$DB_NAME" -v ON_ERROR_STOP=1 -At -c "$1"; }
counts() { sql "SELECT (SELECT count(*) FROM users) || ' ' || (SELECT count(*) FROM trees) || ' ' || (SELECT count(*) FROM persons) || ' ' || (SELECT count(*) FROM _prisma_migrations)"; }
docker run --rm -v "$MEDIA_VOLUME":/data alpine sh -c 'echo "souvenir de test" > /data/restore-check.txt'
before="$(counts)"
read -r users trees _ _ <<< "$before"
[ "$users" -ge 1 ] && [ "$trees" -ge 1 ] || fail "il faut au moins un compte et un arbre avant la sauvegarde (lancer d'abord tools/docker-smoke.sh)"
echo "Avant : $before (comptes, arbres, personnes, migrations)"
# Sauvegarde, mêmes commandes que le README (fichiers écrits dans un dossier temporaire)
DUMP="$WORK/parenthese-db-$(date +%F).dump"
ARCHIVE="parenthese-media-$(date +%F).tar.gz"
docker compose exec -T db pg_dump -U "$DB_USER" -Fc "$DB_NAME" > "$DUMP"
docker run --rm -v "$MEDIA_VOLUME":/data -v "$WORK":/backup alpine tar -czf "/backup/$ARCHIVE" -C /data .
[ -s "$DUMP" ] || fail "dump vide"
# Perte simulée : base vidée, médias effacés
docker compose stop api
sql "DROP SCHEMA public CASCADE; CREATE SCHEMA public;" >/dev/null
docker run --rm -v "$MEDIA_VOLUME":/data alpine find /data -mindepth 1 -delete
[ "$(sql "SELECT count(*) FROM information_schema.tables WHERE table_schema = 'public'")" = 0 ] || fail "la base n'a pas été vidée"
# Restauration, mêmes commandes que le README
docker compose exec -T db pg_restore -U "$DB_USER" -d "$DB_NAME" --clean --if-exists < "$DUMP"
docker run --rm -v "$MEDIA_VOLUME":/data -v "$WORK":/backup alpine tar -xzf "/backup/$ARCHIVE" -C /data
after="$(counts)"
echo "Après : $after"
[ "$after" = "$before" ] || fail "la restauration ne rend pas les mêmes données ($before avant, $after après)"
[ "$(docker run --rm -v "$MEDIA_VOLUME":/data alpine cat /data/restore-check.txt)" = "souvenir de test" ] || fail "fichier des médias absent après restauration"
docker run --rm -v "$MEDIA_VOLUME":/data alpine rm -f /data/restore-check.txt
docker compose start api
echo "OK : sauvegarde et restauration du README vérifiées"