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