Merge branch 'master' of ssh://git.joywaygames.cn:2222/joywaygamess/server-deploy
This commit is contained in:
+253
-35
@@ -1,68 +1,286 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
set -euo pipefail
|
# -E 让 ERR trap 在函数内部同样生效(冷备份模式下必须靠它把服务拉回来)
|
||||||
|
set -Eeuo pipefail
|
||||||
|
|
||||||
# ============================================
|
# ============================================
|
||||||
# Gitea 备份脚本 (MySQL 版)
|
# Gitea 备份脚本 (MySQL 版)
|
||||||
# 备份内容:MySQL 数据库 + Gitea 数据(含 LFS) + 配置
|
#
|
||||||
|
# 备份内容:MySQL 数据库 + Gitea 数据(含仓库/LFS/密钥) + 部署配置
|
||||||
|
#
|
||||||
|
# 用法:
|
||||||
|
# bash backup.sh 热备份(服务不停机,默认)
|
||||||
|
# bash backup.sh --cold 冷备份(停 Gitea 取快照,最一致)
|
||||||
|
# bash backup.sh --keep 60 保留天数(默认 30)
|
||||||
|
# bash backup.sh --verify-last 只校验最近一次备份,不产生新备份
|
||||||
|
#
|
||||||
# 定时执行: crontab -e → 0 3 * * * /opt/gitea/backup.sh
|
# 定时执行: crontab -e → 0 3 * * * /opt/gitea/backup.sh
|
||||||
# ============================================
|
# ============================================
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
cd "$SCRIPT_DIR"
|
cd "$SCRIPT_DIR"
|
||||||
|
|
||||||
# 加载配置
|
DATE=$(date +%Y%m%d_%H%M%S)
|
||||||
if [ ! -f .env ]; then
|
KEEP_DAYS=30
|
||||||
echo "[ERROR] .env 文件不存在" >&2
|
MIN_KEEP_SETS=3 # 无论多旧,至少保留这么多份完整备份
|
||||||
exit 1
|
MODE="hot" # hot | cold | verify
|
||||||
fi
|
SVC_APP="server"
|
||||||
|
SVC_DB="db"
|
||||||
|
DB_NAME="gitea"
|
||||||
|
STOPPED=0
|
||||||
|
|
||||||
|
log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*"; }
|
||||||
|
warn() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] [WARN] $*" >&2; }
|
||||||
|
die() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] [ERROR] $*" >&2; exit 1; }
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
sed -n '5,18p' "$0" | sed 's/^# \{0,1\}//'
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
|
while [ $# -gt 0 ]; do
|
||||||
|
case "$1" in
|
||||||
|
--cold) MODE="cold" ;;
|
||||||
|
--verify-last) MODE="verify" ;;
|
||||||
|
--keep) shift; [ $# -gt 0 ] || die "--keep 需要一个参数"
|
||||||
|
[[ "$1" =~ ^[0-9]+$ ]] || die "--keep 必须是整数天"
|
||||||
|
KEEP_DAYS="$1" ;;
|
||||||
|
-h|--help) usage ;;
|
||||||
|
*) die "未知参数: $1(--help 查看用法)" ;;
|
||||||
|
esac
|
||||||
|
shift
|
||||||
|
done
|
||||||
|
|
||||||
|
# ===== 加载配置 =====
|
||||||
|
[ -f .env ] || die ".env 文件不存在"
|
||||||
|
if grep -q $'\r' .env 2>/dev/null; then sed -i 's/\r$//' .env; fi
|
||||||
set -a
|
set -a
|
||||||
sed -i 's/\r$//' .env
|
# shellcheck disable=SC1091
|
||||||
source .env
|
source .env
|
||||||
set +a
|
set +a
|
||||||
|
|
||||||
BACKUP_DIR="${BACKUP_DIR:-/var/backups/gitea}"
|
BACKUP_DIR="${BACKUP_DIR:-/var/backups/gitea}"
|
||||||
GITEA_DATA_DIR="${GITEA_DATA_DIR:-/var/lib/gitea}"
|
GITEA_DATA_DIR="${GITEA_DATA_DIR:-/var/lib/gitea}"
|
||||||
DATE=$(date +%Y%m%d_%H%M%S)
|
[ -n "${DB_ROOT_PASSWORD:-}" ] || die ".env 中缺少 DB_ROOT_PASSWORD"
|
||||||
KEEP_DAYS=30
|
|
||||||
|
|
||||||
log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*"; }
|
# ===== 前置检查 =====
|
||||||
|
[ "$(id -u)" -eq 0 ] || die "需要 root 权限运行"
|
||||||
|
for c in docker tar gzip find awk; do
|
||||||
|
command -v "$c" >/dev/null 2>&1 || die "缺少命令: $c"
|
||||||
|
done
|
||||||
|
docker compose version >/dev/null 2>&1 || die "docker compose 不可用"
|
||||||
|
[ -d "$GITEA_DATA_DIR" ] || die "数据目录不存在: $GITEA_DATA_DIR"
|
||||||
|
|
||||||
mkdir -p "$BACKUP_DIR"
|
mkdir -p "$BACKUP_DIR"
|
||||||
|
chmod 700 "$BACKUP_DIR"
|
||||||
|
|
||||||
# 1. 备份 MySQL 数据库
|
# 防止 cron 叠加执行:上一次还没跑完就不再起新的
|
||||||
|
exec 9>"$BACKUP_DIR/.backup.lock"
|
||||||
|
flock -n 9 || die "已有另一个备份进程在运行,本次退出"
|
||||||
|
|
||||||
|
# MySQL 容器用 MYSQL_PWD 传密码,避免密码出现在 ps 的命令行里
|
||||||
|
db_exec() { docker compose exec -T -e MYSQL_PWD="$DB_ROOT_PASSWORD" "$SVC_DB" "$@"; }
|
||||||
|
|
||||||
|
db_running() {
|
||||||
|
local cid
|
||||||
|
cid=$(docker compose ps -a -q "$SVC_DB" 2>/dev/null | head -1)
|
||||||
|
[ -n "$cid" ] || return 1
|
||||||
|
[ "$(docker inspect "$cid" --format '{{.State.Running}}' 2>/dev/null)" = "true" ]
|
||||||
|
}
|
||||||
|
|
||||||
|
# =============================================================
|
||||||
|
# 校验:备份产出后必须自证可用,否则等于没备
|
||||||
|
# =============================================================
|
||||||
|
verify_db_dump() {
|
||||||
|
local f="$1" summary tables completed
|
||||||
|
[ -f "$f" ] || { warn "SQL 备份不存在: $f"; return 1; }
|
||||||
|
|
||||||
|
gzip -t "$f" 2>/dev/null || { warn "SQL 备份 gzip 校验失败: $f"; return 1; }
|
||||||
|
|
||||||
|
# 一次解压同时数建表语句和找结束标记。用 awk 读完整个流,不能写成
|
||||||
|
# `gunzip -c | grep -q` —— grep 命中即退出会让 gunzip 收到 SIGPIPE,
|
||||||
|
# 在 pipefail 下把好备份误判成坏的。
|
||||||
|
summary="$(gunzip -c "$f" | awk '
|
||||||
|
/^CREATE TABLE / { n++ }
|
||||||
|
/-- Dump completed/ { d = 1 }
|
||||||
|
END { print n+0, d+0 }
|
||||||
|
')" || { warn "SQL 备份无法完整解压: $f"; return 1; }
|
||||||
|
|
||||||
|
tables="${summary%% *}"; completed="${summary##* }"
|
||||||
|
# "Dump completed" 只有 mysqldump 正常收尾才会写;被 OOM / 断连截断的没有
|
||||||
|
[ "$completed" = "1" ] || { warn "SQL 备份缺少结束标记,导出被截断: $f"; return 1; }
|
||||||
|
[ "$tables" -ge 10 ] || { warn "SQL 备份只有 $tables 条 CREATE TABLE: $f"; return 1; }
|
||||||
|
|
||||||
|
log " SQL 备份校验通过(${tables} 张表,含结束标记)"
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
verify_data_tar() {
|
||||||
|
local f="$1" listing repos
|
||||||
|
[ -f "$f" ] || { warn "数据备份不存在: $f"; return 1; }
|
||||||
|
|
||||||
|
gzip -t "$f" 2>/dev/null || { warn "数据备份 gzip 校验失败: $f"; return 1; }
|
||||||
|
listing="$(tar tzf "$f")" || { warn "数据备份无法列出内容: $f"; return 1; }
|
||||||
|
|
||||||
|
grep -q 'gitea/conf/app\.ini$' <<<"$listing" || { warn "数据备份中没有 app.ini"; return 1; }
|
||||||
|
grep -q '/ssh/' <<<"$listing" || warn "数据备份中没有 SSH host key 目录"
|
||||||
|
grep -q '/jwt/' <<<"$listing" || warn "数据备份中没有 JWT 密钥目录"
|
||||||
|
|
||||||
|
repos=$(grep -c 'repositories/[^/]*/[^/]*\.git/$' <<<"$listing" || true)
|
||||||
|
log " 数据备份校验通过($(wc -l <<<"$listing") 个条目,${repos} 个仓库)"
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
verify_set() {
|
||||||
|
local ts="$1" ok=0
|
||||||
|
verify_db_dump "${BACKUP_DIR}/db_${ts}.sql.gz" || ok=1
|
||||||
|
verify_data_tar "${BACKUP_DIR}/gitea_data_${ts}.tar.gz" || ok=1
|
||||||
|
[ -f "${BACKUP_DIR}/config_${ts}.tar.gz" ] || { warn "配置备份缺失"; ok=1; }
|
||||||
|
return $ok
|
||||||
|
}
|
||||||
|
|
||||||
|
# ===== --verify-last:只校验,不产生新备份 =====
|
||||||
|
if [ "$MODE" = "verify" ]; then
|
||||||
|
last=$(find "$BACKUP_DIR" -maxdepth 1 -name 'db_*.sql.gz' -printf '%f\n' 2>/dev/null \
|
||||||
|
| sed 's/^db_//; s/\.sql\.gz$//' | sort | tail -1)
|
||||||
|
[ -n "$last" ] || die "$BACKUP_DIR 中没有找到任何备份"
|
||||||
|
log "校验最近一次备份: $last"
|
||||||
|
if verify_set "$last"; then log "===== 备份 $last 校验通过 ====="; exit 0
|
||||||
|
else die "备份 $last 校验未通过"; fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# =============================================================
|
||||||
|
# 冷备份:停 Gitea 取一致快照,MySQL 全程保持运行
|
||||||
|
# =============================================================
|
||||||
|
restart_app_if_stopped() {
|
||||||
|
[ "$STOPPED" -eq 1 ] || return 0
|
||||||
|
STOPPED=0
|
||||||
|
log "正在恢复 Gitea 服务..."
|
||||||
|
docker compose up -d "$SVC_APP" >/dev/null 2>&1 || \
|
||||||
|
warn "Gitea 恢复失败!请手动执行: cd $SCRIPT_DIR && docker compose up -d"
|
||||||
|
}
|
||||||
|
trap 'restart_app_if_stopped' EXIT
|
||||||
|
|
||||||
|
db_running || die "MySQL 容器未运行,无法备份数据库: docker compose up -d $SVC_DB"
|
||||||
|
|
||||||
|
# ===== 磁盘空间预检 =====
|
||||||
|
data_kb=$(du -sk "$GITEA_DATA_DIR" 2>/dev/null | cut -f1 || echo 0)
|
||||||
|
[[ "$data_kb" =~ ^[0-9]+$ ]] || data_kb=0
|
||||||
|
avail_kb=$(df -Pk "$BACKUP_DIR" | awk 'NR==2{print $4}')
|
||||||
|
need_kb=$(( data_kb * 6 / 5 + 512000 ))
|
||||||
|
if [ -n "$avail_kb" ] && [ "$avail_kb" -lt "$need_kb" ]; then
|
||||||
|
die "磁盘空间不足: 可用 $((avail_kb/1024))MB,预计需要 $((need_kb/1024))MB"
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "===== 开始备份(模式: $MODE)====="
|
||||||
|
|
||||||
|
if [ "$MODE" = "cold" ]; then
|
||||||
|
log "停止 Gitea 以取得一致快照(MySQL 不停)..."
|
||||||
|
docker compose stop "$SVC_APP" >/dev/null 2>&1
|
||||||
|
STOPPED=1
|
||||||
|
log "Gitea 已停止"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# =============================================================
|
||||||
|
# 1. 数据库
|
||||||
|
# =============================================================
|
||||||
|
# 顺序很重要:先导数据库、后打包文件。
|
||||||
|
# 热备份下两者必然是两个时刻的快照,若中间发生 push:
|
||||||
|
# 先库后文件 → 文件里多出库中没有的对象(孤儿对象,无害)
|
||||||
|
# 先文件后库 → 库里记录了文件中不存在的提交(引用悬空,有害)
|
||||||
|
# 所以先导库是两害相权取其轻。要完全一致请用 --cold。
|
||||||
log "正在备份 MySQL 数据库..."
|
log "正在备份 MySQL 数据库..."
|
||||||
docker compose exec -T db mysqldump \
|
DB_FILE="${BACKUP_DIR}/db_${DATE}.sql.gz"
|
||||||
-u root -p"${DB_ROOT_PASSWORD}" \
|
|
||||||
--single-transaction \
|
|
||||||
--routines \
|
|
||||||
--triggers \
|
|
||||||
--databases gitea \
|
|
||||||
| gzip > "${BACKUP_DIR}/db_${DATE}.sql.gz"
|
|
||||||
log "数据库备份完成: db_${DATE}.sql.gz ($(du -h "${BACKUP_DIR}/db_${DATE}.sql.gz" | cut -f1))"
|
|
||||||
|
|
||||||
# 2. 备份 Gitea 数据(仓库、LFS、附件、头像等)
|
dump_opts=(-u root --single-transaction --routines --triggers
|
||||||
log "正在备份 Gitea 数据目录(含 LFS)..."
|
--hex-blob --default-character-set=utf8mb4 --databases "$DB_NAME")
|
||||||
tar czf "${BACKUP_DIR}/gitea_data_${DATE}.tar.gz" \
|
# 无 GTID 的实例上 mysqldump 会写 SET @@GLOBAL.GTID_PURGED,回灌时报错。
|
||||||
|
# 先收进变量再匹配,避免 grep -q 的 SIGPIPE 让探测结果反转。
|
||||||
|
dump_help="$(db_exec mysqldump --help 2>/dev/null || true)"
|
||||||
|
if grep -q 'set-gtid-purged' <<<"$dump_help"; then
|
||||||
|
dump_opts+=(--set-gtid-purged=OFF)
|
||||||
|
fi
|
||||||
|
|
||||||
|
db_exec mysqldump "${dump_opts[@]}" | gzip > "$DB_FILE"
|
||||||
|
chmod 600 "$DB_FILE"
|
||||||
|
verify_db_dump "$DB_FILE" || die "数据库备份未通过校验,已中止(不清理旧备份)"
|
||||||
|
log "数据库备份完成: db_${DATE}.sql.gz ($(du -h "$DB_FILE" | cut -f1))"
|
||||||
|
|
||||||
|
# =============================================================
|
||||||
|
# 2. Gitea 数据目录(仓库、LFS、附件、密钥)
|
||||||
|
# =============================================================
|
||||||
|
log "正在备份 Gitea 数据目录(含仓库与 LFS)..."
|
||||||
|
DATA_FILE="${BACKUP_DIR}/gitea_data_${DATE}.tar.gz"
|
||||||
|
tar czf "$DATA_FILE" \
|
||||||
-C "$(dirname "$GITEA_DATA_DIR")" \
|
-C "$(dirname "$GITEA_DATA_DIR")" \
|
||||||
"$(basename "$GITEA_DATA_DIR")"
|
"$(basename "$GITEA_DATA_DIR")"
|
||||||
log "数据目录备份完成: gitea_data_${DATE}.tar.gz ($(du -h "${BACKUP_DIR}/gitea_data_${DATE}.tar.gz" | cut -f1))"
|
# 包里有 SSH host key 和 OAuth2 JWT 签名密钥,权限必须收紧
|
||||||
|
chmod 600 "$DATA_FILE"
|
||||||
|
verify_data_tar "$DATA_FILE" || die "数据目录备份未通过校验,已中止(不清理旧备份)"
|
||||||
|
log "数据目录备份完成: gitea_data_${DATE}.tar.gz ($(du -h "$DATA_FILE" | cut -f1))"
|
||||||
|
|
||||||
# 3. 备份配置文件
|
# =============================================================
|
||||||
|
# 3. 部署配置
|
||||||
|
# =============================================================
|
||||||
log "正在备份配置文件..."
|
log "正在备份配置文件..."
|
||||||
tar czf "${BACKUP_DIR}/config_${DATE}.tar.gz" \
|
CONF_FILE="${BACKUP_DIR}/config_${DATE}.tar.gz"
|
||||||
-C "$SCRIPT_DIR" \
|
conf_items=(.env docker-compose.yml)
|
||||||
.env docker-compose.yml nginx/
|
[ -d nginx ] && conf_items+=(nginx)
|
||||||
|
tar czf "$CONF_FILE" -C "$SCRIPT_DIR" "${conf_items[@]}"
|
||||||
|
chmod 600 "$CONF_FILE" # 含 .env 里的数据库密码
|
||||||
|
gzip -t "$CONF_FILE" 2>/dev/null || die "配置备份 gzip 校验失败"
|
||||||
log "配置备份完成: config_${DATE}.tar.gz"
|
log "配置备份完成: config_${DATE}.tar.gz"
|
||||||
|
|
||||||
# 4. 清理过期备份
|
# 冷备份到此结束,尽早把服务放回去,后面的清理不该占用停机时间
|
||||||
log "清理 ${KEEP_DAYS} 天前的备份..."
|
restart_app_if_stopped
|
||||||
deleted=$(find "$BACKUP_DIR" -type f -mtime +${KEEP_DAYS} -print -delete | wc -l)
|
|
||||||
log "已清理 ${deleted} 个过期文件"
|
|
||||||
|
|
||||||
# 5. 输出备份摘要
|
# =============================================================
|
||||||
|
# 4. 清理过期备份
|
||||||
|
# =============================================================
|
||||||
|
# 三条安全约束,缺一不可:
|
||||||
|
# a) 只删本脚本自己产出的三种文件名,绝不碰 upgrade.sh 的 pre-upgrade-*
|
||||||
|
# 回滚锚点,也不碰别人放进这个目录的东西
|
||||||
|
# b) 按「备份集」整组判断,不按单个文件,避免删出半套残废备份
|
||||||
|
# c) 无论多旧,至少保留 MIN_KEEP_SETS 份,防止长期备份失败后
|
||||||
|
# 把最后一份能用的也清掉
|
||||||
|
log "清理 ${KEEP_DAYS} 天前的备份(至少保留最近 ${MIN_KEEP_SETS} 份)..."
|
||||||
|
|
||||||
|
mapfile -t all_sets < <(
|
||||||
|
find "$BACKUP_DIR" -maxdepth 1 -name 'db_*.sql.gz' -printf '%f\n' 2>/dev/null \
|
||||||
|
| sed 's/^db_//; s/\.sql\.gz$//' | sort
|
||||||
|
)
|
||||||
|
total=${#all_sets[@]}
|
||||||
|
deleted=0
|
||||||
|
|
||||||
|
if [ "$total" -gt "$MIN_KEEP_SETS" ]; then
|
||||||
|
# 只考察最老的那些,末尾 MIN_KEEP_SETS 份无条件保留
|
||||||
|
candidates=$(( total - MIN_KEEP_SETS ))
|
||||||
|
for (( i = 0; i < candidates; i++ )); do
|
||||||
|
ts="${all_sets[$i]}"
|
||||||
|
f="${BACKUP_DIR}/db_${ts}.sql.gz"
|
||||||
|
# 用 mtime 判断是否过期;-mmin 比 -mtime 精度更好
|
||||||
|
if [ -n "$(find "$f" -maxdepth 0 -mmin +$(( KEEP_DAYS * 1440 )) 2>/dev/null)" ]; then
|
||||||
|
rm -f "${BACKUP_DIR}/db_${ts}.sql.gz" \
|
||||||
|
"${BACKUP_DIR}/gitea_data_${ts}.tar.gz" \
|
||||||
|
"${BACKUP_DIR}/config_${ts}.tar.gz"
|
||||||
|
log " 已删除过期备份集: $ts"
|
||||||
|
deleted=$(( deleted + 1 ))
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
log "已清理 ${deleted} 份过期备份集,当前保留 $(( total - deleted )) 份"
|
||||||
|
|
||||||
|
# =============================================================
|
||||||
|
# 5. 摘要
|
||||||
|
# =============================================================
|
||||||
echo ""
|
echo ""
|
||||||
log "===== 备份完成 ====="
|
log "===== 备份完成 ====="
|
||||||
log "备份目录: ${BACKUP_DIR}/"
|
log "备份集: ${DATE}(模式: ${MODE})"
|
||||||
ls -lh "${BACKUP_DIR}/"*"${DATE}"* 2>/dev/null || true
|
ls -lh "${BACKUP_DIR}/"*"${DATE}"* 2>/dev/null || true
|
||||||
echo ""
|
echo ""
|
||||||
log "总备份空间占用: $(du -sh "${BACKUP_DIR}" | cut -f1)"
|
log "备份目录: ${BACKUP_DIR}/"
|
||||||
|
log "总占用: $(du -sh "$BACKUP_DIR" | cut -f1)"
|
||||||
|
if [ "$MODE" = "hot" ]; then
|
||||||
|
log "提示: 这是热备份,数据库与仓库文件取自不同时刻。"
|
||||||
|
log " 需要完全一致的快照请用: bash backup.sh --cold"
|
||||||
|
fi
|
||||||
|
log "校验最近备份: bash backup.sh --verify-last"
|
||||||
|
|||||||
+59
-6
@@ -160,6 +160,9 @@ load_env() {
|
|||||||
BACKUP_BASE="${BACKUP_DIR:-/var/backups/gitea}"
|
BACKUP_BASE="${BACKUP_DIR:-/var/backups/gitea}"
|
||||||
DOMAIN="${GITEA_DOMAIN:-}"
|
DOMAIN="${GITEA_DOMAIN:-}"
|
||||||
CURRENT_IMAGE="${GITEA_IMAGE:-gitea/gitea:1.25}"
|
CURRENT_IMAGE="${GITEA_IMAGE:-gitea/gitea:1.25}"
|
||||||
|
# 健康检查探测的本地端口。默认 3000 与生产一致;隔离演练环境
|
||||||
|
# 可在 .env 里设 GITEA_HTTP_PORT,避免探到别的实例上去。
|
||||||
|
HTTP_PORT="${GITEA_HTTP_PORT:-3000}"
|
||||||
|
|
||||||
if [ -z "${DB_ROOT_PASSWORD:-}" ]; then
|
if [ -z "${DB_ROOT_PASSWORD:-}" ]; then
|
||||||
error ".env 中缺少 DB_ROOT_PASSWORD,无法备份数据库"
|
error ".env 中缺少 DB_ROOT_PASSWORD,无法备份数据库"
|
||||||
@@ -177,6 +180,17 @@ db_query() {
|
|||||||
db_exec mysql -u root -N -B -e "$1" 2>/dev/null | tr -d '\r'
|
db_exec mysql -u root -N -B -e "$1" 2>/dev/null | tr -d '\r'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# 输入 KB,输出人类可读。整数除法会把 780KB 显示成 "0MB",
|
||||||
|
# 让人误以为没有数据要备份,所以小于 1MB 时改用 KB 显示。
|
||||||
|
fmt_size() {
|
||||||
|
local kb="${1:-0}"
|
||||||
|
[[ "$kb" =~ ^[0-9]+$ ]] || { echo "未知"; return; }
|
||||||
|
if [ "$kb" -lt 1024 ]; then echo "${kb}KB"
|
||||||
|
elif [ "$kb" -lt 1048576 ]; then echo "$((kb/1024))MB"
|
||||||
|
else echo "$((kb/1048576))GB"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
db_running() {
|
db_running() {
|
||||||
local cid
|
local cid
|
||||||
cid=$(docker compose ps -a -q "$SVC_DB" 2>/dev/null | head -1)
|
cid=$(docker compose ps -a -q "$SVC_DB" 2>/dev/null | head -1)
|
||||||
@@ -250,7 +264,7 @@ preflight() {
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
log "root 权限、依赖命令、数据目录、磁盘空间 均正常"
|
log "root 权限、依赖命令、数据目录、磁盘空间 均正常"
|
||||||
info "数据库 $((db_kb/1024))MB,待备份数据 $((data_kb/1024))MB,可用空间 $((avail_kb/1024))MB"
|
info "数据库 $(fmt_size "$db_kb"),待备份数据 $(fmt_size "$data_kb"),可用空间 $(fmt_size "$avail_kb")"
|
||||||
}
|
}
|
||||||
|
|
||||||
# =============================================================
|
# =============================================================
|
||||||
@@ -346,7 +360,7 @@ detect_current_version() {
|
|||||||
v=$(docker exec "$cid" gitea --version 2>/dev/null | head -1 | awk '{print $3}' | cut -d+ -f1 || true)
|
v=$(docker exec "$cid" gitea --version 2>/dev/null | head -1 | awk '{print $3}' | cut -d+ -f1 || true)
|
||||||
fi
|
fi
|
||||||
if [ -z "$v" ]; then
|
if [ -z "$v" ]; then
|
||||||
v=$(curl -sf --max-time 10 "http://127.0.0.1:3000/api/v1/version" 2>/dev/null \
|
v=$(curl -sf --max-time 10 "http://127.0.0.1:${HTTP_PORT}/api/v1/version" 2>/dev/null \
|
||||||
| grep -o '"version"[[:space:]]*:[[:space:]]*"[^"]*"' \
|
| grep -o '"version"[[:space:]]*:[[:space:]]*"[^"]*"' \
|
||||||
| sed 's/.*"\([^"]*\)"$/\1/' | cut -d+ -f1 || true)
|
| sed 's/.*"\([^"]*\)"$/\1/' | cut -d+ -f1 || true)
|
||||||
fi
|
fi
|
||||||
@@ -473,6 +487,8 @@ cold_backup() {
|
|||||||
printf ' %s\n' "${BACKUP_PATHS[@]}"
|
printf ' %s\n' "${BACKUP_PATHS[@]}"
|
||||||
|
|
||||||
tar czf "$DATA_BACKUP" -C "$(dirname "$DATA_DIR")" "${BACKUP_PATHS[@]}"
|
tar czf "$DATA_BACKUP" -C "$(dirname "$DATA_DIR")" "${BACKUP_PATHS[@]}"
|
||||||
|
# 包里有 SSH host key 和 OAuth2 JWT 签名密钥,权限必须收紧
|
||||||
|
chmod 600 "$DATA_BACKUP"
|
||||||
verify_data_backup "$DATA_BACKUP"
|
verify_data_backup "$DATA_BACKUP"
|
||||||
log "数据备份完成: $(du -h "$DATA_BACKUP" | cut -f1)"
|
log "数据备份完成: $(du -h "$DATA_BACKUP" | cut -f1)"
|
||||||
|
|
||||||
@@ -601,7 +617,7 @@ wait_healthy() {
|
|||||||
|
|
||||||
info "等待 Gitea 就绪(最多 ${max_wait}s,跨版本迁移会比较慢)..."
|
info "等待 Gitea 就绪(最多 ${max_wait}s,跨版本迁移会比较慢)..."
|
||||||
for i in $(seq 1 "$max_wait"); do
|
for i in $(seq 1 "$max_wait"); do
|
||||||
code="$(http_code "http://127.0.0.1:3000/api/v1/version" 3)"
|
code="$(http_code "http://127.0.0.1:${HTTP_PORT}/api/v1/version" 3)"
|
||||||
if [ "$code" != "000" ]; then
|
if [ "$code" != "000" ]; then
|
||||||
log "Gitea 已就绪(${i}s,HTTP ${code})"
|
log "Gitea 已就绪(${i}s,HTTP ${code})"
|
||||||
return 0
|
return 0
|
||||||
@@ -668,7 +684,7 @@ verify_endpoints() {
|
|||||||
step "接口验证"
|
step "接口验证"
|
||||||
local failed=0 code
|
local failed=0 code
|
||||||
|
|
||||||
code="$(http_code "http://127.0.0.1:3000/api/v1/version" 10)"
|
code="$(http_code "http://127.0.0.1:${HTTP_PORT}/api/v1/version" 10)"
|
||||||
case "$code" in
|
case "$code" in
|
||||||
200) log "/api/v1/version → 200" ;;
|
200) log "/api/v1/version → 200" ;;
|
||||||
401|403) log "/api/v1/version → ${code}(已开启强制登录,属预期)" ;;
|
401|403) log "/api/v1/version → ${code}(已开启强制登录,属预期)" ;;
|
||||||
@@ -677,7 +693,7 @@ verify_endpoints() {
|
|||||||
esac
|
esac
|
||||||
|
|
||||||
# 首页:模板损坏(自定义模板与新版不兼容)会在这里暴露成 5xx
|
# 首页:模板损坏(自定义模板与新版不兼容)会在这里暴露成 5xx
|
||||||
code="$(http_code "http://127.0.0.1:3000/" 10)"
|
code="$(http_code "http://127.0.0.1:${HTTP_PORT}/" 10)"
|
||||||
if [ "$code" = "000" ] || [ "$code" -ge 500 ]; then
|
if [ "$code" = "000" ] || [ "$code" -ge 500 ]; then
|
||||||
error "首页 → ${code}(模板渲染失败?)"; failed=1
|
error "首页 → ${code}(模板渲染失败?)"; failed=1
|
||||||
else
|
else
|
||||||
@@ -727,6 +743,18 @@ verify_data_integrity() {
|
|||||||
error "数据库迁移版本倒退: $MIGRATION_BEFORE → $migration_after"
|
error "数据库迁移版本倒退: $MIGRATION_BEFORE → $migration_after"
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# 跨小版本升级必然伴随数据库结构变更,迁移版本理应前进。它没动就说明
|
||||||
|
# 新版本压根没对这个库执行迁移 —— 可能是健康检查探到了别的实例、
|
||||||
|
# 容器连到了另一个数据库,或验证跑在迁移完成之前。
|
||||||
|
# 这种情况下「报成功」远比「报失败」危险,必须拦下来。
|
||||||
|
if [ -n "$CURRENT_VERSION" ] \
|
||||||
|
&& [ "$(ver_series "$CURRENT_VERSION")" != "$(ver_series "$TARGET_VERSION")" ] \
|
||||||
|
&& [ "$migration_after" -eq "$MIGRATION_BEFORE" ]; then
|
||||||
|
error "跨小版本升级 ${CURRENT_VERSION} → ${TARGET_VERSION},迁移版本却没有前进(仍为 ${MIGRATION_BEFORE})"
|
||||||
|
error "新版本似乎没有对这个数据库执行迁移,不能认定升级成功"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 逐表比对:行数只允许持平或增加,减少视为数据丢失
|
# 逐表比对:行数只允许持平或增加,减少视为数据丢失
|
||||||
@@ -866,6 +894,26 @@ on_exit() {
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# 回滚后 .env 会被钉在 pre-upgrade-<时间戳> 这个「只存在于本机」的标签上。
|
||||||
|
# 而升级成功时脚本又会建议 `docker image rm` 掉这类标签 —— 处在回滚状态的人
|
||||||
|
# 照做(或例行清理旧镜像)就会把自己的部署搞挂。必须把这层依赖讲明白。
|
||||||
|
warn_rollback_pinning() {
|
||||||
|
local restored_ver="${1:-}"
|
||||||
|
echo ""
|
||||||
|
warn "══════ 重要:.env 现在钉在回滚镜像上 ══════"
|
||||||
|
warn " GITEA_IMAGE=${ROLLBACK_TAG}"
|
||||||
|
warn "该标签只存在于本机,删掉它服务将无法启动。"
|
||||||
|
warn "清理 docker 镜像时务必跳过它!"
|
||||||
|
if [ -n "$restored_ver" ]; then
|
||||||
|
warn ""
|
||||||
|
warn "确认服务稳定后,可以这样回到常规版本号标签(无需联网重新拉取):"
|
||||||
|
warn " docker tag ${ROLLBACK_TAG} ${IMAGE_REPO}:${restored_ver}"
|
||||||
|
warn " sed -i 's|^GITEA_IMAGE=.*|GITEA_IMAGE=${IMAGE_REPO}:${restored_ver}|' ${SCRIPT_DIR}/.env"
|
||||||
|
warn " cd ${SCRIPT_DIR} && docker compose up -d --force-recreate --no-deps ${SVC_APP}"
|
||||||
|
fi
|
||||||
|
warn "═══════════════════════════════════════════"
|
||||||
|
}
|
||||||
|
|
||||||
do_rollback() {
|
do_rollback() {
|
||||||
trap - ERR EXIT # 先解除,避免回滚过程自身出错导致递归
|
trap - ERR EXIT # 先解除,避免回滚过程自身出错导致递归
|
||||||
SAFETY_STAGE="done"
|
SAFETY_STAGE="done"
|
||||||
@@ -899,6 +947,7 @@ do_rollback() {
|
|||||||
warn "升级失败已回滚。备份保留在:"
|
warn "升级失败已回滚。备份保留在:"
|
||||||
warn " 数据库: $DB_BACKUP"
|
warn " 数据库: $DB_BACKUP"
|
||||||
warn " 数据: $DATA_BACKUP"
|
warn " 数据: $DATA_BACKUP"
|
||||||
|
warn_rollback_pinning "${CURRENT_VERSION:-}"
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -945,8 +994,10 @@ manual_rollback() {
|
|||||||
if wait_healthy 180; then
|
if wait_healthy 180; then
|
||||||
log "回滚完成"
|
log "回滚完成"
|
||||||
verify_endpoints || warn "接口验证有告警,请检查日志"
|
verify_endpoints || warn "接口验证有告警,请检查日志"
|
||||||
|
warn_rollback_pinning "${FROM_VERSION:-}"
|
||||||
else
|
else
|
||||||
error "服务未就绪,请检查: docker compose logs --tail 100 $SVC_APP"
|
error "服务未就绪,请检查: docker compose logs --tail 100 $SVC_APP"
|
||||||
|
warn_rollback_pinning "${FROM_VERSION:-}"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
@@ -1084,7 +1135,9 @@ main() {
|
|||||||
echo ""
|
echo ""
|
||||||
warn "请登录 Web 端确认仓库、议题、LFS 都正常后,再考虑清理回滚镜像:"
|
warn "请登录 Web 端确认仓库、议题、LFS 都正常后,再考虑清理回滚镜像:"
|
||||||
warn " docker image rm $ROLLBACK_TAG"
|
warn " docker image rm $ROLLBACK_TAG"
|
||||||
warn "升级前的备份请至少保留到确认无误之后,不要急着删。"
|
warn "但要清楚代价:删掉之后 'bash upgrade.sh --rollback' 会因找不到镜像而失效,"
|
||||||
|
warn "届时只能靠 SQL 备份手工恢复。建议先跑一段时间确认无误再删。"
|
||||||
|
warn "升级前的备份同理,不要急着删。"
|
||||||
}
|
}
|
||||||
|
|
||||||
main "$@"
|
main "$@"
|
||||||
|
|||||||
Reference in New Issue
Block a user