#!/usr/bin/env bash # ============================================================================ # migrate-restore.sh # ---------------------------------------------------------------------------- # Restaure une archive produite par bin/migrate-backup.sh sur un nouveau # serveur : BDD PostgreSQL, fichiers, config, Redis (si présent). # # Usage : # ./bin/migrate-restore.sh # ./bin/migrate-restore.sh --yes # sans confirmation # ./bin/migrate-restore.sh --docker # forcer docker # ./bin/migrate-restore.sh --no-redis # ignorer Redis # ./bin/migrate-restore.sh --no-files # restaurer la BDD seulement # ./bin/migrate-restore.sh --no-db # restaurer les fichiers seulement # # ⚠️ ATTENTION : la restauration ÉCRASE la base de données et les fichiers # de destination. Une sauvegarde locale est créée dans /tmp avant action. # ============================================================================ set -euo pipefail RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[0;33m'; CYAN='\033[0;36m'; RESET='\033[0m' log() { printf "${CYAN}[restore]${RESET} %s\n" "$*"; } ok() { printf "${GREEN}[ ok ]${RESET} %s\n" "$*"; } warn() { printf "${YELLOW}[ warn ]${RESET} %s\n" "$*"; } err() { printf "${RED}[ error ]${RESET} %s\n" "$*" >&2; } # --- Args -------------------------------------------------------------------- ARCHIVE="" ASSUME_YES=0 FORCE_DOCKER=0 SKIP_REDIS=0 SKIP_FILES=0 SKIP_DB=0 while [[ $# -gt 0 ]]; do case "$1" in -y|--yes) ASSUME_YES=1; shift ;; --docker) FORCE_DOCKER=1; shift ;; --no-redis) SKIP_REDIS=1; shift ;; --no-files) SKIP_FILES=1; shift ;; --no-db) SKIP_DB=1; shift ;; -h|--help) sed -n '2,20p' "$0"; exit 0 ;; -*) err "Option inconnue : $1"; exit 1 ;; *) ARCHIVE="$1"; shift ;; esac done [[ -z "$ARCHIVE" ]] && { err "Usage : $0 [--yes] [--docker] [--no-redis|--no-files|--no-db]"; exit 1; } [[ ! -f "$ARCHIVE" ]] && { err "Archive introuvable : $ARCHIVE"; exit 1; } PROJECT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" cd "$PROJECT_DIR" WORK_DIR="$(mktemp -d -t crm-restore-XXXXXX)" trap 'rm -rf "$WORK_DIR"' EXIT log "Projet : $PROJECT_DIR" log "Archive : $ARCHIVE" log "Workdir : $WORK_DIR" # --- Extraction -------------------------------------------------------------- log "Extraction de l'archive…" tar -xzf "$ARCHIVE" -C "$WORK_DIR" if [[ -f "$WORK_DIR/MANIFEST.txt" ]]; then echo "----- MANIFEST -----" cat "$WORK_DIR/MANIFEST.txt" echo "--------------------" fi # --- Lecture de la config destination --------------------------------------- ENV_FILE="" for f in .env.local .env; do [[ -f "$PROJECT_DIR/$f" ]] && ENV_FILE="$PROJECT_DIR/$f" && break done [[ -z "$ENV_FILE" ]] && { err "Aucun .env trouvé sur le serveur cible."; exit 1; } log "Env cible : $ENV_FILE" DATABASE_URL="$(grep -E '^DATABASE_URL=' "$ENV_FILE" | tail -n1 | cut -d= -f2- | sed -e 's/^"//' -e 's/"$//' -e "s/^'//" -e "s/'$//")" re='^postgres(ql)?://([^:]+):([^@]*)@([^:/]+)(:([0-9]+))?/([^?]+)' if [[ ! "$DATABASE_URL" =~ $re ]]; then err "DATABASE_URL non reconnue : $DATABASE_URL"; exit 1 fi PG_USER="${BASH_REMATCH[2]}" PG_PASS="${BASH_REMATCH[3]}" PG_HOST="${BASH_REMATCH[4]}" PG_PORT="${BASH_REMATCH[6]:-5432}" PG_DB="${BASH_REMATCH[7]}" log "Cible BDD : $PG_USER@$PG_HOST:$PG_PORT/$PG_DB" # --- Confirmation ------------------------------------------------------------ if [[ $ASSUME_YES -eq 0 ]]; then echo warn "Cette opération VA ÉCRASER :" [[ $SKIP_DB -eq 0 ]] && echo " - la base $PG_DB sur $PG_HOST:$PG_PORT" [[ $SKIP_FILES -eq 0 ]] && echo " - les dossiers public/media, public/pdf, public/images, public/seo, var/storage, sauvegarde, .env.local" echo read -r -p "Confirmer ? (tapez 'oui') : " ANSWER [[ "$ANSWER" == "oui" ]] || { err "Annulé."; exit 1; } fi # --- Détection du mode ------------------------------------------------------- USE_DOCKER=0 if [[ $FORCE_DOCKER -eq 1 ]]; then USE_DOCKER=1 elif ! command -v psql >/dev/null 2>&1 || ! command -v pg_restore >/dev/null 2>&1; then if command -v docker >/dev/null 2>&1 && docker ps --format '{{.Names}}' | grep -q '^crm_db$'; then USE_DOCKER=1 warn "psql/pg_restore absents, fallback docker exec crm_db" fi fi # --- Helpers psql / pg_restore ---------------------------------------------- run_psql() { if [[ $USE_DOCKER -eq 1 ]]; then docker exec -i -e PGPASSWORD="$PG_PASS" crm_db psql -U "$PG_USER" -d "$1" -v ON_ERROR_STOP=1 else PGPASSWORD="$PG_PASS" psql -h "$PG_HOST" -p "$PG_PORT" -U "$PG_USER" -d "$1" -v ON_ERROR_STOP=1 fi } run_pg_restore() { local dump_file="$1" if [[ $USE_DOCKER -eq 1 ]]; then docker exec -i -e PGPASSWORD="$PG_PASS" crm_db \ pg_restore -U "$PG_USER" -d "$PG_DB" --no-owner --no-acl --clean --if-exists < "$dump_file" else PGPASSWORD="$PG_PASS" pg_restore \ -h "$PG_HOST" -p "$PG_PORT" -U "$PG_USER" -d "$PG_DB" \ --no-owner --no-acl --clean --if-exists "$dump_file" fi } # --- Sauvegarde de sécurité avant écrasement -------------------------------- SAFETY_DIR="/tmp/crm-restore-safety-$(date +%Y%m%d-%H%M%S)" mkdir -p "$SAFETY_DIR" log "Sauvegarde de sécurité dans : $SAFETY_DIR" if [[ $SKIP_DB -eq 0 ]]; then log "Snapshot BDD courante…" if [[ $USE_DOCKER -eq 1 ]]; then docker exec -e PGPASSWORD="$PG_PASS" crm_db \ pg_dump -U "$PG_USER" -d "$PG_DB" --format=custom --no-owner --no-acl > "$SAFETY_DIR/database.dump" 2>/dev/null \ || warn "Snapshot BDD impossible (base inexistante ?)" else PGPASSWORD="$PG_PASS" pg_dump -h "$PG_HOST" -p "$PG_PORT" -U "$PG_USER" -d "$PG_DB" \ --format=custom --no-owner --no-acl -f "$SAFETY_DIR/database.dump" 2>/dev/null \ || warn "Snapshot BDD impossible (base inexistante ?)" fi fi # --- Restauration BDD -------------------------------------------------------- if [[ $SKIP_DB -eq 0 ]]; then DUMP_FILE="$WORK_DIR/database.dump" [[ ! -f "$DUMP_FILE" ]] && { err "database.dump absent de l'archive."; exit 1; } log "Création/réinitialisation de la base $PG_DB…" # Création si inexistante (depuis postgres) if [[ $USE_DOCKER -eq 1 ]]; then docker exec -e PGPASSWORD="$PG_PASS" crm_db psql -U "$PG_USER" -d postgres -tc \ "SELECT 1 FROM pg_database WHERE datname='$PG_DB'" | grep -q 1 \ || docker exec -e PGPASSWORD="$PG_PASS" crm_db createdb -U "$PG_USER" "$PG_DB" else PGPASSWORD="$PG_PASS" psql -h "$PG_HOST" -p "$PG_PORT" -U "$PG_USER" -d postgres -tc \ "SELECT 1 FROM pg_database WHERE datname='$PG_DB'" | grep -q 1 \ || PGPASSWORD="$PG_PASS" createdb -h "$PG_HOST" -p "$PG_PORT" -U "$PG_USER" "$PG_DB" fi log "pg_restore en cours…" run_pg_restore "$DUMP_FILE" || warn "pg_restore a renvoyé des warnings (souvent bénins)." ok "BDD restaurée" fi # --- Restauration des fichiers ---------------------------------------------- if [[ $SKIP_FILES -eq 0 && -d "$WORK_DIR/files" ]]; then log "Sauvegarde des dossiers existants…" for p in public/media public/images public/pdf public/seo public/tmp-sign var/storage sauvegarde; do if [[ -e "$PROJECT_DIR/$p" ]]; then mkdir -p "$SAFETY_DIR/$(dirname "$p")" cp -a "$PROJECT_DIR/$p" "$SAFETY_DIR/$p" fi done [[ -f "$PROJECT_DIR/.env.local" ]] && cp -a "$PROJECT_DIR/.env.local" "$SAFETY_DIR/.env.local" log "Copie des fichiers depuis l'archive…" # rsync si dispo (delta + permissions), sinon cp -a if command -v rsync >/dev/null 2>&1; then rsync -a "$WORK_DIR/files/" "$PROJECT_DIR/" else cp -a "$WORK_DIR/files/." "$PROJECT_DIR/" fi ok "Fichiers restaurés" fi # --- Restauration Redis ------------------------------------------------------ if [[ $SKIP_REDIS -eq 0 && -f "$WORK_DIR/redis.rdb" ]]; then REDIS_DSN="$(grep -E '^REDIS_DSN=' "$ENV_FILE" | tail -n1 | cut -d= -f2- | sed -e 's/^"//' -e 's/"$//' -e "s/^'//" -e "s/'$//")" rre='^redis://(([^@]*)@)?([^:/]+)(:([0-9]+))?' if [[ -n "$REDIS_DSN" && "$REDIS_DSN" =~ $rre ]]; then R_PASS="${BASH_REMATCH[2]}" R_HOST="${BASH_REMATCH[3]}" R_PORT="${BASH_REMATCH[5]:-6379}" log "Restauration Redis ($R_HOST:$R_PORT)…" if command -v redis-cli >/dev/null 2>&1; then REDIS_AUTH=() [[ -n "$R_PASS" ]] && REDIS_AUTH=(-a "$R_PASS" --no-auth-warning) redis-cli -h "$R_HOST" -p "$R_PORT" "${REDIS_AUTH[@]}" FLUSHALL >/dev/null || true # Restaure clé par clé via DEBUG RELOAD impossible à distance ; # on se contente d'un FLUSHALL : la majorité des données Redis (cache, sessions) # se reconstruit à l'usage. Le RDB reste dans l'archive si besoin manuel. warn "Redis vidé. Le fichier redis.rdb reste dans $WORK_DIR pour usage manuel." warn "Pour réinjecter : copier redis.rdb dans le datadir Redis et redémarrer." cp "$WORK_DIR/redis.rdb" "$SAFETY_DIR/redis.rdb" else warn "redis-cli absent, étape Redis ignorée" fi fi fi # --- Tâches Symfony post-restauration --------------------------------------- log "Tâches Symfony post-restauration…" if [[ -x "$PROJECT_DIR/bin/console" ]]; then php "$PROJECT_DIR/bin/console" cache:clear --no-interaction || warn "cache:clear KO" php "$PROJECT_DIR/bin/console" doctrine:migrations:migrate --no-interaction --allow-no-migration || warn "migrate KO" ok "Console Symfony OK" else warn "bin/console introuvable, étape ignorée" fi cat <