#!/usr/bin/env bash # ShopDB restore. # # The DANGEROUS half of the pair, so it is deliberately guarded. A restore is # only safe onto code whose schema is at least the set's - otherwise restored # rows reference columns the live schema has not applied yet and every query on # those tables 500s. This script refuses an incompatible restore rather than # leaving the site half-broken. # # Usage (from the backup container): # docker compose -f docker-compose.yml -f docker-compose.backup.yml \ # run --rm --entrypoint /opt/backup/restore.sh backup # # It does NOT roll back code - that is git's job (git checkout && build). # Restore code FIRST, bring the schema up (flask db upgrade + plugin # upgrade-all), THEN run this to load the data. # # ASCII only, no emojis - matches the repo style gate. set -euo pipefail BACKUPS_ROOT="${SHOPDB_BACKUPS_ROOT:-/backups}" SETNAME="${1:-}" log() { echo "[restore] $*"; } die() { echo "[restore] ERROR: $*" >&2; exit 1; } [ -n "${SETNAME}" ] || die "usage: restore.sh " SETDIR="${BACKUPS_ROOT}/${SETNAME}" DBDUMP="${SETDIR}/db.sql.gz" # Prefer the local fast path; fall back to pulling the set out of restic. if [ ! -f "${DBDUMP}" ]; then log "local dump not found; restoring set from restic" command -v restic >/dev/null 2>&1 || die "restic not available and no local dump" restic restore latest --target / --include "${SETDIR}" \ || die "restic restore of ${SETNAME} failed" fi [ -f "${DBDUMP}" ] || die "no db.sql.gz for set ${SETNAME}" # Compatibility gate: the running schema must cover the set's stamped heads. log "checking schema compatibility" CURRENT_HEADS="$(python3 /opt/backup/manifest.py \ --out /tmp/current-manifest.json \ --migrations /app/migrations --plugins /app/plugins \ >/dev/null 2>&1 && python3 -c \ 'import json;print(",".join(sorted(json.load(open("/tmp/current-manifest.json"))["schemarev"]["core"])))')" SET_HEADS="$(python3 -c \ 'import json,sys;print(",".join(sorted(json.load(open(sys.argv[1]))["schemarev"]["core"])))' \ "${SETDIR}/manifest.json" 2>/dev/null || echo "")" if [ -z "${SET_HEADS}" ]; then die "set ${SETNAME} has no readable manifest - refusing an unverified restore" fi if [ "${CURRENT_HEADS}" != "${SET_HEADS}" ]; then log "current heads : ${CURRENT_HEADS}" log "set heads : ${SET_HEADS}" die "schema mismatch - bring code to the set's revision, run 'flask db upgrade' and 'flask plugin upgrade-all', then retry" fi # Files first, then DB, so the two land together. if command -v restic >/dev/null 2>&1 && [ -n "${RESTIC_REPOSITORY:-}" ]; then log "restoring instance files from restic" restic restore latest --target / --include /app/instance \ || die "restic restore of instance files failed" fi log "loading database dump" gunzip -c "${DBDUMP}" | mysql \ --host="${MYSQL_HOST}" \ --user="${MYSQL_USER}" \ --password="${MYSQL_PASSWORD}" \ || die "database load failed" rm -f "${BACKUPS_ROOT}/.restore-requested" 2>/dev/null || true log "restore of ${SETNAME} complete"