From 1c6506e3195a604737803dbfc40e5ea399f787ad Mon Sep 17 00:00:00 2001 From: Bruno Charest Date: Fri, 26 Jun 2026 11:53:28 -0400 Subject: [PATCH] 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 --- README-gitea-scripts.md | 372 ++++++++++++++++++++++++++------ docker-compose.yml | 25 ++- gitea-auto-update.sh | 208 +++++++++++++++--- gitea-backup.sh | 463 ++++++++++++++++++++++++++++++--------- gitea-scripts.conf | 75 +++++++ install_cron.sh | 143 +++++++++--- restore.sh | 414 +++++++++++++++++++++++++++++++++++ setup.sh | 467 ++++++++++++++++++++++++++++++++++++++++ 8 files changed, 1927 insertions(+), 240 deletions(-) create mode 100644 gitea-scripts.conf create mode 100644 restore.sh create mode 100644 setup.sh diff --git a/README-gitea-scripts.md b/README-gitea-scripts.md index afbf0b7..f908e5c 100644 --- a/README-gitea-scripts.md +++ b/README-gitea-scripts.md @@ -1,71 +1,151 @@ -# Scripts Gitea — Auto-Update, Backup & Déploiement Cron +# Scripts Gitea — Auto-Update, Backup, Restore & Déploiement Cron -Trois scripts pour la maintenance automatisée de Gitea Docker. +Solution complète pour la maintenance automatisée de Gitea Docker : +backup, mise à jour, restauration et déploiement. + +--- + +## 📂 Structure du projet + +``` +gitea-scripts/ +├── setup.sh ← 🚀 Installateur tout-en-un +├── gitea-scripts.conf ← ⚙️ Configuration unifiée +├── gitea-backup.sh ← 💾 Backup complet (DB + volumes + config) +├── gitea-auto-update.sh ← 🔄 Mise à jour automatique +├── restore.sh ← ♻️ Restauration standalone +├── install_cron.sh ← 🕐 Gestion des crons +└── docker-compose.yml ← 🐳 Stack Docker de référence +``` + +### Architecture + +```mermaid +flowchart TB + subgraph Config["⚙️ Configuration"] + CONF["gitea-scripts.conf
Configuration unifiée"] + end + + subgraph Core["🛠️ Scripts core"] + BACKUP["gitea-backup.sh
💾 Backup quotidien"] + UPDATE["gitea-auto-update.sh
🔄 Mise à jour auto"] + RESTORE["restore.sh
♻️ Restauration"] + end + + subgraph Deploy["🚀 Déploiement"] + SETUP["setup.sh
Installateur complet"] + CRON["install_cron.sh
Gestion crontab"] + end + + subgraph Runtime["⚡ Runtime"] + CRONTAB["Crontab
Backup 2h → Update 4h"] + DOCKER["Docker Compose
Gitea + MySQL"] + end + + subgraph Storage["💾 Stockage"] + LOCAL["Backups locaux
/NFS/BACKUP/..."] + REMOTE["Backup distant
rsync / SCP"] + end + + CONF --> BACKUP + CONF --> UPDATE + CONF --> RESTORE + CONF --> CRON + + SETUP --> CRON + SETUP --> DOCKER + CRON --> CRONTAB + CRONTAB --> BACKUP + CRONTAB --> UPDATE + + BACKUP --> LOCAL + BACKUP --> REMOTE + RESTORE --> LOCAL + RESTORE --> DOCKER + UPDATE --> DOCKER +``` + +```mermaid +flowchart LR + subgraph Daily["📅 Quotidien (cron)"] + direction TB + A["🕐 2h00"] --> B["gitea-backup.sh"] + B --> C["💾 Backup DB + volumes"] + C --> D["📝 Génère restore.sh"] + C --> E["☁️ Sync distante"] + C --> F["🧹 Nettoie vieux backups"] + + G["🕐 4h00"] --> H["gitea-auto-update.sh"] + H --> I{"Nouvelle image ?"} + I -->|"Non"| J["✅ Rien à faire"] + I -->|"Oui"| K["💾 Backup DB rapide"] + K --> L["🚀 docker compose up -d"] + L --> M{"Santé OK ?"} + M -->|"Oui"| N["✅ Mise à jour réussie"] + M -->|"Non"| O["🔄 Rollback auto"] + end + + subgraph Manual["🖐️ Manuel"] + direction TB + P["setup.sh"] --> Q["Installation complète"] + R["restore.sh"] --> S["Restauration backup"] + T["setup.sh --update"] --> U["Màj scripts (git pull)"] + end +``` --- ## 🚀 Installation rapide ```bash -# Cloner les scripts +# Cloner et installer en une commande git clone https://git.dracodev.net/Outils/gitea-scripts.git ~/scripts - -# Installer dans le cron (interactif) -bash ~/scripts/install_cron.sh +bash ~/scripts/setup.sh # Ou forcer sans confirmation -bash ~/scripts/install_cron.sh --force +bash ~/scripts/setup.sh --force ``` --- ## 📋 Prérequis -- Docker + Docker Compose -- `curl`, `tar`, `gzip`, `mysqldump` (dans le conteneur MySQL) -- Droits d'écriture sur `/NFS/BACKUP/DOCKER/dev-prod/gitea/` (backup) +- Docker + Docker Compose (plugin ou standalone) +- `git`, `curl`, `tar`, `gzip`, `crontab` +- `mysqldump` (dans le conteneur MySQL) +- Droits d'écriture sur le répertoire de backup - Droits sudo si les volumes sont protégés --- -## 🔄 gitea-auto-update.sh +## 🛠️ Scripts -Vérifie quotidiennement si une nouvelle image Docker Gitea est disponible et applique la mise à jour. +### 🚀 `setup.sh` — Installation complète -### Paramètres configurables +Installateur tout-en-un qui configure l'ensemble du système : -| Variable | Défaut | Description | -|----------|--------|-------------| -| `COMPOSE_DIR` | `~/dev/git/outils/docker/gitea` | Dossier du docker-compose | -| `COMPOSE_FILE` | `docker-compose.yml` | Nom du fichier compose | -| `CONTAINER_NAME` | `gitea_server` | Conteneur Gitea | -| `IMAGE_NAME` | `gitea/gitea` | Image Docker | -| `IMAGE_TAG` | `latest` | Tag (version) | -| `HEALTH_CHECK_URL` | `http://localhost:3000` | URL vérification santé | -| `HEALTH_CHECK_TIMEOUT` | `30` | Timeout attente (secondes) | - -### Cron recommandé - -```cron -# Tous les jours à 4h du matin -0 4 * * * /home/openclaw/scripts/gitea-auto-update.sh +```bash +bash setup.sh # Installation interactive +bash setup.sh --force # Sans confirmation +bash setup.sh --update # Mise à jour des scripts (git pull) +bash setup.sh --dry-run # Aperçu sans rien installer ``` -### Fonctionnement - -1. Récupère le digest SHA256 de l'image actuelle -2. `docker compose pull` pour télécharger la dernière version -3. Compare les digests → si identique, rien à faire -4. Si différent : backup rapide DB → `docker compose up -d` → vérification santé -5. Nettoie les vieilles images Docker (`image prune`) +**Étapes exécutées :** +1. Vérification des prérequis système +2. Clone/mise à jour depuis Git +3. Permissions d'exécution +4. Configuration des logs +5. Configuration logrotate (rotation automatique des logs) +6. Installation des tâches cron --- -## 💾 gitea-backup.sh +### 💾 `gitea-backup.sh` — Backup complet -Backup complet et portable de Gitea : DB MySQL + volumes Docker + config. +Sauvegarde complète et portable : DB MySQL + volumes Docker + configuration. -### Paramètres configurables +#### Paramètres configurables | Variable | Défaut | Description | |----------|--------|-------------| @@ -82,15 +162,16 @@ Backup complet et portable de Gitea : DB MySQL + volumes Docker + config. | `COMPRESS` | `true` | Compression gzip | | `VERIFY_BACKUP` | `true` | Vérifier intégrité après backup | | `STOP_CONTAINERS` | `false` | Arrêter conteneurs (plus sûr) | +| `REMOTE_BACKUP_ENABLED` | `false` | Activer backup distant (rsync/SCP) | -### Cron recommandé +#### Cron recommandé ```cron # Tous les jours à 2h du matin 0 2 * * * /home/openclaw/scripts/gitea-backup.sh ``` -### Structure d'un backup +#### Structure d'un backup ``` /NFS/BACKUP/DOCKER/dev-prod/gitea/ @@ -104,48 +185,105 @@ Backup complet et portable de Gitea : DB MySQL + volumes Docker + config. └── backup-info.txt ← Métadonnées ``` -### Restauration +#### Backup distant (optionnel) + +Configurer dans `gitea-scripts.conf` : ```bash -cd /NFS/BACKUP/DOCKER/dev-prod/gitea/gitea-backup-20260626-020000/ - -# Dry-run (vérifier sans exécuter) -bash restore.sh --dry-run - -# Restauration réelle -bash restore.sh --force +REMOTE_BACKUP_ENABLED=true +REMOTE_BACKUP_METHOD=rsync # ou scp +REMOTE_BACKUP_HOST=backup.example.com +REMOTE_BACKUP_USER=backup +REMOTE_BACKUP_PATH=/backups/gitea +REMOTE_BACKUP_SSH_KEY=/home/user/.ssh/id_rsa ``` -Le script `restore.sh` est **auto-généré** à chaque backup et contient TOUT ce qu'il faut pour restaurer sur n'importe quel hôte Docker. - --- -## 🚀 install_cron.sh — Déploiement automatisé +### 🔄 `gitea-auto-update.sh` — Mise à jour automatique -Installe (ou désinstalle) les crons d'un seul coup. +Vérifie quotidiennement si une nouvelle image Docker Gitea est disponible. -### Commandes +#### Paramètres configurables + +| Variable | Défaut | Description | +|----------|--------|-------------| +| `COMPOSE_DIR` | `~/dev/git/outils/docker/gitea` | Dossier du docker-compose | +| `APP_SERVICE` | `gitea_server` | Nom du service dans compose | +| `IMAGE_NAME` | `gitea/gitea` | Image Docker | +| `IMAGE_TAG` | `latest` | Tag (version) | +| `HEALTH_CHECK_URL` | `http://localhost:3000` | URL vérification santé | +| `HEALTH_CHECK_TIMEOUT` | `30` | Timeout attente (secondes) | + +#### Fonctionnement + +1. Récupère le digest SHA256 de l'image actuelle +2. `docker compose pull` pour télécharger la dernière version +3. Compare les digests → si identique, rien à faire +4. Si différent : + - Backup rapide de la DB + - Sauvegarde de l'image actuelle pour rollback + - `docker compose up -d` avec la nouvelle image + - Vérification santé + - **Rollback automatique** en cas d'échec +5. Nettoyage ciblé des anciennes images Gitea + +#### Notifications (optionnel) + +```bash +# Telegram +ENABLE_NOTIFICATIONS=true NOTIFY_METHOD=telegram \ + TELEGRAM_BOT_TOKEN=xxx TELEGRAM_CHAT_ID=xxx \ + bash gitea-auto-update.sh + +# ntfy.sh +ENABLE_NOTIFICATIONS=true NOTIFY_METHOD=ntfy \ + NTPY_TOPIC=mon-topic \ + bash gitea-auto-update.sh +``` + +--- + +### ♻️ `restore.sh` — Restauration standalone + +Restaure un backup Gitea sur n'importe quel hôte Docker. + +```bash +# Lister les backups disponibles +bash restore.sh + +# Restauration interactive +bash restore.sh /NFS/BACKUP/DOCKER/dev-prod/gitea/gitea-backup-20260626-020000 + +# Dry-run (vérifier sans exécuter) +bash restore.sh /NFS/BACKUP/DOCKER/dev-prod/gitea/gitea-backup-20260626-020000 --dry-run + +# Restauration forcée (sans confirmation) +bash restore.sh /NFS/BACKUP/DOCKER/dev-prod/gitea/gitea-backup-20260626-020000 --force +``` + +**Étapes de restauration :** +1. Arrêt des conteneurs existants +2. Création des répertoires cibles +3. Restauration du volume MySQL +4. Restauration du volume Gitea +5. Copie du docker-compose.yml et app.ini +6. Démarrage des conteneurs +7. Restauration de la base de données + redémarrage final + +--- + +### 🕐 `install_cron.sh` — Gestion des crons ```bash bash install_cron.sh # Installation interactive bash install_cron.sh --force # Sans confirmation +bash install_cron.sh --dry-run # Aperçu sans rien installer bash install_cron.sh --uninstall # Retirer du cron bash install_cron.sh --status # Vérifier l'état ``` -### Paramètres configurables - -| Variable | Défaut | Description | -|----------|--------|-------------| -| `SCRIPTS_DIR` | `$HOME/scripts` | Dossier des scripts | -| `CRON_BACKUP_SCHEDULE` | `0 2 * * *` | Horaire backup | -| `CRON_UPDATE_SCHEDULE` | `0 4 * * *` | Horaire update | -| `BACKUP_LOG` | `/var/log/gitea-backup-cron.log` | Log backup | -| `UPDATE_LOG` | `/var/log/gitea-update-cron.log` | Log update | - -### Gestion propre - -Le script ajoute un **bloc marqué** dans la crontab : +Le script gère un **bloc marqué** dans la crontab : ```cron # >>> GITEA SCRIPTS (managed by install_cron.sh) <<< @@ -158,14 +296,23 @@ Le `--uninstall` retire uniquement ce bloc, sans toucher au reste de la crontab. --- -## 🔧 Surcharger les paramètres +## ⚙️ Configuration centralisée + +Tous les paramètres sont dans `gitea-scripts.conf`. Les scripts chargent ce +fichier automatiquement s'il est présent dans le même répertoire. + +Les variables d'environnement **surchargent** les valeurs du fichier de +configuration. ```bash # Exemple : changer le dossier de backup -BACKUP_DIR=/mnt/big-disk/gitea /home/openclaw/scripts/gitea-backup.sh +BACKUP_DIR=/mnt/big-disk/gitea bash gitea-backup.sh # Exemple : backup avec arrêt des conteneurs -STOP_CONTAINERS=true /home/openclaw/scripts/gitea-backup.sh +STOP_CONTAINERS=true bash gitea-backup.sh + +# Exemple : backup + sync distante +REMOTE_BACKUP_ENABLED=true bash gitea-backup.sh ``` --- @@ -176,3 +323,90 @@ STOP_CONTAINERS=true /home/openclaw/scripts/gitea-backup.sh |--------|-----| | Auto-update | `/var/log/gitea-update.log` | | Backup | `/var/log/gitea-backup.log` | +| Cron backup | `~/gitea-backup-cron.log` | +| Cron update | `~/gitea-update-cron.log` | + +La rotation des logs est configurée automatiquement par `setup.sh` via +logrotate (rotation hebdomadaire, 12 semaines de rétention, compression). + +--- + +## 🔒 Lock files + +Pour éviter les exécutions concurrentes (backup + update simultanés), chaque +script utilise un lock file dans `/tmp` : + +- `/tmp/gitea-backup.lock` +- `/tmp/gitea-auto-update.lock` + +Si un lock stale est détecté (PID inexistant), il est automatiquement nettoyé. + +--- + +## 🔄 Mise à jour périodique des scripts + +```bash +# Mise à jour manuelle +bash ~/scripts/setup.sh --update + +# Ajouter au cron pour mise à jour automatique (optionnel) +# 0 3 * * 0 bash ~/scripts/setup.sh --update --force +``` + +--- + +## 🐳 Docker Compose + +Le fichier `docker-compose.yml` fourni inclut : +- **Healthchecks** sur les deux services (MySQL + Gitea) +- `depends_on` avec `condition: service_healthy` pour MySQL +- Timezone mapping pour les logs horodatés correctement + +--- + +## 🔧 Dépannage + +### Le backup échoue par manque d'espace +```bash +# Vérifier l'espace disponible +df -h /NFS/BACKUP/DOCKER/dev-prod/gitea + +# Nettoyer les vieux backups manuellement +ls -t /NFS/BACKUP/DOCKER/dev-prod/gitea/gitea-backup-* | tail -n +10 | xargs rm -rf +``` + +### L'auto-update échoue +```bash +# Vérifier les logs +tail -50 /var/log/gitea-update.log + +# Tester manuellement +bash ~/scripts/gitea-auto-update.sh +``` + +### Restauration +```bash +# Lister les backups disponibles +bash ~/scripts/restore.sh + +# Restaurer le plus récent +bash ~/scripts/restore.sh $(ls -td /NFS/BACKUP/DOCKER/dev-prod/gitea/gitea-backup-* | head -1) +``` + +--- + +## 🔐 Sécurité + +⚠️ Les mots de passe dans `gitea-scripts.conf` et `docker-compose.yml` sont +des valeurs par défaut. **Changez-les avant la mise en production :** + +```bash +# Dans gitea-scripts.conf +DB_ROOT_PASSWORD=votre-mot-de-passe-securise +DB_PASSWORD=votre-autre-mot-de-passe + +# Dans docker-compose.yml +MYSQL_ROOT_PASSWORD=votre-mot-de-passe-securise +MYSQL_PASSWORD=votre-autre-mot-de-passe +GITEA__database__PASSWD=votre-autre-mot-de-passe +``` diff --git a/docker-compose.yml b/docker-compose.yml index 82af5f3..be5f5d2 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -28,7 +28,14 @@ services: - "3000:3000" - "222:22" depends_on: - - gitea_db + gitea_db: + condition: service_healthy + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:3000"] + interval: 30s + timeout: 10s + retries: 5 + start_period: 30s gitea_db: image: mysql:8 @@ -43,3 +50,19 @@ services: - gitea volumes: - /DOCKER_CONFIG/gitea/mysql:/var/lib/mysql + healthcheck: + test: + [ + "CMD", + "mysqladmin", + "ping", + "-h", + "localhost", + "-u", + "root", + "-p${MYSQL_ROOT_PASSWORD:-gitea}", + ] + interval: 10s + timeout: 5s + retries: 5 + start_period: 30s diff --git a/gitea-auto-update.sh b/gitea-auto-update.sh index d2d74e6..0b7467f 100755 --- a/gitea-auto-update.sh +++ b/gitea-auto-update.sh @@ -5,43 +5,127 @@ # À placer dans /usr/local/bin/ ou ~/scripts/ # Cron recommandé : 0 4 * * * /usr/local/bin/gitea-auto-update.sh # -# Paramètres configurables (modifier ci-dessous ou passer en variables d'env) +# Paramètres configurables : voir gitea-scripts.conf # ============================================================================= set -euo pipefail -# ─── Configuration (modifiable) ───────────────────────────────────────────── -COMPOSE_DIR="${COMPOSE_DIR:-$HOME/dev/git/outils/gitea}" +# ─── 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) ────── +# Chemins et conteneurs +COMPOSE_DIR="${COMPOSE_DIR:-$HOME/dev/git/outils/docker/gitea}" COMPOSE_FILE="${COMPOSE_FILE:-docker-compose.yml}" -CONTAINER_NAME="${CONTAINER_NAME:-gitea_server}" +APP_SERVICE="${APP_SERVICE:-gitea_server}" +APP_CONTAINER="${APP_CONTAINER:-gitea_server}" +DB_CONTAINER="${DB_CONTAINER:-gitea_db}" IMAGE_NAME="${IMAGE_NAME:-gitea/gitea}" -IMAGE_TAG="${IMAGE_TAG:-latest}" # "latest" ou version spécifique +IMAGE_TAG="${IMAGE_TAG:-latest}" + +# Base de données +DB_NAME="${DB_NAME:-gitea}" +DB_ROOT_PASSWORD="${DB_ROOT_PASSWORD:-gitea}" + +# Logs LOG_FILE="${LOG_FILE:-/var/log/gitea-update.log}" -ENABLE_NOTIFICATIONS="${ENABLE_NOTIFICATIONS:-false}" # true pour envoyer un résumé -NOTIFY_METHOD="${NOTIFY_METHOD:-log}" # log, telegram, ntfy +LOG_DIR="$(dirname "$LOG_FILE")" + +# Notifications +ENABLE_NOTIFICATIONS="${ENABLE_NOTIFICATIONS:-false}" +NOTIFY_METHOD="${NOTIFY_METHOD:-log}" +TELEGRAM_BOT_TOKEN="${TELEGRAM_BOT_TOKEN:-}" TELEGRAM_CHAT_ID="${TELEGRAM_CHAT_ID:-}" NTPY_TOPIC="${NTPY_TOPIC:-}" + +# Santé HEALTH_CHECK_URL="${HEALTH_CHECK_URL:-http://localhost:3000}" HEALTH_CHECK_TIMEOUT="${HEALTH_CHECK_TIMEOUT:-30}" +# Lock file +LOCK_DIR="${LOCK_DIR:-/tmp}" +LOCK_FILE="${LOCK_DIR}/gitea-auto-update.lock" + +# ─── Prérequis : s'assurer que le dossier de log existe ────────────────────── +mkdir -p "$LOG_DIR" 2>/dev/null || true + # ─── Logging ───────────────────────────────────────────────────────────────── log() { - echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE" + 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) - [[ -n "$TELEGRAM_CHAT_ID" ]] && 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 + 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) - [[ -n "$NTPY_TOPIC" ]] && curl -s -H "Title: Gitea Update" -d "$msg" "https://ntfy.sh/$NTPY_TOPIC" > /dev/null 2>&1 || true + if [[ -n "$NTPY_TOPIC" ]]; then + curl -s -H "Title: Gitea Update" -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 "") + # Vérifier si le processus existe encore + 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 + # Stale lock, on le nettoie + 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" +} + +# ─── Nettoyage en cas d'erreur ─────────────────────────────────────────────── +cleanup() { + release_lock +} +trap cleanup EXIT + +# ─── Récupération du digest (compatible POSIX grep) ────────────────────────── +get_image_digest() { + local image="$1" + local digest + # Essayer d'abord avec -E (ERE, disponible presque partout) + digest=$(docker image inspect "$image" --format '{{.RepoDigests}}' 2>/dev/null | grep -oE 'sha256:[a-f0-9]{64}' | head -1 || echo "") + if [[ -z "$digest" ]]; then + # Fallback : utiliser awk + digest=$(docker image inspect "$image" --format '{{.RepoDigests}}' 2>/dev/null | grep -o 'sha256:[a-f0-9]\{64\}' | head -1 || echo "") + fi + echo "$digest" +} + +# ═════════════════════════════════════════════════════════════════════════════ +# MAIN +# ═════════════════════════════════════════════════════════════════════════════ + +acquire_lock + log "════════════════════════════════════════════════════════" log "🔄 Début vérification mise à jour Gitea" @@ -53,23 +137,32 @@ if ! command -v docker &>/dev/null; then exit 1 fi -# ─── Récupérer l'image digest actuelle ────────────────────────────────────── -OLD_DIGEST=$(docker image inspect "${IMAGE_NAME}:${IMAGE_TAG}" --format '{{.RepoDigests}}' 2>/dev/null | grep -oP 'sha256:[a-f0-9]+' || echo "") +# Vérifier si docker compose (plugin) ou docker-compose (standalone) est dispo +if docker compose version &>/dev/null; then + DOCKER_COMPOSE="docker compose" +elif command -v docker-compose &>/dev/null; then + DOCKER_COMPOSE="docker-compose" +else + log "❌ Ni 'docker compose' ni 'docker-compose' n'est disponible" + exit 1 +fi -log "📦 Image actuelle : ${IMAGE_NAME}:${IMAGE_TAG}" +# ─── Récupérer l'image digest actuelle ────────────────────────────────────── +FULL_IMAGE="${IMAGE_NAME}:${IMAGE_TAG}" +OLD_DIGEST=$(get_image_digest "$FULL_IMAGE") + +log "📦 Image actuelle : $FULL_IMAGE" log "🔍 Digest actuel : ${OLD_DIGEST:-non disponible}" # ─── Pull de la nouvelle image ────────────────────────────────────────────── log "📥 Pull de la nouvelle image..." -PULL_OUTPUT=$(docker compose -f "$COMPOSE_FILE" pull "$CONTAINER_NAME" 2>&1) -PULL_EXIT=$? - -if [[ $PULL_EXIT -ne 0 ]]; then - log "❌ Échec du pull : $PULL_OUTPUT" +PULL_OUTPUT=$($DOCKER_COMPOSE -f "$COMPOSE_FILE" pull "$APP_SERVICE" 2>&1) || { + PULL_EXIT=$? + log "❌ Échec du pull (code $PULL_EXIT) : $PULL_OUTPUT" exit 1 -fi +} -NEW_DIGEST=$(docker image inspect "${IMAGE_NAME}:${IMAGE_TAG}" --format '{{.RepoDigests}}' 2>/dev/null | grep -oP 'sha256:[a-f0-9]+' || echo "") +NEW_DIGEST=$(get_image_digest "$FULL_IMAGE") log "🔍 Nouveau digest : ${NEW_DIGEST:-non disponible}" @@ -83,34 +176,68 @@ fi # ─── MISE À JOUR NÉCESSAIRE ───────────────────────────────────────────────── log "🔔 Nouvelle version détectée ! Mise à jour en cours..." -# Backup rapide avant mise à jour (optionnel, le script de backup complet gère le quotidien) +# Sauvegarder l'image actuelle pour rollback éventuel +log "💾 Sauvegarde de l'image actuelle pour rollback..." +OLD_IMAGE_ID=$(docker image inspect "$FULL_IMAGE" --format '{{.ID}}' 2>/dev/null || echo "") +ROLLBACK_TAG="${IMAGE_NAME}:rollback-$(date +%Y%m%d-%H%M%S)" +if [[ -n "$OLD_IMAGE_ID" ]]; then + docker tag "$OLD_IMAGE_ID" "$ROLLBACK_TAG" 2>/dev/null || { + log "⚠️ Impossible de tagger l'image pour rollback (on continue)" + ROLLBACK_TAG="" + } +else + ROLLBACK_TAG="" +fi + +# Backup rapide DB avant mise à jour BACKUP_PRE_DIR="${BACKUP_PRE_DIR:-/tmp/gitea-pre-update-$(date +%Y%m%d-%H%M%S)}" mkdir -p "$BACKUP_PRE_DIR" -log "💾 Backup pré-update vers $BACKUP_PRE_DIR" -docker exec gitea_db mysqldump -u root -pgitea gitea > "$BACKUP_PRE_DIR/gitea_db.sql" 2>/dev/null || { - log "⚠️ Backup pré-update DB échoué, on continue quand même" -} +log "💾 Backup DB pré-update vers $BACKUP_PRE_DIR" +if docker ps --format '{{.Names}}' | grep -q "^${DB_CONTAINER}$"; then + docker exec "$DB_CONTAINER" mysqldump \ + -u root -p"$DB_ROOT_PASSWORD" \ + --single-transaction \ + "$DB_NAME" > "$BACKUP_PRE_DIR/gitea_db.sql" 2>/dev/null || { + log "⚠️ Backup pré-update DB échoué, on continue quand même" + } +else + log "⚠️ Conteneur $DB_CONTAINER non trouvé — pas de backup pré-update" +fi # Recréer le conteneur avec la nouvelle image log "🚀 Redémarrage du conteneur avec la nouvelle image..." -UP_OUTPUT=$(docker compose -f "$COMPOSE_FILE" up -d "$CONTAINER_NAME" 2>&1) -UP_EXIT=$? +UP_OUTPUT=$($DOCKER_COMPOSE -f "$COMPOSE_FILE" up -d "$APP_SERVICE" 2>&1) || { + UP_EXIT=$? + log "❌ Échec du redémarrage (code $UP_EXIT) : $UP_OUTPUT" -if [[ $UP_EXIT -ne 0 ]]; then - log "❌ Échec du redémarrage : $UP_OUTPUT" - log "⚠️ Tentative de rollback..." - docker compose -f "$COMPOSE_FILE" up -d "$CONTAINER_NAME" 2>&1 || true + # Rollback réel : restaurer l'ancienne image + if [[ -n "$ROLLBACK_TAG" ]]; then + log "🔄 ROLLBACK : restauration de l'image $ROLLBACK_TAG..." + docker tag "$ROLLBACK_TAG" "$FULL_IMAGE" 2>/dev/null || true + $DOCKER_COMPOSE -f "$COMPOSE_FILE" up -d "$APP_SERVICE" 2>&1 || true + docker rmi "$ROLLBACK_TAG" 2>/dev/null || true + else + log "⚠️ Rollback impossible (pas d'image sauvegardée)" + $DOCKER_COMPOSE -f "$COMPOSE_FILE" up -d "$APP_SERVICE" 2>&1 || true + fi notify "❌ GITEA UPDATE FAILED sur $(hostname) — rollback effectué" exit 1 +} + +# Nettoyer le tag de rollback si tout va bien +if [[ -n "$ROLLBACK_TAG" ]]; then + docker rmi "$ROLLBACK_TAG" 2>/dev/null || true fi # ─── Vérification santé ───────────────────────────────────────────────────── log "🏥 Vérification santé (timeout=${HEALTH_CHECK_TIMEOUT}s)..." +HEALTH_OK=false for i in $(seq 1 "$HEALTH_CHECK_TIMEOUT"); do STATUS=$(curl -s -o /dev/null -w "%{http_code}" "$HEALTH_CHECK_URL" 2>/dev/null || echo "000") if [[ "$STATUS" == "200" || "$STATUS" == "302" || "$STATUS" == "303" ]]; then log "✅ Gitea répond (HTTP $STATUS) après ${i}s" + HEALTH_OK=true break fi if [[ $i -eq "$HEALTH_CHECK_TIMEOUT" ]]; then @@ -122,7 +249,7 @@ for i in $(seq 1 "$HEALTH_CHECK_TIMEOUT"); do done # ─── Rapport final ────────────────────────────────────────────────────────── -GITEA_VERSION=$(curl -s "$HEALTH_CHECK_URL/api/v1/version" 2>/dev/null | grep -oP '"version":"\K[^"]+' || echo "inconnue") +GITEA_VERSION=$(curl -s "$HEALTH_CHECK_URL/api/v1/version" 2>/dev/null | grep -oE '"version":"[^"]+"' | cut -d'"' -f4 || echo "inconnue") log "✅ Mise à jour réussie !" log "📋 Nouvelle version : $GITEA_VERSION" @@ -132,7 +259,20 @@ log "═══ Fin ──────────────────── notify "✅ Gitea mis à jour vers $GITEA_VERSION sur $(hostname)" -# Nettoyage vieilles images +# ─── Nettoyage ciblé : seulement les vieilles images Gitea ────────────────── +log "🧹 Nettoyage des anciennes images Gitea..." +# Trouver toutes les images gitea/gitea sauf la dernière utilisée +CURRENT_IMAGE_ID=$(docker image inspect "$FULL_IMAGE" --format '{{.ID}}' 2>/dev/null || echo "") +if [[ -n "$CURRENT_IMAGE_ID" ]]; then + # Lister les images Gitea non taggées ou anciennes + docker images "$IMAGE_NAME" --format '{{.ID}} {{.Tag}}' 2>/dev/null | while read -r img_id img_tag; do + if [[ "$img_id" != "$CURRENT_IMAGE_ID" ]] && [[ "$img_tag" != "$IMAGE_TAG" ]]; then + log " 🗑️ Suppression ancienne image : $img_id ($img_tag)" + docker rmi "$img_id" 2>/dev/null || true + fi + done +fi +# Nettoyer aussi les images dangling (sans tag) docker image prune -f 2>/dev/null || true exit 0 diff --git a/gitea-backup.sh b/gitea-backup.sh index 40363bf..556f8c9 100755 --- a/gitea-backup.sh +++ b/gitea-backup.sh @@ -9,16 +9,23 @@ # 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 # -# Le tout est horodaté et stocké dans BACKUP_DIR. -# Un restore peut se faire sur N'IMPORTE QUEL hôte Docker. -# -# Paramètres configurables (modifier ci-dessous ou passer en variables d'env) +# Paramètres configurables : voir gitea-scripts.conf # ============================================================================= set -euo pipefail -# ─── Configuration (modifiable) ───────────────────────────────────────────── +# ─── 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}" @@ -26,6 +33,8 @@ 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}" @@ -38,25 +47,176 @@ VOLUME_DATA="${VOLUME_DATA:-/DOCKER_CONFIG/gitea/data}" VOLUME_MYSQL="${VOLUME_MYSQL:-/DOCKER_CONFIG/gitea/mysql}" # Rétention -RETENTION_DAYS="${RETENTION_DAYS:-30}" # Garder X jours de backups -RETENTION_WEEKLY="${RETENTION_WEEKLY:-4}" # Garder X backups hebdomadaires (dimanche) -RETENTION_MONTHLY="${RETENTION_MONTHLY:-6}" # Garder X backups mensuels (1er du mois) +RETENTION_DAYS="${RETENTION_DAYS:-30}" +RETENTION_WEEKLY="${RETENTION_WEEKLY:-4}" +RETENTION_MONTHLY="${RETENTION_MONTHLY:-6}" # Options -COMPRESS="${COMPRESS:-true}" # Compression gzip -VERIFY_BACKUP="${VERIFY_BACKUP:-true}" # Vérifier l'intégrité après backup -STOP_CONTAINERS="${STOP_CONTAINERS:-false}" # Arrêter les conteneurs pendant le backup (plus sûr) +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() { - echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE" + local msg="[$(date '+%Y-%m-%d %H:%M:%S')] $1" + echo "$msg" + echo "$msg" >> "$LOG_FILE" 2>/dev/null || true } -# ─── Initialisation ───────────────────────────────────────────────────────── +# ─── 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 @@ -66,6 +226,8 @@ 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" @@ -73,30 +235,31 @@ log "📦 Début backup Gitea → $BACKUP_PATH" mkdir -p "$BACKUP_DIR" || { log "❌ Impossible de créer $BACKUP_DIR"; exit 1; } cd "$COMPOSE_DIR" || { log "❌ Répertoire $COMPOSE_DIR introuvable"; exit 1; } -mkdir -p "$BACKUP_PATH" - -# Vérifier espace disque (minimum 2 GB) -AVAILABLE_KB=$(df "$BACKUP_DIR" | tail -1 | awk '{print $4}') -if [[ $AVAILABLE_KB -lt 2097152 ]]; then - log "❌ Espace disque insuffisant : $(($AVAILABLE_KB / 1024)) MB disponibles (min 2 GB requis)" +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" + $DOCKER_COMPOSE -f "$COMPOSE_FILE" stop 2>&1 | tee -a "$LOG_FILE" || true CONTAINERS_WERE_STOPPED=true -else - CONTAINERS_WERE_STOPPED=false fi # ─── 1. Backup base de données MySQL ──────────────────────────────────────── log "🛢️ Dump base de données MySQL ($DB_NAME)..." -# Vérifier que le conteneur DB tourne if docker ps --format '{{.Names}}' | grep -q "^${DB_CONTAINER}$"; then docker exec "$DB_CONTAINER" mysqldump \ -u root -p"$DB_ROOT_PASSWORD" \ @@ -106,30 +269,30 @@ if docker ps --format '{{.Names}}' | grep -q "^${DB_CONTAINER}$"; then --events \ --hex-blob \ --default-character-set=utf8mb4 \ - "$DB_NAME" > "${BACKUP_PATH}/gitea-db.sql" 2>&1 || { + "$DB_NAME" > "${BACKUP_PATH}/gitea-db.sql" 2>"${BACKUP_PATH}/mysqldump.err" || { log "❌ Échec du dump MySQL" - [[ "$CONTAINERS_WERE_STOPPED" == "true" ]] && docker compose -f "$COMPOSE_FILE" start 2>&1 | tee -a "$LOG_FILE" + 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 } - DB_SIZE=$(wc -c < "${BACKUP_PATH}/gitea-db.sql") + 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" + [[ "$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" | awk '{print $5}')" + 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)..." -# On utilise tar depuis l'hôte directement (plus fiable que docker cp pour gros volumes) -# Exclusion des fichiers temporaires et logs inutiles tar -czf "${BACKUP_PATH}/gitea-data.tar.gz" \ -C "$(dirname "$VOLUME_DATA")" \ --exclude='*.log' \ @@ -138,10 +301,10 @@ tar -czf "${BACKUP_PATH}/gitea-data.tar.gz" \ --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" + [[ "$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" | awk '{print $5}') +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)..." @@ -149,10 +312,10 @@ 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" + [[ "$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" | awk '{print $5}') +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 ────────────────────────────────── @@ -163,13 +326,15 @@ cp "$COMPOSE_DIR/$COMPOSE_FILE" "${BACKUP_PATH}/docker-compose.yml" 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" - + $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 @@ -183,6 +348,9 @@ 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 # ============================================================================= @@ -191,12 +359,6 @@ cat > "$RESTORE_SCRIPT" << RESTORE_EOF # ============================================================================= # Usage : bash restore.sh [OPTIONS] # -# Ce script restaure COMPLÈTEMENT Gitea sur n'importe quel hôte Docker : -# 1. Crée les volumes Docker -# 2. Restaure la base de données MySQL -# 3. Restaure les données Gitea -# 4. Lance les conteneurs -# # Options : # --dry-run Affiche ce qui serait fait sans exécuter # --force Ne pas demander confirmation @@ -221,7 +383,7 @@ for arg in "\$@"; do case "\$arg" in --dry-run) DRY_RUN=true ;; --force) FORCE=true ;; - --help) head -30 "\$0"; exit 0 ;; + --help|-h) head -50 "\$0"; exit 0 ;; esac done @@ -246,23 +408,19 @@ if [[ "\$DRY_RUN" == "true" ]]; then echo " 4. Copier docker-compose.yml → \$TARGET_COMPOSE_DIR/" echo " 5. docker compose up -d" echo " 6. Attendre MySQL prêt" -RESTORE_EOF - -# Continuer le script restore (le heredoc était trop long pour un seul bloc) -cat >> "$RESTORE_SCRIPT" << 'RESTORE_EOF' - echo " 7. docker exec gitea_db mysql ... < gitea-db.sql" + echo " 7. docker exec gitea_db mysql ... < $DB_DUMP_FILE" echo " 8. docker compose restart" echo "" exit 0 fi -if [[ "$FORCE" != "true" ]]; then +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 + if [[ "\$CONFIRM" != "RESTORE" ]]; then echo "❌ Restauration annulée." exit 0 fi @@ -270,58 +428,77 @@ fi echo "" echo "📦 Étape 1/6 : Création des répertoires..." -mkdir -p "$TARGET_VOLUME_DATA" "$TARGET_VOLUME_MYSQL" "$TARGET_COMPOSE_DIR" +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")" +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")" +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/" +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" +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" -docker compose up -d +cd "\$TARGET_COMPOSE_DIR" +\$DC up -d echo " ⏳ Attente démarrage MySQL (30s)..." sleep 10 -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=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/gitea-db.sql" -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" +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 - docker exec -i gitea_db mysql -u root -p"$DB_ROOT_PASSWORD" "$DB_NAME" < "$DB_DUMP" + echo "❌ Fichier dump introuvable : \$DB_DUMP" + exit 1 fi echo "" echo "🔄 Redémarrage final des conteneurs..." -docker compose restart +\$DC restart echo "" echo "════════════════════════════════════════════════════════" echo "✅ RESTAURATION TERMINÉE !" echo "" echo " Gitea : http://localhost:3000" -echo " Vérifiez les logs : docker compose logs -f" +echo " Vérifiez les logs : \$DC logs -f" echo "════════════════════════════════════════════════════════" RESTORE_EOF @@ -332,21 +509,24 @@ log " ✅ Script restore créé : restore.sh" 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 : $ARCHIVE" + 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 @@ -355,55 +535,121 @@ if [[ "$VERIFY_BACKUP" == "true" ]]; then 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 : $(docker exec "$APP_CONTAINER" gitea --version 2>/dev/null || echo "inconnue") +Gitea version : $GITEA_VERSION DB type : MySQL 8 DB name : $DB_NAME Compression : $COMPRESS -Containers stopped : $STOP_CONTAINERS -Taille DB : $(ls -lh "${BACKUP_PATH}"/gitea-db.sql* | awk '{print $5}') -Taille data : $DATA_SIZE -Taille mysql : $MYSQL_SIZE +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)..." -# Supprimer les backups plus vieux que RETENTION_DAYS (sauf dimanche et 1er du mois) -find "$BACKUP_DIR" -maxdepth 1 -type d -name "gitea-backup-*" -mtime +"$RETENTION_DAYS" | while read -r backup; do - BASENAME=$(basename "$backup") - # Garder les backups du dimanche et du 1er du mois (gérés séparément) - log " 🗑️ Suppression : $BASENAME" - rm -rf "$backup" +# 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 -# Nettoyage hebdomadaire : garder les N derniers dimanches -if [[ "$DAY_OF_WEEK" == "7" ]]; then - find "$BACKUP_DIR" -maxdepth 1 -type d -name "gitea-backup-*" | sort -r | tail -n +$((RETENTION_WEEKLY + 1)) | while read -r backup; do - # Ne supprimer que si c'est un backup du dimanche - # (on ne touche pas aux backups mensuels du 1er) - [[ "$(basename "$backup")" =~ 01- ]] && continue - log " 🗑️ Suppression hebdo : $(basename "$backup")" - rm -rf "$backup" - done -fi +# ─── 8. Backup distant ────────────────────────────────────────────────────── +sync_remote "$BACKUP_PATH" || true -# Nettoyage mensuel : garder les N derniers 1er du mois -if [[ "$DAY_OF_MONTH" == "01" ]]; then - find "$BACKUP_DIR" -maxdepth 1 -type d -name "gitea-backup-*01-*" | sort -r | tail -n +$((RETENTION_MONTHLY + 1)) | while read -r backup; do - log " 🗑️ Suppression mensuel : $(basename "$backup")" - rm -rf "$backup" - done -fi - -# ─── 8. Rapport final ─────────────────────────────────────────────────────── -TOTAL_SIZE=$(du -sh "$BACKUP_PATH" | cut -f1) -BACKUP_COUNT=$(find "$BACKUP_DIR" -maxdepth 1 -type d -name "gitea-backup-*" | wc -l) +# ─── 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" +log "📊 Total : ${TOTAL_SIZE:-N/A}" log "📋 Backups conservés : $BACKUP_COUNT" log "📂 Destination : $BACKUP_PATH" log "═══ Fin ───────────────────────────────────────────" @@ -414,7 +660,10 @@ 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 diff --git a/gitea-scripts.conf b/gitea-scripts.conf new file mode 100644 index 0000000..1faa861 --- /dev/null +++ b/gitea-scripts.conf @@ -0,0 +1,75 @@ +# ============================================================================= +# gitea-scripts.conf — Configuration unifiée pour tous les scripts Gitea +# ============================================================================= +# Ce fichier est sourcé par tous les scripts (auto-update, backup, install_cron). +# Les valeurs peuvent être surchargées par des variables d'environnement. +# ============================================================================= + +# ─── Chemins ────────────────────────────────────────────────────────────────── +SCRIPTS_DIR="${SCRIPTS_DIR:-$HOME/scripts}" +COMPOSE_DIR="${COMPOSE_DIR:-$HOME/dev/git/outils/docker/gitea}" +COMPOSE_FILE="${COMPOSE_FILE:-docker-compose.yml}" +BACKUP_DIR="${BACKUP_DIR:-/NFS/BACKUP/DOCKER/dev-prod/gitea}" +LOCK_DIR="${LOCK_DIR:-/tmp}" + +# ─── Conteneurs ─────────────────────────────────────────────────────────────── +APP_SERVICE="${APP_SERVICE:-gitea_server}" +APP_CONTAINER="${APP_CONTAINER:-gitea_server}" +DB_SERVICE="${DB_SERVICE:-gitea_db}" +DB_CONTAINER="${DB_CONTAINER:-gitea_db}" + +# ─── Image Docker ───────────────────────────────────────────────────────────── +IMAGE_NAME="${IMAGE_NAME:-gitea/gitea}" +IMAGE_TAG="${IMAGE_TAG:-latest}" + +# ─── 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) ──────────────────────────────────────────── +VOLUME_DATA="${VOLUME_DATA:-/DOCKER_CONFIG/gitea/data}" +VOLUME_MYSQL="${VOLUME_MYSQL:-/DOCKER_CONFIG/gitea/mysql}" + +# ─── URLs ───────────────────────────────────────────────────────────────────── +HEALTH_CHECK_URL="${HEALTH_CHECK_URL:-http://localhost:3000}" +HEALTH_CHECK_TIMEOUT="${HEALTH_CHECK_TIMEOUT:-30}" + +# ─── Logs ───────────────────────────────────────────────────────────────────── +LOG_DIR="${LOG_DIR:-/var/log}" +UPDATE_LOG="${UPDATE_LOG:-$LOG_DIR/gitea-update.log}" +BACKUP_LOG="${BACKUP_LOG:-$LOG_DIR/gitea-backup.log}" +BACKUP_CRON_LOG="${BACKUP_CRON_LOG:-$LOG_DIR/gitea-backup-cron.log}" +UPDATE_CRON_LOG="${UPDATE_CRON_LOG:-$LOG_DIR/gitea-update-cron.log}" +LOG_RETENTION_DAYS="${LOG_RETENTION_DAYS:-90}" + +# ─── Rétention backups ──────────────────────────────────────────────────────── +RETENTION_DAYS="${RETENTION_DAYS:-30}" +RETENTION_WEEKLY="${RETENTION_WEEKLY:-4}" +RETENTION_MONTHLY="${RETENTION_MONTHLY:-6}" + +# ─── Options backup ─────────────────────────────────────────────────────────── +COMPRESS="${COMPRESS:-true}" +VERIFY_BACKUP="${VERIFY_BACKUP:-true}" +STOP_CONTAINERS="${STOP_CONTAINERS:-false}" +ENABLE_NOTIFICATIONS="${ENABLE_NOTIFICATIONS:-false}" + +# ─── Notifications ──────────────────────────────────────────────────────────── +NOTIFY_METHOD="${NOTIFY_METHOD:-log}" +TELEGRAM_BOT_TOKEN="${TELEGRAM_BOT_TOKEN:-}" +TELEGRAM_CHAT_ID="${TELEGRAM_CHAT_ID:-}" +NTPY_TOPIC="${NTPY_TOPIC:-}" + +# ─── Backup distant (optionnel) ─────────────────────────────────────────────── +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}" + +# ─── Cron ───────────────────────────────────────────────────────────────────── +CRON_BACKUP_SCHEDULE="${CRON_BACKUP_SCHEDULE:-0 2 * * *}" +CRON_UPDATE_SCHEDULE="${CRON_UPDATE_SCHEDULE:-0 4 * * *}" +CRON_MARKER="${CRON_MARKER:-# >>> GITEA SCRIPTS (managed by install_cron.sh) <<<}" diff --git a/install_cron.sh b/install_cron.sh index 5a7a4b1..97d1c41 100755 --- a/install_cron.sh +++ b/install_cron.sh @@ -7,13 +7,23 @@ # bash install_cron.sh --force → Installation sans confirmation # bash install_cron.sh --uninstall → Retire les entrées du cron # bash install_cron.sh --status → Vérifie si les crons sont actifs +# bash install_cron.sh --dry-run → Affiche ce qui serait installé # -# Paramètres configurables (modifier ci-dessous ou passer en variables d'env) +# Paramètres configurables : voir gitea-scripts.conf # ============================================================================= set -euo pipefail -# ─── Configuration ────────────────────────────────────────────────────────── +# ─── 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 ──────────────────────────────────────────────────── SCRIPTS_DIR="${SCRIPTS_DIR:-$HOME/scripts}" CRON_BACKUP_SCHEDULE="${CRON_BACKUP_SCHEDULE:-0 2 * * *}" CRON_UPDATE_SCHEDULE="${CRON_UPDATE_SCHEDULE:-0 4 * * *}" @@ -42,6 +52,7 @@ Usage: $0 [OPTION] Options : --force Installation sans confirmation + --dry-run Affiche ce qui serait installé sans rien faire --uninstall Retire les scripts du cron --status Vérifie l'état des crons Gitea --help Ce message @@ -73,9 +84,28 @@ check_prerequisites() { missing=1 fi + # Vérifier aussi la présence du fichier de config (optionnel mais recommandé) + if [[ ! -f "$SCRIPTS_DIR/gitea-scripts.conf" ]]; then + warn "Fichier de configuration introuvable : $SCRIPTS_DIR/gitea-scripts.conf" + info "Les scripts utiliseront leurs valeurs par défaut" + fi + return $missing } +# ─── Construire le bloc cron ──────────────────────────────────────────────── +build_cron_block() { + cat << CRON_BLOCK + +$CRON_MARKER +# Backup quotidien Gitea (DB + volumes) +$CRON_BACKUP_SCHEDULE $SCRIPTS_DIR/$BACKUP_SCRIPT >> $BACKUP_LOG 2>&1 +# Vérification mise à jour Gitea Docker +$CRON_UPDATE_SCHEDULE $SCRIPTS_DIR/$UPDATE_SCRIPT >> $UPDATE_LOG 2>&1 +# >>> END GITEA SCRIPTS <<< +CRON_BLOCK +} + # ─── Status ───────────────────────────────────────────────────────────────── show_status() { echo "" @@ -93,10 +123,15 @@ show_status() { echo "" echo "Entrées actives :" - echo "$current_cron" | grep -A1 "$CRON_MARKER" | grep -v "$CRON_MARKER" | grep -v '^$' | while read -r line; do - local schedule cmd rest + # Extraire toutes les lignes entre les marqueurs + echo "$current_cron" | sed -n "/$CRON_MARKER/,/>>> END GITEA SCRIPTS <</dev/null | cut -c1-19) + log_size=$(du -h "$BACKUP_LOG" 2>/dev/null | cut -f1) + log_lines=$(wc -l < "$BACKUP_LOG" 2>/dev/null || echo "0") + log_date=$(tail -1 "$BACKUP_LOG" 2>/dev/null | cut -c1-19 || echo "N/A") ok "Log backup : $BACKUP_LOG ($log_size, $log_lines lignes, dernier: $log_date)" else warn "Log backup : $BACKUP_LOG (pas encore créé)" @@ -133,9 +179,9 @@ show_status() { if [[ -f "$UPDATE_LOG" ]]; then local log_size2 log_lines2 log_date2 - log_size2=$(du -h "$UPDATE_LOG" | cut -f1) - log_lines2=$(wc -l < "$UPDATE_LOG") - log_date2=$(tail -1 "$UPDATE_LOG" 2>/dev/null | cut -c1-19) + log_size2=$(du -h "$UPDATE_LOG" 2>/dev/null | cut -f1) + log_lines2=$(wc -l < "$UPDATE_LOG" 2>/dev/null || echo "0") + log_date2=$(tail -1 "$UPDATE_LOG" 2>/dev/null | cut -c1-19 || echo "N/A") ok "Log update : $UPDATE_LOG ($log_size2, $log_lines2 lignes, dernier: $log_date2)" else warn "Log update : $UPDATE_LOG (pas encore créé)" @@ -173,7 +219,13 @@ do_install() { ok "Scripts rendus exécutables" # Créer les dossiers de logs si nécessaire - if [[ "$LOG_DIR" != "$HOME"* ]] && [[ ! -w "$LOG_DIR" ]]; then + if [[ ! -d "$LOG_DIR" ]]; then + mkdir -p "$LOG_DIR" 2>/dev/null || { + warn "Impossible de créer $LOG_DIR — les logs iront dans \$HOME" + BACKUP_LOG="$HOME/gitea-backup-cron.log" + UPDATE_LOG="$HOME/gitea-update-cron.log" + } + elif [[ ! -w "$LOG_DIR" ]]; then warn "Pas de droit d'écriture sur $LOG_DIR — les logs iront dans \$HOME" BACKUP_LOG="$HOME/gitea-backup-cron.log" UPDATE_LOG="$HOME/gitea-update-cron.log" @@ -193,19 +245,14 @@ do_install() { # Construire le bloc cron local cron_block - cron_block=$(cat << CRON_BLOCK + cron_block=$(build_cron_block) -$CRON_MARKER -# Backup quotidien Gitea (DB + volumes) -$CRON_BACKUP_SCHEDULE $SCRIPTS_DIR/$BACKUP_SCRIPT >> $BACKUP_LOG 2>&1 -# Vérification mise à jour Gitea Docker -$CRON_UPDATE_SCHEDULE $SCRIPTS_DIR/$UPDATE_SCRIPT >> $UPDATE_LOG 2>&1 -# >>> END GITEA SCRIPTS <<< -CRON_BLOCK -) - - # Appliquer - printf "%s\n" "$current_cron" "$cron_block" | crontab - + # Appliquer : gérer le cas où la crontab est vide + if [[ -z "$current_cron" ]]; then + echo "$cron_block" | crontab - + else + printf "%s\n%s\n" "$current_cron" "$cron_block" | crontab - + fi ok "Crons installés dans la crontab de $CRON_USER" echo "" @@ -226,6 +273,32 @@ CRON_BLOCK echo "════════════════════════════════════════════════════════" } +# ─── Dry run ──────────────────────────────────────────────────────────────── +do_dry_run() { + echo "" + echo "════════════════════════════════════════════════════════" + echo "🔍 DRY RUN — APERÇU DE L'INSTALLATION" + echo "════════════════════════════════════════════════════════" + echo "" + info "Utilisateur : $CRON_USER" + info "Dossier scripts : $SCRIPTS_DIR" + info "Horaire backup : $CRON_BACKUP_SCHEDULE" + info "Horaire update : $CRON_UPDATE_SCHEDULE" + echo "" + + check_prerequisites || true + + echo "" + echo "Bloc qui serait ajouté à la crontab :" + echo "────────────────────────────────────────" + build_cron_block + echo "────────────────────────────────────────" + echo "" + echo "⚠️ Aucune modification n'a été effectuée." + echo " Utilisez --force pour installer sans confirmation." + echo "" +} + # ─── Désinstallation ──────────────────────────────────────────────────────── do_uninstall() { echo "" @@ -246,13 +319,18 @@ do_uninstall() { local new_cron new_cron=$(echo "$current_cron" | sed "/$CRON_MARKER/,/>>> END GITEA SCRIPTS <</dev/null || crontab -r 2>/dev/null || true + # Supprimer les lignes vides en trop + new_cron=$(echo "$new_cron" | sed '/^$/N;/^\n$/d') + + if [[ -z "${new_cron// }" ]]; then + # Crontab complètement vide après suppression + crontab -r 2>/dev/null || true + ok "Crontab vidée et crons Gitea retirés" else echo "$new_cron" | crontab - + ok "Crons Gitea retirés de la crontab de $CRON_USER" fi - ok "Crons Gitea retirés de la crontab de $CRON_USER" echo "" echo " Les scripts eux-mêmes n'ont PAS été supprimés." echo " Ils restent dans $SCRIPTS_DIR/" @@ -264,12 +342,14 @@ do_uninstall() { FORCE=false UNINSTALL=false STATUS=false +DRY_RUN=false for arg in "$@"; do case "$arg" in --force) FORCE=true ;; --uninstall) UNINSTALL=true ;; --status) STATUS=true ;; + --dry-run) DRY_RUN=true ;; --help|-h) usage ;; *) echo "Option inconnue : $arg"; usage ;; esac @@ -285,6 +365,11 @@ if [[ "$UNINSTALL" == "true" ]]; then exit 0 fi +if [[ "$DRY_RUN" == "true" ]]; then + do_dry_run + exit 0 +fi + # Installation if [[ "$FORCE" != "true" ]]; then echo "" diff --git a/restore.sh b/restore.sh new file mode 100644 index 0000000..8013473 --- /dev/null +++ b/restore.sh @@ -0,0 +1,414 @@ +#!/bin/bash +# ============================================================================= +# restore.sh — Restauration Gitea depuis un backup +# ============================================================================= +# Usage : +# bash restore.sh → Restauration interactive +# bash restore.sh --force → Sans confirmation +# bash restore.sh --dry-run → Vérification sans exécuter +# +# Exemples : +# bash restore.sh /NFS/BACKUP/DOCKER/dev-prod/gitea/gitea-backup-20260626-020000 +# bash restore.sh /NFS/BACKUP/DOCKER/dev-prod/gitea/gitea-backup-20260626-020000 --dry-run +# ============================================================================= + +set -euo pipefail + +# ─── Couleurs ─────────────────────────────────────────────────────────────── +RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; CYAN='\033[0;36m'; NC='\033[0m' +ok() { echo -e " ${GREEN}✅${NC} $1"; } +warn() { echo -e " ${YELLOW}⚠️${NC} $1"; } +err() { echo -e " ${RED}❌${NC} $1"; } +info() { echo -e " ${CYAN}📋${NC} $1"; } + +# ─── Usage ────────────────────────────────────────────────────────────────── +usage() { + cat << EOF +Usage: $0 [OPTIONS] + +Arguments : + chemin_backup Dossier du backup à restaurer (ex: gitea-backup-YYYYMMDD-HHMMSS) + +Options : + --force Ne pas demander confirmation + --dry-run Affiche ce qui serait fait sans exécuter + --help Cette aide + +Variables d'environnement : + TARGET_COMPOSE_DIR Dossier docker-compose cible (défaut: ~/dev/git/outils/docker/gitea) + TARGET_VOLUME_DATA Volume data cible (défaut: /DOCKER_CONFIG/gitea/data) + TARGET_VOLUME_MYSQL Volume MySQL cible (défaut: /DOCKER_CONFIG/gitea/mysql) + DB_ROOT_PASSWORD Mot de passe root MySQL (défaut: gitea) + DB_NAME Nom base de données (défaut: gitea) +EOF + exit 0 +} + +# ─── Vérifications ────────────────────────────────────────────────────────── +check_prerequisites() { + local missing=0 + + if ! command -v docker &>/dev/null; then + err "Docker n'est pas installé" + missing=1 + fi + + if ! command -v tar &>/dev/null; then + err "tar n'est pas installé" + missing=1 + fi + + if docker compose version &>/dev/null; then + DOCKER_COMPOSE="docker compose" + elif command -v docker-compose &>/dev/null; then + DOCKER_COMPOSE="docker-compose" + else + err "Ni 'docker compose' ni 'docker-compose' n'est disponible" + missing=1 + fi + + return $missing +} + +# ─── Lister les backups disponibles ───────────────────────────────────────── +list_backups() { + local dir="${1:-.}" + echo "" + echo "Backups disponibles :" + echo "─────────────────────" + local count=0 + for d in "$dir"/gitea-backup-*; do + if [[ -d "$d" ]]; then + count=$((count + 1)) + local info_file="$d/backup-info.txt" + local date_info="" + if [[ -f "$info_file" ]]; then + date_info=$(grep "^Date" "$info_file" 2>/dev/null | sed 's/Date[[:space:]]*:[[:space:]]*//' || echo "inconnue") + fi + echo " $count. $(basename "$d") ($date_info)" + fi + done + + if [[ $count -eq 0 ]]; then + echo " Aucun backup trouvé dans $dir" + return 1 + fi + + echo "" + echo "Utilisez : $0 " + return 0 +} + +# ═════════════════════════════════════════════════════════════════════════════ +# MAIN +# ═════════════════════════════════════════════════════════════════════════════ + +FORCE=false +DRY_RUN=false +BACKUP_PATH="" + +# Détection docker compose (par défaut, sera surchargé après check) +DOCKER_COMPOSE="docker compose" + +for arg in "$@"; do + case "$arg" in + --force) FORCE=true ;; + --dry-run) DRY_RUN=true ;; + --help|-h) usage ;; + --*) echo "Option inconnue : $arg"; usage ;; + *) + if [[ -z "$BACKUP_PATH" ]]; then + BACKUP_PATH="$arg" + fi + ;; + esac +done + +# Si aucun backup spécifié, lister les backups et suggérer +if [[ -z "$BACKUP_PATH" ]]; then + # Chercher dans les emplacements courants + FOUND=false + for search_dir in "/NFS/BACKUP/DOCKER/dev-prod/gitea" "$HOME/backups/gitea" "."; do + if [[ -d "$search_dir" ]]; then + list_backups "$search_dir" && FOUND=true && break + fi + done + if [[ "$FOUND" != "true" ]]; then + echo "" + echo "Usage: $0 [OPTIONS]" + echo " $0 --help pour plus d'informations" + fi + exit 1 +fi + +# Résoudre le chemin absolu +BACKUP_PATH="$(cd "$(dirname "$BACKUP_PATH")" 2>/dev/null && pwd)/$(basename "$BACKUP_PATH")" || BACKUP_PATH="$(realpath "$BACKUP_PATH" 2>/dev/null || echo "$BACKUP_PATH")" + +# Vérifier que le dossier de backup existe et est valide +if [[ ! -d "$BACKUP_PATH" ]]; then + err "Dossier de backup introuvable : $BACKUP_PATH" + echo "" + echo "Recherche de backups dans les emplacements standards..." + for search_dir in "/NFS/BACKUP/DOCKER/dev-prod/gitea" "$HOME/backups/gitea" "."; do + if [[ -d "$search_dir" ]]; then + list_backups "$search_dir" + break + fi + done + exit 1 +fi + +# Vérifier les fichiers requis +MISSING_FILES=() +for required_file in "gitea-mysql.tar.gz" "gitea-data.tar.gz" "docker-compose.yml"; do + if [[ ! -f "$BACKUP_PATH/$required_file" ]]; then + MISSING_FILES+=("$required_file") + fi +done + +# Vérifier le dump SQL (compressé ou non) +if [[ ! -f "$BACKUP_PATH/gitea-db.sql.gz" ]] && [[ ! -f "$BACKUP_PATH/gitea-db.sql" ]]; then + MISSING_FILES+=("gitea-db.sql ou gitea-db.sql.gz") +fi + +if [[ ${#MISSING_FILES[@]} -gt 0 ]]; then + err "Fichiers requis manquants dans le backup :" + for f in "${MISSING_FILES[@]}"; do + err " - $f" + done + echo "" + warn "Ce dossier ne semble pas être un backup Gitea valide." + exit 1 +fi + +echo "" +echo "════════════════════════════════════════════════════════" +echo "🔄 RESTAURATION GITEA" +echo "════════════════════════════════════════════════════════" +echo "" + +# ─── 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}" + +# ─── Afficher les infos du backup ──────────────────────────────────────────── +BACKUP_NAME=$(basename "$BACKUP_PATH") +BACKUP_DATE=${BACKUP_NAME#gitea-backup-} + +if [[ -f "$BACKUP_PATH/backup-info.txt" ]]; then + info "Informations du backup :" + while IFS= read -r line; do + echo " $line" + done < "$BACKUP_PATH/backup-info.txt" + echo "" +else + info "Backup : $BACKUP_NAME" + echo "" +fi + +info "Configuration cible :" +echo " Compose dir : $TARGET_COMPOSE_DIR" +echo " Volume data : $TARGET_VOLUME_DATA" +echo " Volume MySQL: $TARGET_VOLUME_MYSQL" +echo " DB name : $DB_NAME" +echo "" + +# ─── Vérifier les prérequis ───────────────────────────────────────────────── +check_prerequisites || { + echo "" + err "Prérequis manquants — impossible de continuer" + exit 1 +} + +# ─── Dry-run ──────────────────────────────────────────────────────────────── +if [[ "$DRY_RUN" == "true" ]]; then + echo "🔍 DRY RUN — aucune action ne sera effectuée" + echo "" + echo "Actions qui seraient exécutées :" + echo "" + echo " 1. Arrêter les conteneurs Gitea existants" + echo " cd $TARGET_COMPOSE_DIR && $DOCKER_COMPOSE down" + echo "" + echo " 2. Créer les répertoires cibles" + echo " mkdir -p $TARGET_VOLUME_DATA $TARGET_VOLUME_MYSQL" + echo "" + echo " 3. Restaurer le volume MySQL" + echo " rm -rf ${TARGET_VOLUME_MYSQL}/*" + echo " tar -xzf $BACKUP_PATH/gitea-mysql.tar.gz -C $(dirname "$TARGET_VOLUME_MYSQL")" + echo "" + echo " 4. Restaurer le volume Gitea" + echo " rm -rf ${TARGET_VOLUME_DATA}/*" + echo " tar -xzf $BACKUP_PATH/gitea-data.tar.gz -C $(dirname "$TARGET_VOLUME_DATA")" + echo "" + echo " 5. Copier docker-compose.yml" + echo " cp $BACKUP_PATH/docker-compose.yml $TARGET_COMPOSE_DIR/" + echo "" + if [[ -f "$BACKUP_PATH/app.ini" ]]; then + echo " 6. Restaurer app.ini" + echo " mkdir -p ${TARGET_VOLUME_DATA}/gitea/conf" + echo " cp $BACKUP_PATH/app.ini ${TARGET_VOLUME_DATA}/gitea/conf/" + echo "" + fi + echo " 7. Démarrer les conteneurs" + echo " cd $TARGET_COMPOSE_DIR && $DOCKER_COMPOSE up -d" + echo "" + echo " 8. Restaurer la base de données" + echo " docker exec -i gitea_db mysql -u root -p$DB_ROOT_PASSWORD $DB_NAME < dump" + echo "" + echo " 9. Redémarrer" + echo " $DOCKER_COMPOSE restart" + echo "" + exit 0 +fi + +# ─── Confirmation ─────────────────────────────────────────────────────────── +if [[ "$FORCE" != "true" ]]; then + echo "⚠️ ATTENTION : Cette opération va ÉCRASER l'installation Gitea existante !" + echo " Toute donnée non sauvegardée sera PERDUE." + echo "" + echo " Les conteneurs seront arrêtés et les volumes remplacés." + echo "" + read -p "Taper 'RESTORE' (en majuscules) pour confirmer : " CONFIRM + if [[ "$CONFIRM" != "RESTORE" ]]; then + echo "❌ Restauration annulée." + exit 0 + fi + echo "" +fi + +# ═════════════════════════════════════════════════════════════════════════════ +# RESTAURATION +# ═════════════════════════════════════════════════════════════════════════════ + +TIMING_START=$(date +%s) + +# ─── Étape 1 : Arrêter les conteneurs existants ───────────────────────────── +echo "📦 Étape 1/7 : Arrêt des conteneurs existants..." +if [[ -f "$TARGET_COMPOSE_DIR/docker-compose.yml" ]] || [[ -f "$TARGET_COMPOSE_DIR/$COMPOSE_FILE" ]]; then + cd "$TARGET_COMPOSE_DIR" 2>/dev/null && $DOCKER_COMPOSE down 2>&1 || true +fi +ok "Conteneurs arrêtés" + +# ─── Étape 2 : Créer les répertoires ──────────────────────────────────────── +echo "📦 Étape 2/7 : Création des répertoires cibles..." +mkdir -p "$TARGET_VOLUME_DATA" "$TARGET_VOLUME_MYSQL" "$TARGET_COMPOSE_DIR" +ok "Répertoires créés" + +# ─── Étape 3 : Restaurer volume MySQL ─────────────────────────────────────── +echo "📦 Étape 3/7 : Restauration volume MySQL..." +rm -rf "${TARGET_VOLUME_MYSQL:?}"/* +if ! tar -xzf "$BACKUP_PATH/gitea-mysql.tar.gz" -C "$(dirname "$TARGET_VOLUME_MYSQL")"; then + err "Échec de l'extraction du volume MySQL" + exit 1 +fi +ok "Volume MySQL restauré" + +# ─── Étape 4 : Restaurer volume Gitea ─────────────────────────────────────── +echo "📦 Étape 4/7 : Restauration volume Gitea..." +rm -rf "${TARGET_VOLUME_DATA:?}"/* +if ! tar -xzf "$BACKUP_PATH/gitea-data.tar.gz" -C "$(dirname "$TARGET_VOLUME_DATA")"; then + err "Échec de l'extraction du volume Gitea" + exit 1 +fi +ok "Volume Gitea restauré" + +# ─── Étape 5 : Copier docker-compose ──────────────────────────────────────── +echo "📦 Étape 5/7 : Copie docker-compose et configuration..." +cp "$BACKUP_PATH/docker-compose.yml" "$TARGET_COMPOSE_DIR/" +ok "docker-compose.yml copié" + +if [[ -f "$BACKUP_PATH/app.ini" ]]; then + mkdir -p "${TARGET_VOLUME_DATA}/gitea/conf" + cp "$BACKUP_PATH/app.ini" "${TARGET_VOLUME_DATA}/gitea/conf/app.ini" + ok "app.ini restauré" +fi + +# ─── Étape 6 : Démarrer les conteneurs ────────────────────────────────────── +echo "📦 Étape 6/7 : Démarrage des conteneurs..." +cd "$TARGET_COMPOSE_DIR" +if ! $DOCKER_COMPOSE up -d; then + err "Échec du démarrage des conteneurs" + warn "Vérifiez les logs : $DOCKER_COMPOSE logs" + exit 1 +fi +ok "Conteneurs démarrés" + +# Attendre que MySQL soit prêt +echo " ⏳ Attente démarrage MySQL..." +MYSQL_READY=false +sleep 10 +for i in $(seq 1 30); 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 + warn "MySQL n'a pas répondu — on tente la restauration DB quand même..." +fi + +# ─── Étape 7 : Restaurer la base de données ───────────────────────────────── +echo "📦 Étape 7/7 : Restauration base de données..." + +# Créer la base de données si elle n'existe pas +docker exec gitea_db mysql -u root -p"$DB_ROOT_PASSWORD" -e "CREATE DATABASE IF NOT EXISTS \`$DB_NAME\` CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;" 2>/dev/null || true + +# Restaurer le dump +if [[ -f "$BACKUP_PATH/gitea-db.sql.gz" ]]; then + gunzip -c "$BACKUP_PATH/gitea-db.sql.gz" | docker exec -i gitea_db mysql -u root -p"$DB_ROOT_PASSWORD" "$DB_NAME" 2>&1 || { + err "Échec de la restauration du dump SQL (compressé)" + exit 1 + } +elif [[ -f "$BACKUP_PATH/gitea-db.sql" ]]; then + docker exec -i gitea_db mysql -u root -p"$DB_ROOT_PASSWORD" "$DB_NAME" < "$BACKUP_PATH/gitea-db.sql" 2>&1 || { + err "Échec de la restauration du dump SQL" + exit 1 + } +fi +ok "Base de données restaurée" + +# ─── Redémarrage final ────────────────────────────────────────────────────── +echo "" +echo "🔄 Redémarrage final des conteneurs..." +$DOCKER_COMPOSE restart 2>&1 || true + +# ─── Vérification santé ───────────────────────────────────────────────────── +echo "" +echo "🏥 Vérification que Gitea répond..." +for i in $(seq 1 30); do + STATUS=$(curl -s -o /dev/null -w "%{http_code}" "http://localhost:3000" 2>/dev/null || echo "000") + if [[ "$STATUS" == "200" || "$STATUS" == "302" || "$STATUS" == "303" ]]; then + ok "Gitea répond (HTTP $STATUS) après ${i}s" + break + fi + if [[ $i -eq 30 ]]; then + warn "Gitea ne répond pas encore — vérifiez les logs" + fi + sleep 1 +done + +# ─── Résumé ───────────────────────────────────────────────────────────────── +TIMING_END=$(date +%s) +DURATION=$((TIMING_END - TIMING_START)) + +echo "" +echo "════════════════════════════════════════════════════════" +echo "✅ RESTAURATION TERMINÉE !" +echo "" +echo " Durée : ${DURATION}s" +echo " Backup : $BACKUP_NAME" +echo " Cible : $TARGET_COMPOSE_DIR" +echo " Gitea : http://localhost:3000" +echo "" +echo " Logs : $DOCKER_COMPOSE logs -f" +echo " Status : $DOCKER_COMPOSE ps" +echo "════════════════════════════════════════════════════════" + +exit 0 diff --git a/setup.sh b/setup.sh new file mode 100644 index 0000000..e421a05 --- /dev/null +++ b/setup.sh @@ -0,0 +1,467 @@ +#!/bin/bash +# ============================================================================= +# setup.sh — Installation complète des scripts Gitea +# ============================================================================= +# Usage : +# bash setup.sh → Installation interactive complète +# bash setup.sh --force → Installation sans confirmation +# bash setup.sh --update → Mise à jour des scripts (git pull) +# bash setup.sh --dry-run → Affiche ce qui serait fait +# +# Ce script : +# 1. Clone ou met à jour les scripts depuis Git +# 2. Vérifie que tous les prérequis sont installés +# 3. Rend les scripts exécutables +# 4. Crée les dossiers de logs +# 5. Installe les entrées cron +# 6. Configure la rotation des logs (logrotate) +# 7. Propose un premier backup +# ============================================================================= + +set -euo pipefail + +# ─── Couleurs ─────────────────────────────────────────────────────────────── +RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; CYAN='\033[0;36m'; BLUE='\033[0;34m'; NC='\033[0m' +ok() { echo -e " ${GREEN}✅${NC} $1"; } +warn() { echo -e " ${YELLOW}⚠️${NC} $1"; } +err() { echo -e " ${RED}❌${NC} $1"; } +info() { echo -e " ${CYAN}📋${NC} $1"; } +step() { echo -e "\n${BLUE}════════════════════════════════════════════════════════${NC}"; echo -e "${BLUE}🔧 $1${NC}"; echo -e "${BLUE}════════════════════════════════════════════════════════${NC}"; } + +# ─── Configuration ────────────────────────────────────────────────────────── +GIT_REPO="${GIT_REPO:-https://git.dracodev.net/Outils/gitea-scripts.git}" +SCRIPTS_DIR="${SCRIPTS_DIR:-$HOME/scripts}" +BRANCH="${BRANCH:-main}" + +# ─── Usage ────────────────────────────────────────────────────────────────── +usage() { + cat << EOF +Usage: $0 [OPTIONS] + +Options : + --force Installation sans confirmation + --update Mettre à jour les scripts existants (git pull) + --dry-run Affiche ce qui serait fait sans rien installer + --help Cette aide + +Variables d'environnement : + GIT_REPO URL du dépôt Git (défaut: $GIT_REPO) + SCRIPTS_DIR Dossier d'installation (défaut: \$HOME/scripts) + BRANCH Branche Git à utiliser (défaut: main) +EOF + exit 0 +} + +# ─── Vérification des prérequis système ───────────────────────────────────── +check_system_prerequisites() { + step "Étape 1/6 : Vérification des prérequis système" + + local missing=0 + + info "Vérification des commandes requises..." + + for cmd in git docker curl tar gzip crontab; do + if command -v "$cmd" &>/dev/null; then + local version="" + case "$cmd" in + git) version=$(git --version 2>/dev/null | head -1) ;; + docker) version=$(docker --version 2>/dev/null | head -1) ;; + curl) version=$(curl --version 2>/dev/null | head -1) ;; + esac + ok "$cmd ${version:+($version)}" + else + err "$cmd : NON INSTALLÉ" + missing=1 + fi + done + + # Vérifier docker compose + if docker compose version &>/dev/null; then + ok "docker compose (plugin)" + elif command -v docker-compose &>/dev/null; then + ok "docker-compose (standalone)" + else + err "docker compose : NON INSTALLÉ" + missing=1 + fi + + # Vérifier le daemon Docker + if docker info &>/dev/null; then + ok "Docker daemon : actif" + else + warn "Docker daemon : injoignable (vérifier que Docker tourne)" + fi + + if [[ $missing -gt 0 ]]; then + echo "" + err "$missing prérequis manquants. Installez-les avant de continuer." + exit 1 + fi + + return 0 +} + +# ─── Cloner ou mettre à jour les scripts ──────────────────────────────────── +install_scripts() { + step "Étape 2/6 : Installation des scripts" + + if [[ -d "$SCRIPTS_DIR/.git" ]]; then + info "Dépôt existant trouvé dans $SCRIPTS_DIR" + cd "$SCRIPTS_DIR" + local current_remote + current_remote=$(git remote get-url origin 2>/dev/null || echo "") + if [[ "$current_remote" == "$GIT_REPO" ]]; then + info "Mise à jour depuis $GIT_REPO (branche: $BRANCH)..." + git fetch origin "$BRANCH" 2>&1 || { + warn "Impossible de fetch — on continue avec la version actuelle" + } + if git diff --quiet "origin/$BRANCH" 2>/dev/null; then + ok "Scripts déjà à jour" + else + git pull origin "$BRANCH" 2>&1 || { + warn "Impossible de pull — on continue avec la version actuelle" + } + ok "Scripts mis à jour" + fi + else + warn "Remote différent ($current_remote != $GIT_REPO) — pas de mise à jour automatique" + fi + else + info "Clonage de $GIT_REPO vers $SCRIPTS_DIR..." + if [[ -d "$SCRIPTS_DIR" ]]; then + warn "$SCRIPTS_DIR existe déjà mais n'est pas un dépôt Git" + info "Sauvegarde dans ${SCRIPTS_DIR}.bak..." + mv "$SCRIPTS_DIR" "${SCRIPTS_DIR}.bak.$(date +%s)" 2>/dev/null || true + fi + git clone --branch "$BRANCH" "$GIT_REPO" "$SCRIPTS_DIR" 2>&1 || { + err "Échec du clone de $GIT_REPO" + exit 1 + } + ok "Scripts clonés dans $SCRIPTS_DIR" + fi + + # Vérifier que les scripts principaux existent + local scripts_ok=true + for script in "gitea-backup.sh" "gitea-auto-update.sh" "install_cron.sh" "restore.sh" "gitea-scripts.conf"; do + if [[ -f "$SCRIPTS_DIR/$script" ]]; then + ok "$script présent" + else + warn "$script : absent" + scripts_ok=false + fi + done + + if [[ "$scripts_ok" != "true" ]]; then + warn "Certains scripts sont absents — le dépôt peut être incomplet" + fi + + return 0 +} + +# ─── Rendre les scripts exécutables ───────────────────────────────────────── +make_executable() { + step "Étape 3/6 : Permissions des scripts" + + for script in "gitea-backup.sh" "gitea-auto-update.sh" "install_cron.sh" "restore.sh" "setup.sh"; do + if [[ -f "$SCRIPTS_DIR/$script" ]]; then + chmod +x "$SCRIPTS_DIR/$script" + ok "$script → exécutable" + fi + done + + return 0 +} + +# ─── Créer les dossiers de logs ───────────────────────────────────────────── +setup_logs() { + step "Étape 4/6 : Configuration des logs" + + # Charger la config pour connaître LOG_DIR + if [[ -f "$SCRIPTS_DIR/gitea-scripts.conf" ]]; then + source "$SCRIPTS_DIR/gitea-scripts.conf" + fi + LOG_DIR="${LOG_DIR:-/var/log}" + + if [[ -d "$LOG_DIR" ]] && [[ -w "$LOG_DIR" ]]; then + ok "Dossier de logs $LOG_DIR accessible" + elif [[ -d "$LOG_DIR" ]]; then + warn "Pas de droit d'écriture sur $LOG_DIR" + info "Les logs seront écrits dans $HOME/" + else + if mkdir -p "$LOG_DIR" 2>/dev/null; then + ok "Dossier de logs $LOG_DIR créé" + else + warn "Impossible de créer $LOG_DIR — logs dans $HOME/" + fi + fi + + # Créer les fichiers de log s'ils n'existent pas + touch "$HOME/gitea-backup-cron.log" "$HOME/gitea-update-cron.log" 2>/dev/null || true + + return 0 +} + +# ─── Configurer logrotate ─────────────────────────────────────────────────── +setup_logrotate() { + step "Étape 5/6 : Rotation des logs" + + local logrotate_conf="/etc/logrotate.d/gitea-scripts" + local need_sudo=false + + if [[ -f "$logrotate_conf" ]]; then + ok "logrotate déjà configuré : $logrotate_conf" + return 0 + fi + + if [[ ! -w "/etc/logrotate.d" ]]; then + need_sudo=true + fi + + local log_files="$HOME/gitea-backup-cron.log $HOME/gitea-update-cron.log /var/log/gitea-backup.log /var/log/gitea-update.log" + + cat << LOGROTATE +# Rotation des logs Gitea scripts +$log_files { + weekly + rotate 12 + compress + delaycompress + missingok + notifempty + copytruncate + create 0644 $USER $USER +} +LOGROTATE + + echo "" + if [[ "$need_sudo" == "true" ]]; then + warn "Droits sudo requis pour créer $logrotate_conf" + info "Voici la commande à exécuter :" + echo "" + echo " sudo tee $logrotate_conf << 'EOF'" + cat << LOGROTATE +# Rotation des logs Gitea scripts +$log_files { + weekly + rotate 12 + compress + delaycompress + missingok + notifempty + copytruncate + create 0644 $USER $USER +} +LOGROTATE + echo "EOF" + echo "" + + read -p "Exécuter cette commande maintenant ? [O/n] " -r REPLY + if [[ "$REPLY" =~ ^[OoYy]?$ ]]; then + sudo tee "$logrotate_conf" > /dev/null << LOGROTATE_EOF +# Rotation des logs Gitea scripts +$log_files { + weekly + rotate 12 + compress + delaycompress + missingok + notifempty + copytruncate + create 0644 $USER $USER +} +LOGROTATE_EOF + ok "logrotate configuré" + else + info "Vous pourrez configurer logrotate plus tard avec la commande ci-dessus" + fi + else + cat > "$logrotate_conf" << LOGROTATE_EOF +# Rotation des logs Gitea scripts +$log_files { + weekly + rotate 12 + compress + delaycompress + missingok + notifempty + copytruncate + create 0644 $USER $USER +} +LOGROTATE_EOF + ok "logrotate configuré : $logrotate_conf" + fi + + return 0 +} + +# ─── Installer les crons ──────────────────────────────────────────────────── +setup_cron() { + step "Étape 6/6 : Installation des crons" + + if [[ -f "$SCRIPTS_DIR/install_cron.sh" ]]; then + if [[ "$FORCE" == "true" ]]; then + bash "$SCRIPTS_DIR/install_cron.sh" --force + else + bash "$SCRIPTS_DIR/install_cron.sh" + fi + else + err "install_cron.sh introuvable" + return 1 + fi + + return 0 +} + +# ─── Mise à jour (git pull) ───────────────────────────────────────────────── +do_update() { + step "Mise à jour des scripts Gitea" + + if [[ ! -d "$SCRIPTS_DIR/.git" ]]; then + err "$SCRIPTS_DIR n'est pas un dépôt Git — impossible de mettre à jour" + exit 1 + fi + + cd "$SCRIPTS_DIR" + info "Fetch des dernières modifications..." + git fetch origin "$BRANCH" 2>&1 + + if git diff --quiet "origin/$BRANCH" 2>/dev/null; then + ok "Scripts déjà à jour" + else + info "Mise à jour disponible — application..." + git pull origin "$BRANCH" 2>&1 + ok "Scripts mis à jour" + + # Rendre exécutables à nouveau (au cas où) + for script in *.sh; do + [[ -f "$script" ]] && chmod +x "$script" + done + + echo "" + info "Scripts mis à jour : $(git log --oneline -3)" + fi + + exit 0 +} + +# ═════════════════════════════════════════════════════════════════════════════ +# MAIN +# ═════════════════════════════════════════════════════════════════════════════ + +FORCE=false +DRY_RUN=false +UPDATE=false + +for arg in "$@"; do + case "$arg" in + --force) FORCE=true ;; + --dry-run) DRY_RUN=true ;; + --update) UPDATE=true ;; + --help|-h) usage ;; + *) echo "Option inconnue : $arg"; usage ;; + esac +done + +echo "" +echo "╔══════════════════════════════════════════════════════╗" +echo "║ 🛠️ GITEA SCRIPTS — INSTALLATEUR ║" +echo "╚══════════════════════════════════════════════════════╝" +echo "" +info "Dossier scripts : $SCRIPTS_DIR" +info "Dépôt Git : $GIT_REPO" +info "Branche : $BRANCH" +echo "" + +# ─── Mode mise à jour ────────────────────────────────────────────────────── +if [[ "$UPDATE" == "true" ]]; then + do_update +fi + +# ─── Mode dry-run ─────────────────────────────────────────────────────────── +if [[ "$DRY_RUN" == "true" ]]; then + echo "🔍 DRY RUN — aucune action ne sera effectuée" + echo "" + echo "Actions qui seraient exécutées :" + echo "" + echo " 1. Vérifier les prérequis (git, docker, curl, tar, gzip, crontab)" + echo " 2. Cloner/MàJ $GIT_REPO → $SCRIPTS_DIR" + echo " 3. Rendre les scripts exécutables" + echo " 4. Créer les dossiers de logs" + echo " 5. Configurer logrotate" + echo " 6. Installer les crons (backup + auto-update)" + echo "" + exit 0 +fi + +# ─── Confirmation interactive ─────────────────────────────────────────────── +if [[ "$FORCE" != "true" ]]; then + echo "Ce script va installer complètement les scripts de maintenance Gitea :" + echo "" + echo " 1. Vérifier les prérequis système" + echo " 2. Cloner/MàJ les scripts depuis Git" + echo " 3. Rendre les scripts exécutables" + echo " 4. Créer les dossiers de logs" + echo " 5. Configurer la rotation des logs" + echo " 6. Installer les tâches cron (backup + auto-update)" + echo "" + read -p "Continuer l'installation ? [O/n] " -r REPLY + if [[ ! "$REPLY" =~ ^[OoYy]?$ ]]; then + echo "Installation annulée." + exit 0 + fi +fi + +# ─── Exécution des étapes ─────────────────────────────────────────────────── +check_system_prerequisites +install_scripts +make_executable +setup_logs +setup_logrotate +setup_cron + +# ═════════════════════════════════════════════════════════════════════════════ +# RÉSUMÉ FINAL +# ═════════════════════════════════════════════════════════════════════════════ + +echo "" +echo "╔══════════════════════════════════════════════════════╗" +echo "║ ✅ INSTALLATION TERMINÉE ! ║" +echo "╚══════════════════════════════════════════════════════╝" +echo "" +echo "📂 Scripts installés dans : $SCRIPTS_DIR" +echo "" +echo "📋 Commandes utiles :" +echo "" +echo " 🔍 Vérifier l'état : bash $SCRIPTS_DIR/install_cron.sh --status" +echo " 💾 Backup manuel : bash $SCRIPTS_DIR/gitea-backup.sh" +echo " 🔄 Restaurer : bash $SCRIPTS_DIR/restore.sh " +echo " ⬆️ Mise à jour : bash $SCRIPTS_DIR/setup.sh --update" +echo " 🗑️ Désinstaller : bash $SCRIPTS_DIR/install_cron.sh --uninstall" +echo "" +echo "⚙️ Configuration : $SCRIPTS_DIR/gitea-scripts.conf" +echo "" +echo "📊 Logs :" +echo " Backup : ${LOG_DIR:-/var/log}/gitea-backup.log" +echo " Update : ${LOG_DIR:-/var/log}/gitea-update.log" +echo " Cron backup : $HOME/gitea-backup-cron.log" +echo " Cron update : $HOME/gitea-update-cron.log" +echo "" +echo "🕐 Tâches cron :" +echo " Backup : $(grep "$SCRIPTS_DIR/gitea-backup.sh" <(crontab -l 2>/dev/null) | awk '{print $1" "$2" "$3" "$4" "$5}')" +echo " Update : $(grep "$SCRIPTS_DIR/gitea-auto-update.sh" <(crontab -l 2>/dev/null) | awk '{print $1" "$2" "$3" "$4" "$5}')" +echo "" + +# Proposer un premier backup +if [[ "$FORCE" != "true" ]]; then + read -p "💾 Lancer un premier backup maintenant ? [O/n] " -r REPLY + if [[ "$REPLY" =~ ^[OoYy]?$ ]]; then + echo "" + bash "$SCRIPTS_DIR/gitea-backup.sh" + fi +fi + +echo "" +echo "🎉 Prêt ! Les scripts tourneront automatiquement selon le planning cron." +echo "" + +exit 0