Gitea/backup_gitea.sh

981 lines
44 KiB
Bash

#!/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"
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 "$@"