#!/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 set -eu FILE=${1:?usage: extract-instance.sh } INSTANCE=${2:?usage: extract-instance.sh } 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))"