Compare commits
10 Commits
d37cec3309
...
ecc180ca0d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ecc180ca0d | ||
|
|
5ae97c8887 | ||
|
|
1fb0cc6f3f | ||
|
|
553d12aac8 | ||
|
|
31b28e5df2 | ||
|
|
6b882639b1 | ||
|
|
23c6a6fc1d | ||
|
|
0109c690ad | ||
|
|
2c43d8f0ce | ||
|
|
9a04b30913 |
206
bin/migrate-backup.sh
Executable file
206
bin/migrate-backup.sh
Executable file
@@ -0,0 +1,206 @@
|
||||
#!/usr/bin/env bash
|
||||
# ============================================================================
|
||||
# migrate-backup.sh
|
||||
# ----------------------------------------------------------------------------
|
||||
# Crée une archive complète de l'application (BDD + fichiers + config) afin
|
||||
# de migrer le CRM Ludikevent d'un serveur vers un autre.
|
||||
#
|
||||
# Contenu de l'archive :
|
||||
# - dump PostgreSQL (pg_dump --format=custom)
|
||||
# - dump Redis (RDB) si redis-cli est disponible
|
||||
# - dossiers d'uploads : public/media, public/images, public/pdf, public/seo
|
||||
# - var/storage, sauvegarde
|
||||
# - fichiers de config : .env.local, google.json
|
||||
# - manifeste (versions, date, hostname)
|
||||
#
|
||||
# Usage :
|
||||
# ./bin/migrate-backup.sh # archive dans ./sauvegarde/
|
||||
# ./bin/migrate-backup.sh -o /tmp/backup.tar.gz # chemin de sortie explicite
|
||||
# ./bin/migrate-backup.sh --no-redis # ignorer Redis
|
||||
# ./bin/migrate-backup.sh --docker # forcer l'utilisation de docker
|
||||
# ============================================================================
|
||||
set -euo pipefail
|
||||
|
||||
# --- Couleurs ----------------------------------------------------------------
|
||||
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[0;33m'; CYAN='\033[0;36m'; RESET='\033[0m'
|
||||
log() { printf "${CYAN}[backup]${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 --------------------------------------------------------------------
|
||||
PROJECT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
OUTPUT=""
|
||||
SKIP_REDIS=0
|
||||
FORCE_DOCKER=0
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
-o|--output) OUTPUT="$2"; shift 2 ;;
|
||||
--no-redis) SKIP_REDIS=1; shift ;;
|
||||
--docker) FORCE_DOCKER=1; shift ;;
|
||||
-h|--help)
|
||||
sed -n '2,22p' "$0"; exit 0 ;;
|
||||
*) err "Argument inconnu : $1"; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
cd "$PROJECT_DIR"
|
||||
|
||||
TIMESTAMP="$(date +%Y-%m-%d_%H-%M-%S)"
|
||||
HOSTNAME_SHORT="$(hostname -s 2>/dev/null || hostname)"
|
||||
[[ -z "$OUTPUT" ]] && OUTPUT="$PROJECT_DIR/sauvegarde/migrate_${HOSTNAME_SHORT}_${TIMESTAMP}.tar.gz"
|
||||
mkdir -p "$(dirname "$OUTPUT")"
|
||||
|
||||
WORK_DIR="$(mktemp -d -t crm-backup-XXXXXX)"
|
||||
trap 'rm -rf "$WORK_DIR"' EXIT
|
||||
|
||||
log "Projet : $PROJECT_DIR"
|
||||
log "Destination : $OUTPUT"
|
||||
log "Workdir : $WORK_DIR"
|
||||
|
||||
# --- Lecture des variables d'environnement ----------------------------------
|
||||
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 fichier .env trouvé."; exit 1; }
|
||||
log "Env source : $ENV_FILE"
|
||||
|
||||
# Récupère DATABASE_URL (dernière valeur définie, sans guillemets)
|
||||
DATABASE_URL="$(grep -E '^DATABASE_URL=' "$ENV_FILE" | tail -n1 | cut -d= -f2- | sed -e 's/^"//' -e 's/"$//' -e "s/^'//" -e "s/'$//")"
|
||||
[[ -z "$DATABASE_URL" ]] && { err "DATABASE_URL introuvable dans $ENV_FILE"; exit 1; }
|
||||
|
||||
# postgresql://user:pass@host:port/db?...
|
||||
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 "PostgreSQL : $PG_USER@$PG_HOST:$PG_PORT/$PG_DB"
|
||||
|
||||
# --- Détection du mode (natif ou docker) ------------------------------------
|
||||
USE_DOCKER=0
|
||||
if [[ $FORCE_DOCKER -eq 1 ]]; then
|
||||
USE_DOCKER=1
|
||||
elif ! command -v pg_dump >/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 "pg_dump non installé, fallback sur docker exec crm_db"
|
||||
fi
|
||||
fi
|
||||
|
||||
# --- Dump PostgreSQL ---------------------------------------------------------
|
||||
log "Dump PostgreSQL en cours…"
|
||||
DUMP_FILE="$WORK_DIR/database.dump"
|
||||
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 > "$DUMP_FILE"
|
||||
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 "$DUMP_FILE"
|
||||
fi
|
||||
DUMP_SIZE="$(du -h "$DUMP_FILE" | cut -f1)"
|
||||
ok "Dump BDD : $DUMP_SIZE"
|
||||
|
||||
# --- Dump Redis (best effort) ------------------------------------------------
|
||||
if [[ $SKIP_REDIS -eq 0 ]]; then
|
||||
REDIS_DSN="$(grep -E '^REDIS_DSN=' "$ENV_FILE" | tail -n1 | cut -d= -f2- | sed -e 's/^"//' -e 's/"$//' -e "s/^'//" -e "s/'$//")"
|
||||
if [[ -n "$REDIS_DSN" ]]; then
|
||||
# redis://[password@]host[:port]
|
||||
rre='^redis://(([^@]*)@)?([^:/]+)(:([0-9]+))?'
|
||||
if [[ "$REDIS_DSN" =~ $rre ]]; then
|
||||
R_PASS="${BASH_REMATCH[2]}"
|
||||
R_HOST="${BASH_REMATCH[3]}"
|
||||
R_PORT="${BASH_REMATCH[5]:-6379}"
|
||||
REDIS_FILE="$WORK_DIR/redis.rdb"
|
||||
log "Dump Redis ($R_HOST:$R_PORT)…"
|
||||
if command -v redis-cli >/dev/null 2>&1; then
|
||||
if [[ -n "$R_PASS" ]]; then
|
||||
redis-cli -h "$R_HOST" -p "$R_PORT" -a "$R_PASS" --no-auth-warning --rdb "$REDIS_FILE" >/dev/null \
|
||||
&& ok "Dump Redis : $(du -h "$REDIS_FILE" | cut -f1)" \
|
||||
|| warn "Échec dump Redis (ignoré)"
|
||||
else
|
||||
redis-cli -h "$R_HOST" -p "$R_PORT" --rdb "$REDIS_FILE" >/dev/null \
|
||||
&& ok "Dump Redis : $(du -h "$REDIS_FILE" | cut -f1)" \
|
||||
|| warn "Échec dump Redis (ignoré)"
|
||||
fi
|
||||
else
|
||||
warn "redis-cli absent, dump Redis ignoré"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# --- Copie des fichiers (uploads + config) -----------------------------------
|
||||
FILES_DIR="$WORK_DIR/files"
|
||||
mkdir -p "$FILES_DIR"
|
||||
|
||||
PATHS=(
|
||||
"public/media"
|
||||
"public/images"
|
||||
"public/pdf"
|
||||
"public/seo"
|
||||
"public/tmp-sign"
|
||||
"var/storage"
|
||||
"sauvegarde"
|
||||
".env.local"
|
||||
"google.json"
|
||||
)
|
||||
|
||||
log "Archivage des fichiers…"
|
||||
for p in "${PATHS[@]}"; do
|
||||
if [[ -e "$PROJECT_DIR/$p" ]]; then
|
||||
mkdir -p "$FILES_DIR/$(dirname "$p")"
|
||||
cp -a "$PROJECT_DIR/$p" "$FILES_DIR/$p"
|
||||
ok " + $p"
|
||||
else
|
||||
warn " - $p (absent)"
|
||||
fi
|
||||
done
|
||||
|
||||
# Évite de réembarquer l'archive courante si elle est dans sauvegarde/
|
||||
if [[ "$OUTPUT" == "$PROJECT_DIR/sauvegarde/"* && -e "$FILES_DIR/sauvegarde/$(basename "$OUTPUT")" ]]; then
|
||||
rm -f "$FILES_DIR/sauvegarde/$(basename "$OUTPUT")"
|
||||
fi
|
||||
|
||||
# --- Manifeste ---------------------------------------------------------------
|
||||
GIT_COMMIT="$(git -C "$PROJECT_DIR" rev-parse HEAD 2>/dev/null || echo "n/a")"
|
||||
GIT_BRANCH="$(git -C "$PROJECT_DIR" rev-parse --abbrev-ref HEAD 2>/dev/null || echo "n/a")"
|
||||
PHP_VER="$(php -r 'echo PHP_VERSION;' 2>/dev/null || echo "n/a")"
|
||||
|
||||
cat > "$WORK_DIR/MANIFEST.txt" <<EOF
|
||||
Migration backup CRM Ludikevent
|
||||
================================
|
||||
Date : $(date -Iseconds)
|
||||
Hostname : $(hostname)
|
||||
Project dir : $PROJECT_DIR
|
||||
Git branch : $GIT_BRANCH
|
||||
Git commit : $GIT_COMMIT
|
||||
PHP version : $PHP_VER
|
||||
DB target : $PG_USER@$PG_HOST:$PG_PORT/$PG_DB
|
||||
Mode : $([[ $USE_DOCKER -eq 1 ]] && echo "docker" || echo "native")
|
||||
EOF
|
||||
|
||||
# --- Création de l'archive ---------------------------------------------------
|
||||
log "Compression de l'archive…"
|
||||
tar -czf "$OUTPUT" -C "$WORK_DIR" .
|
||||
ARCHIVE_SIZE="$(du -h "$OUTPUT" | cut -f1)"
|
||||
ok "Archive prête : $OUTPUT ($ARCHIVE_SIZE)"
|
||||
|
||||
cat <<EOF
|
||||
|
||||
${GREEN}✔ Backup terminé.${RESET}
|
||||
|
||||
Pour migrer vers un autre serveur :
|
||||
scp "$OUTPUT" user@nouveau-serveur:/tmp/
|
||||
ssh user@nouveau-serveur
|
||||
cd /chemin/vers/crm
|
||||
./bin/migrate-restore.sh /tmp/$(basename "$OUTPUT")
|
||||
EOF
|
||||
242
bin/migrate-restore.sh
Executable file
242
bin/migrate-restore.sh
Executable file
@@ -0,0 +1,242 @@
|
||||
#!/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 <archive.tar.gz>
|
||||
# ./bin/migrate-restore.sh <archive.tar.gz> --yes # sans confirmation
|
||||
# ./bin/migrate-restore.sh <archive.tar.gz> --docker # forcer docker
|
||||
# ./bin/migrate-restore.sh <archive.tar.gz> --no-redis # ignorer Redis
|
||||
# ./bin/migrate-restore.sh <archive.tar.gz> --no-files # restaurer la BDD seulement
|
||||
# ./bin/migrate-restore.sh <archive.tar.gz> --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 <archive.tar.gz> [--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 <<EOF
|
||||
|
||||
${GREEN}✔ Restauration terminée.${RESET}
|
||||
|
||||
Sauvegarde de sécurité (rollback possible) :
|
||||
$SAFETY_DIR
|
||||
|
||||
Étapes de validation conseillées :
|
||||
- Vérifier l'accès web (curl -I https://votre-domaine)
|
||||
- Vérifier les permissions : sudo chown -R bot:www-data var public/media public/pdf
|
||||
- Relancer supervisor / worker messenger si nécessaire
|
||||
EOF
|
||||
@@ -8,9 +8,7 @@ framework:
|
||||
stale_while_revalidate: 3600
|
||||
stale_if_error: 3600
|
||||
session:
|
||||
name: crm_session
|
||||
cookie_lifetime: 3600
|
||||
cookie_secure: auto
|
||||
handler_id: '%env(REDIS_DSN)%'
|
||||
|
||||
#esi: true
|
||||
#fragments: true
|
||||
|
||||
@@ -1271,7 +1271,7 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
|
||||
* lifetime?: int|Param, // Default: 31536000
|
||||
* path?: scalar|Param|null, // Default: "/"
|
||||
* domain?: scalar|Param|null, // Default: null
|
||||
* secure?: true|false|"auto"|Param, // Default: true
|
||||
* secure?: true|false|"auto"|Param, // Default: false
|
||||
* httponly?: bool|Param, // Default: true
|
||||
* samesite?: null|"lax"|"strict"|"none"|Param, // Default: null
|
||||
* always_remember_me?: bool|Param, // Default: false
|
||||
|
||||
@@ -1077,8 +1077,10 @@ class EtlController extends AbstractController
|
||||
}
|
||||
|
||||
#[Route('/etl/connect/keycloak', name: 'connect_keycloak_etl_start')]
|
||||
public function connectKeycloakEtlStart(ClientRegistry $clientRegistry): Response
|
||||
public function connectKeycloakEtlStart(ClientRegistry $clientRegistry, Request $request): Response
|
||||
{
|
||||
$request->getSession()->start();
|
||||
|
||||
$response = $clientRegistry
|
||||
->getClient('keycloak_etl')
|
||||
->redirect(['openid', 'profile', 'email']);
|
||||
|
||||
@@ -26,8 +26,10 @@ class HomeController extends AbstractController
|
||||
{
|
||||
|
||||
#[Route('/intranet/connect/keycloak', name: 'connect_keycloak_start')]
|
||||
public function connect(ClientRegistry $clientRegistry): Response
|
||||
public function connect(ClientRegistry $clientRegistry, Request $request): Response
|
||||
{
|
||||
$request->getSession()->start();
|
||||
|
||||
$response = $clientRegistry
|
||||
->getClient('keycloak')
|
||||
->redirect(['email', 'profile', 'openid'], []);
|
||||
|
||||
@@ -17,6 +17,7 @@ use Symfony\Component\Security\Http\Authenticator\Passport\Passport;
|
||||
use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport;
|
||||
use Symfony\Component\Security\Http\EntryPoint\AuthenticationEntryPointInterface;
|
||||
use Symfony\Component\Uid\Uuid;
|
||||
use KnpU\OAuth2ClientBundle\Security\Exception\InvalidStateAuthenticationException;
|
||||
|
||||
class EtlKeycloakAuthenticator extends OAuth2Authenticator implements AuthenticationEntryPointInterface
|
||||
{
|
||||
@@ -89,6 +90,10 @@ class EtlKeycloakAuthenticator extends OAuth2Authenticator implements Authentica
|
||||
|
||||
public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response
|
||||
{
|
||||
if ($exception instanceof InvalidStateAuthenticationException) {
|
||||
return new RedirectResponse($this->router->generate('etl_login'));
|
||||
}
|
||||
|
||||
$message = strtr($exception->getMessageKey(), $exception->getMessageData());
|
||||
return new Response($message, Response::HTTP_FORBIDDEN);
|
||||
}
|
||||
|
||||
@@ -18,18 +18,22 @@ use Symfony\Component\Security\Http\Authenticator\Passport\Passport;
|
||||
use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport;
|
||||
use Symfony\Component\Security\Http\EntryPoint\AuthenticationEntryPointInterface;
|
||||
use Symfony\Component\Uid\Uuid;
|
||||
use KnpU\OAuth2ClientBundle\Security\Exception\InvalidStateAuthenticationException;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
||||
class KeycloakAuthenticator extends OAuth2Authenticator implements AuthenticationEntryPointInterface
|
||||
{
|
||||
private $clientRegistry;
|
||||
private $entityManager;
|
||||
private $router;
|
||||
private $logger;
|
||||
|
||||
public function __construct(ClientRegistry $clientRegistry, EntityManagerInterface $entityManager, RouterInterface $router)
|
||||
public function __construct(ClientRegistry $clientRegistry, EntityManagerInterface $entityManager, RouterInterface $router, LoggerInterface $logger)
|
||||
{
|
||||
$this->clientRegistry = $clientRegistry;
|
||||
$this->entityManager = $entityManager;
|
||||
$this->router = $router;
|
||||
$this->logger = $logger;
|
||||
}
|
||||
|
||||
public function supports(Request $request): ?bool
|
||||
@@ -91,6 +95,15 @@ class KeycloakAuthenticator extends OAuth2Authenticator implements Authenticatio
|
||||
|
||||
public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response
|
||||
{
|
||||
if ($exception instanceof InvalidStateAuthenticationException) {
|
||||
$this->logger->error('SSO Invalid State', [
|
||||
'session_id' => $request->getSession()->getId(),
|
||||
'has_session' => $request->hasSession(),
|
||||
'state_param' => $request->query->get('state'),
|
||||
]);
|
||||
return new RedirectResponse($this->router->generate('app_home'));
|
||||
}
|
||||
|
||||
$message = strtr($exception->getMessageKey(), $exception->getMessageData());
|
||||
return new Response($message, Response::HTTP_FORBIDDEN);
|
||||
}
|
||||
|
||||
@@ -239,7 +239,7 @@ class ContratPdfService extends Fpdf
|
||||
|
||||
"ARTICLE 6 – CAUTION DE GARANTIE" => "Une caution de garantie peut être exigée selon le type et la valeur du matériel loué :\n• Structures professionnelles Lilian SEGARD - Ludikevent | Selon devis | Restitution immédiate après état des lieux de fin de location et contrôle de conformité\n• Matériel mis en relation (propriétaires privés) | Selon convention propriétaire | Restitution selon accord entre parties\nLa caution peut être conservée totalement ou partiellement en cas de : dégradation du matériel, salissure importante nécessitant un nettoyage approfondi, perte d'éléments ou d'accessoires, non-respect des conditions d'utilisation ayant entraîné des dommages.",
|
||||
|
||||
"ARTICLE 7 – LIVRAISON, INSTALLATION ET RÉCUPÉRATION" => "7.1 – Structures professionnelles Lilian SEGARD - Ludikevent :\nInstallation : L'installation est réalisée exclusivement par le personnel qualifié de Lilian SEGARD - Ludikevent. Le Client doit fournir un terrain plat, propre, dégagé, avec une alimentation électrique 220V à moins de 50 mètres. L'accès doit être praticable pour les véhicules.\nRécupération : La récupération du matériel est effectuée par Lilian SEGARD - Ludikevent aux horaires convenus.\n7.2 – Matériel en mise en relation :\nLilian SEGARD - Ludikevent peut assurer l'installation et la récupération pour le compte du propriétaire privé. Le Client locataire reste tenu d'assurer une surveillance permanente et constante. Des frais de déplacement supplémentaires peuvent s'appliquer au-delà d'un rayon de 30 km depuis Danizy.",
|
||||
"ARTICLE 7 – LIVRAISON, INSTALLATION ET RÉCUPÉRATION" => "7.1 – Structures professionnelles Lilian SEGARD - Ludikevent :\nInstallation : L'installation est réalisée exclusivement par le personnel qualifié de Lilian SEGARD - Ludikevent. Le Client doit fournir un terrain plat, propre, dégagé, avec une alimentation électrique 220V à moins de 50 mètres. L'accès doit être praticable pour les véhicules.\nRécupération : La récupération du matériel est effectuée par Lilian SEGARD - Ludikevent aux horaires convenus.",
|
||||
|
||||
"ARTICLE 8 – ÉTATS DES LIEUX ET TRANSFERT DE RESPONSABILITÉ" => "8.1 – État des lieux d'installation : Un état des lieux contradictoire est OBLIGATOIREMENT réalisé. La signature par le Client vaut reconnaissance de conformité et TRANSFERT COMPLET DE LA RESPONSABILITÉ DU MATÉRIEL AU CLIENT. Le Client assume l'ENTIÈRE et EXCLUSIVE responsabilité du matériel, de son utilisation, de sa surveillance et des dommages qui pourraient en résulter.\n8.2 – État des lieux de fin de location : Un état des lieux contradictoire est réalisé lors de la récupération. Sa signature vaut RETOUR DE LA RESPONSABILITÉ À Lilian SEGARD - Ludikevent.\n8.3 – Absence de signature : En cas d'absence ou de refus de signature, l'état des lieux établi par Lilian SEGARD - Ludikevent fera foi. Le matériel sera réputé avoir été livré en parfait état et la responsabilité du Client reste pleine et entière durant toute la période de location.",
|
||||
|
||||
|
||||
@@ -383,7 +383,7 @@ class DevisPdfService extends Fpdf
|
||||
|
||||
"ARTICLE 6 – CAUTION DE GARANTIE" => "Une caution de garantie peut être exigée selon le type et la valeur du matériel loué :\n• Structures professionnelles Lilian SEGARD - Ludikevent | Selon devis | Restitution immédiate après état des lieux de fin de location et contrôle de conformité\n• Matériel mis en relation (propriétaires privés) | Selon convention propriétaire | Restitution selon accord entre parties\nLa caution peut être conservée totalement ou partiellement en cas de : dégradation du matériel, salissure importante nécessitant un nettoyage approfondi, perte d'éléments ou d'accessoires, non-respect des conditions d'utilisation ayant entraîné des dommages.",
|
||||
|
||||
"ARTICLE 7 – LIVRAISON, INSTALLATION ET RÉCUPÉRATION" => "7.1 – Structures professionnelles Lilian SEGARD - Ludikevent :\nInstallation : L'installation est réalisée exclusivement par le personnel qualifié de Lilian SEGARD - Ludikevent. Le Client doit fournir un terrain plat, propre, dégagé, avec une alimentation électrique 220V à moins de 50 mètres. L'accès doit être praticable pour les véhicules.\nRécupération : La récupération du matériel est effectuée par Lilian SEGARD - Ludikevent aux horaires convenus.\n7.2 – Matériel en mise en relation :\nLilian SEGARD - Ludikevent peut assurer l'installation et la récupération pour le compte du propriétaire privé. Le Client locataire reste tenu d'assurer une surveillance permanente et constante. Des frais de déplacement supplémentaires peuvent s'appliquer au-delà d'un rayon de 30 km depuis Danizy.",
|
||||
"ARTICLE 7 – LIVRAISON, INSTALLATION ET RÉCUPÉRATION" => "7.1 – Structures professionnelles Lilian SEGARD - Ludikevent :\nInstallation : L'installation est réalisée exclusivement par le personnel qualifié de Lilian SEGARD - Ludikevent. Le Client doit fournir un terrain plat, propre, dégagé, avec une alimentation électrique 220V à moins de 50 mètres. L'accès doit être praticable pour les véhicules.\nRécupération : La récupération du matériel est effectuée par Lilian SEGARD - Ludikevent aux horaires convenus.",
|
||||
|
||||
"ARTICLE 8 – ÉTATS DES LIEUX ET TRANSFERT DE RESPONSABILITÉ" => "8.1 – État des lieux d'installation : Un état des lieux contradictoire est OBLIGATOIREMENT réalisé. La signature par le Client vaut reconnaissance de conformité et TRANSFERT COMPLET DE LA RESPONSABILITÉ DU MATÉRIEL AU CLIENT. Le Client assume l'ENTIÈRE et EXCLUSIVE responsabilité du matériel, de son utilisation, de sa surveillance et des dommages qui pourraient en résulter.\n8.2 – État des lieux de fin de location : Un état des lieux contradictoire est réalisé lors de la récupération. Sa signature vaut RETOUR DE LA RESPONSABILITÉ À Lilian SEGARD - Ludikevent.\n8.3 – Absence de signature : En cas d'absence ou de refus de signature, l'état des lieux établi par Lilian SEGARD - Ludikevent fera foi. Le matériel sera réputé avoir été livré en parfait état et la responsabilité du Client reste pleine et entière durant toute la période de location.",
|
||||
|
||||
|
||||
@@ -510,14 +510,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Frais de déplacement #}
|
||||
<div class="flex items-center gap-4 p-5 bg-amber-50 rounded-2xl border border-amber-100">
|
||||
<span class="text-2xl">🌍</span>
|
||||
<p class="text-xs md:text-sm font-bold text-amber-900 leading-tight">
|
||||
Livraison incluse dans un rayon de <span class="underline decoration-amber-400 underline-offset-4">30 km depuis Danizy</span>.
|
||||
Des frais de déplacement s'appliquent au-delà.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user