#!/bin/bash # ============================================================================= # Gitea 备份脚本 (套件版 / Docker 版 通用) # ============================================================================= # 用途: 定期备份 Gitea 数据,防再次出现"卸载清空"惨案 # # 备份内容: # - Gitea 配置文件 (app.ini) # - SQLite 数据库 # - 所有仓库 (git bare repos) # - LFS 数据 # - 附件 / avatars # # 用法: # sudo bash scripts/backup-gitea.sh # 默认备份到 /volume1/backups/gitea/ # sudo bash scripts/backup-gitea.sh /path/to/backup # 自定义备份目录 # sudo bash scripts/backup-gitea.sh --keep 30 # 保留 30 天 # sudo bash scripts/backup-gitea.sh --restore latest # 恢复最新备份(谨慎) # # Cron 建议 (每天凌晨 3 点): # 0 3 * * * /volume1/docker/wecom-it-desk/scripts/backup-gitea.sh >> /var/log/gitea-backup.log 2>&1 # ============================================================================= set -e # 颜色 RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' NC='\033[0m' info() { echo -e "${BLUE}[INFO]${NC} $1"; } ok() { echo -e "${GREEN}[OK]${NC} $1"; } warn() { echo -e "${YELLOW}[WARN]${NC} $1"; } error() { echo -e "${RED}[ERROR]${NC} $1"; exit 1; } # 默认参数 BACKUP_DIR="${1:-/volume1/backups/gitea}" KEEP_DAYS=7 RESTORE="" for arg in "$@"; do case $arg in --keep) KEEP_DAYS="$2"; shift 2 ;; --restore) RESTORE="$2"; shift 2 ;; esac done TIMESTAMP=$(date +%Y%m%d-%H%M%S) BACKUP_NAME="gitea-backup-${TIMESTAMP}" BACKUP_PATH="${BACKUP_DIR}/${BACKUP_NAME}" # ============================================================================= # 路径探测 # ============================================================================= detect_gitea_paths() { info "探测 Gitea 数据目录..." # 套件版默认路径 if [ -d "/volume1/@appdata/gitea" ]; then GITEA_HOME="/volume1/@appdata/gitea" info " 套件版: $GITEA_HOME" # Docker 版常见路径 elif [ -d "/volume1/docker/gitea" ]; then GITEA_HOME="/volume1/docker/gitea" info " Docker 版: $GITEA_HOME" else error "未找到 Gitea 数据目录,请手动指定 GITEA_HOME 环境变量" fi # 找配置和数据子目录 for sub in gitea data config repos lfs avatars; do if [ -d "$GITEA_HOME/$sub" ]; then declare -g "GITEA_${sub^^}=$GITEA_HOME/$sub" fi done # 找 SQLite 数据库 for db in "$GITEA_HOME/gitea/data/gitea.db" "$GITEA_HOME/data/gitea.db"; do [ -f "$db" ] && GITEA_DB="$db" && break done if [ -z "$GITEA_DB" ]; then warn "未找到 SQLite 数据库,可能用 MySQL/Postgres(此脚本不备份 DB)" else info " 数据库: $GITEA_DB" fi } # ============================================================================= # 备份模式 # ============================================================================= do_backup() { info "=== Gitea 备份开始 ===" info "时间: $TIMESTAMP" info "目标: $BACKUP_PATH" mkdir -p "$BACKUP_PATH" detect_gitea_paths # 1. 备份 app.ini 配置 if [ -f "$GITEA_HOME/gitea/conf/app.ini" ]; then mkdir -p "$BACKUP_PATH/conf" cp "$GITEA_HOME/gitea/conf/app.ini" "$BACKUP_PATH/conf/" ok "备份配置 app.ini" fi # 2. 备份 SQLite(必须先停 Gitea,或用 .backup 命令) if [ -n "$GITEA_DB" ] && [ -f "$GITEA_DB" ]; then # 用 sqlite3 .backup(支持热备) if command -v sqlite3 &> /dev/null; then sqlite3 "$GITEA_DB" ".backup '$BACKUP_PATH/gitea.db'" ok "SQLite 热备完成 (sqlite3 .backup)" else warn "无 sqlite3 命令,改用文件复制(可能不一致)" cp "$GITEA_DB" "$BACKUP_PATH/gitea.db" fi fi # 3. 备份仓库 (bare git) if [ -d "$GITEA_HOME/gitea/repos" ]; then info "备份仓库目录(可能大)..." tar -czf "$BACKUP_PATH/repos.tar.gz" \ -C "$GITEA_HOME/gitea" repos 2>/dev/null || \ warn "仓库备份失败(可能权限不足,需 sudo)" ok "仓库 tar 完成: $(du -h "$BACKUP_PATH/repos.tar.gz" 2>/dev/null | cut -f1)" fi # 4. 备份 LFS if [ -d "$GITEA_HOME/gitea/lfs" ]; then tar -czf "$BACKUP_PATH/lfs.tar.gz" \ -C "$GITEA_HOME/gitea" lfs 2>/dev/null || warn "LFS 备份失败" fi # 5. 备份附件 / avatars if [ -d "$GITEA_HOME/gitea/avatars" ]; then cp -r "$GITEA_HOME/gitea/avatars" "$BACKUP_PATH/" 2>/dev/null || true fi # 6. 备份元信息 cat > "$BACKUP_PATH/backup.meta" </dev/null && git rev-parse HEAD 2>/dev/null || echo "N/A") EOF # 7. 打包成单文件(方便转存) info "打包最终备份..." tar -czf "${BACKUP_PATH}.tar.gz" -C "$BACKUP_DIR" "$BACKUP_NAME" rm -rf "$BACKUP_PATH" ok "最终备份: ${BACKUP_PATH}.tar.gz" ok " 大小: $(du -h "${BACKUP_PATH}.tar.gz" | cut -f1)" # 8. 清理旧备份 info "清理 $KEEP_DAYS 天前的旧备份..." local cleaned=0 while IFS= read -r old; do rm -f "$old" cleaned=$((cleaned+1)) done < <(find "$BACKUP_DIR" -maxdepth 1 -name "gitea-backup-*.tar.gz" -mtime +$KEEP_DAYS) ok "清理旧备份: $cleaned 个" info "=== 备份完成 ===" info "建议: 把 ${BACKUP_PATH}.tar.gz scp 到异地(例 NAS2 / 阿里云 OSS / 本地电脑)" } # ============================================================================= # 恢复模式 # ============================================================================= do_restore() { if [ -z "$RESTORE" ]; then error "未指定要恢复的备份名(--restore latest|YYYYMMDD-HHMMSS)" fi if [ "$RESTORE" = "latest" ]; then RESTORE_FILE=$(ls -t "$BACKUP_DIR"/gitea-backup-*.tar.gz 2>/dev/null | head -1) [ -z "$RESTORE_FILE" ] && error "找不到备份文件" else RESTORE_FILE="$BACKUP_DIR/gitea-backup-${RESTORE}.tar.gz" [ -f "$RESTORE_FILE" ] || error "备份文件不存在: $RESTORE_FILE" fi warn "!!! 恢复操作会覆盖当前 Gitea 数据 !!!" warn " 备份文件: $RESTORE_FILE" warn " 按 Ctrl+C 取消,或 5 秒后继续" sleep 5 info "解压备份..." TMP_DIR=$(mktemp -d) tar -xzf "$RESTORE_FILE" -C "$TMP_DIR" BACKUP_CONTENT=$(ls "$TMP_DIR" | head -1) [ -z "$BACKUP_CONTENT" ] && error "备份文件内容为空" detect_gitea_paths EXTRACT_DIR="$TMP_DIR/$BACKUP_CONTENT" # 停 Gitea(套件 / Docker) info "停 Gitea..." if command -v synopkg &> /dev/null; then sudo synopkg stop Gitea 2>/dev/null || true fi docker stop gitea 2>/dev/null || true # 恢复 app.ini if [ -f "$EXTRACT_DIR/conf/app.ini" ]; then cp "$EXTRACT_DIR/conf/app.ini" "$GITEA_HOME/gitea/conf/app.ini" ok "恢复 app.ini" fi # 恢复 DB if [ -f "$EXTRACT_DIR/gitea.db" ]; then cp "$EXTRACT_DIR/gitea.db" "$GITEA_DB" ok "恢复 SQLite" fi # 恢复 repos if [ -f "$EXTRACT_DIR/repos.tar.gz" ]; then rm -rf "$GITEA_HOME/gitea/repos" tar -xzf "$EXTRACT_DIR/repos.tar.gz" -C "$GITEA_HOME/gitea/" ok "恢复 repos" fi # 启动 Gitea info "启动 Gitea..." if command -v synopkg &> /dev/null; then sudo synopkg start Gitea fi docker start gitea 2>/dev/null || true rm -rf "$TMP_DIR" ok "恢复完成" } # ============================================================================= # 入口 # ============================================================================= if [ -n "$RESTORE" ]; then do_restore else do_backup fi