From b0259d2cf2cb81b203a6fe33bd6725002313cf34 Mon Sep 17 00:00:00 2001 From: Johnny Date: Fri, 27 Feb 2026 10:58:51 +0000 Subject: [PATCH] Ajouter backup_gitea.sh --- backup_gitea.sh | 981 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 981 insertions(+) create mode 100644 backup_gitea.sh diff --git a/backup_gitea.sh b/backup_gitea.sh new file mode 100644 index 0000000..fec9d43 --- /dev/null +++ b/backup_gitea.sh @@ -0,0 +1,981 @@ +#!/usr/bin/env bash +# ============================================================================= +# gitea-backup.sh — Sauvegarde & Restauration complète de Gitea +# Version : 1.0.0 +# Usage : sudo ./gitea-backup.sh [backup|restore|list|cron|help] +# ============================================================================= + +set -euo pipefail +IFS=$'\n\t' + +# ── Couleurs ────────────────────────────────────────────────────────────────── +RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m' +BLUE='\033[0;34m'; CYAN='\033[0;36m'; BOLD='\033[1m'; RESET='\033[0m' + +# ── Répertoire du script ────────────────────────────────────────────────────── +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +ENV_FILE="${SCRIPT_DIR}/.env.gitea" +LOG_FILE="/var/log/gitea-backup.log" +TIMESTAMP="$(date +%Y%m%d_%H%M%S)" + +# ============================================================================= +# FONCTIONS UTILITAIRES +# ============================================================================= + +log() { + local level="$1"; shift + local msg="$*" + local ts; ts="$(date '+%Y-%m-%d %H:%M:%S')" + echo -e "${ts} [${level}] ${msg}" >> "${LOG_FILE}" 2>/dev/null || true + case "${level}" in + INFO) echo -e "${GREEN}[INFO]${RESET} ${msg}" ;; + WARN) echo -e "${YELLOW}[WARN]${RESET} ${msg}" ;; + ERROR) echo -e "${RED}[ERROR]${RESET} ${msg}" ;; + STEP) echo -e "${CYAN}[STEP]${RESET} ${BOLD}${msg}${RESET}" ;; + SUCCESS) echo -e "${GREEN}[OK]${RESET} ${msg}" ;; + esac +} + +die() { + log ERROR "$*" + exit 1 +} + +require_root() { + [[ "${EUID}" -eq 0 ]] || die "Ce script doit etre execute en tant que root (sudo)." +} + +require_command() { + command -v "$1" &>/dev/null || die "Commande requise introuvable : $1" +} + +confirm() { + local prompt="${1:-Continuer ?} [o/N] " + read -rp "$(echo -e "${YELLOW}${prompt}${RESET}")" answer + [[ "${answer,,}" =~ ^(o|oui|y|yes)$ ]] +} + +spinner() { + local pid=$1; local msg="${2:-Traitement en cours...}" + local spin='⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏' + local i=0 + while kill -0 "${pid}" 2>/dev/null; do + printf "\r${CYAN}%s${RESET} %s" "${spin:$((i % ${#spin})):1}" "${msg}" + i=$(( i + 1 )); sleep 0.1 + done + printf "\r%-60s\r" " " +} + +hr() { echo -e "${BLUE}$(printf '─%.0s' {1..70})${RESET}"; } + +# ============================================================================= +# CHARGEMENT DE LA CONFIGURATION +# ============================================================================= + +load_env() { + [[ -f "${ENV_FILE}" ]] || die "Fichier .env.gitea introuvable : ${ENV_FILE}" + + # Valeurs par défaut + GITEA_USER="git" + GITEA_ROOT="/opt/gitea" + GITEA_WORK_DIR="/var/lib/gitea" + GITEA_CONF="/etc/gitea/app.ini" + GITEA_BINARY="/usr/local/bin/gitea" + BACKUP_DIR="/opt/Backups/gitea" + KEEP_BACKUPS=7 + INCLUDE_LOG="false" + USE_NATIVE_DUMP="true" + DB_TYPE="" + DB_HOST="localhost" + DB_PORT="" + DB_NAME="" + DB_USER="" + DB_PASS="" + + # Chargement sécurisé + while IFS='=' read -r key value; do + [[ "${key}" =~ ^[[:space:]]*# ]] && continue + [[ -z "${key// /}" ]] && continue + key="${key// /}" + value="${value%%#*}" + value="${value%"${value##*[![:space:]]}"}" + value="${value#\"}" ; value="${value%\"}" + value="${value#\'}" ; value="${value%\'}" + export "${key}=${value}" 2>/dev/null || true + done < "${ENV_FILE}" + + # Vérifications + [[ -f "${GITEA_CONF}" ]] || die "app.ini introuvable : ${GITEA_CONF}" + + mkdir -p "${BACKUP_DIR}" || die "Impossible de creer BACKUP_DIR : ${BACKUP_DIR}" + touch "${LOG_FILE}" 2>/dev/null || LOG_FILE="/tmp/gitea-backup.log" +} + +# ============================================================================= +# DÉTECTION AUTOMATIQUE DEPUIS app.ini +# ============================================================================= + +detect_gitea_config() { + log STEP "Lecture de la configuration Gitea (${GITEA_CONF})" + + _ini_get() { + local section="$1" key="$2" + awk -F '=' \ + -v sec="[${section}]" -v k="${key}" \ + 'in_section && /^\[/ { in_section=0 } + /^\[/ && $0==sec { in_section=1; next } + in_section && /^[[:space:]]*'"${key}"'[[:space:]]*=/ { + sub(/^[^=]+=/, ""); gsub(/^[[:space:]]+|[[:space:]]+$/, ""); print; exit + }' "${GITEA_CONF}" + } + + # Chemins de données + GITEA_REPO_ROOT="${GITEA_WORK_DIR}/repositories" + local repo_path; repo_path=$(_ini_get "repository" "ROOT") + [[ -n "${repo_path}" ]] && GITEA_REPO_ROOT="${repo_path}" + + GITEA_DATA_DIR="${GITEA_WORK_DIR}/data" + local data_path; data_path=$(_ini_get "server" "APP_DATA_PATH") + [[ -n "${data_path}" ]] && GITEA_DATA_DIR="${data_path}" + + GITEA_LOG_DIR="${GITEA_WORK_DIR}/log" + local log_path; log_path=$(_ini_get "log" "ROOT_PATH") + [[ -n "${log_path}" ]] && GITEA_LOG_DIR="${log_path}" + + GITEA_AVATAR_DIR="${GITEA_DATA_DIR}/avatars" + GITEA_ATTACH_DIR="${GITEA_DATA_DIR}/attachments" + GITEA_LFS_DIR="${GITEA_DATA_DIR}/lfs" + + # Base de données (si non forcée dans .env) + if [[ -z "${DB_TYPE}" ]]; then + DB_TYPE=$(_ini_get "database" "DB_TYPE") + fi + [[ -z "${DB_NAME}" ]] && DB_NAME=$(_ini_get "database" "NAME") + [[ -z "${DB_HOST}" ]] && DB_HOST=$(_ini_get "database" "HOST") + [[ -z "${DB_USER}" ]] && DB_USER=$(_ini_get "database" "USER") + [[ -z "${DB_PASS}" ]] && DB_PASS=$(_ini_get "database" "PASSWD") + + # Séparer host:port si fourni ensemble + if [[ "${DB_HOST}" == *:* ]]; then + DB_PORT="${DB_HOST##*:}" + DB_HOST="${DB_HOST%%:*}" + fi + + [[ -n "${DB_TYPE}" ]] || die "Impossible de detecter DB_TYPE depuis app.ini" + [[ -n "${DB_NAME}" ]] || die "Impossible de detecter le nom de la base de donnees" + + # Normalisation du type DB + case "${DB_TYPE,,}" in + postgres|postgresql) DB_TYPE="postgresql" ;; + mysql|mysql2) DB_TYPE="mysql" ;; + sqlite3|sqlite) DB_TYPE="sqlite3" ;; + mssql) DB_TYPE="mssql" ;; + esac + + log INFO "DB type : ${DB_TYPE}" + log INFO "DB name : ${DB_NAME}" + log INFO "Repos : ${GITEA_REPO_ROOT}" + log INFO "Data : ${GITEA_DATA_DIR}" +} + +# ============================================================================= +# SAUVEGARDE DB +# ============================================================================= + +backup_db_postgresql() { + log STEP "Dump PostgreSQL → ${DB_NAME}" + local dump_file="${TMP_DIR}/database.sql" + local pg_opts=(-U "${DB_USER}" -h "${DB_HOST}") + [[ -n "${DB_PORT}" ]] && pg_opts+=(-p "${DB_PORT}") + + if [[ -n "${DB_PASS}" ]]; then + PGPASSWORD="${DB_PASS}" pg_dump "${pg_opts[@]}" \ + --no-owner --no-acl --format=custom \ + -f "${dump_file}" "${DB_NAME}" & + else + pg_dump "${pg_opts[@]}" --no-owner --no-acl --format=custom \ + -f "${dump_file}" "${DB_NAME}" & + fi + spinner $! "Dump PostgreSQL en cours..." + wait $! + log SUCCESS "Dump PostgreSQL termine" +} + +backup_db_mysql() { + log STEP "Dump MySQL/MariaDB → ${DB_NAME}" + local dump_file="${TMP_DIR}/database.sql" + local my_opts=(--single-transaction --routines --triggers --events) + [[ -n "${DB_HOST}" ]] && my_opts+=(-h "${DB_HOST}") + [[ -n "${DB_PORT}" ]] && my_opts+=(-P "${DB_PORT}") + [[ -n "${DB_USER}" ]] && my_opts+=(-u "${DB_USER}") + + if [[ -n "${DB_PASS}" ]]; then + MYSQL_PWD="${DB_PASS}" mysqldump "${my_opts[@]}" "${DB_NAME}" > "${dump_file}" & + else + mysqldump "${my_opts[@]}" "${DB_NAME}" > "${dump_file}" & + fi + spinner $! "Dump MySQL en cours..." + wait $! + log SUCCESS "Dump MySQL termine" +} + +backup_db_sqlite3() { + log STEP "Backup SQLite3 → ${DB_NAME}" + local dump_file="${TMP_DIR}/database.sql" + local sqlite_path="${DB_NAME}" + [[ "${sqlite_path}" != /* ]] && sqlite_path="${GITEA_WORK_DIR}/${sqlite_path}" + [[ -f "${sqlite_path}" ]] || die "Fichier SQLite introuvable : ${sqlite_path}" + # Copie à chaud + dump SQL pour double sécurité + cp "${sqlite_path}" "${TMP_DIR}/database.db" + sqlite3 "${sqlite_path}" .dump > "${dump_file}" & + spinner $! "Dump SQLite3 en cours..." + wait $! + log SUCCESS "Dump SQLite3 termine" +} + +# ============================================================================= +# RESTAURATION DB +# ============================================================================= + +restore_db_postgresql() { + log STEP "Restauration PostgreSQL → ${DB_NAME}" + local dump_file="${TMP_DIR}/database.sql" + local pg_opts=(-U "${DB_USER}" -h "${DB_HOST}") + [[ -n "${DB_PORT}" ]] && pg_opts+=(-p "${DB_PORT}") + + if [[ -n "${DB_PASS}" ]]; then + PGPASSWORD="${DB_PASS}" psql "${pg_opts[@]}" -d postgres \ + -c "SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname='${DB_NAME}';" \ + 2>/dev/null || true + PGPASSWORD="${DB_PASS}" psql "${pg_opts[@]}" -d postgres \ + -c "DROP DATABASE IF EXISTS \"${DB_NAME}\";" 2>/dev/null || true + PGPASSWORD="${DB_PASS}" psql "${pg_opts[@]}" -d postgres \ + -c "CREATE DATABASE \"${DB_NAME}\" OWNER \"${DB_USER}\";" + PGPASSWORD="${DB_PASS}" pg_restore "${pg_opts[@]}" \ + --no-owner --no-acl -d "${DB_NAME}" "${dump_file}" & + else + psql "${pg_opts[@]}" -d postgres \ + -c "SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname='${DB_NAME}';" \ + 2>/dev/null || true + psql "${pg_opts[@]}" -d postgres \ + -c "DROP DATABASE IF EXISTS \"${DB_NAME}\";" 2>/dev/null || true + psql "${pg_opts[@]}" -d postgres \ + -c "CREATE DATABASE \"${DB_NAME}\" OWNER \"${DB_USER}\";" + pg_restore "${pg_opts[@]}" --no-owner --no-acl \ + -d "${DB_NAME}" "${dump_file}" & + fi + spinner $! "Restauration PostgreSQL..." + wait $! || log WARN "pg_restore : avertissements ignorés (objets existants)" + log SUCCESS "Restauration PostgreSQL terminee" +} + +restore_db_mysql() { + log STEP "Restauration MySQL → ${DB_NAME}" + local dump_file="${TMP_DIR}/database.sql" + local my_opts=() + [[ -n "${DB_HOST}" ]] && my_opts+=(-h "${DB_HOST}") + [[ -n "${DB_PORT}" ]] && my_opts+=(-P "${DB_PORT}") + [[ -n "${DB_USER}" ]] && my_opts+=(-u "${DB_USER}") + + if [[ -n "${DB_PASS}" ]]; then + MYSQL_PWD="${DB_PASS}" mysql "${my_opts[@]}" \ + -e "DROP DATABASE IF EXISTS \`${DB_NAME}\`; CREATE DATABASE \`${DB_NAME}\` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;" + MYSQL_PWD="${DB_PASS}" mysql "${my_opts[@]}" "${DB_NAME}" < "${dump_file}" & + else + mysql "${my_opts[@]}" \ + -e "DROP DATABASE IF EXISTS \`${DB_NAME}\`; CREATE DATABASE \`${DB_NAME}\` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;" + mysql "${my_opts[@]}" "${DB_NAME}" < "${dump_file}" & + fi + spinner $! "Restauration MySQL..." + wait $! + log SUCCESS "Restauration MySQL terminee" +} + +restore_db_sqlite3() { + log STEP "Restauration SQLite3 → ${DB_NAME}" + local sqlite_path="${DB_NAME}" + [[ "${sqlite_path}" != /* ]] && sqlite_path="${GITEA_WORK_DIR}/${sqlite_path}" + + if [[ -f "${TMP_DIR}/database.db" ]]; then + # Restauration depuis la copie binaire (plus fiable) + [[ -f "${sqlite_path}" ]] && cp "${sqlite_path}" "${sqlite_path}.pre_restore" + cp "${TMP_DIR}/database.db" "${sqlite_path}" & + spinner $! "Restauration SQLite3 (binaire)..." + wait $! + elif [[ -f "${TMP_DIR}/database.sql" ]]; then + [[ -f "${sqlite_path}" ]] && cp "${sqlite_path}" "${sqlite_path}.pre_restore" + sqlite3 "${sqlite_path}" < "${TMP_DIR}/database.sql" & + spinner $! "Restauration SQLite3 (SQL dump)..." + wait $! + else + die "Aucun fichier de base de donnees SQLite trouve dans l'archive" + fi + log SUCCESS "Restauration SQLite3 terminee" +} + +# ============================================================================= +# SAUVEGARDE PRINCIPALE +# ============================================================================= + +do_backup() { + hr + echo -e "${BOLD}${CYAN}" + echo " ██████╗ ██╗████████╗███████╗ █████╗ " + echo " ██╔════╝ ██║╚══██╔══╝██╔════╝██╔══██╗" + echo " ██║ ███╗██║ ██║ █████╗ ███████║" + echo " ██║ ██║██║ ██║ ██╔══╝ ██╔══██║" + echo " ╚██████╔╝██║ ██║ ███████╗██║ ██║" + echo " ╚═════╝ ╚═╝ ╚═╝ ╚══════╝╚═╝ ╚═╝" + echo -e "${RESET}${BOLD} Sauvegarde Gitea${RESET}" + hr + log INFO "Demarrage de la sauvegarde Gitea — ${TIMESTAMP}" + log INFO "Config : ${GITEA_CONF}" + log INFO "Destination : ${BACKUP_DIR}" + + detect_gitea_config + + TMP_DIR="$(mktemp -d /tmp/gitea_backup_XXXXXX)" + trap 'rm -rf "${TMP_DIR}"' EXIT + + ARCHIVE_NAME="gitea_backup_${TIMESTAMP}.tar.gz" + ARCHIVE_PATH="${BACKUP_DIR}/${ARCHIVE_NAME}" + + # ── Méthode 1 : gitea dump natif (si disponible et activé) ──────────────── + if [[ "${USE_NATIVE_DUMP}" == "true" ]] && [[ -x "${GITEA_BINARY}" ]]; then + log STEP "Utilisation du dump natif Gitea (${GITEA_BINARY})" + local native_out="${TMP_DIR}/native_dump" + mkdir -p "${native_out}" + + sudo -u "${GITEA_USER}" \ + GITEA_WORK_DIR="${GITEA_WORK_DIR}" \ + "${GITEA_BINARY}" dump \ + --config "${GITEA_CONF}" \ + --file "${native_out}/gitea_native_dump.zip" \ + --type zip \ + --skip-lfs-data=false \ + 2>&1 | while read -r line; do log INFO " [gitea dump] ${line}"; done || { + log WARN "gitea dump a echoue, basculement sur la methode manuelle" + USE_NATIVE_DUMP="failed" + } + + if [[ "${USE_NATIVE_DUMP}" != "failed" ]] && \ + [[ -f "${native_out}/gitea_native_dump.zip" ]]; then + log SUCCESS "Dump natif Gitea termine" + # On continue quand même avec la sauvegarde manuelle pour le manifest + fi + fi + + # ── Dump de la base de données ───────────────────────────────────────────── + log STEP "Sauvegarde de la base de donnees (${DB_TYPE})" + mkdir -p "${TMP_DIR}/db" + local TMP_DIR_DB="${TMP_DIR}/db" + local TMP_DIR_SAVE="${TMP_DIR}" + TMP_DIR="${TMP_DIR_DB}" + case "${DB_TYPE}" in + postgresql) backup_db_postgresql ;; + mysql) backup_db_mysql ;; + sqlite3) backup_db_sqlite3 ;; + *) log WARN "Type DB '${DB_TYPE}' non supporte pour le dump manuel" ;; + esac + TMP_DIR="${TMP_DIR_SAVE}" + # Déplacer les dumps dans TMP_DIR + mv "${TMP_DIR_DB}"/* "${TMP_DIR}/" 2>/dev/null || true + + # ── Manifest ────────────────────────────────────────────────────────────── + log STEP "Generation du manifest" + { + echo "=== GITEA BACKUP MANIFEST ===" + echo "Timestamp : ${TIMESTAMP}" + echo "Hostname : $(hostname -f)" + echo "GITEA_CONF : ${GITEA_CONF}" + echo "GITEA_WORK_DIR : ${GITEA_WORK_DIR}" + echo "GITEA_REPO_ROOT: ${GITEA_REPO_ROOT}" + echo "DB_TYPE : ${DB_TYPE}" + echo "DB_NAME : ${DB_NAME}" + echo "DB_HOST : ${DB_HOST}" + echo "" + echo "=== VERSION GITEA ===" + if [[ -x "${GITEA_BINARY}" ]]; then + "${GITEA_BINARY}" --version 2>/dev/null || echo "N/A" + else + echo "Binaire non trouve : ${GITEA_BINARY}" + fi + echo "" + echo "=== STATISTIQUES DEPOTS ===" + if [[ -d "${GITEA_REPO_ROOT}" ]]; then + local repo_count; repo_count=$(find "${GITEA_REPO_ROOT}" -maxdepth 2 \ + -name "*.git" -o -name "HEAD" 2>/dev/null | \ + grep -c "HEAD" || echo "0") + echo "Nombre de depots : ${repo_count}" + echo "Taille totale : $(du -sh "${GITEA_REPO_ROOT}" 2>/dev/null | cut -f1)" + fi + echo "" + echo "=== LFS ===" + if [[ -d "${GITEA_LFS_DIR}" ]]; then + echo "Taille LFS : $(du -sh "${GITEA_LFS_DIR}" 2>/dev/null | cut -f1)" + else + echo "LFS non configure ou vide" + fi + echo "" + echo "=== APP.INI (sections principales) ===" + grep -E '^\[|^APP_NAME|^RUN_USER|^DOMAIN|^ROOT_URL|^HTTP_PORT|^DB_TYPE|^HOST|^NAME' \ + "${GITEA_CONF}" 2>/dev/null | head -40 || echo "N/A" + } > "${TMP_DIR}/manifest.txt" + log SUCCESS "Manifest genere" + + # ── Copie de la configuration ────────────────────────────────────────────── + log STEP "Sauvegarde de la configuration (app.ini)" + mkdir -p "${TMP_DIR}/config" + cp "${GITEA_CONF}" "${TMP_DIR}/config/app.ini" + [[ -f "${ENV_FILE}" ]] && cp "${ENV_FILE}" "${TMP_DIR}/config/.env.gitea.backup" + # Clés SSH Gitea + local gitea_home; gitea_home=$(eval echo "~${GITEA_USER}") + if [[ -d "${gitea_home}/.ssh" ]]; then + cp -r "${gitea_home}/.ssh" "${TMP_DIR}/config/ssh_keys" 2>/dev/null || true + log INFO " Cles SSH incluses" + fi + log SUCCESS "Configuration sauvegardee" + + # ── Archive des dépôts ──────────────────────────────────────────────────── + if [[ -d "${GITEA_REPO_ROOT}" ]]; then + log STEP "Archivage des depots Git (${GITEA_REPO_ROOT})" + ( + tar -czf "${TMP_DIR}/repositories.tar.gz" \ + -C "$(dirname "${GITEA_REPO_ROOT}")" \ + "$(basename "${GITEA_REPO_ROOT}")" 2>/dev/null + ) & + spinner $! "Archivage des depots..." + wait $! + log SUCCESS "Depots archives" + else + log WARN "Dossier repositories introuvable : ${GITEA_REPO_ROOT}" + fi + + # ── Archive des données (avatars, attachments, etc.) ───────────────────── + if [[ -d "${GITEA_DATA_DIR}" ]]; then + log STEP "Archivage des donnees (avatars, attachments...)" + local tar_data_excludes=() + [[ "${INCLUDE_LOG}" != "true" ]] && tar_data_excludes+=("--exclude=${GITEA_DATA_DIR}/log") + ( + tar "${tar_data_excludes[@]+"${tar_data_excludes[@]}"}" \ + -czf "${TMP_DIR}/data.tar.gz" \ + -C "$(dirname "${GITEA_DATA_DIR}")" \ + "$(basename "${GITEA_DATA_DIR}")" 2>/dev/null + ) & + spinner $! "Archivage des donnees..." + wait $! + log SUCCESS "Donnees archivees" + fi + + # ── Logs (optionnel) ────────────────────────────────────────────────────── + if [[ "${INCLUDE_LOG}" == "true" ]] && [[ -d "${GITEA_LOG_DIR}" ]]; then + log STEP "Archivage des logs" + (tar -czf "${TMP_DIR}/logs.tar.gz" \ + -C "$(dirname "${GITEA_LOG_DIR}")" \ + "$(basename "${GITEA_LOG_DIR}")" 2>/dev/null) & + spinner $! "Archivage des logs..." + wait $! + log SUCCESS "Logs archives" + fi + + # ── Assemblage de l'archive finale ──────────────────────────────────────── + log STEP "Assemblage de l'archive finale" + local files_to_pack=() + [[ -f "${TMP_DIR}/database.sql" ]] && files_to_pack+=("database.sql") + [[ -f "${TMP_DIR}/database.db" ]] && files_to_pack+=("database.db") + files_to_pack+=("manifest.txt" "config") + [[ -f "${TMP_DIR}/repositories.tar.gz" ]] && files_to_pack+=("repositories.tar.gz") + [[ -f "${TMP_DIR}/data.tar.gz" ]] && files_to_pack+=("data.tar.gz") + [[ -f "${TMP_DIR}/logs.tar.gz" ]] && files_to_pack+=("logs.tar.gz") + [[ -d "${TMP_DIR}/native_dump" ]] && files_to_pack+=("native_dump") + + (tar -czf "${ARCHIVE_PATH}" -C "${TMP_DIR}" "${files_to_pack[@]}" 2>/dev/null) & + spinner $! "Assemblage final..." + wait $! + + local size; size=$(du -sh "${ARCHIVE_PATH}" | cut -f1) + log SUCCESS "Archive creee : ${ARCHIVE_PATH} (${size})" + + # ── Rotation ────────────────────────────────────────────────────────────── + log STEP "Rotation des archives (conservation : ${KEEP_BACKUPS})" + local count; count=$(find "${BACKUP_DIR}" -maxdepth 1 -name "gitea_backup_*.tar.gz" | wc -l) + if [[ "${count}" -gt "${KEEP_BACKUPS}" ]]; then + local to_delete=$(( count - KEEP_BACKUPS )) + find "${BACKUP_DIR}" -maxdepth 1 -name "gitea_backup_*.tar.gz" \ + -printf '%T+ %p\n' | sort | head -n "${to_delete}" | \ + awk '{print $2}' | while read -r old; do + rm -f "${old}" + log INFO " Supprime : $(basename "${old}")" + done + else + log INFO " Aucune rotation necessaire (${count}/${KEEP_BACKUPS})" + fi + + hr + echo -e "${GREEN}${BOLD} ✔ Sauvegarde Gitea terminee avec succes !${RESET}" + echo -e "${GREEN} Archive : ${ARCHIVE_PATH}${RESET}" + echo -e "${GREEN} Taille : ${size}${RESET}" + hr +} + +# ============================================================================= +# RESTAURATION PRINCIPALE +# ============================================================================= + +do_restore() { + hr + echo -e "${BOLD}${YELLOW}" + echo " ██████╗ ███████╗███████╗████████╗ ██████╗ ██████╗ ███████╗" + echo " ██╔══██╗██╔════╝██╔════╝╚══██╔══╝██╔═══██╗██╔══██╗██╔════╝" + echo " ██████╔╝█████╗ ███████╗ ██║ ██║ ██║██████╔╝█████╗ " + echo " ██╔══██╗██╔══╝ ╚════██║ ██║ ██║ ██║██╔══██╗██╔══╝ " + echo " ██║ ██║███████╗███████║ ██║ ╚██████╔╝██║ ██║███████╗" + echo " ╚═╝ ╚═╝╚══════╝╚══════╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝" + echo -e "${RESET}${BOLD} Restauration Gitea${RESET}" + hr + + # ── Sélection de l'archive ──────────────────────────────────────────────── + local backup_list + mapfile -t backup_list < <(find "${BACKUP_DIR}" -maxdepth 1 \ + -name "gitea_backup_*.tar.gz" | sort -r) + + if [[ ${#backup_list[@]} -eq 0 ]]; then + die "Aucune archive de sauvegarde trouvee dans ${BACKUP_DIR}" + fi + + echo -e "\n${BOLD} Sauvegardes disponibles :${RESET}\n" + local i=1 + for archive in "${backup_list[@]}"; do + local fname; fname=$(basename "${archive}") + local fsize; fsize=$(du -sh "${archive}" | cut -f1) + local fdate; fdate=$(stat -c '%y' "${archive}" | cut -d'.' -f1) + printf " ${CYAN}[%2d]${RESET} %-45s ${YELLOW}%6s${RESET} %s\n" \ + "${i}" "${fname}" "${fsize}" "${fdate}" + i=$(( i + 1 )) + done + echo -e " ${CYAN}[ 0]${RESET} Annuler\n" + hr + + local choice + while true; do + read -rp "$(echo -e "${BOLD} Choisir une archive [0-$((i-1))] : ${RESET}")" choice + [[ "${choice}" =~ ^[0-9]+$ ]] || continue + [[ "${choice}" -eq 0 ]] && { log INFO "Restauration annulee."; return 0; } + [[ "${choice}" -ge 1 && "${choice}" -le ${#backup_list[@]} ]] && break + echo -e "${RED} Choix invalide.${RESET}" + done + + local selected="${backup_list[$((choice-1))]}" + log INFO "Archive selectionnee : $(basename "${selected}")" + + # ── Modes de restauration ───────────────────────────────────────────────── + hr + echo -e "\n${BOLD} Mode de restauration :${RESET}\n" + echo -e " ${CYAN}[1]${RESET} Restauration complete (DB + depots + donnees + config)" + echo -e " ${CYAN}[2]${RESET} Base de donnees uniquement" + echo -e " ${CYAN}[3]${RESET} Depots Git uniquement" + echo -e " ${CYAN}[4]${RESET} Donnees uniquement (avatars, attachments, LFS)" + echo -e " ${CYAN}[5]${RESET} Configuration uniquement (app.ini + cles SSH)" + echo -e " ${CYAN}[0]${RESET} Annuler\n" + hr + + local mode + while true; do + read -rp "$(echo -e "${BOLD} Mode [0-5] : ${RESET}")" mode + [[ "${mode}" =~ ^[0-5]$ ]] && break + echo -e "${RED} Choix invalide.${RESET}" + done + [[ "${mode}" -eq 0 ]] && { log INFO "Restauration annulee."; return 0; } + + # ── Confirmation ────────────────────────────────────────────────────────── + hr + echo -e "\n${RED}${BOLD} ATTENTION : Cette operation va ecraser les donnees existantes !${RESET}" + echo -e " Archive : $(basename "${selected}")" + local mode_label + case "${mode}" in + 1) mode_label="Complete (DB + depots + donnees + config)" ;; + 2) mode_label="Base de donnees uniquement" ;; + 3) mode_label="Depots Git uniquement" ;; + 4) mode_label="Donnees uniquement" ;; + 5) mode_label="Configuration uniquement" ;; + esac + echo -e " Mode : ${BOLD}${mode_label}${RESET}" + echo "" + + confirm " Confirmer la restauration ?" || { log INFO "Restauration annulee."; return 0; } + + detect_gitea_config + + # ── Extraction ──────────────────────────────────────────────────────────── + TMP_DIR="$(mktemp -d /tmp/gitea_restore_XXXXXX)" + trap 'rm -rf "${TMP_DIR}"' EXIT + + log STEP "Extraction de l'archive" + (tar -xzf "${selected}" -C "${TMP_DIR}") & + spinner $! "Extraction en cours..." + wait $! + log SUCCESS "Archive extraite" + + if [[ -f "${TMP_DIR}/manifest.txt" ]]; then + hr + echo -e "${BOLD} Informations de la sauvegarde :${RESET}" + head -20 "${TMP_DIR}/manifest.txt" | sed 's/^/ /' + hr + fi + + # ── Arrêt de Gitea avant restauration ───────────────────────────────────── + if [[ "${mode}" -ne 5 ]]; then + log STEP "Arret du service Gitea" + if systemctl is-active --quiet gitea 2>/dev/null; then + systemctl stop gitea + log SUCCESS "Service Gitea arrete" + # On redémarrera à la fin + GITEA_WAS_RUNNING=true + else + log WARN "Service Gitea non actif (ou non gere par systemd)" + GITEA_WAS_RUNNING=false + fi + fi + + # ── Restauration DB ─────────────────────────────────────────────────────── + if [[ "${mode}" -eq 1 || "${mode}" -eq 2 ]]; then + [[ -f "${TMP_DIR}/database.sql" ]] || [[ -f "${TMP_DIR}/database.db" ]] || \ + die "Aucun dump de base de donnees dans l'archive" + case "${DB_TYPE}" in + postgresql) restore_db_postgresql ;; + mysql) restore_db_mysql ;; + sqlite3) restore_db_sqlite3 ;; + *) die "Type DB non supporte : ${DB_TYPE}" ;; + esac + fi + + # ── Restauration dépôts ─────────────────────────────────────────────────── + if [[ "${mode}" -eq 1 || "${mode}" -eq 3 ]]; then + if [[ -f "${TMP_DIR}/repositories.tar.gz" ]]; then + log STEP "Restauration des depots Git" + local repo_parent; repo_parent="$(dirname "${GITEA_REPO_ROOT}")" + if [[ -d "${GITEA_REPO_ROOT}" ]]; then + mv "${GITEA_REPO_ROOT}" "${GITEA_REPO_ROOT}.pre_restore_${TIMESTAMP}" + log INFO " Anciens depots sauvegardes : $(basename "${GITEA_REPO_ROOT}").pre_restore_${TIMESTAMP}" + fi + (tar -xzf "${TMP_DIR}/repositories.tar.gz" -C "${repo_parent}") & + spinner $! "Restauration des depots..." + wait $! + log SUCCESS "Depots restaures dans ${GITEA_REPO_ROOT}" + else + log WARN "Aucune archive de depots trouvee dans la sauvegarde" + fi + fi + + # ── Restauration données ────────────────────────────────────────────────── + if [[ "${mode}" -eq 1 || "${mode}" -eq 4 ]]; then + if [[ -f "${TMP_DIR}/data.tar.gz" ]]; then + log STEP "Restauration des donnees (avatars, attachments, LFS...)" + local data_parent; data_parent="$(dirname "${GITEA_DATA_DIR}")" + if [[ -d "${GITEA_DATA_DIR}" ]]; then + mv "${GITEA_DATA_DIR}" "${GITEA_DATA_DIR}.pre_restore_${TIMESTAMP}" + log INFO " Anciennes donnees sauvegardees" + fi + (tar -xzf "${TMP_DIR}/data.tar.gz" -C "${data_parent}") & + spinner $! "Restauration des donnees..." + wait $! + log SUCCESS "Donnees restaurees dans ${GITEA_DATA_DIR}" + else + log WARN "Aucune archive de donnees trouvee dans la sauvegarde" + fi + fi + + # ── Restauration configuration ──────────────────────────────────────────── + if [[ "${mode}" -eq 1 || "${mode}" -eq 5 ]]; then + if [[ -f "${TMP_DIR}/config/app.ini" ]]; then + log STEP "Restauration de la configuration" + cp "${GITEA_CONF}" "${GITEA_CONF}.pre_restore_${TIMESTAMP}" 2>/dev/null || true + cp "${TMP_DIR}/config/app.ini" "${GITEA_CONF}" + log SUCCESS "app.ini restaure" + fi + if [[ -d "${TMP_DIR}/config/ssh_keys" ]]; then + local gitea_home; gitea_home=$(eval echo "~${GITEA_USER}") + cp -r "${TMP_DIR}/config/ssh_keys/." "${gitea_home}/.ssh/" 2>/dev/null || true + log SUCCESS "Cles SSH restaurees" + fi + fi + + # ── Ajustement des permissions ──────────────────────────────────────────── + log STEP "Ajustement des permissions pour '${GITEA_USER}'" + [[ -d "${GITEA_REPO_ROOT}" ]] && \ + chown -R "${GITEA_USER}:${GITEA_USER}" "${GITEA_REPO_ROOT}" 2>/dev/null || true + [[ -d "${GITEA_DATA_DIR}" ]] && \ + chown -R "${GITEA_USER}:${GITEA_USER}" "${GITEA_DATA_DIR}" 2>/dev/null || true + local gitea_home; gitea_home=$(eval echo "~${GITEA_USER}") + [[ -d "${gitea_home}/.ssh" ]] && \ + chmod 700 "${gitea_home}/.ssh" && \ + chmod 600 "${gitea_home}/.ssh/"* 2>/dev/null || true + log SUCCESS "Permissions ajustees" + + # ── Redémarrage de Gitea ────────────────────────────────────────────────── + if [[ "${GITEA_WAS_RUNNING:-false}" == "true" ]]; then + log STEP "Redemarrage du service Gitea" + systemctl start gitea && log SUCCESS "Service Gitea redemarre" || \ + log WARN "Echec du redemarrage — relancer manuellement : systemctl start gitea" + fi + + hr + echo -e "${GREEN}${BOLD} ✔ Restauration Gitea terminee avec succes !${RESET}" + hr +} + +# ============================================================================= +# LISTING DES SAUVEGARDES +# ============================================================================= + +do_list() { + hr + echo -e "${BOLD} Sauvegardes disponibles dans : ${BACKUP_DIR}${RESET}\n" + local count=0 + while IFS= read -r archive; do + local fname; fname=$(basename "${archive}") + local fsize; fsize=$(du -sh "${archive}" | cut -f1) + local fdate; fdate=$(stat -c '%y' "${archive}" | cut -d'.' -f1) + printf " ${CYAN}%-50s${RESET} ${YELLOW}%6s${RESET} %s\n" "${fname}" "${fsize}" "${fdate}" + count=$(( count + 1 )) + done < <(find "${BACKUP_DIR}" -maxdepth 1 -name "gitea_backup_*.tar.gz" | sort -r) + [[ "${count}" -eq 0 ]] && echo -e " ${YELLOW}Aucune sauvegarde trouvee.${RESET}" + echo "" + echo -e " Total : ${BOLD}${count} archive(s)${RESET}" + hr +} + +# ============================================================================= +# PLANIFICATION CRON +# ============================================================================= + +do_cron() { + local script_path; script_path="$(realpath "${BASH_SOURCE[0]}")" + local cron_tag="# gitea-backup-auto" + local cron_user="root" + + hr + echo -e "${BOLD}${CYAN} Planification des sauvegardes automatiques (Cron)${RESET}" + hr + + echo -e "\n${BOLD} Entrees cron actuelles pour gitea-backup :${RESET}\n" + local existing + existing=$(crontab -u "${cron_user}" -l 2>/dev/null | grep "${cron_tag}" || true) + if [[ -n "${existing}" ]]; then + echo "${existing}" | while read -r line; do + echo -e " ${YELLOW}${line}${RESET}" + done + else + echo -e " ${YELLOW}Aucune planification active.${RESET}" + fi + + echo "" + hr + echo -e "\n${BOLD} Choisir une frequence de sauvegarde :${RESET}\n" + echo -e " ${CYAN}[1]${RESET} Quotidienne — tous les jours a 03h00" + echo -e " ${CYAN}[2]${RESET} Biquotidienne — 2x par jour a 03h00 et 15h00" + echo -e " ${CYAN}[3]${RESET} Hebdomadaire — tous les lundis a 03h00" + echo -e " ${CYAN}[4]${RESET} Mensuelle — le 1er du mois a 03h00" + echo -e " ${CYAN}[5]${RESET} Personnalisee — saisir une expression cron manuellement" + echo -e " ${CYAN}[6]${RESET} Supprimer — retirer toutes les planifications" + echo -e " ${CYAN}[0]${RESET} Retour au menu principal" + echo "" + hr + + local cron_choice + while true; do + read -rp "$(echo -e "${BOLD} Votre choix [0-6] : ${RESET}")" cron_choice + [[ "${cron_choice}" =~ ^[0-6]$ ]] && break + echo -e "${RED} Choix invalide.${RESET}" + done + [[ "${cron_choice}" -eq 0 ]] && return 0 + + if [[ "${cron_choice}" -eq 6 ]]; then + local current_cron + current_cron=$(crontab -u "${cron_user}" -l 2>/dev/null \ + | grep -v "${cron_tag}" || true) + if [[ -n "${current_cron}" ]]; then + echo "${current_cron}" | crontab -u "${cron_user}" - + else + crontab -u "${cron_user}" - <<< "" + fi + log SUCCESS "Planifications gitea-backup supprimees" + return 0 + fi + + local cron_expr="" + case "${cron_choice}" in + 1) cron_expr="0 3 * * *" ;; + 2) cron_expr="0 3,15 * * *" ;; + 3) cron_expr="0 3 * * 1" ;; + 4) cron_expr="0 3 1 * *" ;; + 5) + echo -e "\n ${BOLD}Format :${RESET} minute heure jour_mois mois jour_semaine" + echo -e " ${YELLOW}Exemples :${RESET}" + echo -e " 0 3 * * * -> tous les jours a 03h00" + echo -e " 30 2 * * 0 -> tous les dimanches a 02h30" + echo -e " 0 4 */2 * * -> tous les 2 jours a 04h00" + echo "" + while true; do + read -rp "$(echo -e "${BOLD} Expression cron : ${RESET}")" cron_expr + local field_count; field_count=$(echo "${cron_expr}" | wc -w) + [[ "${field_count}" -eq 5 ]] && break + echo -e "${RED} Expression invalide (5 champs requis).${RESET}" + done + ;; + esac + + local mail_opt='MAILTO=""' + echo "" + read -rp "$(echo -e "${YELLOW} Envoyer les logs cron par email ? (laisser vide pour ignorer) : ${RESET}")" mail_addr + [[ -n "${mail_addr}" ]] && mail_opt="MAILTO=${mail_addr}" + + local cron_line="${cron_expr} ${script_path} backup ${cron_tag}" + echo "" + hr + echo -e " ${BOLD}Entree cron qui sera ajoutee :${RESET}\n" + [[ "${mail_opt}" != 'MAILTO=""' ]] && echo -e " ${YELLOW}${mail_opt}${RESET}" + echo -e " ${YELLOW}${cron_line}${RESET}" + echo "" + + confirm " Confirmer l'installation de cette tache cron ?" || { + log INFO "Planification annulee." + return 0 + } + + local clean_cron + clean_cron=$(crontab -u "${cron_user}" -l 2>/dev/null \ + | grep -v "${cron_tag}" || true) + + { + [[ -n "${clean_cron}" ]] && echo "${clean_cron}" + echo "${mail_opt}" + echo "${cron_line}" + } | crontab -u "${cron_user}" - + + log SUCCESS "Tache cron installee pour root" + + if ! systemctl is-active --quiet cron 2>/dev/null && \ + ! systemctl is-active --quiet crond 2>/dev/null; then + log WARN "Le service cron ne semble pas actif : systemctl start cron" + else + log INFO "Service cron actif" + fi + + hr + echo -e "${GREEN}${BOLD} Planification cron configuree avec succes !${RESET}" + hr +} + +# ============================================================================= +# AIDE +# ============================================================================= + +do_help() { + hr + echo -e "${BOLD} gitea-backup.sh — Outil de sauvegarde/restauration Gitea${RESET}\n" + echo -e " ${BOLD}Usage :${RESET}" + echo -e " sudo $0 backup Creer une sauvegarde complete" + echo -e " sudo $0 restore Menu interactif de restauration" + echo -e " sudo $0 list Lister les sauvegardes disponibles" + echo -e " sudo $0 cron Gerer la planification automatique" + echo -e " sudo $0 help Afficher cette aide\n" + echo -e " ${BOLD}Configuration (.env.gitea) :${RESET}" + echo -e " GITEA_USER Utilisateur systeme qui fait tourner Gitea (defaut: git)" + echo -e " GITEA_ROOT Repertoire d'installation du binaire" + echo -e " GITEA_WORK_DIR Repertoire de travail Gitea (data, repos...)" + echo -e " GITEA_CONF Chemin vers app.ini" + echo -e " GITEA_BINARY Chemin vers le binaire gitea" + echo -e " BACKUP_DIR Dossier de stockage des archives" + echo -e " KEEP_BACKUPS Nombre d'archives a conserver (defaut: 7)" + echo -e " INCLUDE_LOG Inclure les logs dans la sauvegarde (true/false)" + echo -e " USE_NATIVE_DUMP Utiliser 'gitea dump' natif en complement (true/false)" + echo -e " DB_TYPE Forcer le type DB (postgresql|mysql|sqlite3)" + echo -e " DB_HOST/PORT Surcharger host/port de la DB" + echo -e " DB_NAME/USER/PASS Surcharger les credentials DB\n" + echo -e " ${BOLD}Contenu d'une archive :${RESET}" + echo -e " database.sql Dump complet de la base de donnees" + echo -e " manifest.txt Infos systeme, version, stats depots" + echo -e " config/ app.ini + cles SSH" + echo -e " repositories.tar.gz Tous les depots Git" + echo -e " data.tar.gz Avatars, attachments, LFS" + echo -e " native_dump/ Archive gitea dump natif (si active)\n" + echo -e " ${BOLD}Journal :${RESET} ${LOG_FILE}" + hr +} + +# ============================================================================= +# MENU INTERACTIF +# ============================================================================= + +interactive_menu() { + while true; do + clear + hr + echo -e "${BOLD}${CYAN}" + echo " ██████╗ ██╗████████╗███████╗ █████╗ ██████╗ █████╗ ██████╗██╗ ██╗██╗ ██╗██████╗" + echo " ██╔════╝ ██║╚══██╔══╝██╔════╝██╔══██╗ ██╔══██╗██╔══██╗██╔════╝██║ ██╔╝██║ ██║██╔══██╗" + echo " ██║ ███╗██║ ██║ █████╗ ███████║ ██████╔╝███████║██║ █████╔╝ ██║ ██║██████╔╝" + echo " ██║ ██║██║ ██║ ██╔══╝ ██╔══██║ ██╔══██╗██╔══██║██║ ██╔═██╗ ██║ ██║██╔═══╝" + echo " ╚██████╔╝██║ ██║ ███████╗██║ ██║ ██████╔╝██║ ██║╚██████╗██║ ██╗╚██████╔╝██║" + echo " ╚═════╝ ╚═╝ ╚═╝ ╚══════╝╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝ ╚═════╝╚═╝ ╚═╝ ╚═════╝ ╚═╝" + echo -e "${RESET}${BOLD} Gestionnaire de Sauvegarde Gitea${RESET}" + hr + # Infos live + local gitea_status="inconnu" + systemctl is-active --quiet gitea 2>/dev/null && gitea_status="${GREEN}actif${RESET}" \ + || gitea_status="${RED}inactif${RESET}" + local backup_count; backup_count=$(find "${BACKUP_DIR}" -maxdepth 1 \ + -name "gitea_backup_*.tar.gz" 2>/dev/null | wc -l) + echo -e " ${BOLD}Config :${RESET} ${GITEA_CONF}" + echo -e " ${BOLD}Service Gitea :${RESET} $(echo -e "${gitea_status}") ${BOLD}Archives :${RESET} ${backup_count} disponible(s)" + hr + echo "" + echo -e " ${CYAN}[1]${RESET} ${BOLD}Creer une sauvegarde${RESET} - Sauvegarde complete (DB + depots + donnees + config)" + echo -e " ${CYAN}[2]${RESET} ${BOLD}Restaurer${RESET} - Menu interactif de restauration" + echo -e " ${CYAN}[3]${RESET} ${BOLD}Lister les sauvegardes${RESET} - Voir les archives disponibles" + echo -e " ${CYAN}[4]${RESET} ${BOLD}Planification Cron${RESET} - Gerer les sauvegardes automatiques" + echo -e " ${CYAN}[5]${RESET} ${BOLD}Afficher l'aide${RESET} - Documentation complete" + echo -e " ${CYAN}[0]${RESET} ${BOLD}Quitter${RESET}" + echo "" + hr + read -rp "$(echo -e "${BOLD} Votre choix : ${RESET}")" choice + echo "" + case "${choice}" in + 1) do_backup ;; + 2) do_restore ;; + 3) do_list ;; + 4) do_cron ;; + 5) do_help ;; + 0) echo -e "${GREEN} Au revoir !${RESET}\n"; exit 0 ;; + *) echo -e "${RED} Choix invalide.${RESET}" ; sleep 1; continue ;; + esac + echo "" + echo -e "${BLUE} Retour au menu dans 3 secondes...${RESET}" + sleep 3 + done +} + +# ============================================================================= +# POINT D'ENTRÉE +# ============================================================================= + +main() { + require_root + require_command tar + require_command gzip + load_env + + local cmd="${1:-menu}" + case "${cmd}" in + backup) do_backup ;; + restore) do_restore ;; + list) do_list ;; + cron) do_cron ;; + help|-h|--help) do_help ;; + menu|"") interactive_menu ;; + *) echo -e "${RED}Commande inconnue : ${cmd}${RESET}"; do_help; exit 1 ;; + esac +} + +main "$@" \ No newline at end of file