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

849 lines
27 KiB
Bash
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
#
# 凭据: deploy/nexus-1panel.secrets.sh(与 SECRETS.md 一致,gitignore
# 私人仓库可: git add -f deploy/nexus-1panel.secrets.sh && git push
# 或 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
凭据: deploy/nexus-1panel.secrets.sh(推荐)或 /root/.nexus-deploy.env
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"
elif [[ ! -f "$NEXUS_DEPLOY_ENV" ]]; then
warn "未找到 $(secrets_file_path)"
warn " 请复制: cp deploy/nexus-1panel.secrets.sh.example deploy/nexus-1panel.secrets.sh 并填入凭据"
warn " 或: bash $0 init-bundle (从本机 SECRETS 生成,见 deploy/README-1panel.md"
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/Redis/Nexus 内存与连接池已套用)"
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