init
This commit is contained in:
184
ansible/restore.sh.j2
Normal file
184
ansible/restore.sh.j2
Normal 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}"
|
||||
37
migrations/Version20260409141035.php
Normal file
37
migrations/Version20260409141035.php
Normal 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 \'[]\'');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user