Files
Nexus/deploy/nexus-1panel.sh
T
Nexus Deploy 646929ddff feat(deploy): register nx globally via install-nx-cli.sh
Centralize chmod and /usr/local/bin symlinks; install, upgrade, update, and every nx invocation refresh the global nx command.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-06 04:43:00 +08:00

909 lines
29 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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
NO_CACHE=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 --no-cache --dry-run --check(仅查 Git,不重建)
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 '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 "全新安装:为本机生成唯一 NEXUS 密钥(MySQL/Redis 请自行安装)..."
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"
}
purge_legacy_bundled_services() {
local root="$1"
local s
for s in \
"$root/deploy/uninstall-mysql-compose.sh" \
"$root/deploy/uninstall-redis-compose.sh"; do
if [[ -x "$s" ]]; then
step "清理遗留 Compose 内置服务: $(basename "$s")"
NEXUS_ROOT="$root" bash "$s" || true
fi
done
}
compose_up() {
local root="$1"
save_host_profile "$root"
purge_legacy_bundled_services "$root"
info "档位: ${NEXUS_PROFILE}(仅 Nexus 容器;MySQL/Redis 请自行安装)"
info "构建并启动(首次约 1020 分钟)..."
compose_cmd "$root" up -d --build --remove-orphans
}
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 ""
NEXUS_ROOT="$root" bash "$root/deploy/install-nx-cli.sh" 2>/dev/null || true
info "运维菜单: nx 或 nexus-update"
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 db_url cname
if [[ "$NO_BACKUP" == true ]]; then
warn "已跳过备份 (--no-backup)"
return 0
fi
if ! command -v mysqldump >/dev/null 2>&1; then
warn "未安装 mysqldump,跳过 MySQL 备份"
return 0
fi
cname="$(docker ps --format '{{.Names}}' | grep -E 'nexus-prod-nexus|nexus-nexus-1' | head -1 || true)"
db_url=""
if [[ -n "$cname" ]]; then
db_url="$(docker exec "$cname" grep -E '^NEXUS_DATABASE_URL=' /app/.env 2>/dev/null | head -1 | cut -d= -f2- | tr -d '"' | tr -d "'" || true)"
fi
if [[ -z "$db_url" ]]; then
warn "未找到 NEXUS_DATABASE_URL(外置 MySQL 需先完成安装向导),跳过备份"
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 ! python3 - "$db_url" "$file" <<'PY'
import sys
import urllib.parse
from subprocess import run
url = sys.argv[1].replace("mysql+aiomysql://", "mysql://", 1)
out = sys.argv[2]
p = urllib.parse.urlparse(url)
user = urllib.parse.unquote(p.username or "")
password = urllib.parse.unquote(p.password or "")
host = p.hostname or "127.0.0.1"
if host == "host.docker.internal":
host = "127.0.0.1"
port = str(p.port or 3306)
db = (p.path or "/nexus").lstrip("/").split("?")[0]
env = {**__import__("os").environ, "MYSQL_PWD": password}
cmd = [
"mysqldump", "-h", host, "-P", port, "-u", user,
"--single-transaction", "--routines", "--triggers", "--set-gtid-purged=OFF", db,
]
r = run(cmd, env=env, capture_output=True, text=True)
if r.returncode != 0:
sys.stderr.write(r.stderr or r.stdout or "mysqldump failed\n")
sys.exit(r.returncode)
open(out, "w", encoding="utf-8").write(r.stdout)
PY
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
NEXUS_ROOT="$NEXUS_ROOT" bash "$NEXUS_ROOT/deploy/install-nx-cli.sh" 2>/dev/null || true
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
show_pending || true
if [[ "$CHECK_ONLY" == true ]]; then
exit 0
fi
if [[ "$DRY_RUN" == true ]]; then
info "dry-run 结束。"
exit 0
fi
local behind
behind="$(commits_behind)"
if [[ "$behind" != "0" ]]; then
backup_mysql "$NEXUS_ROOT"
git_upgrade
else
info "Git 已最新,仍重建 Docker 镜像与容器(避免旧 entrypoint 缓存)"
if [[ "$NO_BACKUP" != true ]]; then
backup_mysql "$NEXUS_ROOT"
fi
fi
apply_pool_to_env_prod "$NEXUS_ROOT" "$(profile_env_file "$NEXUS_ROOT")"
save_host_profile "$NEXUS_ROOT"
purge_legacy_bundled_services "$NEXUS_ROOT"
if [[ "$NO_CACHE" == true ]]; then
step "无缓存重建 Nexus 镜像..."
compose_cmd "$NEXUS_ROOT" build --no-cache nexus
fi
step "重建容器(--remove-orphans 清理旧 mysql/redis..."
compose_cmd "$NEXUS_ROOT" up -d --build --remove-orphans
if [[ -x "$NEXUS_ROOT/deploy/sync-install-wizard-to-container.sh" ]]; then
NEXUS_ROOT="$NEXUS_ROOT" bash "$NEXUS_ROOT/deploy/sync-install-wizard-to-container.sh" || true
fi
if [[ "$PRUNE_IMAGES" == true ]]; then
docker image prune -f >/dev/null 2>&1 || true
fi
NEXUS_ROOT="$NEXUS_ROOT" bash "$NEXUS_ROOT/deploy/install-nx-cli.sh" 2>/dev/null || true
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 ;;
--no-cache|--rebuild) NO_CACHE=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