diff --git a/backup.sh b/backup.sh new file mode 100755 index 0000000..0151844 --- /dev/null +++ b/backup.sh @@ -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'" diff --git a/restore.sh b/restore.sh new file mode 100755 index 0000000..ff1f4bb --- /dev/null +++ b/restore.sh @@ -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_.tar.gz et contient: +# db.sql.gz, public_uploads.tar.gz, var_state.tar.gz, env.local, cert/, manifest.txt +# +# Usage: +# ./restore.sh [--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 [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_) +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"