Dump -Fc avec vérification de relecture et rotation, test de restauration qui compare les comptages table par table, extraction par instance, contrôle de fraîcheur, units systemd et procédure de restauration écrite. Le dump Mongo est volontairement en lecture seule : aucun mongorestore, pour qu'aucune erreur de manipulation ne puisse écrire dans la prod. Ce qui manque pour que ce soit une sauvegarde : BACKUP_DEST n'est pas tranché. Tant qu'il est vide, le dump reste sur la machine qui héberge la base et ne protège de rien — le script le dit à chaque exécution. Et notify.sh ne fait qu'un logger, donc les trois surveillances écrivent dans un journal que personne ne lit. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
88 lines
5.4 KiB
Bash
88 lines
5.4 KiB
Bash
#!/bin/sh
|
|
# Extrait les données d'une seule instance depuis un dump global.
|
|
#
|
|
# Ne touche JAMAIS la base de production : le dump est restauré dans une base
|
|
# jetable, réduit à l'instance demandée, puis re-dumpé. La cible d'un
|
|
# pg_restore est toujours une base neuve.
|
|
#
|
|
# usage: extract-instance.sh <dump-global> <instanceId>
|
|
set -eu
|
|
|
|
FILE=${1:?usage: extract-instance.sh <dump-global> <instanceId>}
|
|
INSTANCE=${2:?usage: extract-instance.sh <dump-global> <instanceId>}
|
|
CONTAINER=${CONTAINER:-myim_postgres}
|
|
DB_USER=${POSTGRES_USER:-mym}
|
|
OUT=${OUT:-./backups}
|
|
SCRATCH="extract_$(date -u +%s)"
|
|
|
|
psql() { docker exec "$CONTAINER" psql -qtAX -v ON_ERROR_STOP=1 --username="$DB_USER" --dbname="$1" -c "$2"; }
|
|
|
|
docker exec "$CONTAINER" createdb --username="$DB_USER" "$SCRATCH"
|
|
trap 'docker exec "$CONTAINER" dropdb --username="$DB_USER" --if-exists "$SCRATCH" >/dev/null 2>&1' EXIT
|
|
|
|
docker exec -i "$CONTAINER" pg_restore \
|
|
--username="$DB_USER" --dbname="$SCRATCH" --no-owner --no-privileges < "$FILE"
|
|
|
|
if [ "$(psql "$SCRATCH" "select count(*) from \"Instances\" where \"Id\" = '$INSTANCE'")" != "1" ]; then
|
|
echo "Instance '$INSTANCE' absente de ce dump. Instances disponibles :" >&2
|
|
psql "$SCRATCH" 'select "Id" || ' ' || "Name" from "Instances"' >&2 || true
|
|
exit 1
|
|
fi
|
|
|
|
# "is distinct from" et non "<>" : une ligne dont InstanceId est NULL n appartient
|
|
# a aucune instance, et "<> x" ne la supprime pas — elle fuiterait dans l export
|
|
# de chaque client. Repere en verifiant que la somme des extractions egale le global.
|
|
#
|
|
# Suppression des enfants avant les parents. Les 7 premières tables n'ont pas
|
|
# d'InstanceId : elles se rattachent par leur parent, et c'est là que se cachent
|
|
# les oublis silencieux — un quiz sans ses questions, un agenda sans ses events.
|
|
psql "$SCRATCH" "
|
|
begin;
|
|
delete from \"QuizQuestions\" q where
|
|
(q.\"SectionQuizId\" is not null and q.\"SectionQuizId\" not in (select \"Id\" from \"Sections\" where \"InstanceId\" = '$INSTANCE'))
|
|
or (q.\"GuidedStepId\" is not null and q.\"GuidedStepId\" not in (select s.\"Id\" from \"GuidedSteps\" s join \"GuidedPaths\" p on p.\"Id\" = s.\"GuidedPathId\" where p.\"InstanceId\" = '$INSTANCE'))
|
|
or (q.\"SectionQuizId\" is null and q.\"GuidedStepId\" is null);
|
|
delete from \"MapAnnotations\" a where
|
|
(a.\"SectionEventId\" is not null and a.\"SectionEventId\" not in (select \"Id\" from \"Sections\" where \"InstanceId\" = '$INSTANCE'))
|
|
or (a.\"ProgrammeBlockId\" is not null and a.\"ProgrammeBlockId\" not in (select b.\"Id\" from \"ProgrammeBlocks\" b join \"Sections\" s on s.\"Id\" = b.\"SectionEventId\" where s.\"InstanceId\" = '$INSTANCE'));
|
|
delete from \"GuidedSteps\" s where s.\"GuidedPathId\" not in (select \"Id\" from \"GuidedPaths\" where \"InstanceId\" = '$INSTANCE');
|
|
delete from \"EventAgendas\" e where coalesce(e.\"SectionAgendaId\", e.\"SectionEventId\") not in (select \"Id\" from \"Sections\" where \"InstanceId\" = '$INSTANCE');
|
|
delete from \"GeoPoints\" g where coalesce(g.\"SectionMapId\", g.\"SectionEventId\") not in (select \"Id\" from \"Sections\" where \"InstanceId\" = '$INSTANCE');
|
|
delete from \"ProgrammeBlocks\" b where b.\"SectionEventId\" not in (select \"Id\" from \"Sections\" where \"InstanceId\" = '$INSTANCE');
|
|
delete from \"AppConfigurationLinks\" l where
|
|
l.\"ConfigurationId\" not in (select \"Id\" from \"Configurations\" where \"InstanceId\" = '$INSTANCE')
|
|
or (l.\"ApplicationInstanceId\" is not null and l.\"ApplicationInstanceId\" not in (select \"Id\" from \"ApplicationInstances\" where \"InstanceId\" = '$INSTANCE'))
|
|
or (l.\"DeviceId\" is not null and l.\"DeviceId\" not in (select \"Id\" from \"Devices\" where \"InstanceId\" = '$INSTANCE'));
|
|
|
|
delete from \"ApplicationInstances\" where \"InstanceId\" is distinct from '$INSTANCE';
|
|
delete from \"GuidedPaths\" where \"InstanceId\" is distinct from '$INSTANCE';
|
|
delete from \"Sections\" where \"InstanceId\" is distinct from '$INSTANCE';
|
|
delete from \"Devices\" where \"InstanceId\" is distinct from '$INSTANCE';
|
|
delete from \"Configurations\" where \"InstanceId\" is distinct from '$INSTANCE';
|
|
delete from \"Resources\" where \"InstanceId\" is distinct from '$INSTANCE';
|
|
delete from \"Users\" where \"InstanceId\" is distinct from '$INSTANCE';
|
|
delete from \"ApiKeys\" where \"InstanceId\" is distinct from '$INSTANCE';
|
|
delete from \"AuditLogs\" where \"InstanceId\" is distinct from '$INSTANCE';
|
|
delete from \"ContentEmbeddings\" where \"InstanceId\" is distinct from '$INSTANCE';
|
|
delete from \"PushNotifications\" where \"InstanceId\" is distinct from '$INSTANCE';
|
|
delete from \"QuestionThemeMonthlies\" where \"InstanceId\" is distinct from '$INSTANCE';
|
|
delete from \"VisitEvents\" where \"InstanceId\" is distinct from '$INSTANCE';
|
|
delete from \"VisitorQuestions\" where \"InstanceId\" is distinct from '$INSTANCE';
|
|
delete from \"Instances\" where \"Id\" <> '$INSTANCE';
|
|
commit;"
|
|
|
|
mkdir -p "$OUT"
|
|
DEST="$OUT/instance_${INSTANCE}_$(date -u +%Y%m%dT%H%M%SZ).dump"
|
|
docker exec "$CONTAINER" pg_dump --username="$DB_USER" --dbname="$SCRATCH" \
|
|
--format=custom --compress=6 --no-owner --no-privileges > "$DEST"
|
|
|
|
echo "--- contenu extrait ---"
|
|
psql "$SCRATCH" "
|
|
select table_name || ' ' || (xpath('/row/c/text()',
|
|
query_to_xml('select count(*) as c from public.\"' || table_name || '\"', false, true, '')))[1]::text::int
|
|
from information_schema.tables
|
|
where table_schema = 'public' and table_type = 'BASE TABLE'
|
|
order by table_name" | awk '{ if ($2 > 0) printf " %-24s %s\n", $1, $2 }'
|
|
|
|
echo "$DEST ($(du -h "$DEST" | cut -f1))"
|