Add server migration scripts: backup.sh (export) and restore.sh (import)

backup.sh genere un bundle de migration tar.gz autonome contenant:
- dump PostgreSQL gzippe (pg_dump --clean --if-exists --no-owner)
- archive public/uploads (events, billets, logos) repere via flysystem.yaml
- archive var/ (payouts, billets, invoices, unsubscribed.json)
- .env.local et config/cert/ (clef S/MIME), exclus si SKIP_SECRETS=1
- manifest.txt avec metadata (host, git commit/branche, date) et checksums SHA-256

restore.sh restaure un bundle sur un nouveau serveur:
- verifie les checksums SHA-256 avant toute action
- affiche un plan de restauration et demande confirmation (sauf --yes)
- options --skip-db, --skip-uploads, --skip-var, --skip-secrets
- cree des sauvegardes de securite avant ecrasement (.env.local, public/uploads, var/)
- verifie que le service db-master tourne avant restore PostgreSQL
- rappelle les etapes post-migration (migrate_prod, clear_prod)

Les deux scripts utilisent des variables d'env override (COMPOSE_FILE, DB_SERVICE,
DB_USER, DB_NAME) et s'appuient sur docker-compose-prod.yml par defaut.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Serreau Jovann
2026-04-10 13:27:33 +02:00
parent cda80990c7
commit bb082c8368
2 changed files with 427 additions and 0 deletions

173
backup.sh Executable file
View File

@@ -0,0 +1,173 @@
#!/bin/bash
# E-Ticket - Script d'export pour migration de serveur
# Genere un bundle complet et autonome pour migrer l'application sur un autre serveur.
#
# Le bundle contient:
# - dump de la base PostgreSQL (db.sql.gz)
# - uploads Vich/Flysystem (events, billets, logos) (public_uploads.tar.gz)
# - etat applicatif var/ (var_state.tar.gz)
# payouts/, billets/, invoices/, unsubscribed.json
# - configuration runtime sensible (env.local, cert/)
# - manifest avec checksums SHA-256 (manifest.txt)
#
# Le tout est ensuite empaquete dans une archive .tar.gz transferable par scp/rsync.
#
# Usage:
# ./backup.sh [DOSSIER_DESTINATION]
#
# Variables d'environnement supportees:
# COMPOSE_FILE : fichier docker-compose a utiliser (defaut: docker-compose-prod.yml)
# DB_SERVICE : service Docker de la base (defaut: db-master)
# DB_USER : utilisateur PostgreSQL (defaut: e-ticket)
# DB_NAME : base de donnees a exporter (defaut: e-ticket)
# SKIP_SECRETS : '1' pour exclure .env.local + cert (defaut: inclus)
set -euo pipefail
# --- Configuration ---
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
DEST_DIR="${1:-${SCRIPT_DIR}/var/migration}"
COMPOSE_FILE="${COMPOSE_FILE:-${SCRIPT_DIR}/docker-compose-prod.yml}"
DB_SERVICE="${DB_SERVICE:-db-master}"
DB_USER="${DB_USER:-e-ticket}"
DB_NAME="${DB_NAME:-e-ticket}"
SKIP_SECRETS="${SKIP_SECRETS:-0}"
DATE="$(date +%Y%m%d_%H%M%S)"
BUNDLE_NAME="e_ticket_migration_${DATE}"
WORK_DIR="${DEST_DIR}/${BUNDLE_NAME}"
BUNDLE_FILE="${DEST_DIR}/${BUNDLE_NAME}.tar.gz"
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*"
}
err() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] ERREUR: $*" >&2
}
cleanup() {
if [ -d "${WORK_DIR}" ]; then
rm -rf "${WORK_DIR}"
fi
}
# Cleanup du WORK_DIR meme en cas d'erreur (le BUNDLE_FILE final reste)
trap cleanup EXIT
# --- Verifications prealables ---
if [ ! -f "${COMPOSE_FILE}" ]; then
err "Fichier docker-compose introuvable: ${COMPOSE_FILE}"
exit 1
fi
if ! docker compose -f "${COMPOSE_FILE}" ps --services --status running 2>/dev/null | grep -qx "${DB_SERVICE}"; then
err "Le service '${DB_SERVICE}' n'est pas demarre"
err "Demarrer la stack avec: make start_prod"
exit 1
fi
mkdir -p "${WORK_DIR}"
log "=== Export de migration E-Ticket ==="
log "Bundle: ${BUNDLE_NAME}"
log "Destination: ${DEST_DIR}"
# --- 1. Dump base de donnees ---
log "[1/5] Dump PostgreSQL '${DB_NAME}' depuis ${DB_SERVICE}..."
if ! docker compose -f "${COMPOSE_FILE}" exec -T "${DB_SERVICE}" \
pg_dump -U "${DB_USER}" -d "${DB_NAME}" \
--clean --if-exists --no-owner --no-privileges --quote-all-identifiers \
| gzip > "${WORK_DIR}/db.sql.gz"; then
err "Echec du dump PostgreSQL"
exit 1
fi
if [ ! -s "${WORK_DIR}/db.sql.gz" ]; then
err "Le dump PostgreSQL est vide"
exit 1
fi
log " OK ($(du -h "${WORK_DIR}/db.sql.gz" | cut -f1))"
# --- 2. Uploads publics (events, billets, logos) ---
log "[2/5] Archivage public/uploads (events, billets, logos)..."
if [ -d "${SCRIPT_DIR}/public/uploads" ]; then
tar -czf "${WORK_DIR}/public_uploads.tar.gz" -C "${SCRIPT_DIR}/public" uploads
log " OK ($(du -h "${WORK_DIR}/public_uploads.tar.gz" | cut -f1))"
else
err "public/uploads introuvable"
exit 1
fi
# --- 3. Etat applicatif var/ (PDFs generes, desinscrits...) ---
log "[3/5] Archivage etat applicatif var/ (payouts, billets, invoices, unsubscribed.json)..."
VAR_ITEMS=()
for item in payouts billets invoices unsubscribed.json; do
if [ -e "${SCRIPT_DIR}/var/${item}" ]; then
VAR_ITEMS+=("${item}")
fi
done
if [ ${#VAR_ITEMS[@]} -gt 0 ]; then
tar -czf "${WORK_DIR}/var_state.tar.gz" -C "${SCRIPT_DIR}/var" "${VAR_ITEMS[@]}"
log " OK - elements inclus: ${VAR_ITEMS[*]} ($(du -h "${WORK_DIR}/var_state.tar.gz" | cut -f1))"
else
log " Aucun element a archiver dans var/ (saut)"
fi
# --- 4. Configuration runtime sensible ---
if [ "${SKIP_SECRETS}" = "1" ]; then
log "[4/5] Secrets exclus (SKIP_SECRETS=1)"
log " ATTENTION: vous devrez deployer .env.local et config/cert/ manuellement sur le nouveau serveur"
else
log "[4/5] Archivage configuration sensible (.env.local + config/cert/)..."
SECRET_ITEMS=()
if [ -f "${SCRIPT_DIR}/.env.local" ]; then
cp -p "${SCRIPT_DIR}/.env.local" "${WORK_DIR}/env.local"
chmod 600 "${WORK_DIR}/env.local"
SECRET_ITEMS+=(".env.local")
fi
if [ -d "${SCRIPT_DIR}/config/cert" ]; then
mkdir -p "${WORK_DIR}/cert"
cp -p "${SCRIPT_DIR}/config/cert/." "${WORK_DIR}/cert/"
chmod -R go-rwx "${WORK_DIR}/cert"
SECRET_ITEMS+=("config/cert/")
fi
if [ ${#SECRET_ITEMS[@]} -gt 0 ]; then
log " OK - elements inclus: ${SECRET_ITEMS[*]}"
else
log " Aucun secret trouve a archiver (saut)"
fi
fi
# --- 5. Manifest avec checksums et metadata ---
log "[5/5] Generation du manifest..."
{
echo "# E-Ticket migration bundle"
echo "bundle_name: ${BUNDLE_NAME}"
echo "created_at: $(date --iso-8601=seconds)"
echo "source_host: $(hostname)"
echo "git_commit: $(git -C "${SCRIPT_DIR}" rev-parse HEAD 2>/dev/null || echo 'unknown')"
echo "git_branch: $(git -C "${SCRIPT_DIR}" rev-parse --abbrev-ref HEAD 2>/dev/null || echo 'unknown')"
echo "db_name: ${DB_NAME}"
echo "db_user: ${DB_USER}"
echo "skip_secrets: ${SKIP_SECRETS}"
echo
echo "# SHA-256 checksums"
(cd "${WORK_DIR}" && find . -type f ! -name 'manifest.txt' -print0 | sort -z | xargs -0 sha256sum)
} > "${WORK_DIR}/manifest.txt"
log " OK"
# --- Empaquetage final ---
log "Empaquetage du bundle dans ${BUNDLE_FILE}..."
tar -czf "${BUNDLE_FILE}" -C "${DEST_DIR}" "${BUNDLE_NAME}"
BUNDLE_SIZE="$(du -h "${BUNDLE_FILE}" | cut -f1)"
BUNDLE_SHA="$(sha256sum "${BUNDLE_FILE}" | cut -d' ' -f1)"
log "=== Export termine ==="
log "Bundle : ${BUNDLE_FILE}"
log "Taille : ${BUNDLE_SIZE}"
log "SHA-256 : ${BUNDLE_SHA}"
log
log "Pour transferer vers le nouveau serveur:"
log " scp '${BUNDLE_FILE}' user@nouveau-serveur:/var/www/e-ticket/"
log "Puis sur le nouveau serveur:"
log " ./restore.sh '${BUNDLE_NAME}.tar.gz'"

254
restore.sh Executable file
View File

@@ -0,0 +1,254 @@
#!/bin/bash
# E-Ticket - Script d'import pour migration de serveur
# Restaure un bundle de migration genere par backup.sh sur un nouveau serveur.
#
# Le bundle est attendu au format e_ticket_migration_<DATE>.tar.gz et contient:
# db.sql.gz, public_uploads.tar.gz, var_state.tar.gz, env.local, cert/, manifest.txt
#
# Usage:
# ./restore.sh <bundle.tar.gz> [--yes] [--skip-db] [--skip-uploads] [--skip-var] [--skip-secrets]
#
# Variables d'environnement supportees:
# COMPOSE_FILE : fichier docker-compose a utiliser (defaut: docker-compose-prod.yml)
# DB_SERVICE : service Docker de la base (defaut: db-master)
# DB_USER : utilisateur PostgreSQL (defaut: e-ticket)
# DB_NAME : base de donnees a restaurer (defaut: e-ticket)
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
COMPOSE_FILE="${COMPOSE_FILE:-${SCRIPT_DIR}/docker-compose-prod.yml}"
DB_SERVICE="${DB_SERVICE:-db-master}"
DB_USER="${DB_USER:-e-ticket}"
DB_NAME="${DB_NAME:-e-ticket}"
BUNDLE=""
ASSUME_YES="false"
SKIP_DB="false"
SKIP_UPLOADS="false"
SKIP_VAR="false"
SKIP_SECRETS="false"
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*"
}
err() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] ERREUR: $*" >&2
}
usage() {
cat <<'EOF'
Usage:
./restore.sh <bundle.tar.gz> [options]
Options:
--yes, -y Mode non interactif (pas de confirmation)
--skip-db Ne pas restaurer la base de donnees
--skip-uploads Ne pas restaurer public/uploads
--skip-var Ne pas restaurer var/ (payouts, billets, invoices, unsubscribed.json)
--skip-secrets Ne pas restaurer .env.local et config/cert/
-h, --help Affiche cette aide
ATTENTION: La restauration ECRASE les donnees existantes !
EOF
}
# --- Parsing des arguments ---
while [ $# -gt 0 ]; do
case "$1" in
--yes|-y) ASSUME_YES="true"; shift ;;
--skip-db) SKIP_DB="true"; shift ;;
--skip-uploads) SKIP_UPLOADS="true"; shift ;;
--skip-var) SKIP_VAR="true"; shift ;;
--skip-secrets) SKIP_SECRETS="true"; shift ;;
-h|--help) usage; exit 0 ;;
--*) err "Option inconnue: $1"; usage; exit 1 ;;
*)
if [ -z "${BUNDLE}" ]; then
BUNDLE="$1"
else
err "Argument en trop: $1"
usage
exit 1
fi
shift
;;
esac
done
if [ -z "${BUNDLE}" ]; then
err "Aucun bundle specifie"
usage
exit 1
fi
if [ ! -f "${BUNDLE}" ]; then
err "Bundle introuvable: ${BUNDLE}"
exit 1
fi
# --- Extraction du bundle dans un dossier temporaire ---
TMP_DIR="$(mktemp -d -t e_ticket_restore_XXXXXX)"
cleanup() {
rm -rf "${TMP_DIR}"
}
trap cleanup EXIT
log "=== Restauration du bundle de migration E-Ticket ==="
log "Bundle: ${BUNDLE}"
log "Extraction dans ${TMP_DIR}..."
if ! tar -xzf "${BUNDLE}" -C "${TMP_DIR}"; then
err "Echec d'extraction du bundle"
exit 1
fi
# Trouve le dossier extrait (e_ticket_migration_<DATE>)
EXTRACTED_DIR="$(find "${TMP_DIR}" -maxdepth 1 -mindepth 1 -type d -name 'e_ticket_migration_*' | head -n1)"
if [ -z "${EXTRACTED_DIR}" ] || [ ! -d "${EXTRACTED_DIR}" ]; then
err "Bundle invalide: dossier e_ticket_migration_* introuvable"
exit 1
fi
# --- Verification du manifest et des checksums ---
if [ ! -f "${EXTRACTED_DIR}/manifest.txt" ]; then
err "Manifest manquant dans le bundle"
exit 1
fi
log "Verification des checksums..."
(cd "${EXTRACTED_DIR}" && grep -E '^[a-f0-9]{64} ' manifest.txt | sha256sum -c --quiet) || {
err "Checksums invalides - bundle corrompu"
exit 1
}
log " OK - bundle integre"
echo
echo "================================================================"
echo " Manifest du bundle:"
echo "================================================================"
grep -E '^(bundle_name|created_at|source_host|git_commit|git_branch|db_name|skip_secrets):' "${EXTRACTED_DIR}/manifest.txt" || true
echo "================================================================"
# --- Verification des composants disponibles ---
HAS_DB="false"
HAS_UPLOADS="false"
HAS_VAR="false"
HAS_ENV="false"
HAS_CERT="false"
[ -f "${EXTRACTED_DIR}/db.sql.gz" ] && HAS_DB="true"
[ -f "${EXTRACTED_DIR}/public_uploads.tar.gz" ] && HAS_UPLOADS="true"
[ -f "${EXTRACTED_DIR}/var_state.tar.gz" ] && HAS_VAR="true"
[ -f "${EXTRACTED_DIR}/env.local" ] && HAS_ENV="true"
[ -d "${EXTRACTED_DIR}/cert" ] && HAS_CERT="true"
echo
echo " Plan de restauration:"
echo "----------------------------------------------------------------"
[ "${HAS_DB}" = "true" ] && [ "${SKIP_DB}" = "false" ] && echo " [X] Base de donnees -> ${DB_NAME} (ECRASE)" || echo " [ ] Base de donnees"
[ "${HAS_UPLOADS}" = "true" ] && [ "${SKIP_UPLOADS}" = "false" ] && echo " [X] public/uploads/ (ECRASE, sauvegarde de securite)" || echo " [ ] public/uploads/"
[ "${HAS_VAR}" = "true" ] && [ "${SKIP_VAR}" = "false" ] && echo " [X] var/ (payouts, billets, invoices, unsubscribed.json)" || echo " [ ] var/"
[ "${HAS_ENV}" = "true" ] && [ "${SKIP_SECRETS}" = "false" ] && echo " [X] .env.local (ECRASE)" || echo " [ ] .env.local"
[ "${HAS_CERT}" = "true" ] && [ "${SKIP_SECRETS}" = "false" ] && echo " [X] config/cert/ (ECRASE)" || echo " [ ] config/cert/"
echo "----------------------------------------------------------------"
echo
if [ "${ASSUME_YES}" != "true" ]; then
read -r -p "Confirmer la restauration ? (tapez 'oui' pour continuer) : " CONFIRM
if [ "${CONFIRM}" != "oui" ]; then
log "Restauration annulee par l'utilisateur"
exit 0
fi
fi
# --- 1. Secrets (en premier pour que la stack puisse demarrer si besoin) ---
if [ "${SKIP_SECRETS}" = "false" ]; then
if [ "${HAS_ENV}" = "true" ]; then
log "[secrets] Restauration de .env.local..."
if [ -f "${SCRIPT_DIR}/.env.local" ]; then
cp -p "${SCRIPT_DIR}/.env.local" "${SCRIPT_DIR}/.env.local.before_restore_$(date +%Y%m%d_%H%M%S)"
fi
cp -p "${EXTRACTED_DIR}/env.local" "${SCRIPT_DIR}/.env.local"
chmod 600 "${SCRIPT_DIR}/.env.local"
log " OK"
fi
if [ "${HAS_CERT}" = "true" ]; then
log "[secrets] Restauration de config/cert/..."
mkdir -p "${SCRIPT_DIR}/config/cert"
cp -p "${EXTRACTED_DIR}/cert/." "${SCRIPT_DIR}/config/cert/"
chmod -R go-rwx "${SCRIPT_DIR}/config/cert"
log " OK"
fi
fi
# --- 2. Base de donnees ---
if [ "${HAS_DB}" = "true" ] && [ "${SKIP_DB}" = "false" ]; then
log "[db] Verification du service ${DB_SERVICE}..."
if [ ! -f "${COMPOSE_FILE}" ]; then
err "Fichier docker-compose introuvable: ${COMPOSE_FILE}"
exit 1
fi
if ! docker compose -f "${COMPOSE_FILE}" ps --services --status running 2>/dev/null | grep -qx "${DB_SERVICE}"; then
err "Le service '${DB_SERVICE}' n'est pas demarre. Lancer: make start_prod"
exit 1
fi
log "[db] Restauration de la base ${DB_NAME}..."
if ! gunzip -c "${EXTRACTED_DIR}/db.sql.gz" \
| docker compose -f "${COMPOSE_FILE}" exec -T "${DB_SERVICE}" \
psql -U "${DB_USER}" -d "${DB_NAME}" -v ON_ERROR_STOP=1 >/dev/null; then
err "Echec de la restauration PostgreSQL"
exit 1
fi
log " OK"
fi
# --- 3. public/uploads ---
if [ "${HAS_UPLOADS}" = "true" ] && [ "${SKIP_UPLOADS}" = "false" ]; then
log "[uploads] Restauration de public/uploads..."
if [ -d "${SCRIPT_DIR}/public/uploads" ]; then
SAFETY="${SCRIPT_DIR}/public/uploads.before_restore_$(date +%Y%m%d_%H%M%S)"
log " Sauvegarde de securite: ${SAFETY}"
mv "${SCRIPT_DIR}/public/uploads" "${SAFETY}"
fi
mkdir -p "${SCRIPT_DIR}/public"
if ! tar -xzf "${EXTRACTED_DIR}/public_uploads.tar.gz" -C "${SCRIPT_DIR}/public"; then
err "Echec d'extraction de public_uploads.tar.gz"
exit 1
fi
log " OK"
fi
# --- 4. var/ (PDFs generes, unsubscribed.json...) ---
if [ "${HAS_VAR}" = "true" ] && [ "${SKIP_VAR}" = "false" ]; then
log "[var] Restauration de var/ (payouts, billets, invoices, unsubscribed.json)..."
mkdir -p "${SCRIPT_DIR}/var"
# Sauvegarde de securite des elements existants qui seront ecrases
SAFETY_VAR="${SCRIPT_DIR}/var/.before_restore_$(date +%Y%m%d_%H%M%S)"
mkdir -p "${SAFETY_VAR}"
for item in payouts billets invoices unsubscribed.json; do
if [ -e "${SCRIPT_DIR}/var/${item}" ]; then
mv "${SCRIPT_DIR}/var/${item}" "${SAFETY_VAR}/"
fi
done
# Si rien n'a ete sauvegarde, on supprime le dossier vide
if [ -z "$(ls -A "${SAFETY_VAR}")" ]; then
rmdir "${SAFETY_VAR}"
else
log " Sauvegarde de securite: ${SAFETY_VAR}"
fi
if ! tar -xzf "${EXTRACTED_DIR}/var_state.tar.gz" -C "${SCRIPT_DIR}/var"; then
err "Echec d'extraction de var_state.tar.gz"
exit 1
fi
log " OK"
fi
log "=== Migration terminee avec succes ==="
log
log "Etapes recommandees post-migration:"
log " 1. make start_prod (si pas deja fait)"
log " 2. make migrate_prod (appliquer les migrations Doctrine eventuelles)"
log " 3. make clear_prod (vider le cache)"
log " 4. Verifier le bon fonctionnement de l'application"