185 lines
6.6 KiB
Plaintext
185 lines
6.6 KiB
Plaintext
|
|
#!/bin/bash
|
||
|
|
# CRM SITECONSEIL - Restore script (database + files)
|
||
|
|
# Usage:
|
||
|
|
# /usr/local/bin/crm-siteconseil-restore.sh # restore the latest backup
|
||
|
|
# /usr/local/bin/crm-siteconseil-restore.sh 20260410_143000 # restore a specific backup
|
||
|
|
# /usr/local/bin/crm-siteconseil-restore.sh --list # list available backups
|
||
|
|
# /usr/local/bin/crm-siteconseil-restore.sh -y 20260410_143000 # skip confirmation
|
||
|
|
#
|
||
|
|
# WARNING: this script will REPLACE the current database and files.
|
||
|
|
|
||
|
|
set -euo pipefail
|
||
|
|
|
||
|
|
#######################################
|
||
|
|
# Configuration
|
||
|
|
#######################################
|
||
|
|
APP_DIR="{{ app_dir | default('/var/www/crm-siteconseil') }}"
|
||
|
|
COMPOSE_FILE="${APP_DIR}/docker-compose-prod.yml"
|
||
|
|
BACKUP_DIR="{{ backup_dir | default('/var/backups/crm-siteconseil') }}"
|
||
|
|
|
||
|
|
DB_SERVICE="{{ db_service | default('db-master') }}"
|
||
|
|
DB_USER="{{ db_user | default('crm-siteconseil') }}"
|
||
|
|
DB_NAME="{{ db_name | default('crm-siteconseil') }}"
|
||
|
|
|
||
|
|
LOG_PREFIX="[$(date '+%Y-%m-%d %H:%M:%S')]"
|
||
|
|
ASSUME_YES=0
|
||
|
|
TARGET=""
|
||
|
|
|
||
|
|
#######################################
|
||
|
|
# Helpers
|
||
|
|
#######################################
|
||
|
|
log() { echo "${LOG_PREFIX} $*"; }
|
||
|
|
fail() { echo "${LOG_PREFIX} ERROR: $*" >&2; exit 1; }
|
||
|
|
|
||
|
|
usage() {
|
||
|
|
cat <<EOF
|
||
|
|
Usage: $0 [options] [BACKUP_ID]
|
||
|
|
|
||
|
|
Options:
|
||
|
|
-y, --yes Skip confirmation prompt
|
||
|
|
-l, --list List available backups and exit
|
||
|
|
-h, --help Show this help
|
||
|
|
|
||
|
|
If no BACKUP_ID is given, the most recent backup is used.
|
||
|
|
BACKUP_ID is the timestamped folder name (ex: 20260410_143000).
|
||
|
|
EOF
|
||
|
|
}
|
||
|
|
|
||
|
|
list_backups() {
|
||
|
|
if [ ! -d "${BACKUP_DIR}" ]; then
|
||
|
|
fail "Backup directory not found: ${BACKUP_DIR}"
|
||
|
|
fi
|
||
|
|
log "Available backups in ${BACKUP_DIR}:"
|
||
|
|
find "${BACKUP_DIR}" -mindepth 1 -maxdepth 1 -type d -printf '%f\t%TY-%Tm-%Td %TH:%TM\t%s bytes\n' \
|
||
|
|
| sort -r \
|
||
|
|
| awk -F'\t' '{ printf " %-20s %s\n", $1, $2 }'
|
||
|
|
}
|
||
|
|
|
||
|
|
#######################################
|
||
|
|
# Args parsing
|
||
|
|
#######################################
|
||
|
|
while [ $# -gt 0 ]; do
|
||
|
|
case "$1" in
|
||
|
|
-y|--yes) ASSUME_YES=1; shift ;;
|
||
|
|
-l|--list) list_backups; exit 0 ;;
|
||
|
|
-h|--help) usage; exit 0 ;;
|
||
|
|
-*) fail "Unknown option: $1" ;;
|
||
|
|
*) TARGET="$1"; shift ;;
|
||
|
|
esac
|
||
|
|
done
|
||
|
|
|
||
|
|
#######################################
|
||
|
|
# Resolve backup directory
|
||
|
|
#######################################
|
||
|
|
if [ -z "${TARGET}" ]; then
|
||
|
|
TARGET="$(find "${BACKUP_DIR}" -mindepth 1 -maxdepth 1 -type d -printf '%f\n' | sort -r | head -n 1 || true)"
|
||
|
|
[ -z "${TARGET}" ] && fail "No backup found in ${BACKUP_DIR}"
|
||
|
|
log "No backup specified, using latest: ${TARGET}"
|
||
|
|
fi
|
||
|
|
|
||
|
|
SOURCE_DIR="${BACKUP_DIR}/${TARGET}"
|
||
|
|
[ -d "${SOURCE_DIR}" ] || fail "Backup not found: ${SOURCE_DIR}"
|
||
|
|
|
||
|
|
DB_FILE="${SOURCE_DIR}/database.dump"
|
||
|
|
UPLOADS_FILE="${SOURCE_DIR}/uploads.tar.gz"
|
||
|
|
SHARE_FILE="${SOURCE_DIR}/share.tar.gz"
|
||
|
|
|
||
|
|
#######################################
|
||
|
|
# Confirmation
|
||
|
|
#######################################
|
||
|
|
log "About to restore from: ${SOURCE_DIR}"
|
||
|
|
[ -f "${DB_FILE}" ] && log " - database.dump ($(du -h "${DB_FILE}" | cut -f1))"
|
||
|
|
[ -f "${UPLOADS_FILE}" ] && log " - uploads.tar.gz ($(du -h "${UPLOADS_FILE}" | cut -f1))"
|
||
|
|
[ -f "${SHARE_FILE}" ] && log " - share.tar.gz ($(du -h "${SHARE_FILE}" | cut -f1))"
|
||
|
|
log "Target: database ${DB_NAME}@${DB_SERVICE}, files in ${APP_DIR}"
|
||
|
|
log "WARNING: existing database and files will be REPLACED."
|
||
|
|
|
||
|
|
if [ "${ASSUME_YES}" -ne 1 ]; then
|
||
|
|
read -r -p "Type 'RESTORE' to confirm: " CONFIRM
|
||
|
|
[ "${CONFIRM}" = "RESTORE" ] || fail "Aborted by user"
|
||
|
|
fi
|
||
|
|
|
||
|
|
#######################################
|
||
|
|
# 1. Restore database
|
||
|
|
#######################################
|
||
|
|
if [ -f "${DB_FILE}" ]; then
|
||
|
|
log "Restoring database ${DB_NAME}..."
|
||
|
|
|
||
|
|
# Drop and recreate the database to ensure a clean state
|
||
|
|
docker compose -f "${COMPOSE_FILE}" exec -T "${DB_SERVICE}" \
|
||
|
|
psql -U "${DB_USER}" -d postgres -v ON_ERROR_STOP=1 -c \
|
||
|
|
"SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = '${DB_NAME}' AND pid <> pg_backend_pid();" \
|
||
|
|
>/dev/null
|
||
|
|
|
||
|
|
docker compose -f "${COMPOSE_FILE}" exec -T "${DB_SERVICE}" \
|
||
|
|
psql -U "${DB_USER}" -d postgres -v ON_ERROR_STOP=1 -c "DROP DATABASE IF EXISTS \"${DB_NAME}\";" \
|
||
|
|
>/dev/null
|
||
|
|
|
||
|
|
docker compose -f "${COMPOSE_FILE}" exec -T "${DB_SERVICE}" \
|
||
|
|
psql -U "${DB_USER}" -d postgres -v ON_ERROR_STOP=1 -c "CREATE DATABASE \"${DB_NAME}\" OWNER \"${DB_USER}\";" \
|
||
|
|
>/dev/null
|
||
|
|
|
||
|
|
if ! docker compose -f "${COMPOSE_FILE}" exec -T "${DB_SERVICE}" \
|
||
|
|
pg_restore -U "${DB_USER}" -d "${DB_NAME}" --no-owner --no-acl --clean --if-exists \
|
||
|
|
< "${DB_FILE}"; then
|
||
|
|
fail "pg_restore failed"
|
||
|
|
fi
|
||
|
|
|
||
|
|
log "Database restored OK"
|
||
|
|
else
|
||
|
|
log "WARNING: no database.dump in backup, skipping DB restore"
|
||
|
|
fi
|
||
|
|
|
||
|
|
#######################################
|
||
|
|
# 2. Restore public/uploads
|
||
|
|
#######################################
|
||
|
|
if [ -f "${UPLOADS_FILE}" ]; then
|
||
|
|
log "Restoring public/uploads..."
|
||
|
|
BACKUP_OLD="${APP_DIR}/public/uploads.bak.$(date +%s)"
|
||
|
|
if [ -d "${APP_DIR}/public/uploads" ]; then
|
||
|
|
mv "${APP_DIR}/public/uploads" "${BACKUP_OLD}"
|
||
|
|
log "Existing uploads moved to ${BACKUP_OLD}"
|
||
|
|
fi
|
||
|
|
if tar -xzf "${UPLOADS_FILE}" -C "${APP_DIR}/public"; then
|
||
|
|
log "Uploads restored OK"
|
||
|
|
else
|
||
|
|
log "ERROR: uploads extraction failed, rolling back"
|
||
|
|
rm -rf "${APP_DIR}/public/uploads"
|
||
|
|
[ -d "${BACKUP_OLD}" ] && mv "${BACKUP_OLD}" "${APP_DIR}/public/uploads"
|
||
|
|
fail "uploads restore failed"
|
||
|
|
fi
|
||
|
|
else
|
||
|
|
log "WARNING: no uploads.tar.gz in backup, skipping uploads restore"
|
||
|
|
fi
|
||
|
|
|
||
|
|
#######################################
|
||
|
|
# 3. Restore var/share
|
||
|
|
#######################################
|
||
|
|
if [ -f "${SHARE_FILE}" ]; then
|
||
|
|
log "Restoring var/share..."
|
||
|
|
BACKUP_OLD="${APP_DIR}/var/share.bak.$(date +%s)"
|
||
|
|
if [ -d "${APP_DIR}/var/share" ]; then
|
||
|
|
mv "${APP_DIR}/var/share" "${BACKUP_OLD}"
|
||
|
|
log "Existing share moved to ${BACKUP_OLD}"
|
||
|
|
fi
|
||
|
|
if tar -xzf "${SHARE_FILE}" -C "${APP_DIR}/var"; then
|
||
|
|
log "Share restored OK"
|
||
|
|
else
|
||
|
|
log "ERROR: share extraction failed, rolling back"
|
||
|
|
rm -rf "${APP_DIR}/var/share"
|
||
|
|
[ -d "${BACKUP_OLD}" ] && mv "${BACKUP_OLD}" "${APP_DIR}/var/share"
|
||
|
|
fail "share restore failed"
|
||
|
|
fi
|
||
|
|
else
|
||
|
|
log "WARNING: no share.tar.gz in backup, skipping share restore"
|
||
|
|
fi
|
||
|
|
|
||
|
|
#######################################
|
||
|
|
# 4. Post-restore: clear caches
|
||
|
|
#######################################
|
||
|
|
log "Clearing application cache..."
|
||
|
|
docker compose -f "${COMPOSE_FILE}" exec -T php php bin/console cache:clear --no-warmup >/dev/null 2>&1 || \
|
||
|
|
log "WARNING: cache:clear failed (php service down?)"
|
||
|
|
|
||
|
|
log "Restore completed from ${TARGET}"
|