All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 2m42s
359 lines
13 KiB
Bash
359 lines
13 KiB
Bash
#!/bin/bash
|
|
# ==============================================================================
|
|
# Wordly.art - PostgreSQL Backup vers NAS Synology via SSH/rsync
|
|
# ==============================================================================
|
|
# Sauvegarde la base PostgreSQL et l'archive DR sur le NAS via SSH/rsync.
|
|
# Pas de montage CIFS — rsync SSH direct sur /volume1/backups/wordly.
|
|
#
|
|
# CRON (installé par install-crontab.sh) :
|
|
# 0 */6 * * * bash /opt/wordly/scripts/backup-to-nas.sh >> /var/log/wordly-backup.log 2>&1
|
|
#
|
|
# Usage :
|
|
# ./backup-to-nas.sh # Backup complet → NAS
|
|
# ./backup-to-nas.sh --full # Identique (alias explicite)
|
|
# ./backup-to-nas.sh --list # Lister les archives disponibles sur le NAS
|
|
# ==============================================================================
|
|
|
|
set -euo pipefail
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
PROJECT_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
|
|
|
# ==============================================================================
|
|
# CHARGER LE .env
|
|
# ==============================================================================
|
|
ENV_FILE="${PROJECT_ROOT}/.env"
|
|
if [ -f "${ENV_FILE}" ]; then
|
|
set -a
|
|
set +u
|
|
source "${ENV_FILE}"
|
|
set -u
|
|
set +a
|
|
else
|
|
echo "ERROR: .env introuvable : ${ENV_FILE}" >&2
|
|
exit 1
|
|
fi
|
|
|
|
# ==============================================================================
|
|
# CONFIGURATION (depuis .env)
|
|
# ==============================================================================
|
|
|
|
# NAS SSH
|
|
NAS_HOST="${NAS_HOST:-192.168.1.146}"
|
|
NAS_USER="${NAS_USER:-wordly-backup}"
|
|
NAS_PATH="${NAS_PATH:-/volume1/backups/wordly}"
|
|
NAS_SSH_PORT="${NAS_SSH_PORT:-22}"
|
|
NAS_SSH_KEY="${NAS_SSH_KEY:-/root/.ssh/wordly_nas_key}"
|
|
|
|
# PostgreSQL
|
|
POSTGRES_CONTAINER="${POSTGRES_CONTAINER:-wordly-postgres}"
|
|
POSTGRES_USER="${POSTGRES_USER:-translate}"
|
|
POSTGRES_DB="${POSTGRES_DB:-translate_db}"
|
|
POSTGRES_PASSWORD="${POSTGRES_PASSWORD:?POSTGRES_PASSWORD doit être défini dans .env}"
|
|
|
|
# Rétention sur le NAS (nombre d'archives à garder)
|
|
DAILY_RETENTION=${DAILY_RETENTION:-7}
|
|
WEEKLY_RETENTION=${WEEKLY_RETENTION:-4}
|
|
MONTHLY_RETENTION=${MONTHLY_RETENTION:-6}
|
|
|
|
# Telegram
|
|
TELEGRAM_BOT_TOKEN="${TELEGRAM_BOT_TOKEN:-}"
|
|
TELEGRAM_CHAT_ID="${TELEGRAM_CHAT_ID:-}"
|
|
|
|
# ==============================================================================
|
|
# INTERNALS
|
|
# ==============================================================================
|
|
TIMESTAMP=$(date +"%Y%m%d_%H%M%S")
|
|
DAY_OF_WEEK=$(date +"%u") # 1=Lun, 7=Dim
|
|
DAY_OF_MONTH=$(date +"%d")
|
|
SNAPSHOT_NAME="wordly_dr_${TIMESTAMP}.tar.gz"
|
|
LOCAL_TMP="/tmp/wordly_backup_${TIMESTAMP}"
|
|
SSH_CMD="ssh -i ${NAS_SSH_KEY} -p ${NAS_SSH_PORT} -o BatchMode=yes -o ConnectTimeout=10"
|
|
RSYNC_CMD="rsync -az -e 'ssh -i ${NAS_SSH_KEY} -p ${NAS_SSH_PORT} -o BatchMode=yes -o ConnectTimeout=10'"
|
|
|
|
RED='\033[0;31m'
|
|
GREEN='\033[0;32m'
|
|
YELLOW='\033[1;33m'
|
|
NC='\033[0m'
|
|
LOG_PREFIX="[Backup ${TIMESTAMP}]"
|
|
|
|
log() { echo "${LOG_PREFIX} $1" >&2; }
|
|
log_success() { echo -e "${LOG_PREFIX} ${GREEN}✅ $1${NC}" >&2; }
|
|
log_error() { echo -e "${LOG_PREFIX} ${RED}❌ ERROR: $1${NC}" >&2; }
|
|
log_warning() { echo -e "${LOG_PREFIX} ${YELLOW}⚠️ $1${NC}" >&2; }
|
|
|
|
# ==============================================================================
|
|
# TELEGRAM
|
|
# ==============================================================================
|
|
send_telegram() {
|
|
local message="$1"
|
|
if [ -n "${TELEGRAM_BOT_TOKEN}" ] && [ -n "${TELEGRAM_CHAT_ID}" ]; then
|
|
curl -s -X POST "https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage" \
|
|
-d "chat_id=${TELEGRAM_CHAT_ID}" \
|
|
-d "text=${message}" \
|
|
-d "parse_mode=Markdown" \
|
|
>/dev/null 2>&1 || true
|
|
fi
|
|
}
|
|
|
|
# ==============================================================================
|
|
# PRÉREQUIS
|
|
# ==============================================================================
|
|
check_prerequisites() {
|
|
log "Vérification des prérequis..."
|
|
|
|
# Clé SSH
|
|
if [ ! -f "${NAS_SSH_KEY}" ]; then
|
|
log_error "Clé SSH introuvable : ${NAS_SSH_KEY}"
|
|
log_error "Lancez d'abord : sudo bash scripts/setup-nas.sh"
|
|
exit 1
|
|
fi
|
|
|
|
# Connectivité SSH vers le NAS
|
|
if ! ${SSH_CMD} "${NAS_USER}@${NAS_HOST}" "echo OK" >/dev/null 2>&1; then
|
|
log_error "Impossible de se connecter au NAS ${NAS_HOST} via SSH."
|
|
log_error "Vérifiez : ssh -i ${NAS_SSH_KEY} ${NAS_USER}@${NAS_HOST}"
|
|
send_telegram "🚨 *Wordly Backup ÉCHOUÉ*
|
|
NAS inaccessible : ${NAS_HOST}
|
|
Date : $(date '+%Y-%m-%d %H:%M:%S')"
|
|
exit 1
|
|
fi
|
|
log_success "NAS SSH : OK"
|
|
|
|
# Docker + container PostgreSQL
|
|
if ! docker ps --format '{{.Names}}' 2>/dev/null | grep -q "^${POSTGRES_CONTAINER}$"; then
|
|
log_error "Container PostgreSQL '${POSTGRES_CONTAINER}' n'est pas en cours d'exécution !"
|
|
send_telegram "🚨 *Wordly Backup ÉCHOUÉ*
|
|
PostgreSQL container non trouvé
|
|
Date : $(date '+%Y-%m-%d %H:%M:%S')"
|
|
exit 1
|
|
fi
|
|
log_success "PostgreSQL container : OK"
|
|
}
|
|
|
|
# ==============================================================================
|
|
# BACKUP POSTGRESQL
|
|
# ==============================================================================
|
|
backup_postgres() {
|
|
log "Dump PostgreSQL de '${POSTGRES_DB}'..."
|
|
mkdir -p "${LOCAL_TMP}"
|
|
|
|
local dump_file="${LOCAL_TMP}/db_${TIMESTAMP}.dump.gz"
|
|
|
|
if ! docker exec \
|
|
-e PGPASSWORD="${POSTGRES_PASSWORD}" \
|
|
"${POSTGRES_CONTAINER}" \
|
|
pg_dump \
|
|
-U "${POSTGRES_USER}" \
|
|
-d "${POSTGRES_DB}" \
|
|
--format=custom \
|
|
--no-owner \
|
|
--no-acl \
|
|
2>/dev/null | gzip > "${dump_file}"; then
|
|
log_error "pg_dump a échoué !"
|
|
send_telegram "🚨 *Wordly Backup ÉCHOUÉ*
|
|
pg_dump error sur ${POSTGRES_DB}
|
|
Date : $(date '+%Y-%m-%d %H:%M:%S')"
|
|
rm -rf "${LOCAL_TMP}"
|
|
exit 1
|
|
fi
|
|
|
|
# Vérification taille
|
|
local size_bytes
|
|
size_bytes=$(stat -c %s "${dump_file}" 2>/dev/null || stat -f %z "${dump_file}")
|
|
local min_bytes=1024 # 1KB minimum (safe for new/small databases)
|
|
|
|
if [ "${size_bytes}" -lt "${min_bytes}" ]; then
|
|
log_error "Dump trop petit ($(numfmt --to=iec ${size_bytes})) — base de données vide ?"
|
|
send_telegram "🚨 *Wordly Backup ÉCHOUÉ*
|
|
Dump PostgreSQL trop petit : $(numfmt --to=iec ${size_bytes})
|
|
Date : $(date '+%Y-%m-%d %H:%M:%S')"
|
|
rm -rf "${LOCAL_TMP}"
|
|
exit 1
|
|
fi
|
|
|
|
log_success "Dump PostgreSQL : $(numfmt --to=iec ${size_bytes})"
|
|
echo "${dump_file}"
|
|
}
|
|
|
|
# ==============================================================================
|
|
# CRÉER L'ARCHIVE DR (dump + .env + docker-compose + configs)
|
|
# ==============================================================================
|
|
create_dr_archive() {
|
|
local dump_file="$1"
|
|
log "Construction de l'archive DR..."
|
|
|
|
# Copier les fichiers de config
|
|
[ -f "${PROJECT_ROOT}/.env" ] && cp "${PROJECT_ROOT}/.env" "${LOCAL_TMP}/.env.production"
|
|
[ -f "${PROJECT_ROOT}/docker-compose.yml" ] && cp "${PROJECT_ROOT}/docker-compose.yml" "${LOCAL_TMP}/"
|
|
[ -d "${PROJECT_ROOT}/docker" ] && cp -r "${PROJECT_ROOT}/docker" "${LOCAL_TMP}/"
|
|
|
|
# Compresser
|
|
local archive_path="/tmp/${SNAPSHOT_NAME}"
|
|
tar -czf "${archive_path}" -C "${LOCAL_TMP}" .
|
|
rm -rf "${LOCAL_TMP}"
|
|
|
|
# Vérification intégrité
|
|
if ! gzip -t "${archive_path}" 2>/dev/null; then
|
|
log_error "Archive DR corrompue !"
|
|
rm -f "${archive_path}"
|
|
exit 1
|
|
fi
|
|
|
|
local size
|
|
size=$(du -h "${archive_path}" | cut -f1)
|
|
log_success "Archive DR créée : ${SNAPSHOT_NAME} (${size})"
|
|
echo "${archive_path}|${size}"
|
|
}
|
|
|
|
# ==============================================================================
|
|
# ENVOYER SUR LE NAS VIA SCP/rsync SSH
|
|
# ==============================================================================
|
|
push_to_nas() {
|
|
local archive_path="$1"
|
|
local size="$2"
|
|
|
|
log "Transfert vers le NAS via rsync SSH..."
|
|
log " Source : ${archive_path}"
|
|
log " Dest : ${NAS_USER}@${NAS_HOST}:${NAS_PATH}/snapshots/${SNAPSHOT_NAME}"
|
|
|
|
# Dossier quotidien/hebdo/mensuel sur le NAS
|
|
local nas_dest="${NAS_PATH}/snapshots"
|
|
|
|
# Transfer principal
|
|
if ! rsync -az \
|
|
-e "ssh -i ${NAS_SSH_KEY} -p ${NAS_SSH_PORT} -o BatchMode=yes -o ConnectTimeout=30" \
|
|
"${archive_path}" \
|
|
"${NAS_USER}@${NAS_HOST}:${nas_dest}/${SNAPSHOT_NAME}"; then
|
|
log_error "rsync vers le NAS a échoué !"
|
|
send_telegram "🚨 *Wordly Backup ÉCHOUÉ*
|
|
rsync SSH vers ${NAS_HOST} a échoué
|
|
Fichier local conservé : ${archive_path}
|
|
Date : $(date '+%Y-%m-%d %H:%M:%S')"
|
|
# Garder le fichier local comme fallback
|
|
mkdir -p "${PROJECT_ROOT}/backups/emergency"
|
|
mv "${archive_path}" "${PROJECT_ROOT}/backups/emergency/${SNAPSHOT_NAME}"
|
|
log_warning "Archive conservée localement : ${PROJECT_ROOT}/backups/emergency/${SNAPSHOT_NAME}"
|
|
exit 1
|
|
fi
|
|
|
|
log_success "Archive transférée sur le NAS : ${nas_dest}/${SNAPSHOT_NAME}"
|
|
|
|
# Copie hebdomadaire (dimanche)
|
|
if [ "${DAY_OF_WEEK}" = "7" ]; then
|
|
${SSH_CMD} "${NAS_USER}@${NAS_HOST}" \
|
|
"cp ${nas_dest}/${SNAPSHOT_NAME} ${NAS_PATH}/snapshots/weekly_${SNAPSHOT_NAME}" 2>/dev/null || true
|
|
log "Archive hebdomadaire copiée."
|
|
fi
|
|
|
|
# Copie mensuelle (1er du mois)
|
|
if [ "${DAY_OF_MONTH}" = "01" ]; then
|
|
${SSH_CMD} "${NAS_USER}@${NAS_HOST}" \
|
|
"cp ${nas_dest}/${SNAPSHOT_NAME} ${NAS_PATH}/snapshots/monthly_${SNAPSHOT_NAME}" 2>/dev/null || true
|
|
log "Archive mensuelle copiée."
|
|
fi
|
|
|
|
# Nettoyage local
|
|
rm -f "${archive_path}"
|
|
}
|
|
|
|
# ==============================================================================
|
|
# ROTATION DES ARCHIVES SUR LE NAS
|
|
# ==============================================================================
|
|
cleanup_nas() {
|
|
log "Rotation des archives sur le NAS (conservation : ${DAILY_RETENTION} jours)..."
|
|
|
|
# Supprimer les archives wordly_dr_* plus vieilles que DAILY_RETENTION
|
|
${SSH_CMD} "${NAS_USER}@${NAS_HOST}" \
|
|
"find ${NAS_PATH}/snapshots -name 'wordly_dr_*.tar.gz' -mtime +${DAILY_RETENTION} -delete 2>/dev/null; \
|
|
find ${NAS_PATH}/snapshots -name 'weekly_*.tar.gz' | sort -r | tail -n +$((WEEKLY_RETENTION + 1)) | xargs rm -f 2>/dev/null; \
|
|
find ${NAS_PATH}/snapshots -name 'monthly_*.tar.gz' | sort -r | tail -n +$((MONTHLY_RETENTION + 1)) | xargs rm -f 2>/dev/null; \
|
|
echo OK" | grep -q "OK"
|
|
|
|
log_success "Rotation des archives OK"
|
|
}
|
|
|
|
# ==============================================================================
|
|
# SYNCHRONISER LES SCRIPTS SUR LE NAS (pour restauration depuis .98)
|
|
# ==============================================================================
|
|
sync_scripts() {
|
|
rsync -az \
|
|
-e "ssh -i ${NAS_SSH_KEY} -p ${NAS_SSH_PORT} -o BatchMode=yes" \
|
|
--exclude="__pycache__" \
|
|
--exclude="*.pyc" \
|
|
"${SCRIPT_DIR}/" \
|
|
"${NAS_USER}@${NAS_HOST}:${NAS_PATH}/scripts/" 2>/dev/null || true
|
|
}
|
|
|
|
# ==============================================================================
|
|
# LISTER LES ARCHIVES DISPONIBLES
|
|
# ==============================================================================
|
|
list_archives() {
|
|
log "Archives disponibles sur le NAS :"
|
|
${SSH_CMD} "${NAS_USER}@${NAS_HOST}" \
|
|
"ls -lht ${NAS_PATH}/snapshots/wordly_dr_*.tar.gz 2>/dev/null || echo '(aucune archive)'"
|
|
}
|
|
|
|
# ==============================================================================
|
|
# MAIN
|
|
# ==============================================================================
|
|
main() {
|
|
case "${1:-}" in
|
|
--list)
|
|
ENV_FILE="${PROJECT_ROOT}/.env"
|
|
[ -f "${ENV_FILE}" ] && { set -a; source "${ENV_FILE}"; set +a; }
|
|
list_archives
|
|
exit 0
|
|
;;
|
|
--full|*)
|
|
;;
|
|
esac
|
|
|
|
echo ""
|
|
echo "================================================================="
|
|
echo " Wordly.art — Backup → NAS Synology 192.168.1.146"
|
|
echo " DB : ${POSTGRES_DB}"
|
|
echo " NAS : ${NAS_USER}@${NAS_HOST}:${NAS_PATH}"
|
|
echo " $(date '+%Y-%m-%d %H:%M:%S')"
|
|
echo "================================================================="
|
|
echo ""
|
|
|
|
check_prerequisites
|
|
|
|
# 1. Dump PostgreSQL
|
|
local dump_file
|
|
dump_file=$(backup_postgres)
|
|
|
|
# 2. Créer l'archive DR
|
|
local archive_info
|
|
archive_info=$(create_dr_archive "${dump_file}")
|
|
local archive_path="${archive_info%%|*}"
|
|
local archive_size="${archive_info##*|}"
|
|
|
|
# 3. Envoyer sur le NAS via rsync SSH
|
|
push_to_nas "${archive_path}" "${archive_size}"
|
|
|
|
# 4. Rotation
|
|
cleanup_nas
|
|
|
|
# 5. Sync scripts
|
|
sync_scripts
|
|
|
|
# 6. Notification Telegram
|
|
send_telegram "✅ *Wordly.art Backup OK*
|
|
Archive : \`${SNAPSHOT_NAME}\`
|
|
Taille : ${archive_size}
|
|
NAS : \`${NAS_PATH}/snapshots/\`
|
|
Date : $(date '+%Y-%m-%d %H:%M:%S')"
|
|
|
|
echo ""
|
|
log_success "================================================================="
|
|
log_success "Backup complet terminé !"
|
|
log_success " Archive : ${NAS_PATH}/snapshots/${SNAPSHOT_NAME}"
|
|
log_success " Lister : bash scripts/backup-to-nas.sh --list"
|
|
log_success "================================================================="
|
|
echo ""
|
|
}
|
|
|
|
main "$@"
|