#!/usr/bin/env bash set -euo pipefail # ============================================ # Gitea 迁移脚本(旧服务器 → 新服务器) # 用法: # bash migrate.sh export # 旧服务器:导出迁移包 # bash migrate.sh import <迁移包路径> # 新服务器:导入迁移包 # bash migrate.sh verify # 迁移后:验证完整性 # # 迁移流程: # 旧服务器 → export → scp 传输 → 新服务器 → import → verify # ============================================ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" cd "$SCRIPT_DIR" RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' CYAN='\033[0;36m' NC='\033[0m' log() { echo -e "${GREEN}[INFO]${NC} $*"; } warn() { echo -e "${YELLOW}[WARN]${NC} $*"; } error() { echo -e "${RED}[ERROR]${NC} $*" >&2; } step() { echo -e "\n${CYAN}========== $* ==========${NC}"; } confirm() { echo -en "${YELLOW}[确认]${NC} $* [y/N]: " read -r reply [[ "$reply" =~ ^[Yy]$ ]] } # ===== 前置检查 ===== preflight() { if [ "$(id -u)" -ne 0 ]; then error "请使用 root 用户运行: sudo bash migrate.sh" exit 1 fi } load_env() { if [ ! -f .env ]; then error ".env 文件不存在" exit 1 fi set -a source .env set +a } # ============================================================= # 导出模式 — 在旧服务器执行 # ============================================================= do_export() { step "迁移导出 — 旧服务器" load_env GITEA_DATA_DIR="${GITEA_DATA_DIR:-/var/lib/gitea}" MYSQL_DATA_DIR="${MYSQL_DATA_DIR:-/var/lib/mysql/gitea}" BACKUP_DIR="${BACKUP_DIR:-/var/backups/gitea}" DATE=$(date +%Y%m%d_%H%M%S) EXPORT_DIR="${BACKUP_DIR}/migrate_${DATE}" EXPORT_ARCHIVE="${BACKUP_DIR}/gitea_migrate_${DATE}.tar.gz" mkdir -p "$EXPORT_DIR" # --- 1. 显示当前状态 --- step "1/6 当前环境信息" log "域名: ${GITEA_DOMAIN:-未设置}" log "数据目录: ${GITEA_DATA_DIR}" log "MySQL 数据: ${MYSQL_DATA_DIR}" if docker compose ps --status running 2>/dev/null | grep -q gitea; then GITEA_VER=$(curl -sf http://127.0.0.1:3000/api/v1/version 2>/dev/null | grep -oP '"version"\s*:\s*"\K[^"]+' || echo "未知") log "Gitea 版本: ${GITEA_VER}" fi MYSQL_VER=$(docker compose exec -T db mysql --version 2>/dev/null | grep -oP 'Distrib \K[0-9.]+' || echo "未知") log "MySQL 版本: ${MYSQL_VER}" echo "" warn "导出过程中 Gitea 将停止服务,以保证数据一致性。" if ! confirm "是否继续?"; then log "已取消" rm -rf "$EXPORT_DIR" exit 0 fi # --- 2. 停止 Gitea,保留 MySQL 用于导出 --- step "2/6 停止 Gitea 容器" docker compose stop server 2>/dev/null || true log "Gitea 已停止" # --- 3. 导出 MySQL 数据库 --- step "3/6 导出 MySQL 数据库" log "使用 mysqldump 导出(比 gitea dump 的 XORM 更可靠)..." docker compose exec -T db mysqldump \ -u root -p"${DB_ROOT_PASSWORD}" \ --single-transaction \ --routines \ --triggers \ --databases gitea \ > "${EXPORT_DIR}/gitea-db.sql" DB_SIZE=$(du -h "${EXPORT_DIR}/gitea-db.sql" | cut -f1) log "数据库导出完成: gitea-db.sql (${DB_SIZE})" # --- 4. 停止所有容器 --- step "4/6 停止全部容器" docker compose down log "全部容器已停止" # --- 5. 打包 Gitea 数据 --- step "5/6 打包 Gitea 数据目录" if [ -d "$GITEA_DATA_DIR" ]; then log "正在打包 ${GITEA_DATA_DIR} ..." tar czf "${EXPORT_DIR}/gitea-data.tar.gz" \ -C "$(dirname "$GITEA_DATA_DIR")" \ "$(basename "$GITEA_DATA_DIR")" DATA_SIZE=$(du -h "${EXPORT_DIR}/gitea-data.tar.gz" | cut -f1) log "数据打包完成: gitea-data.tar.gz (${DATA_SIZE})" else warn "数据目录 ${GITEA_DATA_DIR} 不存在,跳过" fi # --- 5b. 打包部署配置 --- log "正在打包部署配置..." tar czf "${EXPORT_DIR}/gitea-config.tar.gz" \ -C "$SCRIPT_DIR" \ --exclude='.git' \ --exclude='data' \ --exclude='backups' \ .env docker-compose.yml .env.example \ $([ -d nginx ] && echo "nginx/") \ $([ -f backup.sh ] && echo "backup.sh") \ $([ -f deploy.sh ] && echo "deploy.sh") \ $([ -f upgrade.sh ] && echo "upgrade.sh") \ $([ -f migrate.sh ] && echo "migrate.sh") \ $([ -f .gitignore ] && echo ".gitignore") \ $([ -f README.md ] && echo "README.md") \ 2>/dev/null || true log "配置打包完成: gitea-config.tar.gz" # --- 5c. 记录元信息 --- cat > "${EXPORT_DIR}/migrate-meta.txt" <" exit 1 fi step "迁移导入 — 新服务器" # --- 1. 解压迁移包 --- step "1/9 解压迁移包" WORK_DIR=$(mktemp -d) tar xzf "$archive" -C "$WORK_DIR" MIGRATE_DIR=$(find "$WORK_DIR" -maxdepth 1 -type d -name 'migrate_*' | head -1) if [ -z "$MIGRATE_DIR" ]; then error "无效的迁移包格式" rm -rf "$WORK_DIR" exit 1 fi # 显示元信息 if [ -f "${MIGRATE_DIR}/migrate-meta.txt" ]; then log "迁移包信息:" grep -v '^#' "${MIGRATE_DIR}/migrate-meta.txt" | while IFS='=' read -r key val; do printf " %-20s %s\n" "$key:" "$val" done fi # --- 2. 检查必要文件 --- step "2/9 检查迁移包内容" local has_db=false has_data=false has_config=false [ -f "${MIGRATE_DIR}/gitea-db.sql" ] && has_db=true [ -f "${MIGRATE_DIR}/gitea-data.tar.gz" ] && has_data=true [ -f "${MIGRATE_DIR}/gitea-config.tar.gz" ] && has_config=true log "数据库导出: $(${has_db} && echo '✓' || echo '✗')" log "数据目录: $(${has_data} && echo '✓' || echo '✗')" log "部署配置: $(${has_config} && echo '✓' || echo '✗')" if ! $has_db || ! $has_data; then error "迁移包缺少必要文件(需要 gitea-db.sql 和 gitea-data.tar.gz)" rm -rf "$WORK_DIR" exit 1 fi echo "" warn "导入将覆盖当前服务器上的 Gitea 数据。" if ! confirm "是否继续?"; then log "已取消" rm -rf "$WORK_DIR" exit 0 fi # --- 3. 恢复配置文件 --- step "3/9 恢复部署配置" if $has_config; then tar xzf "${MIGRATE_DIR}/gitea-config.tar.gz" -C "$SCRIPT_DIR" log "配置文件已恢复到 ${SCRIPT_DIR}/" fi # 加载 .env load_env GITEA_DATA_DIR="${GITEA_DATA_DIR:-/var/lib/gitea}" MYSQL_DATA_DIR="${MYSQL_DATA_DIR:-/var/lib/mysql/gitea}" # --- 4. 提示修改配置 --- step "4/9 检查配置" echo "" warn "如果新服务器的域名或 IP 与旧服务器不同,请先修改 .env:" echo -e " ${CYAN}vi ${SCRIPT_DIR}/.env${NC}" echo "" echo " 当前配置:" echo " GITEA_DOMAIN=${GITEA_DOMAIN:-未设置}" echo " GITEA_DATA_DIR=${GITEA_DATA_DIR}" echo " MYSQL_DATA_DIR=${MYSQL_DATA_DIR}" echo "" if ! confirm "配置正确,继续导入?(如需修改请选 N,改完后重新运行 import)"; then log "已暂停。修改 .env 后重新运行:" echo -e " ${CYAN}bash migrate.sh import ${archive}${NC}" rm -rf "$WORK_DIR" exit 0 fi # --- 5. 停止现有服务 --- step "5/9 停止现有服务" docker compose down 2>/dev/null || true # --- 6. 安装基础设施(如新服务器未部署过) --- step "6/9 检查基础设施" if ! command -v docker &>/dev/null; then warn "Docker 未安装。请先运行 deploy.sh 安装基础设施,或手动安装 Docker。" echo -e " ${CYAN}bash deploy.sh${NC}" rm -rf "$WORK_DIR" exit 1 fi if ! docker compose version &>/dev/null; then error "Docker Compose V2 未安装" rm -rf "$WORK_DIR" exit 1 fi log "Docker 和 Compose 已就绪" # --- 7. 恢复 Gitea 数据目录 --- step "7/9 恢复 Gitea 数据" mkdir -p "$(dirname "$GITEA_DATA_DIR")" if [ -d "$GITEA_DATA_DIR" ] && [ "$(ls -A "$GITEA_DATA_DIR" 2>/dev/null)" ]; then warn "数据目录 ${GITEA_DATA_DIR} 非空" if confirm "是否清空后恢复?(选 N 则覆盖合并)"; then rm -rf "${GITEA_DATA_DIR:?}/"* fi fi tar xzf "${MIGRATE_DIR}/gitea-data.tar.gz" -C "$(dirname "$GITEA_DATA_DIR")" chown -R 1000:1000 "$GITEA_DATA_DIR" log "Gitea 数据已恢复到 ${GITEA_DATA_DIR}" # --- 8. 恢复数据库 --- step "8/9 恢复 MySQL 数据库" # 确保 MySQL 数据目录存在 mkdir -p "$MYSQL_DATA_DIR" # 启动 MySQL(清空后让其自动初始化) log "启动 MySQL 容器..." docker compose up -d db # 等待 MySQL 就绪 log "等待 MySQL 就绪..." local retries=0 while ! docker compose exec -T db mysqladmin ping -h localhost -u root -p"${DB_ROOT_PASSWORD}" --silent 2>/dev/null; do retries=$((retries + 1)) if [ "$retries" -ge 60 ]; then error "MySQL 启动超时(60 次重试)" rm -rf "$WORK_DIR" exit 1 fi sleep 2 done log "MySQL 已就绪" # 导入数据库 log "正在导入数据库(这可能需要几分钟)..." docker compose exec -T db mysql \ -u root -p"${DB_ROOT_PASSWORD}" \ --default-character-set=utf8mb4 \ < "${MIGRATE_DIR}/gitea-db.sql" log "数据库导入完成" # --- 9. 启动 Gitea 并执行迁移后操作 --- step "9/9 启动 Gitea 并验证" docker compose up -d log "容器已启动,等待 Gitea 就绪..." local retries=0 while ! curl -sf http://127.0.0.1:3000/api/v1/version &>/dev/null; do retries=$((retries + 1)) if [ "$retries" -ge 60 ]; then warn "Gitea 启动超时,请手动检查: docker compose logs server" break fi sleep 2 done if curl -sf http://127.0.0.1:3000/api/v1/version &>/dev/null; then GITEA_VER=$(curl -sf http://127.0.0.1:3000/api/v1/version | grep -oP '"version"\s*:\s*"\K[^"]+') log "Gitea ${GITEA_VER} 启动成功" fi # 重新生成 Git Hooks(迁移后必须执行) log "重新生成 Git Hooks..." docker compose exec -u git server \ /usr/local/bin/gitea -c /data/gitea/conf/app.ini admin regenerate hooks || \ warn "regenerate hooks 失败,请手动执行" # 运行 doctor 检查 log "运行 doctor 诊断..." docker compose exec -u git server \ /usr/local/bin/gitea -c /data/gitea/conf/app.ini doctor check --all --fix 2>&1 | \ tail -5 || warn "doctor check 返回异常,请检查日志" # 清理 rm -rf "$WORK_DIR" echo "" log "===== 导入完成 =====" log "Gitea 已在新服务器运行" echo "" warn "迁移后请检查以下事项:" echo " 1. 访问 https://${GITEA_DOMAIN:-你的域名} 确认页面正常" echo " 2. 如域名/IP 变更,需更新 DNS 解析" echo " 3. 如需 HTTPS,需重新申请 SSL 证书:" echo -e " ${CYAN}certbot certonly --webroot -w /var/www/certbot -d ${GITEA_DOMAIN:-你的域名}${NC}" echo " 4. 测试 Git clone / push 操作" echo " 5. 测试 SSH 克隆: git clone ssh://git@${GITEA_DOMAIN:-你的域名}:${SSH_PORT:-2222}/用户/仓库.git" echo "" log "运行验证命令检查完整性: bash migrate.sh verify" } # ============================================================= # 验证模式 — 迁移后验证完整性 # ============================================================= do_verify() { step "迁移后验证" load_env GITEA_DATA_DIR="${GITEA_DATA_DIR:-/var/lib/gitea}" local pass=0 fail=0 check() { if eval "$2" &>/dev/null; then echo -e " ${GREEN}✓${NC} $1" pass=$((pass + 1)) else echo -e " ${RED}✗${NC} $1" fail=$((fail + 1)) fi } echo "" log "检查服务状态:" check "Gitea 容器运行中" "docker compose ps --status running | grep -q gitea" check "MySQL 容器运行中" "docker compose ps --status running | grep -q gitea-db" log "检查 API:" check "Gitea API 可达" "curl -sf http://127.0.0.1:3000/api/v1/version" if curl -sf http://127.0.0.1:3000/api/v1/version &>/dev/null; then GITEA_VER=$(curl -sf http://127.0.0.1:3000/api/v1/version | grep -oP '"version"\s*:\s*"\K[^"]+') log " Gitea 版本: ${GITEA_VER}" fi log "检查数据库:" check "MySQL 连接正常" "docker compose exec -T db mysqladmin ping -h localhost -u root -p'${DB_ROOT_PASSWORD}' --silent" # 检查仓库数量 REPO_COUNT=$(docker compose exec -T db mysql -u root -p"${DB_ROOT_PASSWORD}" -N -e \ "SELECT COUNT(*) FROM gitea.repository;" 2>/dev/null || echo "0") log " 仓库数量: ${REPO_COUNT}" USER_COUNT=$(docker compose exec -T db mysql -u root -p"${DB_ROOT_PASSWORD}" -N -e \ "SELECT COUNT(*) FROM gitea.user;" 2>/dev/null || echo "0") log " 用户数量: ${USER_COUNT}" log "检查数据目录:" check "Gitea 数据目录存在" "[ -d '${GITEA_DATA_DIR}/gitea' ]" check "仓库目录存在" "[ -d '${GITEA_DATA_DIR}/gitea/repositories' ] || [ -d '${GITEA_DATA_DIR}/git/repositories' ]" check "app.ini 存在" "[ -f '${GITEA_DATA_DIR}/gitea/conf/app.ini' ]" # 统计仓库目录下的裸仓库数量 if [ -d "${GITEA_DATA_DIR}/gitea/repositories" ]; then DISK_REPOS=$(find "${GITEA_DATA_DIR}/gitea/repositories" -name "*.git" -type d -maxdepth 3 2>/dev/null | wc -l) log " 磁盘上的仓库目录: ${DISK_REPOS}" fi log "检查网络:" check "SSH 端口可监听" "docker compose port server 2222" if command -v nginx &>/dev/null; then check "Nginx 运行中" "systemctl is-active nginx" check "Nginx 配置正确" "nginx -t" else warn " Nginx 未安装(如需 HTTPS 请运行 deploy.sh 或手动安装)" fi echo "" log "===== 验证结果: ${pass} 通过, ${fail} 失败 =====" if [ "$fail" -gt 0 ]; then warn "存在失败项,请检查上方输出并排查问题" echo " 查看日志: docker compose logs" return 1 else log "所有检查通过!迁移成功。" fi } # ============================================================= # 主入口 # ============================================================= usage() { echo "Gitea 迁移脚本" echo "" echo "用法:" echo " bash migrate.sh export # 旧服务器:导出迁移包" echo " bash migrate.sh import <迁移包路径> # 新服务器:导入迁移包" echo " bash migrate.sh verify # 迁移后:验证完整性" echo "" echo "迁移流程:" echo " 1. 旧服务器: bash migrate.sh export" echo " 2. 传输: scp gitea_migrate_xxx.tar.gz root@新服务器:/opt/gitea/" echo " 3. 新服务器: bash migrate.sh import gitea_migrate_xxx.tar.gz" echo " 4. 新服务器: bash migrate.sh verify" } main() { preflight case "${1:-}" in export) do_export ;; import) if [ -z "${2:-}" ]; then error "请指定迁移包路径" echo "用法: bash migrate.sh import <迁移包路径>" exit 1 fi do_import "$2" ;; verify) do_verify ;; -h|--help|help) usage ;; *) usage exit 1 ;; esac } main "$@"