#!/bin/bash # scripts/backup.sh - Backup complet Nextcloud set -euo pipefail # Variables globales SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" cd "$PROJECT_ROOT" # Charger .env en premier if [ ! -f .env ]; then echo "ERROR: Fichier .env introuvable" exit 1 fi set -a # shellcheck disable=SC1091 source .env set +a # Configuration DATE=$(date +%Y%m%d_%H%M%S) LOCK_FILE="/tmp/nextcloud_backup.lock" LOG_DIR="./logs" LOG_FILE="$LOG_DIR/backup_$(date +%Y%m%d_%H%M%S).log" BACKUP_DIR="${BACKUP_DESTINATION:-./backups}" BACKUP_RETENTION_DAYS="${BACKUP_RETENTION_DAYS:-7}" BACKUP_NAME="nextcloud_backup_$DATE" BACKUP_PATH="$BACKUP_DIR/$BACKUP_NAME" MAINTENANCE_ENABLED=false mkdir -p "$LOG_DIR" # Charger les fonctions communes (log avec couleurs) # shellcheck disable=SC1091 source "$SCRIPT_DIR/common.sh" if [ -z "${MYSQL_DATABASE:-}" ] || [ -z "${MYSQL_USER:-}" ] || [ -z "${MYSQL_PASSWORD:-}" ]; then log "ERROR" "Variables MysqlSQL non définies dans .env" exit 1 fi # Vérifier qu'un backup n'est pas déjà en cours if [ -f "$LOCK_FILE" ]; then log "ERROR" "Une sauvegarde est déjà en cours. Si aucune sauvegarde n'est en cours, supprimez le fichier: $LOCK_FILE" exit 1 fi touch "$LOCK_FILE" mkdir -p "$LOG_DIR" # Fonction de nettoyage en cas d'erreur cleanup() { local exit_code=$? if [ "$exit_code" -ne 0 ]; then log "ERROR" "Erreur détectée (code: $exit_code), nettoyage..." fi # Désactiver le mode maintenance si activé if [ "$MAINTENANCE_ENABLED" = true ]; then log "INFO" "Désactivation du mode maintenance..." docker-compose exec -T -u www-data nextcloud php occ maintenance:mode --off 2>>"$LOG_FILE" || true fi # Nettoyer le backup partiel if [ -d "$BACKUP_PATH" ]; then log "INFO" "Nettoyage du backup partiel..." rm -rf "${BACKUP_PATH:?}" fi # Supprimer le lock rm -f "$LOCK_FILE" if [ "$exit_code" -eq 0 ]; then log "SUCCESS" "Backup terminé avec succès" else log "ERROR" "Backup échoué avec code: $exit_code" fi exit "$exit_code" } trap cleanup EXIT INT TERM log "INFO" "=== Démarrage du backup: $BACKUP_NAME ===" log "INFO" "Log file: $LOG_FILE" # Vérifier l'espace disque disponible log "INFO" "Vérification de l'espace disque..." # Calculer l'espace requis (taille data + db avec 20% de marge) # Calculer depuis le container avec les mêmes exclusions que le backup log "INFO" "Calcul de la taille réelle (avec exclusions)..." DATA_SIZE=$(docker-compose exec -T nextcloud du -sb \ --exclude='appdata_*/preview' \ --exclude='*/cache' \ --exclude='*/thumbnails' \ /var/www/html/data 2>/dev/null | awk '{print $1}' || echo "0") # Note: On n'inclut pas la DB car mysqldump est beaucoup plus petit que les fichiers MySQL bruts # Typiquement: fichiers MySQL = 650MB → dump SQL = 500KB (compression ~99%) # On ajoute juste 10MB fixe pour DB + config + apps (généralement < 1MB au final) DB_ESTIMATE=10485760 # 10MB # Additionner avec 20% de marge REQUIRED_SPACE=$(echo "$DATA_SIZE + $DB_ESTIMATE" | awk '{total=$1+$3; print int(total*1.2)}') if [ -z "$REQUIRED_SPACE" ] || [ "$REQUIRED_SPACE" = "0" ]; then log "WARN" "Impossible de calculer l'espace requis, estimation à 500MB" REQUIRED_SPACE=500000000 # 500MB par défaut (plus réaliste que 2GB) fi # Obtenir l'espace disponible (enlever les espaces/newlines) AVAILABLE_SPACE=$(df -B1 "$BACKUP_DIR" | awk 'NR==2 {print $4}' | tr -d '[:space:]') if [ -z "$AVAILABLE_SPACE" ]; then log "ERROR" "Impossible de déterminer l'espace disque disponible" exit 1 fi # Calculer estimation avec compression # Note: Nextcloud a beaucoup de fichiers déjà compressés (images, PDFs) # donc la compression gzip est peu efficace (~30% au lieu de 90%) if [ "$REQUIRED_SPACE" -gt 0 ] 2>/dev/null; then ESTIMATED_COMPRESSED=$((REQUIRED_SPACE * 7 / 10)) # 70% de la taille (30% de compression) log "INFO" "Espace requis (non compressé + 20%): $(numfmt --to=iec-i --suffix=B "$REQUIRED_SPACE" 2>/dev/null || echo "$REQUIRED_SPACE bytes")" log "INFO" "Espace estimé après compression: $(numfmt --to=iec-i --suffix=B "$ESTIMATED_COMPRESSED" 2>/dev/null || echo "$ESTIMATED_COMPRESSED bytes")" else log "INFO" "Espace requis: Impossible à calculer (utilisation du fallback)" fi log "INFO" "Espace disponible: $(numfmt --to=iec-i --suffix=B "$AVAILABLE_SPACE" 2>/dev/null || echo "$AVAILABLE_SPACE bytes")" # Comparaison sécurisée avec validation if [ "$AVAILABLE_SPACE" -lt "$REQUIRED_SPACE" ] 2>/dev/null; then log "ERROR" "Espace disque insuffisant" exit 1 fi # Créer le dossier de backup mkdir -p "$BACKUP_PATH" # 1. Activer le mode maintenance log "INFO" "Activation du mode maintenance..." if docker-compose exec -T -u www-data nextcloud php occ maintenance:mode --on 2>>"$LOG_FILE"; then MAINTENANCE_ENABLED=true log "INFO" "Mode maintenance activé" else log "ERROR" "Impossible d'activer le mode maintenance" exit 1 fi # 2. Backup de la base de données log "INFO" "Backup de la base de données..." START_TIME=$(date +%s) if ! docker-compose exec -T db sh -c "MYSQL_PWD=\"\$MYSQL_PASSWORD\" mysqldump \ -u\"\$MYSQL_USER\" \ \"\$MYSQL_DATABASE\" \ --single-transaction \ --quick \ --lock-tables=false" >"$BACKUP_PATH/database.sql" 2>>"$LOG_FILE"; then log "ERROR" "Erreur lors du backup de la base de données" exit 1 fi END_TIME=$(date +%s) DB_SIZE=$(du -h "$BACKUP_PATH/database.sql" | cut -f1) log "INFO" "Base de données sauvegardée: $DB_SIZE ($((END_TIME - START_TIME))s)" # 3. Backup des fichiers de config log "INFO" "Backup de la configuration..." START_TIME=$(date +%s) if ! docker-compose exec -T -u www-data nextcloud tar -czf - -C /var/www/html/config . >"$BACKUP_PATH/config.tar.gz" 2>>"$LOG_FILE"; then log "ERROR" "Erreur lors du backup de la configuration" exit 1 fi END_TIME=$(date +%s) CONFIG_SIZE=$(du -h "$BACKUP_PATH/config.tar.gz" | cut -f1) log "INFO" "Configuration sauvegardée: $CONFIG_SIZE ($((END_TIME - START_TIME))s)" # 4. Backup des données utilisateurs log "INFO" "Backup des données utilisateurs..." START_TIME=$(date +%s) if ! docker-compose exec -T -u www-data nextcloud tar -czf - \ -C /var/www/html/data \ --exclude='appdata_*/preview' \ --exclude='*/cache' \ --exclude='*/thumbnails' \ . >"$BACKUP_PATH/data.tar.gz" 2>>"$LOG_FILE"; then log "ERROR" "Erreur lors du backup des données" exit 1 fi END_TIME=$(date +%s) DATA_SIZE=$(du -h "$BACKUP_PATH/data.tar.gz" | cut -f1) log "INFO" "Données sauvegardées: $DATA_SIZE ($((END_TIME - START_TIME))s)" # 5. Backup des apps personnalisées log "INFO" "Backup des apps personnalisées..." if docker-compose exec -T nextcloud [ -d /var/www/html/custom_apps ] 2>>"$LOG_FILE"; then if docker-compose exec -T -u www-data nextcloud tar -czf - \ -C /var/www/html/custom_apps . >"$BACKUP_PATH/apps.tar.gz" 2>>"$LOG_FILE"; then APPS_SIZE=$(du -h "$BACKUP_PATH/apps.tar.gz" | cut -f1) log "INFO" "Apps sauvegardées: $APPS_SIZE" else log "WARN" "Erreur lors du backup des apps personnalisées" fi else log "INFO" "Pas d'apps personnalisées à sauvegarder" fi # 6. Désactiver le mode maintenance log "INFO" "Désactivation du mode maintenance..." if docker-compose exec -T -u www-data nextcloud php occ maintenance:mode --off 2>>"$LOG_FILE"; then MAINTENANCE_ENABLED=false log "INFO" "Mode maintenance désactivé" else log "WARN" "Impossible de désactiver le mode maintenance" fi # 7. Créer une archive complète log "INFO" "Compression finale..." START_TIME=$(date +%s) if ! tar -czf "$BACKUP_DIR/$BACKUP_NAME.tar.gz" -C "$BACKUP_DIR" "$BACKUP_NAME/" 2>>"$LOG_FILE"; then log "ERROR" "Erreur lors de la compression" exit 1 fi END_TIME=$(date +%s) ARCHIVE_SIZE=$(du -h "$BACKUP_DIR/$BACKUP_NAME.tar.gz" | cut -f1) log "INFO" "Archive créée: $ARCHIVE_SIZE ($((END_TIME - START_TIME))s)" # 8. Générer le checksum SHA256 log "INFO" "Génération du checksum SHA256..." if cd "$BACKUP_DIR" && sha256sum "$BACKUP_NAME.tar.gz" >"$BACKUP_NAME.tar.gz.sha256"; then cd "$PROJECT_ROOT" CHECKSUM=$(cut -d' ' -f1 "$BACKUP_DIR/$BACKUP_NAME.tar.gz.sha256") log "INFO" "Checksum: $CHECKSUM" else cd "$PROJECT_ROOT" log "WARN" "Impossible de générer le checksum" fi # Supprimer le dossier temporaire après compression réussie rm -rf "${BACKUP_PATH:?}" # 9. Nettoyer les vieux backups log "INFO" "Nettoyage des backups > $BACKUP_RETENTION_DAYS jours..." DELETED_COUNT=0 while IFS= read -r -d '' file; do CHECKSUM_FILE="${file}.sha256" log "INFO" "Suppression: $(basename "$file")" rm -f "$file" "$CHECKSUM_FILE" 2>>"$LOG_FILE" || true DELETED_COUNT=$((DELETED_COUNT + 1)) done < <(find "$BACKUP_DIR" -name "nextcloud_backup_*.tar.gz" -type f -mtime +"$BACKUP_RETENTION_DAYS" -print0 2>/dev/null || true) if [ "$DELETED_COUNT" -gt 0 ]; then log "INFO" "$DELETED_COUNT ancien(s) backup(s) supprimé(s)" else log "INFO" "Aucun ancien backup à supprimer" fi # 10. Statistiques finales log "INFO" "=== Statistiques du backup ===" log "INFO" "Archive: $BACKUP_DIR/$BACKUP_NAME.tar.gz" log "INFO" "Taille: $ARCHIVE_SIZE" log "INFO" "Checksum: ${CHECKSUM:-N/A}" log "INFO" "Rétention: $BACKUP_RETENTION_DAYS jours" log "INFO" "Backups disponibles: $(find "$BACKUP_DIR" -name "nextcloud_backup_*.tar.gz" -type f 2>/dev/null | wc -l)"