Files
Nexus/deploy/nexus-1panel.sh
T

845 lines
27 KiB
Bash
Raw Normal View History

#!/usr/bin/env bash
# Nexus 6.0 — 1Panel + Docker 一键安装 / 升级(参数已全部内置,无需 export)
#
# 新机 1Panel 终端一条命令:
# curl -fsSL "http://66.154.115.8:3000/admin/Nexus/raw/branch/main/deploy/nexus-1panel.sh" | bash -s install
#
# 已 clone 到 /opt/nexus:
# cd /opt/nexus && bash deploy/nexus-1panel.sh install --skip-clone
#
# 其他:
# bash deploy/nexus-1panel.sh install --profile 4c16g
# bash deploy/nexus-1panel.sh upgrade
# bash deploy/nexus-1panel.sh check
# bash deploy/nexus-1panel.sh ports
#
# 公共仓库匿名 clone,无需 Gitea 令牌。
# 仅私人仓库可选: deploy/nexus-1panel.secrets.sh 或 /root/.nexus-deploy.env
# 或 clone 时 URL 带令牌,脚本会从 git remote 自动解析
set -euo pipefail
# ═══════════════════════════════════════════════════════════════════
# 内置路径/域名(无敏感信息,可提交 Git)
# ═══════════════════════════════════════════════════════════════════
NEXUS_ROOT="${NEXUS_ROOT:-/opt/nexus}"
NEXUS_GITEA_HOST="${NEXUS_GITEA_HOST:-66.154.115.8:3000}"
NEXUS_GITEA_REPO="${NEXUS_GITEA_REPO:-admin/Nexus.git}"
NEXUS_DOMAIN="${NEXUS_DOMAIN:-api.synaglobal.vip}"
NEXUS_PROFILE="${NEXUS_PROFILE:-2c8g}"
NEXUS_FROM_ENV="${NEXUS_FROM_ENV:-/www/wwwroot/api.synaglobal.vip/.env}"
NEXUS_BACKUP_DIR="${NEXUS_BACKUP_DIR:-/var/backups/nexus}"
NEXUS_DEPLOY_ENV="${NEXUS_DEPLOY_ENV:-/root/.nexus-deploy.env}"
NEXUS_PUBLISH_PORT="${NEXUS_PUBLISH_PORT:-8600}"
GIT_BRANCH="${NEXUS_GIT_BRANCH:-main}"
COMPOSE_FILE="docker/docker-compose.prod.yml"
ENV_PROD="docker/.env.prod"
HOST_PROFILE_FILE="docker/.host-profile"
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
CYAN='\033[0;36m'
NC='\033[0m'
info() { echo -e "${GREEN}[INFO]${NC} $*"; }
warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
error() { echo -e "${RED}[ERROR]${NC} $*" >&2; }
step() { echo -e "${CYAN}[STEP]${NC} $*"; }
SKIP_CLONE=false
FRESH_INSTALL=false
REUSE_SECRETS=false
FROM_ENV="${NEXUS_FROM_ENV}"
NO_BACKUP=false
PRUNE_IMAGES=false
CHECK_ONLY=false
DRY_RUN=false
CUSTOM_CPUS=""
CUSTOM_MEM_GB=""
usage() {
cat <<'EOF'
Nexus 1Panel 一键脚本
bash deploy/nexus-1panel.sh install [选项] 克隆/配置/启动
bash deploy/nexus-1panel.sh upgrade [选项] 拉代码+备份+重建
bash deploy/nexus-1panel.sh check 检查远程是否有更新
bash deploy/nexus-1panel.sh ports 检查 Gitea/8600/80/443 与防火墙
bash deploy/nexus-1panel.sh init-token <令牌> 覆盖内置 Gitea 令牌(一般不需要)
资源档位(三选一):
--profile 2c8g|4c16g|1c4g 预设(默认 2c8g = 2核8G
--cpus N --mem-gb N 自动匹配最近预设
安装选项:
--skip-clone 已在 /opt/nexus,仅 pull
--fresh 全新安装(本机自动生成唯一密钥 + /app/install.html
--from-env PATH 从旧 .env 迁机导入密钥
--reuse-secrets 使用 secrets 文件中的 NEXUS 密钥(勿用于新机)
--domain NAME 覆盖域名
升级选项:
--no-backup --prune --dry-run
Git: 公共仓库匿名 clone(无需令牌);私人仓库见 init-token
EOF
exit 0
}
script_dir() {
dirname "$(readlink -f "${BASH_SOURCE[0]}")"
}
secrets_file_path() {
echo "$(script_dir)/nexus-1panel.secrets.sh"
}
load_token_from_git_remote() {
local root="${1:-$NEXUS_ROOT}"
[[ -n "${NEXUS_GITEA_TOKEN:-}" ]] && return 0
[[ -d "${root}/.git" ]] || return 0
local url
url="$(git -C "$root" remote get-url origin 2>/dev/null || true)"
if [[ "$url" =~ http://[^:]+:([^@]+)@ ]]; then
NEXUS_GITEA_TOKEN="${BASH_REMATCH[1]}"
info "已从 git remote 解析 Gitea 令牌"
fi
}
load_secrets() {
local secrets_file
secrets_file="$(secrets_file_path)"
if [[ -f "$secrets_file" ]]; then
# shellcheck disable=SC1090
set -a
source "$secrets_file"
set +a
info "已加载 $secrets_file"
fi
if [[ -f "$NEXUS_DEPLOY_ENV" ]]; then
# shellcheck disable=SC1090
set -a
source "$NEXUS_DEPLOY_ENV"
set +a
info "已加载 $NEXUS_DEPLOY_ENV"
fi
load_token_from_git_remote "$NEXUS_ROOT"
if [[ "$FRESH_INSTALL" == true ]]; then
unset NEXUS_SECRET_KEY NEXUS_API_KEY NEXUS_ENCRYPTION_KEY
info "全新安装:忽略 secrets 中的 NEXUS 密钥,将为本机自动生成"
fi
}
resolve_profile() {
if [[ -n "$CUSTOM_CPUS" && -n "$CUSTOM_MEM_GB" ]]; then
local c m
c="$CUSTOM_CPUS"
m="$CUSTOM_MEM_GB"
if [[ "$c" -le 1 && "$m" -le 4 ]]; then
NEXUS_PROFILE="1c4g"
elif [[ "$c" -le 2 && "$m" -le 8 ]]; then
NEXUS_PROFILE="2c8g"
else
NEXUS_PROFILE="4c16g"
fi
info "规格 ${c}C${m}G → 档位 ${NEXUS_PROFILE}"
return 0
fi
case "$NEXUS_PROFILE" in
1c4g|2c8g|4c16g) ;;
*)
error "未知档位: $NEXUS_PROFILE(可用: 1c4g, 2c8g, 4c16g"
exit 1
;;
esac
}
profile_env_file() {
local root="$1"
echo "$root/docker/profiles/${NEXUS_PROFILE}.env"
}
save_host_profile() {
local root="$1"
echo "$NEXUS_PROFILE" >"$root/$HOST_PROFILE_FILE"
}
load_saved_profile() {
local root="$1"
if [[ -f "$root/$HOST_PROFILE_FILE" ]]; then
NEXUS_PROFILE="$(tr -d '\r\n' <"$root/$HOST_PROFILE_FILE")"
info "使用已保存档位: $NEXUS_PROFILE"
fi
}
apply_pool_to_env_prod() {
local root="$1" prof="$2"
local env_file="$root/$ENV_PROD"
[[ -f "$env_file" ]] || return 0
local pool overflow
pool="$(grep -E '^NEXUS_DB_POOL_SIZE=' "$prof" | cut -d= -f2-)"
overflow="$(grep -E '^NEXUS_DB_MAX_OVERFLOW=' "$prof" | cut -d= -f2-)"
[[ -n "$pool" ]] && sed -i "s|^NEXUS_DB_POOL_SIZE=.*|NEXUS_DB_POOL_SIZE=${pool}|" "$env_file"
[[ -n "$overflow" ]] && sed -i "s|^NEXUS_DB_MAX_OVERFLOW=.*|NEXUS_DB_MAX_OVERFLOW=${overflow}|" "$env_file"
}
compose_cmd() {
local root="$1"
shift
local prof
prof="$(profile_env_file "$root")"
if [[ ! -f "$prof" ]]; then
error "缺少档位文件: $prof"
exit 1
fi
cd "$root"
docker compose -f "$COMPOSE_FILE" --env-file "$ENV_PROD" --env-file "$prof" "$@"
}
rand_secret() {
openssl rand -hex 24 2>/dev/null || head -c 24 /dev/urandom | od -An -tx1 | tr -d ' \n'
}
rand_pass() {
openssl rand -hex 16 2>/dev/null || head -c 16 /dev/urandom | od -An -tx1 | tr -d ' \n'
}
read_env_val() {
local file="$1" key="$2"
grep -E "^${key}=" "$file" 2>/dev/null | head -1 | cut -d= -f2- | tr -d '\r"' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//'
}
parse_gitea_endpoint() {
local hp="${NEXUS_GITEA_HOST}"
GITEA_HOST="${hp%%:*}"
GITEA_PORT="${hp##*:}"
if [[ "$GITEA_HOST" == "$GITEA_PORT" || -z "$GITEA_PORT" ]]; then
GITEA_PORT=3000
fi
}
nexus_publish_port() {
local root="${1:-}"
local p="${NEXUS_PUBLISH_PORT:-8600}"
if [[ -n "$root" && -f "$root/$ENV_PROD" ]]; then
local v
v="$(read_env_val "$root/$ENV_PROD" NEXUS_PUBLISH_PORT)"
[[ -n "$v" ]] && p="$v"
fi
echo "$p"
}
tcp_reachable() {
local host="$1" port="$2" t="${3:-3}"
if command -v nc >/dev/null 2>&1; then
nc -z -w "$t" "$host" "$port" 2>/dev/null
return $?
fi
if command -v timeout >/dev/null 2>&1; then
timeout "$t" bash -c "exec 3<>/dev/tcp/${host}/${port}" 2>/dev/null \
&& { exec 3<&- 3>&-; return 0; }
else
bash -c "exec 3<>/dev/tcp/${host}/${port}" 2>/dev/null \
&& { exec 3<&- 3>&-; return 0; }
fi
return 1
}
local_port_listening() {
local port="$1"
if command -v ss >/dev/null 2>&1; then
ss -tlnH 2>/dev/null | grep -qE ":${port}[[:space:]]"
return $?
fi
if command -v netstat >/dev/null 2>&1; then
netstat -tln 2>/dev/null | grep -qE ":${port}[[:space:]]"
return $?
fi
curl -sf --max-time 2 "http://127.0.0.1:${port}/health" >/dev/null 2>&1
}
firewall_may_block_port() {
local port="$1"
if command -v ufw >/dev/null 2>&1; then
local st
st="$(ufw status 2>/dev/null | head -1 || true)"
if [[ "$st" == *"active"* ]]; then
ufw status 2>/dev/null | grep -qE "${port}/tcp[[:space:]]+ALLOW" && return 1
return 0
fi
fi
if command -v firewall-cmd >/dev/null 2>&1 && firewall-cmd --state 2>/dev/null | grep -qi running; then
firewall-cmd --list-ports 2>/dev/null | grep -q "${port}/tcp" && return 1
firewall-cmd --list-services 2>/dev/null | grep -qE '^(http|https)$' \
&& [[ "$port" == "80" || "$port" == "443" ]] && return 1
return 0
fi
return 1
}
remind_firewall() {
local port="$1" label="$2"
if firewall_may_block_port "$port"; then
warn "防火墙可能未放行 ${port}/tcp${label}"
warn " 本机: ufw allow ${port}/tcp 或 1Panel → 主机 → 防火墙"
warn " 云厂商控制台: 安全组入站放行 ${port}/tcp"
fi
}
check_ports_preflight() {
local root="${1:-$NEXUS_ROOT}"
local nport issues=0
parse_gitea_endpoint
nport="$(nexus_publish_port "$root")"
step "检查端口与防火墙..."
if tcp_reachable "$GITEA_HOST" "$GITEA_PORT"; then
info "Gitea ${GITEA_HOST}:${GITEA_PORT} — 可达(git 拉取)"
else
warn "Gitea ${GITEA_HOST}:${GITEA_PORT} — 无法连接,clone/pull 会失败"
warn " 请确认 Gitea 在线,且本机出站与该端口未被防火墙/安全组拦截"
issues=$((issues + 1))
fi
if local_port_listening "$nport"; then
info "127.0.0.1:${nport} — 已有监听(可能为旧 Nexus 实例)"
else
info "127.0.0.1:${nport} — 尚未监听(安装后将由 Compose 绑定)"
fi
for spec in "80:HTTP" "443:HTTPS"; do
local p="${spec%%:*}" label="${spec##*:}"
if local_port_listening "$p"; then
info "本机 :${p} — 已监听(${label} / 1Panel"
else
warn "本机 :${p} — 未监听,外网 ${label} 暂不可用"
warn " 安装完成后: 1Panel → 网站 → 反代 http://127.0.0.1:${nport} 并申请 SSL"
issues=$((issues + 1))
fi
remind_firewall "$p" "${label} 公网"
done
remind_firewall 22 "SSH(远程维护)"
if [[ "$issues" -gt 0 ]]; then
warn "共 ${issues} 项端口提醒(可继续安装,但请先处理 Gitea 不可达问题)"
else
info "端口预检通过"
fi
echo ""
}
check_ports_post() {
local root="$1"
local nport health_ext
nport="$(nexus_publish_port "$root")"
step "检查服务端口..."
if local_port_listening "$nport"; then
info "Nexus 127.0.0.1:${nport} — 已监听"
else
warn "127.0.0.1:${nport} — 未监听,请执行: cd $root && docker compose ... ps"
fi
if local_port_listening 443; then
health_ext="$(curl -sk --max-time 8 -o /dev/null -w '%{http_code}' "https://${NEXUS_DOMAIN}/health" 2>/dev/null || echo "000")"
if [[ "$health_ext" == "200" ]]; then
info "https://${NEXUS_DOMAIN}/health — 外网可达 (HTTP 200)"
else
warn "https://${NEXUS_DOMAIN}/health — 返回 HTTP ${health_ext}"
warn " 若外网打不开: 1Panel 反代是否指向 127.0.0.1:${nport},安全组是否放行 443"
fi
else
warn "本机 443 未监听 — 请在 1Panel 创建站点并启用 HTTPS,否则浏览器无法访问 ${NEXUS_DOMAIN}"
remind_firewall 443 "HTTPS"
fi
if ! local_port_listening 80; then
info "本机 80 未监听(仅 HTTPS 时正常)"
fi
echo ""
}
cmd_ports() {
require_root
check_ports_preflight "$NEXUS_ROOT"
if [[ -f "$NEXUS_ROOT/$ENV_PROD" ]]; then
check_ports_post "$NEXUS_ROOT"
fi
}
require_root() {
if [[ "$(id -u)" -ne 0 ]]; then
error "请切换到 root 执行(su -),不要用 sudo 包一层"
exit 1
fi
}
require_token() {
if [[ -n "${NEXUS_GITEA_TOKEN:-}" ]]; then
return 0
fi
info "未配置 Gitea 令牌,按公共仓库匿名 clone/pull"
}
git_remote_url() {
if [[ -n "${NEXUS_GITEA_TOKEN:-}" ]]; then
echo "http://admin:${NEXUS_GITEA_TOKEN}@${NEXUS_GITEA_HOST}/${NEXUS_GITEA_REPO}"
else
echo "http://${NEXUS_GITEA_HOST}/${NEXUS_GITEA_REPO}"
fi
}
ensure_packages() {
if command -v apt-get >/dev/null 2>&1; then
export DEBIAN_FRONTEND=noninteractive
apt-get update -qq
apt-get install -y -qq git curl openssl ca-certificates >/dev/null 2>&1 || true
fi
}
ensure_docker() {
if docker compose version >/dev/null 2>&1; then
info "Docker Compose: $(docker compose version --short 2>/dev/null || docker compose version)"
return 0
fi
error "未检测到 docker compose。请在 1Panel → 容器 安装 Docker。"
exit 1
}
clone_or_pull() {
local url
url="$(git_remote_url)"
if [[ "$SKIP_CLONE" == true ]]; then
if [[ ! -d "$NEXUS_ROOT/.git" ]]; then
error "--skip-clone 但 $NEXUS_ROOT 不是 git 仓库"
exit 1
fi
info "在 $NEXUS_ROOT 执行 git pull..."
git -C "$NEXUS_ROOT" remote set-url origin "$url" 2>/dev/null || true
git -C "$NEXUS_ROOT" fetch origin "$GIT_BRANCH" --prune
git -C "$NEXUS_ROOT" reset --hard "origin/${GIT_BRANCH}"
return 0
fi
mkdir -p "$(dirname "$NEXUS_ROOT")"
if [[ -d "$NEXUS_ROOT/.git" ]]; then
info "仓库已存在,同步 origin/${GIT_BRANCH}..."
git -C "$NEXUS_ROOT" remote set-url origin "$url" 2>/dev/null || true
git -C "$NEXUS_ROOT" fetch origin "$GIT_BRANCH" --prune
git -C "$NEXUS_ROOT" reset --hard "origin/${GIT_BRANCH}"
return 0
fi
if [[ -d "$NEXUS_ROOT" ]] && [[ -n "$(ls -A "$NEXUS_ROOT" 2>/dev/null)" ]]; then
error "目录非空且不是 git 仓库: $NEXUS_ROOT"
exit 1
fi
rm -rf "$NEXUS_ROOT"
info "克隆 Nexus → $NEXUS_ROOT ..."
git clone "$url" "$NEXUS_ROOT"
git -C "$NEXUS_ROOT" checkout "$GIT_BRANCH" 2>/dev/null || true
}
write_temp_secrets_env() {
local f
f="$(mktemp /tmp/nexus-secrets-import.XXXXXX.env)"
{
printf 'MYSQL_ROOT_PASSWORD=%s\n' "${MYSQL_ROOT_PASSWORD:-$(rand_pass)}"
printf 'MYSQL_PASSWORD=%s\n' "${MYSQL_PASSWORD:-$(rand_pass)}"
printf 'NEXUS_SECRET_KEY=%s\n' "$NEXUS_SECRET_KEY"
printf 'NEXUS_API_KEY=%s\n' "$NEXUS_API_KEY"
printf 'NEXUS_ENCRYPTION_KEY=%s\n' "$NEXUS_ENCRYPTION_KEY"
} >"$f"
chmod 600 "$f"
echo "$f"
}
write_env_prod() {
local root="$1"
local env_file="$root/$ENV_PROD"
local example="$root/docker/.env.prod.example"
local gen="$root/scripts/generate_nexus_secrets.py"
local prof backup tmp
if [[ ! -f "$example" ]]; then
error "缺少 $example"
exit 1
fi
if [[ ! -f "$gen" ]]; then
error "缺少 $gen"
exit 1
fi
if [[ -f "$env_file" ]]; then
warn "$env_file 已存在,仅更新域名与连接池"
sed -i "s|^NEXUS_CORS_ORIGINS=.*|NEXUS_CORS_ORIGINS=https://${NEXUS_DOMAIN}|" "$env_file"
sed -i "s|^NEXUS_API_BASE_URL=.*|NEXUS_API_BASE_URL=https://${NEXUS_DOMAIN}|" "$env_file"
apply_pool_to_env_prod "$root" "$(profile_env_file "$root")"
chmod 600 "$env_file"
return 0
fi
prof="$(profile_env_file "$root")"
backup="/root/.nexus-install-secrets-$(date +%Y%m%d%H%M%S).env"
if [[ -n "$FROM_ENV" && -f "$FROM_ENV" ]]; then
info "迁机:从 $FROM_ENV 导入密钥..."
python3 "$gen" write-prod \
--template "$example" \
--output "$env_file" \
--domain "$NEXUS_DOMAIN" \
--profile-env "$prof" \
--from-env "$FROM_ENV" \
--backup "$backup"
elif [[ "$FRESH_INSTALL" == true ]]; then
info "全新安装:为本机生成唯一 MYSQL / NEXUS 密钥..."
python3 "$gen" write-prod \
--template "$example" \
--output "$env_file" \
--domain "$NEXUS_DOMAIN" \
--profile-env "$prof" \
--fresh \
--wizard-pending \
--backup "$backup"
info "密钥备份: $backup(仅本机,勿复制到其他服务器)"
info "完成后访问 https://${NEXUS_DOMAIN}/app/install.html"
elif [[ "$REUSE_SECRETS" == true && -n "${NEXUS_SECRET_KEY:-}" && -n "${NEXUS_API_KEY:-}" && -n "${NEXUS_ENCRYPTION_KEY:-}" ]]; then
warn "使用 secrets 中的 NEXUS 密钥(仅迁机/复用,新机勿用 --reuse-secrets"
tmp="$(write_temp_secrets_env)"
python3 "$gen" write-prod \
--template "$example" \
--output "$env_file" \
--domain "$NEXUS_DOMAIN" \
--profile-env "$prof" \
--from-env "$tmp" \
--backup "$backup"
rm -f "$tmp"
else
info "未迁机导入:为本机生成唯一密钥..."
python3 "$gen" write-prod \
--template "$example" \
--output "$env_file" \
--domain "$NEXUS_DOMAIN" \
--profile-env "$prof" \
--fresh \
--wizard-pending \
--backup "$backup"
info "密钥备份: $backup"
if [[ "$FRESH_INSTALL" != true ]]; then
info "完成后访问 https://${NEXUS_DOMAIN}/app/install.html"
fi
fi
chmod 600 "$env_file"
info "已写入 $env_file"
}
compose_up() {
local root="$1"
save_host_profile "$root"
info "档位: ${NEXUS_PROFILE}MySQL/Nexus 内存与连接池已套用;Redis 请自行安装)"
info "构建并启动(首次约 1020 分钟)..."
compose_cmd "$root" up -d --build
}
verify_install_wizard() {
local root="$1"
local code
code=$(curl -s -o /dev/null -w '%{http_code}' "http://127.0.0.1:8600/app/install.html" 2>/dev/null || echo "000")
if [[ "$code" == "200" ]]; then
info " /app/install.html → HTTP 200"
return 0
fi
warn " /app/install.html → HTTP $code(尝试同步静态文件…)"
if [[ -x "$root/deploy/sync-install-wizard-to-container.sh" ]]; then
NEXUS_ROOT="$root" bash "$root/deploy/sync-install-wizard-to-container.sh" || true
code=$(curl -s -o /dev/null -w '%{http_code}' "http://127.0.0.1:8600/app/install.html" 2>/dev/null || echo "000")
if [[ "$code" == "200" ]]; then
info " /app/install.html → HTTP 200(已同步)"
return 0
fi
fi
warn " 请重建镜像: cd $root && docker compose -f docker/docker-compose.prod.yml --env-file docker/.env.prod build --no-cache nexus"
return 1
}
verify_health() {
local root="$1" title="${2:-安装}"
local i health spa
info "等待服务就绪..."
for i in $(seq 1 60); do
health=$(curl -sf "http://127.0.0.1:8600/health" 2>/dev/null || true)
if [[ "$health" == "ok" ]]; then
spa=$(curl -s -o /dev/null -w '%{http_code}' "http://127.0.0.1:8600/app/" 2>/dev/null || echo "000")
verify_install_wizard "$root" || true
echo ""
info "═══ ${title}完成 ═══"
info " 档位: ${NEXUS_PROFILE}"
info " /health → ok"
info " /app/ → HTTP $spa"
info " 目录: $root"
if [[ -d "$root/.git" ]]; then
info " 版本: $(git -C "$root" log -1 --oneline)"
fi
echo ""
info "1Panel → 网站 → 反代 http://127.0.0.1:8600 域名: ${NEXUS_DOMAIN}"
info "配置示例: $root/deploy/1panel/openresty-nexus.conf.example"
if [[ "$FRESH_INSTALL" == true ]] || ! grep -qE '^NEXUS_SECRET_KEY=.+$' "$root/$ENV_PROD" 2>/dev/null; then
info "浏览器: https://${NEXUS_DOMAIN}/app/install.html"
else
info "浏览器: https://${NEXUS_DOMAIN}/app/"
fi
echo ""
info "运维菜单: nx 或 bash $root/deploy/nx god"
info " (安装 nx 命令: ln -sf $root/deploy/nx /usr/local/bin/nx)"
check_ports_post "$root"
return 0
fi
sleep 5
done
error "健康检查超时"
check_ports_post "$root" || true
compose_cmd "$root" logs --tail=80 nexus || true
exit 1
}
ensure_repo() {
if [[ ! -d "$NEXUS_ROOT/.git" ]]; then
error "未找到 $NEXUS_ROOT,请先: bash $0 install"
exit 1
fi
if [[ ! -f "$NEXUS_ROOT/$ENV_PROD" ]]; then
error "缺少 $NEXUS_ROOT/$ENV_PROD"
exit 1
fi
}
git_fetch() {
local url
url="$(git_remote_url)"
git -C "$NEXUS_ROOT" remote set-url origin "$url" 2>/dev/null || true
git -C "$NEXUS_ROOT" fetch origin "$GIT_BRANCH" --prune
}
commits_behind() {
git -C "$NEXUS_ROOT" rev-list "HEAD..origin/${GIT_BRANCH}" --count 2>/dev/null || echo "0"
}
show_pending() {
local behind
behind="$(commits_behind)"
info "当前: $(git -C "$NEXUS_ROOT" log -1 --oneline)"
info "远程: $(git -C "$NEXUS_ROOT" log -1 --oneline "origin/${GIT_BRANCH}")"
if [[ "$behind" == "0" ]]; then
info "已是最新。"
return 1
fi
warn "落后 ${behind} 个提交:"
git -C "$NEXUS_ROOT" log --oneline "HEAD..origin/${GIT_BRANCH}"
return 0
}
backup_mysql() {
local root="$1" ts file mysql_root
if [[ "$NO_BACKUP" == true ]]; then
warn "已跳过备份 (--no-backup)"
return 0
fi
mysql_root="$(read_env_val "$root/$ENV_PROD" MYSQL_ROOT_PASSWORD)"
if [[ -z "$mysql_root" ]]; then
warn "无法读取 MYSQL_ROOT_PASSWORD,跳过备份"
return 0
fi
mkdir -p "$NEXUS_BACKUP_DIR"
ts="$(date +%Y%m%d-%H%M%S)"
file="${NEXUS_BACKUP_DIR}/nexus-before-upgrade-${ts}.sql"
step "备份 MySQL → $file"
if ! compose_cmd "$root" exec -T mysql \
mysqldump -uroot -p"${mysql_root}" --single-transaction --routines --triggers nexus \
>"$file" 2>/dev/null; then
error "mysqldump 失败,已中止"
rm -f "$file"
exit 1
fi
gzip -f "$file"
info "备份: ${file}.gz"
}
git_upgrade() {
local before after
before="$(git -C "$NEXUS_ROOT" rev-parse HEAD)"
step "同步 origin/${GIT_BRANCH}"
git -C "$NEXUS_ROOT" reset --hard "origin/${GIT_BRANCH}"
after="$(git -C "$NEXUS_ROOT" rev-parse HEAD)"
info "代码: ${before:0:7}${after:0:7}"
}
cmd_init_token() {
require_root
local tok="${1:-}"
if [[ -z "$tok" ]]; then
error "用法: $0 init-token <Gitea令牌>"
exit 1
fi
printf 'NEXUS_GITEA_TOKEN=%s\n' "$tok" >"$NEXUS_DEPLOY_ENV"
chmod 600 "$NEXUS_DEPLOY_ENV"
info "已写入 $NEXUS_DEPLOY_ENV"
}
cmd_install() {
echo ""
echo "=========================================="
echo " Nexus 1Panel 一键安装"
echo " 域名: ${NEXUS_DOMAIN} 档位: ${NEXUS_PROFILE}"
echo "=========================================="
echo ""
require_root
require_token
resolve_profile
ensure_packages
ensure_docker
check_ports_preflight "$NEXUS_ROOT"
clone_or_pull
write_env_prod "$NEXUS_ROOT"
compose_up "$NEXUS_ROOT"
verify_health "$NEXUS_ROOT" "安装"
}
cmd_upgrade() {
echo ""
echo "=========================================="
echo " Nexus 1Panel 一键升级"
echo "=========================================="
echo ""
require_root
require_token
ensure_docker
ensure_repo
load_saved_profile "$NEXUS_ROOT"
resolve_profile
check_ports_preflight "$NEXUS_ROOT"
git_fetch
if ! show_pending; then
exit 0
fi
if [[ "$CHECK_ONLY" == true ]]; then
exit 0
fi
if [[ "$DRY_RUN" == true ]]; then
info "dry-run 结束。"
exit 0
fi
backup_mysql "$NEXUS_ROOT"
git_upgrade
apply_pool_to_env_prod "$NEXUS_ROOT" "$(profile_env_file "$NEXUS_ROOT")"
save_host_profile "$NEXUS_ROOT"
step "重建容器..."
compose_cmd "$NEXUS_ROOT" up -d --build
if [[ "$PRUNE_IMAGES" == true ]]; then
docker image prune -f >/dev/null 2>&1 || true
fi
verify_health "$NEXUS_ROOT" "升级"
}
cmd_check() {
require_root
require_token
ensure_repo
git_fetch
show_pending || true
}
parse_common_install() {
while [[ $# -gt 0 ]]; do
case "$1" in
-h|--help) usage ;;
--skip-clone) SKIP_CLONE=true; shift ;;
--fresh) FRESH_INSTALL=true; shift ;;
--reuse-secrets) REUSE_SECRETS=true; shift ;;
--profile) NEXUS_PROFILE="$2"; shift 2 ;;
--cpus) CUSTOM_CPUS="$2"; shift 2 ;;
--mem-gb|--memory-gb) CUSTOM_MEM_GB="$2"; shift 2 ;;
--domain) NEXUS_DOMAIN="$2"; shift 2 ;;
--from-env) FROM_ENV="$2"; shift 2 ;;
--root) NEXUS_ROOT="$2"; shift 2 ;;
--no-backup) NO_BACKUP=true; shift ;;
--prune|--prune-images) PRUNE_IMAGES=true; shift ;;
--dry-run) DRY_RUN=true; shift ;;
*)
error "未知参数: $1"
usage
;;
esac
done
}
parse_common_upgrade() {
while [[ $# -gt 0 ]]; do
case "$1" in
-h|--help) usage ;;
--profile) NEXUS_PROFILE="$2"; shift 2 ;;
--cpus) CUSTOM_CPUS="$2"; shift 2 ;;
--mem-gb|--memory-gb) CUSTOM_MEM_GB="$2"; shift 2 ;;
--root) NEXUS_ROOT="$2"; shift 2 ;;
--branch) GIT_BRANCH="$2"; shift 2 ;;
--no-backup) NO_BACKUP=true; shift ;;
--prune|--prune-images) PRUNE_IMAGES=true; shift ;;
--dry-run) DRY_RUN=true; shift ;;
--check) CHECK_ONLY=true; shift ;;
*)
error "未知参数: $1"
usage
;;
esac
done
}
main() {
local cmd="${1:-install}"
shift || true
load_secrets
case "$cmd" in
install)
parse_common_install "$@"
cmd_install
;;
upgrade)
parse_common_upgrade "$@"
cmd_upgrade
;;
check)
CHECK_ONLY=true
parse_common_upgrade "$@"
cmd_check
;;
init-token)
cmd_init_token "${1:-}"
;;
ports)
cmd_ports
;;
-h|--help|help)
usage
;;
*)
error "未知子命令: $cmd"
usage
;;
esac
}
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
main "$@"
fi