This commit is contained in:
Serreau Jovann
2026-04-10 17:47:57 +02:00
parent 857683cf70
commit 88ebba6ce5
2 changed files with 221 additions and 0 deletions

184
ansible/restore.sh.j2 Normal file
View File

@@ -0,0 +1,184 @@
#!/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}"

View File

@@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20260409141035 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('CREATE TABLE customer_payment_method (id INT GENERATED BY DEFAULT AS IDENTITY NOT NULL, stripe_payment_method_id VARCHAR(255) NOT NULL, type VARCHAR(20) NOT NULL, last4 VARCHAR(4) DEFAULT NULL, brand VARCHAR(50) DEFAULT NULL, country VARCHAR(2) DEFAULT NULL, is_default BOOLEAN NOT NULL, created_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, customer_id INT NOT NULL, PRIMARY KEY (id))');
$this->addSql('CREATE INDEX IDX_E20C953C9395C3F3 ON customer_payment_method (customer_id)');
$this->addSql('CREATE INDEX idx_customer_payment_method_default ON customer_payment_method (customer_id, is_default)');
$this->addSql('ALTER TABLE customer_payment_method ADD CONSTRAINT FK_E20C953C9395C3F3 FOREIGN KEY (customer_id) REFERENCES customer (id) ON DELETE CASCADE NOT DEFERRABLE');
$this->addSql('ALTER TABLE contrat ALTER services DROP DEFAULT');
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE customer_payment_method DROP CONSTRAINT FK_E20C953C9395C3F3');
$this->addSql('DROP TABLE customer_payment_method');
$this->addSql('ALTER TABLE contrat ALTER services SET DEFAULT \'[]\'');
}
}