Files
docker-stack-template/gitea-backup.sh
T
bruno 1c6506e319 Correction et enrichissement complet des scripts Gitea
- Corrections de bugs (14): grep portable, df robuste, rollback reel,
  retention intelligente, nettoyage cible, lock anti-concurrence, etc.
- Nouveaux fichiers: setup.sh (installateur tout-en-un), restore.sh
  (restauration standalone), gitea-scripts.conf (config unifiee)
- Features: backup distant (rsync/SCP), lock files, logrotate, dry-run,
  healthchecks Docker, notifications Telegram/ntfy.sh
- Documentation enrichie avec diagrammes d'architecture mermaid
2026-06-26 11:53:28 -04:00

670 lines
27 KiB
Bash
Executable File

#!/bin/bash
# =============================================================================
# gitea-backup.sh — Backup complet Gitea (DB + volumes + config)
# =============================================================================
# À placer dans /usr/local/bin/ ou ~/scripts/
# Cron recommandé : 0 2 * * * /usr/local/bin/gitea-backup.sh
#
# Ce script sauvegarde :
# 1. La base de données MySQL (dump SQL complet)
# 2. Les volumes Docker (data, mysql)
# 3. Le fichier docker-compose.yml et la config
# 4. Génère un script restore.sh autonome
#
# Paramètres configurables : voir gitea-scripts.conf
# =============================================================================
set -euo pipefail
# ─── Charger la configuration commune ─────────────────────────────────────────
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
CONFIG_FILE="${CONFIG_FILE:-$SCRIPT_DIR/gitea-scripts.conf}"
if [[ -f "$CONFIG_FILE" ]]; then
# shellcheck source=/dev/null
source "$CONFIG_FILE"
fi
# ─── Configuration locale (après chargement conf, permet surcharge env) ──────
BACKUP_DIR="${BACKUP_DIR:-/NFS/BACKUP/DOCKER/dev-prod/gitea}"
COMPOSE_DIR="${COMPOSE_DIR:-$HOME/dev/git/outils/docker/gitea}"
COMPOSE_FILE="${COMPOSE_FILE:-docker-compose.yml}"
# Conteneurs
DB_CONTAINER="${DB_CONTAINER:-gitea_db}"
APP_CONTAINER="${APP_CONTAINER:-gitea_server}"
DB_SERVICE="${DB_SERVICE:-gitea_db}"
APP_SERVICE="${APP_SERVICE:-gitea_server}"
# Base de données
DB_NAME="${DB_NAME:-gitea}"
DB_USER="${DB_USER:-gitea}"
DB_PASSWORD="${DB_PASSWORD:-chab30}"
DB_ROOT_PASSWORD="${DB_ROOT_PASSWORD:-gitea}"
# Volumes Docker (chemins HÔTE, pas conteneur)
VOLUME_DATA="${VOLUME_DATA:-/DOCKER_CONFIG/gitea/data}"
VOLUME_MYSQL="${VOLUME_MYSQL:-/DOCKER_CONFIG/gitea/mysql}"
# Rétention
RETENTION_DAYS="${RETENTION_DAYS:-30}"
RETENTION_WEEKLY="${RETENTION_WEEKLY:-4}"
RETENTION_MONTHLY="${RETENTION_MONTHLY:-6}"
# Options
COMPRESS="${COMPRESS:-true}"
VERIFY_BACKUP="${VERIFY_BACKUP:-true}"
STOP_CONTAINERS="${STOP_CONTAINERS:-false}"
ENABLE_NOTIFICATIONS="${ENABLE_NOTIFICATIONS:-false}"
# Log
LOG_FILE="${LOG_FILE:-/var/log/gitea-backup.log}"
LOG_DIR="$(dirname "$LOG_FILE")"
# Lock file
LOCK_DIR="${LOCK_DIR:-/tmp}"
LOCK_FILE="${LOCK_DIR}/gitea-backup.lock"
# Backup distant
REMOTE_BACKUP_ENABLED="${REMOTE_BACKUP_ENABLED:-false}"
REMOTE_BACKUP_METHOD="${REMOTE_BACKUP_METHOD:-rsync}"
REMOTE_BACKUP_HOST="${REMOTE_BACKUP_HOST:-}"
REMOTE_BACKUP_USER="${REMOTE_BACKUP_USER:-}"
REMOTE_BACKUP_PATH="${REMOTE_BACKUP_PATH:-}"
REMOTE_BACKUP_SSH_KEY="${REMOTE_BACKUP_SSH_KEY:-$HOME/.ssh/id_rsa}"
# Notifications
NOTIFY_METHOD="${NOTIFY_METHOD:-log}"
TELEGRAM_BOT_TOKEN="${TELEGRAM_BOT_TOKEN:-}"
TELEGRAM_CHAT_ID="${TELEGRAM_CHAT_ID:-}"
NTPY_TOPIC="${NTPY_TOPIC:-}"
# ─── Prérequis : s'assurer que le dossier de log existe ──────────────────────
mkdir -p "$LOG_DIR" 2>/dev/null || true
# ─── Logging ─────────────────────────────────────────────────────────────────
log() {
local msg="[$(date '+%Y-%m-%d %H:%M:%S')] $1"
echo "$msg"
echo "$msg" >> "$LOG_FILE" 2>/dev/null || true
}
# ─── Notification ────────────────────────────────────────────────────────────
notify() {
local msg="$1"
case "$NOTIFY_METHOD" in
telegram)
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=$msg" > /dev/null 2>&1 || true
fi
;;
ntfy)
if [[ -n "$NTPY_TOPIC" ]]; then
curl -s -H "Title: Gitea Backup" -d "$msg" "https://ntfy.sh/$NTPY_TOPIC" > /dev/null 2>&1 || true
fi
;;
esac
}
# ─── Lock file ───────────────────────────────────────────────────────────────
acquire_lock() {
if [[ -f "$LOCK_FILE" ]]; then
local pid
pid=$(cat "$LOCK_FILE" 2>/dev/null || echo "")
if [[ -n "$pid" ]] && kill -0 "$pid" 2>/dev/null; then
log "⚠️ Une autre instance (PID $pid) est déjà en cours d'exécution — arrêt"
exit 0
fi
log "⚠️ Lock stale détecté (PID $pid n'existe plus), on continue"
rm -f "$LOCK_FILE"
fi
echo $$ > "$LOCK_FILE"
}
release_lock() {
rm -f "$LOCK_FILE"
}
cleanup() {
release_lock
}
trap cleanup EXIT
# ─── Détection docker compose ────────────────────────────────────────────────
detect_compose() {
if docker compose version &>/dev/null; then
echo "docker compose"
elif command -v docker-compose &>/dev/null; then
echo "docker-compose"
else
echo ""
fi
}
# ─── Vérification espace disque (robuste) ────────────────────────────────────
check_disk_space() {
local path="$1"
local min_kb="${2:-2097152}" # 2 GB par défaut
local available_kb
# Méthode robuste : utiliser df -P (format POSIX, pas de wrapping)
available_kb=$(df -P "$path" 2>/dev/null | tail -1 | awk '{print $4}')
if [[ -z "$available_kb" || "$available_kb" -le 0 ]]; then
# Fallback avec --output (GNU df)
available_kb=$(df --output=avail "$path" 2>/dev/null | tail -1 | tr -d ' ' || echo "0")
fi
if [[ "$available_kb" -lt "$min_kb" ]]; then
return 1
fi
echo "$available_kb"
return 0
}
# ─── Backup distant ──────────────────────────────────────────────────────────
sync_remote() {
local backup_path="$1"
if [[ "$REMOTE_BACKUP_ENABLED" != "true" ]]; then
return 0
fi
if [[ -z "$REMOTE_BACKUP_HOST" ]] || [[ -z "$REMOTE_BACKUP_USER" ]] || [[ -z "$REMOTE_BACKUP_PATH" ]]; then
log "⚠️ Backup distant : configuration incomplète, ignoré"
return 0
fi
log "☁️ Synchronisation distante ($REMOTE_BACKUP_METHOD) vers ${REMOTE_BACKUP_USER}@${REMOTE_BACKUP_HOST}:${REMOTE_BACKUP_PATH}..."
local ssh_opts="-o StrictHostKeyChecking=no -o ConnectTimeout=10"
if [[ -f "$REMOTE_BACKUP_SSH_KEY" ]]; then
ssh_opts="$ssh_opts -i $REMOTE_BACKUP_SSH_KEY"
fi
case "$REMOTE_BACKUP_METHOD" in
rsync)
if command -v rsync &>/dev/null; then
rsync -avz -e "ssh $ssh_opts" \
"$backup_path" \
"${REMOTE_BACKUP_USER}@${REMOTE_BACKUP_HOST}:${REMOTE_BACKUP_PATH}/" 2>&1 || {
log "⚠️ Échec rsync distant"
return 1
}
log " ✅ Synchronisation rsync réussie"
else
log "⚠️ rsync non installé, backup distant ignoré"
return 1
fi
;;
scp)
scp $ssh_opts -r "$backup_path" \
"${REMOTE_BACKUP_USER}@${REMOTE_BACKUP_HOST}:${REMOTE_BACKUP_PATH}/" 2>&1 || {
log "⚠️ Échec SCP distant"
return 1
}
log " ✅ Synchronisation SCP réussie"
;;
*)
log "⚠️ Méthode de backup distant inconnue : $REMOTE_BACKUP_METHOD"
return 1
;;
esac
return 0
}
# ═════════════════════════════════════════════════════════════════════════════
# MAIN
# ═════════════════════════════════════════════════════════════════════════════
acquire_lock
TIMESTAMP=$(date +%Y%m%d-%H%M%S)
DATE_LABEL=$(date +%Y-%m-%d)
DAY_OF_WEEK=$(date +%u) # 1=lundi, 7=dimanche
DAY_OF_MONTH=$(date +%d)
BACKUP_NAME="gitea-backup-${TIMESTAMP}"
BACKUP_PATH="${BACKUP_DIR}/${BACKUP_NAME}"
RESTORE_SCRIPT="${BACKUP_PATH}/restore.sh"
DOCKER_COMPOSE=$(detect_compose)
log "════════════════════════════════════════════════════════"
log "📦 Début backup Gitea → $BACKUP_PATH"
# ─── Pré-vérifications ──────────────────────────────────────────────────────
mkdir -p "$BACKUP_DIR" || { log "❌ Impossible de créer $BACKUP_DIR"; exit 1; }
cd "$COMPOSE_DIR" || { log "❌ Répertoire $COMPOSE_DIR introuvable"; exit 1; }
if [[ -z "$DOCKER_COMPOSE" ]]; then
log "❌ Ni 'docker compose' ni 'docker-compose' n'est disponible"
exit 1
fi
mkdir -p "$BACKUP_PATH"
# Vérifier espace disque (minimum 2 GB)
AVAILABLE_KB=$(check_disk_space "$BACKUP_DIR" 2097152) || {
log "❌ Espace disque insuffisant (moins de 2 GB disponibles sur $BACKUP_DIR)"
exit 1
}
log "💾 Espace disque : $((AVAILABLE_KB / 1024)) MB disponibles"
# ─── Optionnel : arrêter les conteneurs pour un backup consistant ───────────
CONTAINERS_WERE_STOPPED=false
if [[ "$STOP_CONTAINERS" == "true" ]]; then
log "⏸️ Arrêt temporaire des conteneurs..."
$DOCKER_COMPOSE -f "$COMPOSE_FILE" stop 2>&1 | tee -a "$LOG_FILE" || true
CONTAINERS_WERE_STOPPED=true
fi
# ─── 1. Backup base de données MySQL ────────────────────────────────────────
log "🛢️ Dump base de données MySQL ($DB_NAME)..."
if docker ps --format '{{.Names}}' | grep -q "^${DB_CONTAINER}$"; then
docker exec "$DB_CONTAINER" mysqldump \
-u root -p"$DB_ROOT_PASSWORD" \
--single-transaction \
--routines \
--triggers \
--events \
--hex-blob \
--default-character-set=utf8mb4 \
"$DB_NAME" > "${BACKUP_PATH}/gitea-db.sql" 2>"${BACKUP_PATH}/mysqldump.err" || {
log "❌ Échec du dump MySQL"
cat "${BACKUP_PATH}/mysqldump.err" >> "$LOG_FILE" 2>/dev/null || true
[[ "$CONTAINERS_WERE_STOPPED" == "true" ]] && $DOCKER_COMPOSE -f "$COMPOSE_FILE" start 2>&1 | tee -a "$LOG_FILE" || true
exit 1
}
rm -f "${BACKUP_PATH}/mysqldump.err"
DB_SIZE=$(wc -c < "${BACKUP_PATH}/gitea-db.sql" 2>/dev/null || echo "0")
log " ✅ Dump MySQL OK (${DB_SIZE} bytes)"
else
log "❌ Conteneur $DB_CONTAINER non trouvé (pas en cours d'exécution)"
[[ "$CONTAINERS_WERE_STOPPED" == "true" ]] && $DOCKER_COMPOSE -f "$COMPOSE_FILE" start 2>&1 | tee -a "$LOG_FILE" || true
exit 1
fi
# Compresser le dump si demandé
if [[ "$COMPRESS" == "true" ]]; then
gzip -f "${BACKUP_PATH}/gitea-db.sql"
log " 📦 Compressé : $(ls -lh "${BACKUP_PATH}/gitea-db.sql.gz" 2>/dev/null | awk '{print $5}')"
fi
# ─── 2. Backup volumes Docker ───────────────────────────────────────────────
log "📁 Backup volume data ($VOLUME_DATA)..."
tar -czf "${BACKUP_PATH}/gitea-data.tar.gz" \
-C "$(dirname "$VOLUME_DATA")" \
--exclude='*.log' \
--exclude='log/*' \
--exclude='tmp/*' \
--exclude='cache/*' \
"$(basename "$VOLUME_DATA")" 2>&1 || {
log "❌ Échec tar du volume data"
[[ "$CONTAINERS_WERE_STOPPED" == "true" ]] && $DOCKER_COMPOSE -f "$COMPOSE_FILE" start 2>&1 | tee -a "$LOG_FILE" || true
exit 1
}
DATA_SIZE=$(ls -lh "${BACKUP_PATH}/gitea-data.tar.gz" 2>/dev/null | awk '{print $5}')
log " ✅ Volume data sauvegardé ($DATA_SIZE)"
log "📁 Backup volume MySQL ($VOLUME_MYSQL)..."
tar -czf "${BACKUP_PATH}/gitea-mysql.tar.gz" \
-C "$(dirname "$VOLUME_MYSQL")" \
"$(basename "$VOLUME_MYSQL")" 2>&1 || {
log "❌ Échec tar du volume MySQL"
[[ "$CONTAINERS_WERE_STOPPED" == "true" ]] && $DOCKER_COMPOSE -f "$COMPOSE_FILE" start 2>&1 | tee -a "$LOG_FILE" || true
exit 1
}
MYSQL_SIZE=$(ls -lh "${BACKUP_PATH}/gitea-mysql.tar.gz" 2>/dev/null | awk '{print $5}')
log " ✅ Volume MySQL sauvegardé ($MYSQL_SIZE)"
# ─── 3. Sauvegarde docker-compose + config ──────────────────────────────────
log "📋 Sauvegarde docker-compose et configuration..."
cp "$COMPOSE_DIR/$COMPOSE_FILE" "${BACKUP_PATH}/docker-compose.yml"
# Sauvegarder aussi le app.ini si présent
if [[ -f "${VOLUME_DATA}/gitea/conf/app.ini" ]]; then
cp "${VOLUME_DATA}/gitea/conf/app.ini" "${BACKUP_PATH}/app.ini"
log " ✅ app.ini sauvegardé"
else
log " ⚠️ app.ini non trouvé dans ${VOLUME_DATA}/gitea/conf/"
fi
# ─── Redémarrer les conteneurs si on les avait arrêtés ──────────────────────
if [[ "$CONTAINERS_WERE_STOPPED" == "true" ]]; then
log "▶️ Redémarrage des conteneurs..."
$DOCKER_COMPOSE -f "$COMPOSE_FILE" start 2>&1 | tee -a "$LOG_FILE" || true
# Attendre que Gitea réponde
for i in $(seq 1 30); do
if curl -s -o /dev/null -w "%{http_code}" "http://localhost:3000" 2>/dev/null | grep -qE "200|302|303"; then
log "✅ Gitea répond après ${i}s"
break
fi
sleep 1
done
fi
# ─── 4. Générer le script de restore ────────────────────────────────────────
log "📝 Génération du script de restore..."
DB_DUMP_FILE="gitea-db.sql"
[[ "$COMPRESS" == "true" ]] && DB_DUMP_FILE="gitea-db.sql.gz"
cat > "$RESTORE_SCRIPT" << RESTORE_EOF
#!/bin/bash
# =============================================================================
# RESTORE SCRIPT — Gitea Backup $TIMESTAMP
# Généré le $(date)
# =============================================================================
# Usage : bash restore.sh [OPTIONS]
#
# Options :
# --dry-run Affiche ce qui serait fait sans exécuter
# --force Ne pas demander confirmation
# --help Cette aide
# =============================================================================
set -euo pipefail
# ─── Configuration cible ────────────────────────────────────────────────────
TARGET_COMPOSE_DIR="\${TARGET_COMPOSE_DIR:-\$HOME/dev/git/outils/docker/gitea}"
TARGET_VOLUME_DATA="\${TARGET_VOLUME_DATA:-/DOCKER_CONFIG/gitea/data}"
TARGET_VOLUME_MYSQL="\${TARGET_VOLUME_MYSQL:-/DOCKER_CONFIG/gitea/mysql}"
DB_ROOT_PASSWORD="\${DB_ROOT_PASSWORD:-gitea}"
DB_NAME="\${DB_NAME:-gitea}"
DB_USER="\${DB_USER:-gitea}"
DB_PASSWORD="\${DB_PASSWORD:-chab30}"
DRY_RUN=false
FORCE=false
for arg in "\$@"; do
case "\$arg" in
--dry-run) DRY_RUN=true ;;
--force) FORCE=true ;;
--help|-h) head -50 "\$0"; exit 0 ;;
esac
done
SCRIPT_DIR="\$(cd "\$(dirname "\$0")" && pwd)"
BACKUP_DIR="\$SCRIPT_DIR"
echo "════════════════════════════════════════════════════════"
echo "🔄 RESTAURATION GITEA"
echo " Source : \$BACKUP_DIR"
echo " Date : $TIMESTAMP"
echo " Cible : \$TARGET_COMPOSE_DIR"
echo "════════════════════════════════════════════════════════"
if [[ "\$DRY_RUN" == "true" ]]; then
echo ""
echo "🔍 DRY RUN — aucune action ne sera effectuée"
echo ""
echo "Actions qui seraient exécutées :"
echo " 1. mkdir -p \$TARGET_VOLUME_DATA \$TARGET_VOLUME_MYSQL"
echo " 2. Extraire gitea-mysql.tar.gz → \$TARGET_VOLUME_MYSQL"
echo " 3. Extraire gitea-data.tar.gz → \$TARGET_VOLUME_DATA"
echo " 4. Copier docker-compose.yml → \$TARGET_COMPOSE_DIR/"
echo " 5. docker compose up -d"
echo " 6. Attendre MySQL prêt"
echo " 7. docker exec gitea_db mysql ... < $DB_DUMP_FILE"
echo " 8. docker compose restart"
echo ""
exit 0
fi
if [[ "\$FORCE" != "true" ]]; then
echo ""
echo "⚠️ ATTENTION : Ceci va ÉCRASER les données Gitea existantes !"
echo " Toute donnée non sauvegardée sera PERDUE."
echo ""
read -p "Confirmer la restauration ? (taper 'RESTORE' en majuscules) : " CONFIRM
if [[ "\$CONFIRM" != "RESTORE" ]]; then
echo "❌ Restauration annulée."
exit 0
fi
fi
echo ""
echo "📦 Étape 1/6 : Création des répertoires..."
mkdir -p "\$TARGET_VOLUME_DATA" "\$TARGET_VOLUME_MYSQL" "\$TARGET_COMPOSE_DIR"
echo "📦 Étape 2/6 : Restauration volume MySQL..."
rm -rf "\${TARGET_VOLUME_MYSQL:?}"/*
tar -xzf "\$BACKUP_DIR/gitea-mysql.tar.gz" -C "\$(dirname "\$TARGET_VOLUME_MYSQL")"
echo "📦 Étape 3/6 : Restauration volume Gitea..."
rm -rf "\${TARGET_VOLUME_DATA:?}"/*
tar -xzf "\$BACKUP_DIR/gitea-data.tar.gz" -C "\$(dirname "\$TARGET_VOLUME_DATA")"
echo "📦 Étape 4/6 : Copie docker-compose..."
cp "\$BACKUP_DIR/docker-compose.yml" "\$TARGET_COMPOSE_DIR/"
# Restaurer app.ini si présent
if [[ -f "\$BACKUP_DIR/app.ini" ]]; then
mkdir -p "\${TARGET_VOLUME_DATA}/gitea/conf"
cp "\$BACKUP_DIR/app.ini" "\${TARGET_VOLUME_DATA}/gitea/conf/app.ini"
echo " ✅ app.ini restauré"
fi
# Détecter docker compose ou docker-compose
if docker compose version &>/dev/null; then
DC="docker compose"
elif command -v docker-compose &>/dev/null; then
DC="docker-compose"
else
echo "❌ Ni 'docker compose' ni 'docker-compose' n'est disponible"
exit 1
fi
echo "📦 Étape 5/6 : Démarrage des conteneurs..."
cd "\$TARGET_COMPOSE_DIR"
\$DC up -d
echo " ⏳ Attente démarrage MySQL (30s)..."
sleep 10
MYSQL_READY=false
for i in \$(seq 1 20); do
if docker exec gitea_db mysqladmin ping -u root -p"\$DB_ROOT_PASSWORD" --silent 2>/dev/null; then
echo " ✅ MySQL prêt après \$((10 + i))s"
MYSQL_READY=true
break
fi
sleep 1
done
if [[ "\$MYSQL_READY" != "true" ]]; then
echo "⚠️ MySQL n'a pas répondu dans le temps imparti, tentative de restore DB quand même..."
fi
echo "📦 Étape 6/6 : Restauration base de données..."
DB_DUMP="\$BACKUP_DIR/$DB_DUMP_FILE"
if [[ -f "\${DB_DUMP}.gz" ]]; then
gunzip -c "\${DB_DUMP}.gz" | docker exec -i gitea_db mysql -u root -p"\$DB_ROOT_PASSWORD" "\$DB_NAME"
elif [[ -f "\$DB_DUMP" ]]; then
docker exec -i gitea_db mysql -u root -p"\$DB_ROOT_PASSWORD" "\$DB_NAME" < "\$DB_DUMP"
else
echo "❌ Fichier dump introuvable : \$DB_DUMP"
exit 1
fi
echo ""
echo "🔄 Redémarrage final des conteneurs..."
\$DC restart
echo ""
echo "════════════════════════════════════════════════════════"
echo "✅ RESTAURATION TERMINÉE !"
echo ""
echo " Gitea : http://localhost:3000"
echo " Vérifiez les logs : \$DC logs -f"
echo "════════════════════════════════════════════════════════"
RESTORE_EOF
chmod +x "$RESTORE_SCRIPT"
log " ✅ Script restore créé : restore.sh"
# ─── 5. Vérification intégrité ──────────────────────────────────────────────
if [[ "$VERIFY_BACKUP" == "true" ]]; then
log "🔍 Vérification intégrité des archives..."
ERRORS=0
# Vérifier chaque archive .tar.gz (avec gestion du glob vide)
shopt -s nullglob
for ARCHIVE in "$BACKUP_PATH"/*.tar.gz; do
if ! tar -tzf "$ARCHIVE" > /dev/null 2>&1; then
log " ❌ Archive corrompue : $(basename "$ARCHIVE")"
ERRORS=$((ERRORS + 1))
fi
done
shopt -u nullglob
if [[ "$COMPRESS" == "true" ]]; then
if ! gunzip -t "${BACKUP_PATH}/gitea-db.sql.gz" 2>/dev/null; then
log " ❌ Dump SQL corrompu"
ERRORS=$((ERRORS + 1))
fi
fi
if [[ $ERRORS -gt 0 ]]; then
log "❌ $ERRORS erreur(s) d'intégrité détectée(s)"
exit 1
fi
log " ✅ Toutes les archives sont valides"
fi
# ─── 6. Métadonnées du backup ───────────────────────────────────────────────
GITEA_VERSION="inconnue"
if docker ps --format '{{.Names}}' | grep -q "^${APP_CONTAINER}$"; then
GITEA_VERSION=$(docker exec "$APP_CONTAINER" gitea --version 2>/dev/null || echo "inconnue")
fi
DB_DISPLAY_SIZE=$(ls -lh "${BACKUP_PATH}"/gitea-db.sql* 2>/dev/null | awk '{print $5}' | head -1)
cat > "${BACKUP_PATH}/backup-info.txt" << INFO
Date : $(date)
Hôte : $(hostname)
Gitea version : $GITEA_VERSION
DB type : MySQL 8
DB name : $DB_NAME
Compression : $COMPRESS
Containers stopped : $CONTAINERS_WERE_STOPPED
Taille DB : ${DB_DISPLAY_SIZE:-N/A}
Taille data : ${DATA_SIZE:-N/A}
Taille mysql : ${MYSQL_SIZE:-N/A}
INFO
# ─── 7. Nettoyage des vieux backups ─────────────────────────────────────────
log "🧹 Nettoyage backups (rétention: ${RETENTION_DAYS}j, ${RETENTION_WEEKLY} hebdo, ${RETENTION_MONTHLY} mensuels)..."
# Pour chaque backup, déterminer s'il doit être gardé
find "$BACKUP_DIR" -maxdepth 1 -type d -name "gitea-backup-*" -printf '%f\n' 2>/dev/null | sort -r | while read -r backup_name; do
backup_path="$BACKUP_DIR/$backup_name"
# Déterminer le jour du mois et le jour de la semaine à partir du nom
# Format: gitea-backup-YYYYMMDD-HHMMSS
local_date=$(echo "$backup_name" | grep -oE '[0-9]{8}' | head -1)
if [[ -z "$local_date" ]]; then
continue
fi
local_day=${local_date:6:2} # Jour du mois (DD)
local_month=${local_date:4:2}
local_year=${local_date:0:4}
# Déterminer le jour de la semaine (1=lundi, 7=dimanche)
local_dow=$(date -d "${local_year}-${local_month}-${local_day}" +%u 2>/dev/null || echo "0")
# Âge en jours
backup_ts=$(date -d "${local_year}-${local_month}-${local_day}" +%s 2>/dev/null || echo "0")
now_ts=$(date +%s)
age_days=$(( (now_ts - backup_ts) / 86400 ))
# Décisions de rétention
KEEP=false
REASON=""
# Toujours garder : backups récents
if [[ "$age_days" -le "$RETENTION_DAYS" ]]; then
KEEP=true
REASON="récent (${age_days}j <= ${RETENTION_DAYS}j)"
fi
# Toujours garder : backups du dimanche (hebdomadaire) dans la limite RETENTION_WEEKLY
if [[ "$local_dow" == "7" ]]; then
# Compter combien de backups du dimanche on garde déjà
weekly_count=$(find "$BACKUP_DIR" -maxdepth 1 -type d -name "gitea-backup-*" -printf '%f\n' 2>/dev/null | grep -oE '^gitea-backup-[0-9]{8}' | while read -r wb; do
wb_date=$(echo "$wb" | grep -oE '[0-9]{8}')
wb_dow=$(date -d "${wb_date:0:4}-${wb_date:4:2}-${wb_date:6:2}" +%u 2>/dev/null || echo "0")
if [[ "$wb_dow" == "7" ]]; then
echo "$wb"
fi
done | sort -r | head -n "$RETENTION_WEEKLY" | wc -l)
# Si ce backup fait partie des N derniers dimanches, on garde
weekly_list=$(find "$BACKUP_DIR" -maxdepth 1 -type d -name "gitea-backup-*" -printf '%f\n' 2>/dev/null | grep -oE '^gitea-backup-[0-9]{8}' | while read -r wb; do
wb_date=$(echo "$wb" | grep -oE '[0-9]{8}')
wb_dow=$(date -d "${wb_date:0:4}-${wb_date:4:2}-${wb_date:6:2}" +%u 2>/dev/null || echo "0")
if [[ "$wb_dow" == "7" ]]; then
echo "$wb"
fi
done | sort -r | head -n "$RETENTION_WEEKLY")
if echo "$weekly_list" | grep -qF "$backup_name"; then
KEEP=true
REASON="hebdomadaire (dimanche, dans les ${RETENTION_WEEKLY} derniers)"
fi
fi
# Toujours garder : backups du 1er du mois (mensuel) dans la limite RETENTION_MONTHLY
if [[ "$local_day" == "01" ]]; then
monthly_list=$(find "$BACKUP_DIR" -maxdepth 1 -type d -name "gitea-backup-*" -printf '%f\n' 2>/dev/null | grep -oE '^gitea-backup-[0-9]{8}' | while read -r mb; do
mb_date=$(echo "$mb" | grep -oE '[0-9]{8}')
mb_day=${mb_date:6:2}
if [[ "$mb_day" == "01" ]]; then
echo "$mb"
fi
done | sort -r | head -n "$RETENTION_MONTHLY")
if echo "$monthly_list" | grep -qF "$backup_name"; then
KEEP=true
REASON="mensuel (1er du mois, dans les ${RETENTION_MONTHLY} derniers)"
fi
fi
if [[ "$KEEP" == "true" ]]; then
log " 💾 Gardé : $backup_name ($REASON)"
else
log " 🗑️ Suppression : $backup_name"
rm -rf "$backup_path"
fi
done
# ─── 8. Backup distant ──────────────────────────────────────────────────────
sync_remote "$BACKUP_PATH" || true
# ─── 9. Rapport final ───────────────────────────────────────────────────────
TOTAL_SIZE=$(du -sh "$BACKUP_PATH" 2>/dev/null | cut -f1)
BACKUP_COUNT=$(find "$BACKUP_DIR" -maxdepth 1 -type d -name "gitea-backup-*" 2>/dev/null | wc -l)
log "✅ Backup terminé !"
log "📊 Total : ${TOTAL_SIZE:-N/A}"
log "📋 Backups conservés : $BACKUP_COUNT"
log "📂 Destination : $BACKUP_PATH"
log "═══ Fin ───────────────────────────────────────────"
echo ""
echo "📂 $BACKUP_PATH"
echo " ├── gitea-db.sql.gz ← Dump MySQL"
echo " ├── gitea-data.tar.gz ← Volume /data"
echo " ├── gitea-mysql.tar.gz ← Volume MySQL"
echo " ├── docker-compose.yml ← Stack Docker"
echo " ├── app.ini ← Configuration Gitea"
echo " ├── restore.sh ← Script de restore"
echo " └── backup-info.txt ← Métadonnées"
notify "✅ Backup Gitea terminé sur $(hostname) — $TOTAL_SIZE — $BACKUP_PATH"
exit 0