外观
Nginx 通用升级脚本
约 6455 字大约 22 分钟
ShellNginx升级运维工具
2026-05-15
自动检测 Nginx 安装方式(源码编译 / YUM / APT / APK / Zypper / Docker),按原方式平滑升级,支持自动备份、回滚与清理。
功能特性
| 特性 | 说明 |
|---|---|
| 🔍 自动检测 | 自动识别源码编译、YUM、APT、APK、Zypper、Docker 等安装方式 |
| 🔄 平滑升级 | 源码编译支持 USR2 热升级,零中断 |
| 💾 自动备份 | 升级前自动备份二进制和配置文件 |
| ↩️ 一键回滚 | 升级失败自动回滚,也支持手动选择备份回滚 |
| 🧹 备份清理 | 支持清理旧备份释放磁盘空间 |
| ✅ 配置验证 | 升级前用新二进制测试现有配置兼容性 |
| 📋 详细日志 | 全程记录操作日志 |
快速开始
使用 curl:
curl -sL https://script.merma.cn/scripts/shell/UpgradeScript/update_nginx.sh -o /tmp/update_nginx.sh && bash /tmp/update_nginx.sh; rm -f /tmp/update_nginx.sh使用 wget:
wget -qO /tmp/update_nginx.sh https://script.merma.cn/scripts/shell/UpgradeScript/update_nginx.sh && bash /tmp/update_nginx.sh; rm -f /tmp/update_nginx.sh演示效果
root@localhost ~ # ./update_nginx.sh
============================================
Nginx 通用升级/回滚脚本
支持: 源码 / YUM / APT / APK / Zypper / Docker
============================================
请选择操作:
[1] 升级 Nginx
[2] 回滚 Nginx
[3] 清理旧备份
请输入选择 (1/2/3): 1
[INFO] 正在检测Nginx安装方式...
[INFO] 检测到软链接: /usr/local/sbin/nginx -> /mnt/application/nginx/sbin/nginx
[INFO] 检测到源码编译安装
========== 当前Nginx信息 ==========
[INFO] 安装方式: source
[INFO] 二进制路径: /mnt/application/nginx/sbin/nginx
[INFO] 当前版本: 1.28.0
[INFO] 安装前缀: /mnt/application/nginx
[INFO] 配置文件: /mnt/application/nginx/conf/nginx.conf
[INFO] PID文件: /mnt/application/nginx/logs/nginx.pid
[INFO] 编译参数: --prefix=/mnt/application/nginx --user=root --group=root ...
[INFO] 软链接: /usr/local/sbin/nginx
请输入要升级到的Nginx版本号(例如 1.30.1): 1.30.1
[INFO] 目标版本: 1.30.1
即将执行升级:
安装方式: source
1.28.0 -> 1.30.1
编译参数: --prefix=/mnt/application/nginx ...
[INFO] 检查编译依赖...
[INFO] 依赖检查通过
[INFO] 正在备份到: /mnt/application/nginx/backup_20260515_113110
[INFO] 备份完成
[INFO] 下载源码: https://nginx.org/download/nginx-1.30.1.tar.gz
[INFO] 源码准备完成
[INFO] 开始编译 Nginx 1.30.1...
[INFO] 执行 configure...
[INFO] 编译中(4核心并行),请耐心等待...
[INFO] 编译成功: 1.30.1
[INFO] 用新二进制测试现有配置...
[INFO] 配置兼容性测试通过
[INFO] 开始平滑升级...
[INFO] 二进制已替换
[INFO] 当前master PID: 1796587
[INFO] 优雅停止旧进程...
[INFO] 启动新版本: /mnt/application/nginx/sbin/nginx
[INFO] 新版本已启动
========== 升级结果 ==========
[INFO] 升级方式: source
[INFO] 原版本: 1.28.0
[INFO] 新版本: 1.30.1
[INFO] 版本验证: 通过
[INFO] 进程状态: 运行中 (PID: 1800068)
[INFO] 配置测试: 通过
[INFO] 备份位置: /mnt/application/nginx/backup_20260515_113110
[INFO] 升级日志: /tmp/nginx_upgrade_20260515_113102.log
[INFO] 回滚命令: cp /mnt/application/nginx/backup_20260515_113110/nginx.old /mnt/application/nginx/sbin/nginx && nginx -s reload
[INFO] 全部完成!环境要求
- 操作系统: Linux
- Shell: Bash 4.0+
- 权限: root
- 依赖: wget, tar, gcc, make(源码编译方式)
脚本源码
点击展开查看完整源码
#!/bin/bash
set -euo pipefail
# ============================================================
# Nginx 通用升级脚本
# 自动检测安装方式(源码编译/yum/apt/docker),按原方式平滑升级
# ============================================================
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
CYAN='\033[0;36m'
NC='\033[0m'
LOG_FILE="/tmp/nginx_upgrade_$(date +%Y%m%d_%H%M%S).log"
BACKUP_DIR=""
BUILD_DIR=""
ROLLBACK_NEEDED=false
NGINX_SBIN=""
NGINX_PREFIX=""
NGINX_PID_PATH=""
NGINX_CONF=""
CURRENT_VERSION=""
TARGET_VERSION=""
CONFIGURE_ARGS=""
INSTALL_METHOD="" # source / yum / apt / docker
DOCKER_CONTAINER=""
log() { echo -e "${GREEN}[INFO]${NC} $1" | tee -a "$LOG_FILE"; }
warn() { echo -e "${YELLOW}[WARN]${NC} $1" | tee -a "$LOG_FILE"; }
err() { echo -e "${RED}[ERROR]${NC} $1" | tee -a "$LOG_FILE"; }
title() { echo -e "${CYAN}$1${NC}" | tee -a "$LOG_FILE"; }
# --- 回滚函数 ---
rollback() {
if [ -n "$BACKUP_DIR" ] && [ -d "$BACKUP_DIR" ]; then
log "从备份恢复: $BACKUP_DIR"
if [ -f "$BACKUP_DIR/nginx.old" ] && [ -n "$NGINX_SBIN" ]; then
cp -f "$BACKUP_DIR/nginx.old" "$NGINX_SBIN"
log "二进制文件已回滚"
if [ -f "${NGINX_PID_PATH}.oldbin" ]; then
kill -QUIT $(cat "$NGINX_PID_PATH") 2>/dev/null || true
kill -HUP $(cat "${NGINX_PID_PATH}.oldbin") 2>/dev/null || true
elif [ -f "$NGINX_PID_PATH" ]; then
"$NGINX_SBIN" -s reload 2>/dev/null || true
fi
log "回滚完成"
fi
else
err "无备份可用,请手动恢复"
fi
}
cleanup() {
if [ "$ROLLBACK_NEEDED" = true ]; then
err "升级过程中出错,正在执行回滚..."
rollback
fi
if [ -n "${BUILD_DIR:-}" ] && [ -d "$BUILD_DIR" ]; then
rm -rf "$BUILD_DIR"
fi
}
trap cleanup EXIT
# --- 检查root权限 ---
check_root() {
if [ "$(id -u)" -ne 0 ]; then
err "此脚本需要root权限运行"
exit 1
fi
}
# === 检测安装方式 ===
# 全局数组:存储检测到的所有nginx实例
DETECTED_INSTANCES=()
DETECTED_LABELS=()
detect_install_method() {
log "正在检测Nginx安装方式..."
local instance_count=0
# --- 1. 检测Docker容器中的nginx ---
if command -v docker &>/dev/null; then
local containers=$(timeout 3 docker ps --format '{{.Names}}' 2>/dev/null || true)
if [ -n "$containers" ]; then
while read -r cname; do
[ -z "$cname" ] && continue
local cimage=$(docker inspect --format='{{.Config.Image}}' "$cname" 2>/dev/null || true)
local confidence=""
# 确认: 镜像名明确是nginx官方镜像
if echo "$cimage" | grep -qE '^nginx(:|$)' || echo "$cimage" | grep -qE '/nginx(:|$)'; then
confidence="confirmed"
else
# 疑似: 通过exec检测容器内是否有nginx进程
if timeout 3 docker exec "$cname" nginx -v &>/dev/null 2>&1; then
confidence="suspected"
fi
fi
if [ -n "$confidence" ]; then
local cver=$(timeout 3 docker exec "$cname" nginx -v 2>&1 | sed -n 's|.*nginx/\([0-9.]*\).*|\1|p' || echo "未知")
instance_count=$((instance_count+1))
DETECTED_INSTANCES+=("docker|${cname}|${cver}|${confidence}|${cimage}")
if [ "$confidence" = "confirmed" ]; then
DETECTED_LABELS+=("Docker容器: ${cname} (镜像: ${cimage}, 版本: ${cver})")
else
DETECTED_LABELS+=("Docker容器: ${cname} [疑似] (镜像: ${cimage}, 版本: ${cver})")
fi
fi
done <<< "$containers"
fi
fi
# --- 2. 检测宿主机上的nginx ---
local host_sbin=""
# 方式1: 通过PATH查找
if command -v nginx &>/dev/null; then
host_sbin=$(which nginx)
fi
# 方式2: 从运行中的进程获取(排除容器内进程)
if [ -z "$host_sbin" ]; then
local running_pid=$(pgrep -x nginx | head -1)
if [ -n "$running_pid" ] && [ -f "/proc/$running_pid/exe" ]; then
local cgroup_info=$(cat /proc/$running_pid/cgroup 2>/dev/null || true)
if ! echo "$cgroup_info" | grep -qE 'docker|containerd'; then
host_sbin=$(readlink -f "/proc/$running_pid/exe")
fi
fi
fi
# 方式3: 搜索本地文件系统
if [ -z "$host_sbin" ]; then
host_sbin=$(timeout 10 find / -xdev -name "nginx" -type f -executable \
-not -path "*/modules/*" -not -path "*/tmp/*" -not -path "*/build*" \
-not -path "*/overlay*" 2>/dev/null \
| while read -r f; do
"$f" -v 2>&1 | grep -q "nginx version" && echo "$f" && break
done || true)
fi
if [ -n "$host_sbin" ] && [ -f "$host_sbin" ]; then
# 解析软链接
local real_sbin=$(readlink -f "$host_sbin")
[ "$real_sbin" != "$host_sbin" ] && host_sbin="$real_sbin"
local host_ver=$("$host_sbin" -v 2>&1 | sed -n 's|.*nginx/\([0-9.]*\).*|\1|p')
local host_method=""
# 判断安装方式
if command -v rpm &>/dev/null && rpm -qf "$host_sbin" &>/dev/null; then
if command -v dnf &>/dev/null; then host_method="dnf"; else host_method="yum"; fi
elif command -v dpkg &>/dev/null && dpkg -S "$host_sbin" &>/dev/null 2>&1; then
host_method="apt"
elif command -v apk &>/dev/null && apk info --who-owns "$host_sbin" &>/dev/null 2>&1; then
host_method="apk"
elif command -v zypper &>/dev/null && rpm -qf "$host_sbin" &>/dev/null 2>&1; then
host_method="zypper"
else
host_method="source"
fi
instance_count=$((instance_count+1))
DETECTED_INSTANCES+=("${host_method}|${host_sbin}|${host_ver}||")
local method_label=""
case "$host_method" in
source) method_label="源码编译" ;;
yum|dnf) method_label="YUM/DNF" ;;
apt) method_label="APT" ;;
apk) method_label="APK" ;;
zypper) method_label="Zypper" ;;
esac
DETECTED_LABELS+=("宿主机${method_label}: ${host_sbin} (版本: ${host_ver})")
fi
# --- 3. 根据检测结果决定下一步 ---
if [ $instance_count -eq 0 ]; then
err "未检测到任何Nginx实例"
read -p "请输入nginx二进制文件完整路径(留空退出): " manual_path
if [ -z "$manual_path" ] || [ ! -f "$manual_path" ]; then
err "无法继续"
exit 1
fi
local manual_ver=$("$manual_path" -v 2>&1 | sed -n 's|.*nginx/\([0-9.]*\).*|\1|p')
INSTALL_METHOD="source"
NGINX_SBIN="$manual_path"
return
elif [ $instance_count -eq 1 ]; then
# 只有一个实例,自动选择
select_instance 0
else
# 多个实例,让用户选择
echo ""
title "检测到多个Nginx实例:"
echo ""
for i in "${!DETECTED_LABELS[@]}"; do
printf " [%d] %s\n" $((i+1)) "${DETECTED_LABELS[$i]}"
done
echo ""
read -p "请选择要操作的实例 (1-${instance_count}): " inst_choice
if ! echo "$inst_choice" | grep -qE '^[0-9]+$' || [ "$inst_choice" -lt 1 ] || [ "$inst_choice" -gt $instance_count ]; then
err "无效选择"
exit 1
fi
select_instance $((inst_choice-1))
fi
}
# 根据选择设置全局变量
select_instance() {
local idx=$1
local entry="${DETECTED_INSTANCES[$idx]}"
local method=$(echo "$entry" | cut -d'|' -f1)
local location=$(echo "$entry" | cut -d'|' -f2)
local version=$(echo "$entry" | cut -d'|' -f3)
local confidence=$(echo "$entry" | cut -d'|' -f4)
INSTALL_METHOD="$method"
CURRENT_VERSION="$version"
if [ "$method" = "docker" ]; then
DOCKER_CONTAINER="$location"
log "已选择Docker容器: $DOCKER_CONTAINER (版本: $version)"
if [ "$confidence" = "suspected" ]; then
warn "该容器为疑似Nginx(非官方nginx镜像),请确认容器内确实运行Nginx"
read -p "继续?(y/N): " confirm_suspected
if [[ ! "$confirm_suspected" =~ ^[yY]$ ]]; then
exit 0
fi
fi
else
NGINX_SBIN="$location"
log "已选择宿主机Nginx: $NGINX_SBIN (版本: $version, 方式: $method)"
fi
}
# === 获取当前nginx详细信息 ===
detect_nginx_info() {
local v_output=$("$NGINX_SBIN" -V 2>&1)
CURRENT_VERSION=$("$NGINX_SBIN" -v 2>&1 | sed -n 's|.*nginx/\([0-9.]*\).*|\1|p')
NGINX_PREFIX=$(echo "$v_output" | sed -n 's|.*--prefix=\([^ ]*\).*|\1|p')
NGINX_PREFIX=${NGINX_PREFIX:-/etc/nginx}
NGINX_PID_PATH=$(echo "$v_output" | sed -n 's|.*--pid-path=\([^ ]*\).*|\1|p')
CONFIGURE_ARGS=$(echo "$v_output" | grep "configure arguments:" | sed 's/configure arguments: //')
NGINX_CONF=$(echo "$v_output" | sed -n 's|.*--conf-path=\([^ ]*\).*|\1|p')
NGINX_CONF=${NGINX_CONF:-$NGINX_PREFIX/conf/nginx.conf}
# PID路径兜底
if [ -z "$NGINX_PID_PATH" ]; then
if [ -f "$NGINX_PREFIX/logs/nginx.pid" ]; then
NGINX_PID_PATH="$NGINX_PREFIX/logs/nginx.pid"
elif [ -f "/run/nginx.pid" ]; then
NGINX_PID_PATH="/run/nginx.pid"
elif [ -f "/var/run/nginx.pid" ]; then
NGINX_PID_PATH="/var/run/nginx.pid"
else
NGINX_PID_PATH="$NGINX_PREFIX/logs/nginx.pid"
fi
fi
echo ""
title "========== 当前Nginx信息 =========="
log "安装方式: $INSTALL_METHOD"
log "二进制路径: $NGINX_SBIN"
log "当前版本: $CURRENT_VERSION"
log "安装前缀: $NGINX_PREFIX"
log "配置文件: $NGINX_CONF"
log "PID文件: $NGINX_PID_PATH"
[ -n "$CONFIGURE_ARGS" ] && log "编译参数: $CONFIGURE_ARGS"
# 显示软链接信息
local symlinks=$(find /usr/local/sbin /usr/sbin /usr/bin /usr/local/bin -lname "*nginx*" 2>/dev/null || true)
if [ -n "$symlinks" ]; then
log "软链接: $symlinks"
fi
}
# === 获取目标版本 ===
get_target_version() {
echo ""
read -p "请输入要升级到的Nginx版本号(例如 1.30.1): " TARGET_VERSION
if [ -z "$TARGET_VERSION" ]; then
err "版本号不能为空"
exit 1
fi
if ! echo "$TARGET_VERSION" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+$'; then
err "版本号格式不正确,应为 x.y.z"
exit 1
fi
if [ "$TARGET_VERSION" = "$CURRENT_VERSION" ]; then
warn "目标版本与当前版本相同,无需升级"
exit 0
fi
log "目标版本: $TARGET_VERSION"
}
# === 备份 ===
do_backup() {
local backup_base=""
if [ "$INSTALL_METHOD" = "source" ]; then
backup_base="$NGINX_PREFIX"
else
backup_base="/opt/nginx_backup"
fi
BACKUP_DIR="${backup_base}/backup_$(date +%Y%m%d_%H%M%S)"
mkdir -p "$BACKUP_DIR"
log "正在备份到: $BACKUP_DIR"
# 备份二进制
cp -f "$NGINX_SBIN" "$BACKUP_DIR/nginx.old"
# 备份配置
local conf_dir=$(dirname "$NGINX_CONF")
cp -a "$conf_dir" "$BACKUP_DIR/conf"
# 备份html(源码安装才有)
[ -d "$NGINX_PREFIX/html" ] && cp -a "$NGINX_PREFIX/html" "$BACKUP_DIR/html"
# 记录元信息
echo "version=$CURRENT_VERSION" > "$BACKUP_DIR/meta.txt"
echo "method=$INSTALL_METHOD" >> "$BACKUP_DIR/meta.txt"
echo "sbin=$NGINX_SBIN" >> "$BACKUP_DIR/meta.txt"
echo "configure=$CONFIGURE_ARGS" >> "$BACKUP_DIR/meta.txt"
log "备份完成"
}
# ============================================================
# 源码编译升级流程
# ============================================================
source_check_deps() {
log "检查编译依赖..."
local missing=()
for cmd in gcc make wget tar; do
command -v $cmd &>/dev/null || missing+=("$cmd")
done
if [ ${#missing[@]} -gt 0 ]; then
warn "缺少依赖: ${missing[*]},正在安装..."
if command -v dnf &>/dev/null; then
dnf install -y gcc gcc-c++ make wget tar pcre-devel zlib-devel openssl-devel >> "$LOG_FILE" 2>&1
elif command -v yum &>/dev/null; then
yum install -y gcc gcc-c++ make wget tar pcre-devel zlib-devel openssl-devel >> "$LOG_FILE" 2>&1
elif command -v apt-get &>/dev/null; then
apt-get update && apt-get install -y build-essential wget libpcre3-dev zlib1g-dev libssl-dev >> "$LOG_FILE" 2>&1
elif command -v apk &>/dev/null; then
apk add gcc g++ make wget tar pcre-dev zlib-dev openssl-dev linux-headers >> "$LOG_FILE" 2>&1
elif command -v zypper &>/dev/null; then
zypper install -y gcc gcc-c++ make wget tar pcre-devel zlib-devel libopenssl-devel >> "$LOG_FILE" 2>&1
else
err "无法自动安装依赖,请手动安装: ${missing[*]}"
exit 1
fi
fi
log "依赖检查通过"
}
source_download() {
BUILD_DIR=$(mktemp -d /tmp/nginx_build_XXXXXX)
local url="https://nginx.org/download/nginx-${TARGET_VERSION}.tar.gz"
log "下载源码: $url"
# 检查磁盘空间(编译至少需要200MB)
local free_mb=$(df -m /tmp | awk 'NR==2{print $4}')
if [ -n "$free_mb" ] && [ "$free_mb" -lt 200 ]; then
err "/tmp 剩余空间不足(${free_mb}MB),编译至少需要200MB"
exit 1
fi
# 兼容老版本wget(不一定支持--show-progress)
if wget --help 2>&1 | grep -q '\-\-show-progress'; then
wget -q --show-progress -O "$BUILD_DIR/nginx-${TARGET_VERSION}.tar.gz" "$url" 2>&1 | tee -a "$LOG_FILE" || true
else
wget -O "$BUILD_DIR/nginx-${TARGET_VERSION}.tar.gz" "$url" 2>&1 | tee -a "$LOG_FILE" || true
fi
# 检查下载结果
echo ""
if [ ! -f "$BUILD_DIR/nginx-${TARGET_VERSION}.tar.gz" ]; then
err "下载失败,请检查版本号或网络"
exit 1
fi
local filesize=$(stat -c%s "$BUILD_DIR/nginx-${TARGET_VERSION}.tar.gz" 2>/dev/null || stat -f%z "$BUILD_DIR/nginx-${TARGET_VERSION}.tar.gz")
if [ "$filesize" -lt 1024 ]; then
err "文件异常(${filesize}字节),版本号可能不存在"
exit 1
fi
tar -zxf "$BUILD_DIR/nginx-${TARGET_VERSION}.tar.gz" -C "$BUILD_DIR"
[ ! -d "$BUILD_DIR/nginx-${TARGET_VERSION}" ] && err "解压失败" && exit 1
log "源码准备完成"
}
source_compile() {
log "开始编译 Nginx ${TARGET_VERSION}..."
cd "$BUILD_DIR/nginx-${TARGET_VERSION}"
log "执行 configure..."
if ! eval "./configure $CONFIGURE_ARGS" >> "$LOG_FILE" 2>&1; then
err "configure 失败,详见: $LOG_FILE"
exit 1
fi
local cores=$(nproc 2>/dev/null || echo 2)
log "编译中(${cores}核心并行),请耐心等待..."
# 后台编译并显示进度
make -j"$cores" >> "$LOG_FILE" 2>&1 &
local make_pid=$!
local elapsed=0
while kill -0 "$make_pid" 2>/dev/null; do
sleep 5
elapsed=$((elapsed+5))
printf "\r 编译中... 已耗时 ${elapsed}s"
done
printf "\r \r"
wait "$make_pid" || { err "编译失败,详见: $LOG_FILE"; exit 1; }
[ ! -f "objs/nginx" ] && err "编译产物不存在" && exit 1
local new_ver=$(./objs/nginx -v 2>&1 | sed -n 's|.*nginx/\([0-9.]*\).*|\1|p')
if [ "$new_ver" != "$TARGET_VERSION" ]; then
err "版本不匹配: 期望 $TARGET_VERSION, 实际 $new_ver"
exit 1
fi
log "编译成功: $new_ver"
}
source_test_config() {
log "用新二进制测试现有配置..."
cd "$BUILD_DIR/nginx-${TARGET_VERSION}"
if ! ./objs/nginx -t -c "$NGINX_CONF" >> "$LOG_FILE" 2>&1; then
err "配置文件与新版本不兼容!请先修复配置"
./objs/nginx -t -c "$NGINX_CONF" 2>&1 | tee -a "$LOG_FILE"
exit 1
fi
log "配置兼容性测试通过"
}
source_graceful_upgrade() {
log "开始平滑升级..."
ROLLBACK_NEEDED=true
cd "$BUILD_DIR/nginx-${TARGET_VERSION}"
cp -f objs/nginx "$NGINX_SBIN"
log "二进制已替换"
# nginx未运行的情况
if [ ! -f "$NGINX_PID_PATH" ] || ! kill -0 $(cat "$NGINX_PID_PATH" 2>/dev/null) 2>/dev/null; then
warn "Nginx当前未运行,直接启动新版本"
if "$NGINX_SBIN" -c "$NGINX_CONF" >> "$LOG_FILE" 2>&1; then
ROLLBACK_NEEDED=false
log "新版本已启动"
return
else
err "启动失败"
return 1
fi
fi
local old_pid=$(cat "$NGINX_PID_PATH")
log "当前master PID: $old_pid"
# 尝试USR2热升级
if try_usr2_upgrade "$old_pid"; then
ROLLBACK_NEEDED=false
log "USR2 热升级完成(零中断)"
return
fi
# USR2失败,回退到优雅重启方式
warn "USR2热升级失败,回退到优雅重启方式(中断时间极短)"
fallback_graceful_restart "$old_pid"
}
try_usr2_upgrade() {
local old_pid=$1
# 检查nginx是否以完整路径启动(决定USR2能否成功)
local cmdline=$(tr '\0' ' ' < /proc/$old_pid/cmdline 2>/dev/null | awk '{print $1}')
if [ -n "$cmdline" ] && [[ "$cmdline" != /* ]]; then
warn "nginx以相对路径 '$cmdline' 启动,USR2可能失败"
warn "直接使用优雅重启方式"
return 1
fi
log "发送 USR2 信号..."
kill -USR2 "$old_pid"
local i=0
while [ ! -f "${NGINX_PID_PATH}.oldbin" ] && [ $i -lt 10 ]; do
sleep 1; i=$((i+1))
done
if [ ! -f "${NGINX_PID_PATH}.oldbin" ]; then
warn "USR2: 等待10秒后新master未启动"
return 1
fi
local new_pid=$(cat "$NGINX_PID_PATH")
log "新master PID: $new_pid"
# 优雅关闭旧worker
log "发送 WINCH 信号,关闭旧worker..."
kill -WINCH "$old_pid"
i=0
while [ $i -lt 60 ]; do
local workers=$(ps -o pid= --ppid "$old_pid" 2>/dev/null | wc -l || \
pgrep -P "$old_pid" 2>/dev/null | wc -l || echo 1)
[ "$workers" -eq 0 ] && break
sleep 1; i=$((i+1))
done
# 验证新进程
if ! kill -0 "$new_pid" 2>/dev/null; then
err "新master异常退出"
return 1
fi
# 关闭旧master
log "发送 QUIT 信号,关闭旧master..."
kill -QUIT "$old_pid" 2>/dev/null || true
i=0
while kill -0 "$old_pid" 2>/dev/null && [ $i -lt 10 ]; do
sleep 1; i=$((i+1))
done
rm -f "${NGINX_PID_PATH}.oldbin" 2>/dev/null
return 0
}
fallback_graceful_restart() {
local old_pid=$1
log "优雅停止旧进程..."
kill -QUIT "$old_pid"
# 等待旧进程退出
local i=0
while kill -0 "$old_pid" 2>/dev/null && [ $i -lt 30 ]; do
sleep 1; i=$((i+1))
done
# 如果QUIT没退出,强制停止
if kill -0 "$old_pid" 2>/dev/null; then
warn "QUIT超时,发送TERM信号..."
kill -TERM "$old_pid" 2>/dev/null || true
sleep 2
fi
# 用完整路径启动新版本
log "启动新版本: $NGINX_SBIN"
if "$NGINX_SBIN" -c "$NGINX_CONF" >> "$LOG_FILE" 2>&1; then
ROLLBACK_NEEDED=false
log "新版本已启动"
else
err "新版本启动失败"
return 1
fi
}
upgrade_by_source() {
source_check_deps
do_backup
source_download
source_compile
source_test_config
source_graceful_upgrade
}
# ============================================================
# YUM/DNF 包管理器升级流程
# ============================================================
upgrade_by_yum() {
do_backup
# 检测nginx源
local repo_file=$(grep -rl "nginx.org" /etc/yum.repos.d/ 2>/dev/null | head -1)
if [ -z "$repo_file" ]; then
log "未检测到nginx官方源,正在添加..."
cat > /etc/yum.repos.d/nginx.repo << 'REPO'
[nginx-stable]
name=nginx stable repo
baseurl=http://nginx.org/packages/centos/$releasever/$basearch/
gpgcheck=1
enabled=1
gpgkey=https://nginx.org/keys/nginx_signing.key
module_hotfixes=true
[nginx-mainline]
name=nginx mainline repo
baseurl=http://nginx.org/packages/mainline/centos/$releasever/$basearch/
gpgcheck=1
enabled=0
gpgkey=https://nginx.org/keys/nginx_signing.key
module_hotfixes=true
REPO
log "nginx官方源已添加"
fi
# 判断目标版本属于stable还是mainline
local minor=$(echo "$TARGET_VERSION" | cut -d. -f2)
if [ $((minor % 2)) -eq 1 ]; then
log "目标版本为mainline分支,启用mainline源..."
if command -v dnf &>/dev/null; then
dnf config-manager --set-enabled nginx-mainline 2>/dev/null || \
sed -i '/\[nginx-mainline\]/,/^$/s/enabled=0/enabled=1/' /etc/yum.repos.d/nginx.repo
else
sed -i '/\[nginx-mainline\]/,/^$/s/enabled=0/enabled=1/' /etc/yum.repos.d/nginx.repo
fi
fi
# 检查目标版本是否可用
log "检查目标版本 $TARGET_VERSION 是否可用..."
local available=""
if command -v dnf &>/dev/null; then
available=$(dnf list available nginx --showduplicates 2>/dev/null | grep "$TARGET_VERSION" || true)
else
available=$(yum list available nginx --showduplicates 2>/dev/null | grep "$TARGET_VERSION" || true)
fi
if [ -z "$available" ]; then
err "在YUM源中未找到版本 $TARGET_VERSION"
err "可用版本:"
yum list available nginx --showduplicates 2>/dev/null | grep nginx | tail -10 | tee -a "$LOG_FILE"
exit 1
fi
log "开始升级..."
if command -v dnf &>/dev/null; then
dnf upgrade -y "nginx-${TARGET_VERSION}" 2>&1 | tee -a "$LOG_FILE"
else
yum update -y "nginx-${TARGET_VERSION}" 2>&1 | tee -a "$LOG_FILE"
fi
# 重载配置
if systemctl is-active nginx &>/dev/null; then
log "重载Nginx配置..."
nginx -t >> "$LOG_FILE" 2>&1 && systemctl reload nginx
fi
log "YUM升级完成"
}
# ============================================================
# APT 包管理器升级流程
# ============================================================
upgrade_by_apt() {
do_backup
# 检测nginx源
local has_repo=$(grep -r "nginx.org" /etc/apt/sources.list /etc/apt/sources.list.d/ 2>/dev/null | head -1)
if [ -z "$has_repo" ]; then
log "未检测到nginx官方源,正在添加..."
apt-get install -y curl gnupg2 ca-certificates lsb-release >> "$LOG_FILE" 2>&1
curl -fsSL https://nginx.org/keys/nginx_signing.key | gpg --dearmor -o /usr/share/keyrings/nginx-archive-keyring.gpg
local codename=$(lsb_release -cs)
local os_name=$(. /etc/os-release && echo "$ID")
echo "deb [signed-by=/usr/share/keyrings/nginx-archive-keyring.gpg] http://nginx.org/packages/${os_name} ${codename} nginx" \
> /etc/apt/sources.list.d/nginx.list
log "nginx官方源已添加"
fi
apt-get update >> "$LOG_FILE" 2>&1
# 检查目标版本是否可用
log "检查目标版本 $TARGET_VERSION 是否可用..."
local available=$(apt-cache showpkg nginx 2>/dev/null | grep "$TARGET_VERSION" || true)
if [ -z "$available" ]; then
err "在APT源中未找到版本 $TARGET_VERSION"
err "可用版本:"
apt-cache policy nginx 2>/dev/null | tee -a "$LOG_FILE"
exit 1
fi
log "开始升级..."
apt-get install -y --only-upgrade "nginx=${TARGET_VERSION}*" 2>&1 | tee -a "$LOG_FILE"
if systemctl is-active nginx &>/dev/null; then
log "重载Nginx配置..."
nginx -t >> "$LOG_FILE" 2>&1 && systemctl reload nginx
fi
log "APT升级完成"
}
# ============================================================
# APK 包管理器升级流程 (Alpine)
# ============================================================
upgrade_by_apk() {
do_backup
log "Alpine APK 升级..."
# Alpine通过更新仓库获取新版本
apk update >> "$LOG_FILE" 2>&1
local available=$(apk policy nginx 2>/dev/null | grep "$TARGET_VERSION" || true)
if [ -z "$available" ]; then
warn "APK源中未找到精确版本 $TARGET_VERSION,尝试升级到最新可用版本"
fi
apk upgrade nginx 2>&1 | tee -a "$LOG_FILE"
if rc-service nginx status &>/dev/null 2>&1; then
log "重载Nginx..."
nginx -t >> "$LOG_FILE" 2>&1 && rc-service nginx reload
elif systemctl is-active nginx &>/dev/null; then
nginx -t >> "$LOG_FILE" 2>&1 && systemctl reload nginx
fi
log "APK升级完成"
}
# ============================================================
# Zypper 包管理器升级流程 (openSUSE/SLES)
# ============================================================
upgrade_by_zypper() {
do_backup
log "Zypper 升级..."
zypper refresh >> "$LOG_FILE" 2>&1
local available=$(zypper search -s nginx 2>/dev/null | grep "$TARGET_VERSION" || true)
if [ -z "$available" ]; then
err "Zypper源中未找到版本 $TARGET_VERSION"
err "可用版本:"
zypper search -s nginx 2>/dev/null | tee -a "$LOG_FILE"
exit 1
fi
zypper update -y nginx 2>&1 | tee -a "$LOG_FILE"
if systemctl is-active nginx &>/dev/null; then
log "重载Nginx..."
nginx -t >> "$LOG_FILE" 2>&1 && systemctl reload nginx
fi
log "Zypper升级完成"
}
# ============================================================
# Docker 升级流程
# ============================================================
upgrade_by_docker() {
log "Docker方式升级"
echo ""
log "检测到Nginx容器: $DOCKER_CONTAINER"
# 检测是否由docker-compose管理
local compose_file=""
local compose_project=""
local compose_service=""
# 方式1: 从容器label获取compose信息
compose_file=$(docker inspect --format='{{index .Config.Labels "com.docker.compose.project.config_files"}}' "$DOCKER_CONTAINER" 2>/dev/null || true)
compose_project=$(docker inspect --format='{{index .Config.Labels "com.docker.compose.project"}}' "$DOCKER_CONTAINER" 2>/dev/null || true)
compose_service=$(docker inspect --format='{{index .Config.Labels "com.docker.compose.service"}}' "$DOCKER_CONTAINER" 2>/dev/null || true)
# 方式2: 从working_dir获取
if [ -z "$compose_file" ] && [ -n "$compose_project" ]; then
local working_dir=$(docker inspect --format='{{index .Config.Labels "com.docker.compose.project.working_dir"}}' "$DOCKER_CONTAINER" 2>/dev/null || true)
if [ -n "$working_dir" ]; then
for f in docker-compose.yml docker-compose.yaml compose.yml compose.yaml; do
[ -f "$working_dir/$f" ] && compose_file="$working_dir/$f" && break
done
fi
fi
if [ -n "$compose_file" ] && [ -f "$compose_file" ]; then
upgrade_docker_compose "$compose_file" "$compose_service"
else
upgrade_docker_plain
fi
}
# --- docker-compose 方式升级 ---
upgrade_docker_compose() {
local compose_file=$1
local service_name=$2
log "检测到 docker-compose 管理"
log " Compose文件: $compose_file"
log " 服务名: $service_name"
echo ""
# 获取当前镜像
local current_image=$(docker inspect --format='{{.Config.Image}}' "$DOCKER_CONTAINER")
local new_image="nginx:${TARGET_VERSION}"
log " 当前镜像: $current_image"
log " 目标镜像: $new_image"
echo ""
echo -e " ${GREEN}升级方式${NC}: 修改compose文件中的镜像版本,然后重建容器"
echo -e " ${YELLOW}注意${NC}: 挂载卷、端口、网络等配置由compose文件管理,不会丢失"
echo ""
read -p "确认升级?(y/N): " confirm
if [[ ! "$confirm" =~ ^[yY]$ ]]; then
log "用户取消"
exit 0
fi
# 备份compose文件
cp -f "$compose_file" "${compose_file}.bak.$(date +%Y%m%d_%H%M%S)"
log "已备份compose文件"
# 修改镜像版本(只替换版本号,保留仓库地址和后缀)
local compose_dir=$(dirname "$compose_file")
local old_image_line=$(grep "image:.*nginx" "$compose_file" | head -1)
if [ -z "$old_image_line" ]; then
warn "未能在compose文件中找到nginx镜像配置"
warn "请手动修改 $compose_file 中的镜像版本为 $TARGET_VERSION"
exit 1
fi
log "当前镜像配置: $old_image_line"
# 提取当前镜像全名(去掉 image: 前缀和空格)
local old_full_image=$(echo "$old_image_line" | sed 's/.*image:[[:space:]]*//' | sed 's/[[:space:]]*$//')
# 智能替换版本号:保留仓库前缀和后缀(如 -alpine)
# 例: registry.cn/nginx:1.28.0-alpine -> registry.cn/nginx:1.30.1-alpine
local new_full_image=$(echo "$old_full_image" | sed "s/\([0-9]\+\.[0-9]\+\.[0-9]\+\)/$TARGET_VERSION/")
echo ""
echo -e " 镜像变更: ${RED}${old_full_image}${NC} -> ${GREEN}${new_full_image}${NC}"
echo ""
read -p "镜像名称是否正确?(y/N): " img_confirm
if [[ ! "$img_confirm" =~ ^[yY]$ ]]; then
read -p "请输入完整的目标镜像名称: " new_full_image
fi
sed -i "s|${old_full_image}|${new_full_image}|" "$compose_file"
log "已更新: $old_full_image -> $new_full_image"
# 拉取新镜像
log "拉取新镜像..."
local compose_cmd=""
if command -v docker-compose &>/dev/null; then
compose_cmd="docker-compose"
elif docker compose version &>/dev/null 2>&1; then
compose_cmd="docker compose"
else
err "未找到 docker-compose 或 docker compose 命令"
exit 1
fi
cd "$compose_dir"
$compose_cmd -f "$compose_file" pull "$service_name" 2>&1 | tee -a "$LOG_FILE"
# 重建容器
log "重建容器(自动停止旧容器并启动新容器)..."
$compose_cmd -f "$compose_file" up -d "$service_name" 2>&1 | tee -a "$LOG_FILE"
# 验证
sleep 2
if docker ps | grep -q "$DOCKER_CONTAINER\|$service_name"; then
log "docker-compose 升级成功"
else
err "容器未正常启动,正在回滚compose文件..."
local latest_bak=$(ls -t "${compose_file}.bak."* 2>/dev/null | head -1)
[ -n "$latest_bak" ] && cp -f "$latest_bak" "$compose_file"
$compose_cmd -f "$compose_file" up -d "$service_name" 2>&1 | tee -a "$LOG_FILE"
exit 1
fi
}
# --- 普通docker方式升级 ---
upgrade_docker_plain() {
log "普通Docker容器(非docker-compose)"
echo ""
local current_image=$(docker inspect --format='{{.Config.Image}}' "$DOCKER_CONTAINER")
local new_image="nginx:${TARGET_VERSION}"
log " 当前镜像: $current_image"
log " 目标镜像: $new_image"
echo ""
echo -e " ${YELLOW}注意${NC}: 将停止旧容器,用相同参数启动新容器"
echo ""
read -p "确认升级?(y/N): " confirm
if [[ ! "$confirm" =~ ^[yY]$ ]]; then
log "用户取消"
exit 0
fi
log "拉取镜像: $new_image"
docker pull "$new_image" 2>&1 | tee -a "$LOG_FILE"
# 导出容器运行参数
local run_cmd=$(docker inspect --format='{{range .Mounts}}-v {{.Source}}:{{.Destination}} {{end}}' "$DOCKER_CONTAINER")
local ports=$(docker inspect --format='{{range $p, $conf := .NetworkSettings.Ports}}{{range $conf}}-p {{.HostPort}}:{{$p}} {{end}}{{end}}' "$DOCKER_CONTAINER" | sed 's|/tcp||g')
local container_name=$(docker inspect --format='{{.Name}}' "$DOCKER_CONTAINER" | sed 's|^/||')
local restart_policy=$(docker inspect --format='{{.HostConfig.RestartPolicy.Name}}' "$DOCKER_CONTAINER")
local restart_flag=""
[ -n "$restart_policy" ] && [ "$restart_policy" != "no" ] && restart_flag="--restart=$restart_policy"
log "停止旧容器..."
docker stop "$DOCKER_CONTAINER" >> "$LOG_FILE" 2>&1
docker rename "$DOCKER_CONTAINER" "${container_name}_old_$(date +%s)" >> "$LOG_FILE" 2>&1
log "启动新容器..."
eval "docker run -d --name $container_name $restart_flag $ports $run_cmd $new_image" 2>&1 | tee -a "$LOG_FILE"
if docker ps | grep -q "$container_name"; then
log "Docker升级成功"
log "旧容器已重命名保留,确认无误后可删除"
else
err "新容器启动失败,正在恢复..."
docker rm "$container_name" 2>/dev/null || true
local old_name=$(docker ps -a --format '{{.Names}}' | grep "${container_name}_old_" | head -1)
[ -n "$old_name" ] && docker rename "$old_name" "$container_name" && docker start "$container_name"
exit 1
fi
}
# ============================================================
# 升级后验证
# ============================================================
post_check() {
echo ""
title "========== 升级结果 =========="
if [ "$INSTALL_METHOD" = "docker" ]; then
local final_ver=$(docker exec "$DOCKER_CONTAINER" nginx -v 2>&1 | sed -n 's|.*nginx/\([0-9.]*\).*|\1|p' || echo "未知")
else
local final_ver=$("$NGINX_SBIN" -v 2>&1 | sed -n 's|.*nginx/\([0-9.]*\).*|\1|p')
fi
log "升级方式: $INSTALL_METHOD"
log "原版本: $CURRENT_VERSION"
log "新版本: $final_ver"
if [ "$final_ver" = "$TARGET_VERSION" ]; then
log "版本验证: 通过"
else
err "版本验证: 失败(期望 $TARGET_VERSION,实际 $final_ver)"
fi
if [ "$INSTALL_METHOD" != "docker" ]; then
if [ -f "$NGINX_PID_PATH" ] && kill -0 $(cat "$NGINX_PID_PATH") 2>/dev/null; then
log "进程状态: 运行中 (PID: $(cat "$NGINX_PID_PATH"))"
else
warn "进程状态: 未运行"
fi
if "$NGINX_SBIN" -t &>/dev/null; then
log "配置测试: 通过"
else
err "配置测试: 失败"
fi
fi
echo ""
[ -n "$BACKUP_DIR" ] && log "备份位置: $BACKUP_DIR"
log "升级日志: $LOG_FILE"
if [ "$INSTALL_METHOD" = "source" ] && [ -n "$BACKUP_DIR" ]; then
log "回滚命令: cp $BACKUP_DIR/nginx.old $NGINX_SBIN && nginx -s reload"
fi
echo ""
}
# ============================================================
# 回滚功能
# ============================================================
do_rollback() {
log "正在查找可用备份..."
# 查找备份目录
local backup_base=""
if [ -n "$NGINX_PREFIX" ]; then
backup_base="$NGINX_PREFIX"
fi
# 同时搜索两个可能的备份位置
local backups=()
if [ -n "$backup_base" ] && [ -d "$backup_base" ]; then
while IFS= read -r dir; do
backups+=("$dir")
done < <(find "$backup_base" -maxdepth 1 -type d -name "backup_*" 2>/dev/null | sort -r)
fi
if [ -d "/opt/nginx_backup" ]; then
while IFS= read -r dir; do
backups+=("$dir")
done < <(find "/opt/nginx_backup" -maxdepth 1 -type d -name "backup_*" 2>/dev/null | sort -r)
fi
if [ ${#backups[@]} -eq 0 ]; then
err "未找到任何备份目录"
err "备份通常位于: ${NGINX_PREFIX}/backup_* 或 /opt/nginx_backup/backup_*"
exit 1
fi
echo ""
title "可用备份列表:"
echo ""
for i in "${!backups[@]}"; do
local bdir="${backups[$i]}"
local ver="未知"
local method="未知"
local btime=$(basename "$bdir" | sed 's/backup_//')
# 格式化时间: 20260515_113110 -> 2026-05-15 11:31:10
local ftime=$(echo "$btime" | sed 's/\([0-9]\{4\}\)\([0-9]\{2\}\)\([0-9]\{2\}\)_\([0-9]\{2\}\)\([0-9]\{2\}\)\([0-9]\{2\}\)/\1-\2-\3 \4:\5:\6/')
if [ -f "$bdir/meta.txt" ]; then
ver=$(grep "^version=" "$bdir/meta.txt" | cut -d= -f2)
method=$(grep "^method=" "$bdir/meta.txt" | cut -d= -f2)
elif [ -f "$bdir/version.txt" ]; then
ver=$(cat "$bdir/version.txt")
fi
printf " [%d] 版本: ${CYAN}%s${NC} | 方式: %s | 时间: %s\n" $((i+1)) "$ver" "$method" "$ftime"
done
echo ""
read -p "请选择要回滚到的备份编号: " choice
if ! echo "$choice" | grep -qE '^[0-9]+$' || [ "$choice" -lt 1 ] || [ "$choice" -gt ${#backups[@]} ]; then
err "无效选择"
exit 1
fi
local selected="${backups[$((choice-1))]}"
log "选择备份: $selected"
# 验证备份完整性
if [ ! -f "$selected/nginx.old" ]; then
err "备份不完整: 缺少 nginx.old 二进制文件"
exit 1
fi
# 执行回滚
log "正在恢复二进制文件..."
cp -f "$selected/nginx.old" "$NGINX_SBIN"
# 询问是否恢复配置
if [ -d "$selected/conf" ]; then
echo ""
echo -e "${YELLOW}是否恢复配置文件?${NC}"
echo -e " ${GREEN}y${NC} = 同时将配置文件恢复到备份时的状态(覆盖当前配置)"
echo -e " ${RED}N${NC} = 只回滚二进制版本,保留当前配置文件不动"
echo ""
read -p "恢复配置文件?(y/N): " restore_conf
if [[ "$restore_conf" =~ ^[yY]$ ]]; then
local conf_dir=$(dirname "$NGINX_CONF")
cp -a "$selected/conf/"* "$conf_dir/"
log "配置文件已恢复"
fi
fi
# 测试配置
if ! "$NGINX_SBIN" -t >> "$LOG_FILE" 2>&1; then
err "回滚后配置测试失败!"
"$NGINX_SBIN" -t 2>&1
exit 1
fi
# 重启nginx以加载旧二进制(reload只重载配置,不换二进制)
if [ -f "$NGINX_PID_PATH" ] && kill -0 $(cat "$NGINX_PID_PATH") 2>/dev/null; then
log "重启Nginx以加载旧版本二进制..."
local pid=$(cat "$NGINX_PID_PATH")
kill -QUIT "$pid" 2>/dev/null || true
local i=0
while kill -0 "$pid" 2>/dev/null && [ $i -lt 15 ]; do
sleep 1; i=$((i+1))
done
"$NGINX_SBIN" -c "$NGINX_CONF"
else
log "启动Nginx..."
"$NGINX_SBIN" -c "$NGINX_CONF"
fi
local rolled_ver=$("$NGINX_SBIN" -v 2>&1 | sed -n 's|.*nginx/\([0-9.]*\).*|\1|p')
echo ""
log "回滚完成!"
log "当前版本: $rolled_ver"
if [ -f "$NGINX_PID_PATH" ] && kill -0 $(cat "$NGINX_PID_PATH") 2>/dev/null; then
log "进程状态: 运行中 (PID: $(cat "$NGINX_PID_PATH"))"
fi
}
# ============================================================
# 主流程
# ============================================================
main() {
echo ""
echo "============================================"
echo " Nginx 通用升级/回滚脚本"
echo " 支持: 源码 / YUM / APT / APK / Zypper / Docker"
echo "============================================"
echo ""
check_root
echo -e "请选择操作:"
echo ""
echo -e " [1] ${GREEN}升级 Nginx${NC}"
echo -e " [2] ${YELLOW}回滚 Nginx${NC}"
echo -e " [3] ${CYAN}清理旧备份${NC}"
echo ""
read -p "请输入选择 (1/2/3): " action_choice
case "$action_choice" in
1)
detect_install_method
if [ "$INSTALL_METHOD" != "docker" ]; then
detect_nginx_info
fi
get_target_version
echo ""
title "即将执行升级:"
echo " 安装方式: $INSTALL_METHOD"
echo " ${CURRENT_VERSION} -> ${TARGET_VERSION}"
[ "$INSTALL_METHOD" = "source" ] && echo " 编译参数: $CONFIGURE_ARGS"
echo ""
case "$INSTALL_METHOD" in
source) upgrade_by_source ;;
yum) upgrade_by_yum ;;
dnf) upgrade_by_yum ;;
apt) upgrade_by_apt ;;
apk) upgrade_by_apk ;;
zypper) upgrade_by_zypper ;;
docker) upgrade_by_docker ;;
*) err "未知安装方式: $INSTALL_METHOD"; exit 1 ;;
esac
post_check
if [ -n "$BUILD_DIR" ] && [ -d "$BUILD_DIR" ]; then
rm -rf "$BUILD_DIR"
BUILD_DIR=""
fi
log "全部完成!"
;;
2)
detect_install_method
if [ "$INSTALL_METHOD" != "docker" ]; then
detect_nginx_info
fi
do_rollback
;;
3)
detect_install_method
if [ "$INSTALL_METHOD" != "docker" ]; then
detect_nginx_info
fi
clean_backups
;;
*)
err "无效选择,请输入 1、2 或 3"
exit 1
;;
esac
}
# === 清理旧备份 ===
clean_backups() {
log "正在查找备份..."
local backup_base=""
[ -n "$NGINX_PREFIX" ] && backup_base="$NGINX_PREFIX"
local backups=()
if [ -n "$backup_base" ] && [ -d "$backup_base" ]; then
while IFS= read -r dir; do
backups+=("$dir")
done < <(find "$backup_base" -maxdepth 1 -type d -name "backup_*" 2>/dev/null | sort -r)
fi
if [ -d "/opt/nginx_backup" ]; then
while IFS= read -r dir; do
backups+=("$dir")
done < <(find "/opt/nginx_backup" -maxdepth 1 -type d -name "backup_*" 2>/dev/null | sort -r)
fi
if [ ${#backups[@]} -eq 0 ]; then
log "没有找到任何备份"
exit 0
fi
echo ""
title "当前备份列表:"
echo ""
local total_size=0
for i in "${!backups[@]}"; do
local bdir="${backups[$i]}"
local ver="未知"
local btime=$(basename "$bdir" | sed 's/backup_//')
local ftime=$(echo "$btime" | sed 's/\([0-9]\{4\}\)\([0-9]\{2\}\)\([0-9]\{2\}\)_\([0-9]\{2\}\)\([0-9]\{2\}\)\([0-9]\{2\}\)/\1-\2-\3 \4:\5:\6/')
local size=$(du -sh "$bdir" 2>/dev/null | awk '{print $1}')
[ -f "$bdir/meta.txt" ] && ver=$(grep "^version=" "$bdir/meta.txt" | cut -d= -f2)
printf " [%d] 版本: %s | 时间: %s | 大小: %s\n" $((i+1)) "$ver" "$ftime" "$size"
done
echo ""
echo -e " ${GREEN}a${NC} = 只保留最新一个备份,删除其余所有"
echo -e " ${YELLOW}编号${NC} = 删除指定备份(多个用空格分隔,如: 2 3 4)"
echo -e " ${RED}q${NC} = 取消"
echo ""
read -p "请输入选择: " clean_choice
if [ "$clean_choice" = "q" ]; then
exit 0
elif [ "$clean_choice" = "a" ]; then
for i in "${!backups[@]}"; do
[ $i -eq 0 ] && continue
rm -rf "${backups[$i]}"
log "已删除: ${backups[$i]}"
done
log "清理完成,保留最新备份: ${backups[0]}"
else
for idx in $clean_choice; do
if echo "$idx" | grep -qE '^[0-9]+$' && [ "$idx" -ge 1 ] && [ "$idx" -le ${#backups[@]} ]; then
rm -rf "${backups[$((idx-1))]}"
log "已删除: ${backups[$((idx-1))]}"
else
warn "跳过无效编号: $idx"
fi
done
log "清理完成"
fi
}
main "$@"