Files
J-Board-Lite/scripts/install-jetboard-standalone.sh
2026-05-01 01:50:37 +10:00

604 lines
18 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
set -euo pipefail
GH_REPO="${GH_REPO:-JetSprow/J-Board-Lite}"
BRANCH="${BRANCH:-main}"
APP_DIR="${APP_DIR:-}"
REWRITE_ENV="${REWRITE_ENV:-}"
SKIP_NODE_INSTALL="${SKIP_NODE_INSTALL:-0}"
NODE_MAJOR="${NODE_MAJOR:-20}"
SERVICE_NAME="${JBOARD_SERVICE_NAME:-jetboard}"
SERVICE_USER="${JBOARD_SERVICE_USER:-jetboard}"
CONFIG_FILE="${JETBOARD_CONFIG:-/etc/jetboard.conf}"
APP_PORT=""
PUBLIC_URL=""
SUBSCRIPTION_PUBLIC_URL=""
SITE_NAME=""
ADMIN_EMAIL=""
ADMIN_PASSWORD=""
ADMIN_NAME=""
NEXTAUTH_SECRET=""
ENCRYPTION_KEY=""
ENV_REUSED="0"
RUNTIME_NODE_OPTIONS=""
is_interactive() {
[ -r /dev/tty ] && [ -w /dev/tty ]
}
prompt_print() {
if is_interactive; then
printf '%b' "$*" > /dev/tty
else
printf '%b' "$*" >&2
fi
}
prompt_read() {
local __var="$1"
if is_interactive; then
IFS= read -r "$__var" < /dev/tty || true
else
return 1
fi
}
run_as_root() {
if [ "$(id -u)" -eq 0 ]; then
"$@"
elif command -v sudo >/dev/null 2>&1; then
sudo "$@"
else
echo "需要 root 权限。请使用 root 用户运行,或先安装 sudo。" >&2
exit 1
fi
}
line() {
printf '%s\n' "------------------------------------------------------------"
}
section() {
echo
line
printf '%s\n' "$1"
line
}
need_command() {
command -v "$1" >/dev/null 2>&1
}
install_base_packages() {
section "安装基础依赖"
if need_command apt-get; then
run_as_root apt-get update
run_as_root apt-get install -y ca-certificates curl git openssl python3 make g++ build-essential
elif need_command dnf; then
run_as_root dnf install -y ca-certificates curl git openssl python3 make gcc gcc-c++
elif need_command yum; then
run_as_root yum install -y ca-certificates curl git openssl python3 make gcc gcc-c++
elif need_command apk; then
run_as_root apk add --no-cache ca-certificates curl git openssl python3 make g++
else
echo "无法识别包管理器请先手动安装curl git openssl nodejs npm python3 make g++" >&2
exit 1
fi
}
node_major_version() {
if ! need_command node; then
echo 0
return
fi
local version
version="$(node -v 2>/dev/null | sed 's/^v//; s/\..*$//' || true)"
case "$version" in
''|*[!0-9]*) echo 0 ;;
*) echo "$version" ;;
esac
}
install_node() {
if [ "$SKIP_NODE_INSTALL" = "1" ]; then
return
fi
local current_major
current_major="$(node_major_version)"
if [ "$current_major" -ge "$NODE_MAJOR" ] && need_command npm; then
echo "Node.js $(node -v) 已满足要求。"
return
fi
section "安装 Node.js ${NODE_MAJOR}"
local setup_tmp
setup_tmp="$(mktemp)"
if need_command apt-get; then
curl -fsSL "https://deb.nodesource.com/setup_${NODE_MAJOR}.x" -o "$setup_tmp"
run_as_root bash "$setup_tmp"
run_as_root apt-get install -y nodejs
elif need_command dnf; then
curl -fsSL "https://rpm.nodesource.com/setup_${NODE_MAJOR}.x" -o "$setup_tmp"
run_as_root bash "$setup_tmp"
run_as_root dnf install -y nodejs
elif need_command yum; then
curl -fsSL "https://rpm.nodesource.com/setup_${NODE_MAJOR}.x" -o "$setup_tmp"
run_as_root bash "$setup_tmp"
run_as_root yum install -y nodejs
elif need_command apk; then
run_as_root apk add --no-cache nodejs npm
else
echo "无法自动安装 Node.js请先手动安装 Node.js ${NODE_MAJOR}+ 和 npm。" >&2
exit 1
fi
rm -f "$setup_tmp"
current_major="$(node_major_version)"
if [ "$current_major" -lt "$NODE_MAJOR" ] || ! need_command npm; then
echo "Node.js 安装后仍不满足要求,请检查 node/npm。" >&2
exit 1
fi
}
random_hex() {
openssl rand -hex "$1"
}
random_password() {
openssl rand -hex 12
}
server_ip() {
curl -fsS --max-time 3 https://api.ipify.org 2>/dev/null || hostname -I 2>/dev/null | awk '{print $1}' || echo "127.0.0.1"
}
normalize_url() {
local value="$1"
value="${value%/}"
if [ -z "$value" ]; then
printf '%s' "$value"
return
fi
case "$value" in
http://*|https://*) printf '%s' "$value" ;;
*) printf 'https://%s' "$value" ;;
esac
}
prompt_value() {
local label="$1"
local default="$2"
local help="${3:-}"
local value=""
if [ -n "$help" ]; then
prompt_print "$help\n"
fi
if is_interactive; then
prompt_print "$label [$default]: "
prompt_read value || true
fi
if [ -z "$value" ]; then
value="$default"
fi
printf '%s' "$value"
}
prompt_generated() {
local label="$1"
local default="$2"
local help="${3:-}"
local value=""
if [ -n "$help" ]; then
prompt_print "$help\n"
fi
if is_interactive; then
prompt_print "$label [回车自动生成]: "
prompt_read value || true
fi
if [ -z "$value" ]; then
value="$default"
fi
printf '%s' "$value"
}
prompt_yes_no() {
local label="$1"
local default="$2"
local value=""
if ! is_interactive; then
printf '%s' "$default"
return
fi
prompt_print "$label [$default]: "
prompt_read value || true
value="${value:-$default}"
case "$value" in
y|Y|yes|YES|Yes) printf 'y' ;;
*) printf 'n' ;;
esac
}
resolve_default_app_dir() {
local source="${BASH_SOURCE[0]:-}"
local dir=""
if [ -n "$source" ] && [ -f "$source" ]; then
dir="$(cd -- "$(dirname -- "$source")" && pwd)"
if [ -f "$dir/../package.json" ]; then
cd -- "$dir/.." && pwd
return
fi
fi
echo "/opt/jboard"
}
git_in_repo() {
if [ -w "$APP_DIR/.git" ] || [ -w "$APP_DIR" ]; then
git -c safe.directory="$APP_DIR" "$@"
else
run_as_root git -c safe.directory="$APP_DIR" "$@"
fi
}
run_in_app_dir() {
if [ -w "$APP_DIR" ] && { [ ! -f "$APP_DIR/.env" ] || [ -r "$APP_DIR/.env" ]; } && [ "$(id -u)" -ne 0 ]; then
"$@"
else
run_as_root "$@"
fi
}
load_resource_helpers() {
local helper="$APP_DIR/scripts/lib-standalone-profile.sh"
if [ -f "$helper" ]; then
# shellcheck disable=SC1090
. "$helper"
else
echo "未找到本机资源检测脚本:$helper,将使用默认构建策略。"
fi
}
prepare_repo() {
section "准备 J-Board Lite 代码"
local default_dir
default_dir="$(resolve_default_app_dir)"
APP_DIR="$(prompt_value "安装目录" "${APP_DIR:-$default_dir}" "如果你已经在仓库目录里运行脚本,直接回车即可。")"
if [ -d "$APP_DIR/.git" ]; then
echo "检测到已有仓库:$APP_DIR"
cd "$APP_DIR"
git_in_repo fetch origin "$BRANCH"
git_in_repo checkout "$BRANCH"
git_in_repo pull --ff-only origin "$BRANCH"
elif [ -e "$APP_DIR" ] && [ "$(find "$APP_DIR" -mindepth 1 -maxdepth 1 2>/dev/null | wc -l | tr -d ' ')" != "0" ]; then
echo "安装目录已存在且不为空:$APP_DIR" >&2
echo "请换一个目录,或设置 APP_DIR 指向空目录/已有 J-Board Lite 仓库。" >&2
exit 1
else
run_as_root mkdir -p "$(dirname "$APP_DIR")"
run_as_root git clone --branch "$BRANCH" "https://github.com/${GH_REPO}.git" "$APP_DIR"
cd "$APP_DIR"
fi
}
load_existing_env() {
if [ -f .env ]; then
if [ -r .env ]; then
set -a
# shellcheck disable=SC1091
. ./.env
set +a
else
echo ".env 存在但当前用户不可读取;如需显示管理员密码,请使用 root 重新运行或查看 .env。"
fi
APP_PORT="${APP_PORT:-3000}"
PUBLIC_URL="${NEXTAUTH_URL:-}"
SUBSCRIPTION_PUBLIC_URL="${SUBSCRIPTION_URL:-}"
SITE_NAME="${SITE_NAME:-J-Board Lite}"
ADMIN_EMAIL="${ADMIN_EMAIL:-admin@jboard.local}"
ADMIN_PASSWORD="${ADMIN_PASSWORD:-}"
ADMIN_NAME="${ADMIN_NAME:-Admin}"
fi
}
env_escape() {
printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g; s/\$/\\$/g'
}
write_env() {
local tmp
tmp="$(mktemp)"
{
printf '# J-Board Lite standalone panel\n'
printf 'APP_PORT="%s"\n' "$(env_escape "$APP_PORT")"
printf 'SITE_NAME="%s"\n' "$(env_escape "$SITE_NAME")"
printf '\n# SQLite for standalone runtime\n'
printf 'DATABASE_URL="file:%s/storage/jboard.db"\n' "$(env_escape "$APP_DIR")"
printf '\n# NextAuth\n'
printf 'NEXTAUTH_SECRET="%s"\n' "$(env_escape "$NEXTAUTH_SECRET")"
printf 'NEXTAUTH_URL="%s"\n' "$(env_escape "$PUBLIC_URL")"
printf 'SUBSCRIPTION_URL="%s"\n' "$(env_escape "$SUBSCRIPTION_PUBLIC_URL")"
printf '\n# Must be at least 32 bytes, used for AES-256-GCM encryption\n'
printf 'ENCRYPTION_KEY="%s"\n' "$(env_escape "$ENCRYPTION_KEY")"
printf '\n# Initial admin account, used by npm run db:seed on first install\n'
printf 'ADMIN_EMAIL="%s"\n' "$(env_escape "$ADMIN_EMAIL")"
printf 'ADMIN_PASSWORD="%s"\n' "$(env_escape "$ADMIN_PASSWORD")"
printf 'ADMIN_NAME="%s"\n' "$(env_escape "$ADMIN_NAME")"
} > "$tmp"
if [ -f .env ]; then
run_as_root cp .env ".env.backup.$(date +%Y%m%d%H%M%S)"
fi
run_as_root install -m 0600 "$tmp" .env
rm -f "$tmp"
}
configure_env() {
section "生成 .env 配置"
if [ -f .env ] && [ -z "$REWRITE_ENV" ]; then
local answer
answer="$(prompt_yes_no "检测到已有 .env是否重新生成" "n")"
if [ "$answer" = "n" ]; then
ENV_REUSED="1"
load_existing_env
echo "沿用现有 .env。"
return
fi
elif [ "${REWRITE_ENV:-0}" = "0" ] && [ -f .env ]; then
ENV_REUSED="1"
load_existing_env
echo "沿用现有 .env。"
return
fi
local ip default_url
ip="$(server_ip)"
default_url="http://${ip}:3000"
SITE_NAME="$(prompt_value "站点名称" "J-Board Lite")"
PUBLIC_URL="$(prompt_value "网站访问地址" "$default_url" "这里请填写你准备反向代理到本机 3000 端口的面板域名,例如 https://panel.example.com。没有域名时可先回车用 IP:3000 测试。")"
PUBLIC_URL="$(normalize_url "$PUBLIC_URL")"
SUBSCRIPTION_PUBLIC_URL="$(prompt_value "订阅访问地址" "$PUBLIC_URL" "用于生成客户端订阅链接。可以和网站地址相同,也可以填单独反代到本面板的订阅域名,例如 https://sub.example.com。")"
SUBSCRIPTION_PUBLIC_URL="$(normalize_url "$SUBSCRIPTION_PUBLIC_URL")"
APP_PORT="$(prompt_value "本机监听端口" "3000" "反向代理目标会是 http://127.0.0.1:端口,默认 3000。")"
ADMIN_EMAIL="$(prompt_value "管理员邮箱" "admin@jboard.local")"
ADMIN_PASSWORD="$(prompt_generated "管理员密码" "$(random_password)" "回车会生成一个安全密码,部署完成后会在结果中显示一次。")"
ADMIN_NAME="$(prompt_value "管理员昵称" "Admin")"
NEXTAUTH_SECRET="$(prompt_generated "NEXTAUTH_SECRET" "$(random_hex 32)")"
ENCRYPTION_KEY="$(prompt_generated "ENCRYPTION_KEY" "$(random_hex 32)" "生产使用后不要更换 ENCRYPTION_KEY否则已加密的面板密码、Token、凭据会无法解密。")"
write_env
echo ".env 已写入:$APP_DIR/.env"
}
ensure_service_user() {
if id "$SERVICE_USER" >/dev/null 2>&1; then
return
fi
section "创建运行用户:$SERVICE_USER"
if need_command useradd; then
run_as_root useradd --system --user-group --home "$APP_DIR" --shell /usr/sbin/nologin "$SERVICE_USER"
elif need_command adduser; then
run_as_root adduser -S -D -H -h "$APP_DIR" -s /sbin/nologin "$SERVICE_USER"
else
echo "无法创建系统用户,请手动创建 $SERVICE_USER" >&2
exit 1
fi
}
prepare_runtime_tree() {
[ -f "$APP_DIR/.next/standalone/server.js" ] || {
echo "未找到 standalone 构建产物:$APP_DIR/.next/standalone/server.js" >&2
exit 1
}
run_in_app_dir mkdir -p "$APP_DIR/.next/standalone/.next"
run_in_app_dir rm -rf "$APP_DIR/.next/standalone/.next/static"
run_in_app_dir cp -R "$APP_DIR/.next/static" "$APP_DIR/.next/standalone/.next/"
if [ -d "$APP_DIR/public" ]; then
run_in_app_dir rm -rf "$APP_DIR/.next/standalone/public"
run_in_app_dir cp -R "$APP_DIR/public" "$APP_DIR/.next/standalone/public"
fi
if [ -d "$APP_DIR/data" ]; then
run_in_app_dir rm -rf "$APP_DIR/.next/standalone/data"
run_in_app_dir cp -R "$APP_DIR/data" "$APP_DIR/.next/standalone/data"
fi
}
install_dependencies_and_build() {
section "安装依赖并构建"
if command -v jboard_prepare_standalone_build_env >/dev/null 2>&1; then
jboard_prepare_standalone_build_env "$APP_DIR"
jboard_print_standalone_profile
RUNTIME_NODE_OPTIONS="${JBOARD_RUNTIME_NODE_OPTIONS:-}"
else
export NEXT_TELEMETRY_DISABLED=1
fi
run_as_root mkdir -p "$APP_DIR/storage" "$APP_DIR/backups"
run_in_app_dir env \
NEXT_TELEMETRY_DISABLED="${NEXT_TELEMETRY_DISABLED:-1}" \
NODE_OPTIONS="${NODE_OPTIONS:-}" \
NPM_CONFIG_JOBS="${NPM_CONFIG_JOBS:-}" \
npm ci --no-audit --no-fund
run_in_app_dir env \
NEXT_TELEMETRY_DISABLED="${NEXT_TELEMETRY_DISABLED:-1}" \
NODE_OPTIONS="${NODE_OPTIONS:-}" \
NPM_CONFIG_JOBS="${NPM_CONFIG_JOBS:-}" \
npm run build
prepare_runtime_tree
run_in_app_dir npm run db:push
run_in_app_dir npm run db:seed
run_as_root chown -R "$SERVICE_USER" "$APP_DIR/storage" "$APP_DIR/backups"
}
write_service() {
section "安装 systemd 服务"
if ! need_command systemctl; then
echo "当前系统没有 systemd无法自动安装服务。请使用支持 systemd 的 Linux 发行版。" >&2
exit 1
fi
local tmp
tmp="$(mktemp)"
{
printf '[Unit]\n'
printf 'Description=J-Board Lite standalone panel\n'
printf 'After=network-online.target\n'
printf 'Wants=network-online.target\n'
printf '\n[Service]\n'
printf 'Type=simple\n'
printf 'User=%s\n' "$SERVICE_USER"
printf 'WorkingDirectory=%s/.next/standalone\n' "$APP_DIR"
printf 'EnvironmentFile=%s/.env\n' "$APP_DIR"
printf 'Environment=NODE_ENV=production\n'
printf 'Environment=PORT=%s\n' "${APP_PORT:-3000}"
printf 'Environment=HOSTNAME=0.0.0.0\n'
if [ -n "$RUNTIME_NODE_OPTIONS" ]; then
printf 'Environment=NODE_OPTIONS=%s\n' "$RUNTIME_NODE_OPTIONS"
fi
printf 'ExecStart=/usr/bin/env node server.js\n'
printf 'Restart=always\n'
printf 'RestartSec=3\n'
printf 'NoNewPrivileges=true\n'
printf 'PrivateTmp=true\n'
printf '\n[Install]\n'
printf 'WantedBy=multi-user.target\n'
} > "$tmp"
run_as_root install -m 0644 "$tmp" "/etc/systemd/system/${SERVICE_NAME}.service"
rm -f "$tmp"
run_as_root systemctl daemon-reload
run_as_root systemctl enable --now "$SERVICE_NAME"
}
install_jetboard_command() {
section "安装 JetBoard 管理命令"
local config_tmp wrapper_tmp
config_tmp="$(mktemp)"
wrapper_tmp="$(mktemp)"
{
printf '# JetBoard standalone command configuration\n'
printf 'JBOARD_DEPLOY_MODE="standalone"\n'
printf 'JBOARD_APP_DIR="%s"\n' "$(env_escape "$APP_DIR")"
printf 'JBOARD_SERVICE_NAME="%s"\n' "$(env_escape "$SERVICE_NAME")"
printf 'JBOARD_SERVICE_USER="%s"\n' "$(env_escape "$SERVICE_USER")"
printf 'GH_REPO="%s"\n' "$(env_escape "$GH_REPO")"
printf 'BRANCH="%s"\n' "$(env_escape "$BRANCH")"
} > "$config_tmp"
{
printf '#!/usr/bin/env bash\n'
printf 'set -euo pipefail\n'
printf 'CONFIG_FILE="${JETBOARD_CONFIG:-/etc/jetboard.conf}"\n'
printf 'if [ -r "$CONFIG_FILE" ]; then\n'
printf ' # shellcheck disable=SC1090\n'
printf ' . "$CONFIG_FILE"\n'
printf 'fi\n'
printf 'APP_DIR="${JBOARD_APP_DIR:-/opt/jboard}"\n'
printf 'exec "$APP_DIR/scripts/jetboard.sh" "$@"\n'
} > "$wrapper_tmp"
run_as_root mkdir -p /usr/local/bin
run_as_root install -m 0644 "$config_tmp" "$CONFIG_FILE"
run_as_root install -m 0755 "$wrapper_tmp" /usr/local/bin/jetboard
run_as_root ln -sf /usr/local/bin/jetboard /usr/local/bin/JetBoard || true
run_as_root chmod +x "$APP_DIR/scripts/jetboard.sh" || true
rm -f "$config_tmp" "$wrapper_tmp"
echo "已安装jetboard也可使用 JetBoard"
}
wait_for_app() {
section "检查启动状态"
local url="http://127.0.0.1:${APP_PORT:-3000}/api/public/app-info"
local ok="0"
for _ in $(seq 1 30); do
if curl -fsS "$url" >/dev/null 2>&1; then
ok="1"
break
fi
sleep 2
done
run_as_root systemctl --no-pager status "$SERVICE_NAME" || true
echo
if [ "$ok" = "1" ]; then
echo "健康检查通过:$url"
else
echo "健康检查暂未通过请查看日志jetboard logs"
fi
}
print_summary() {
local proxy_target="http://127.0.0.1:${APP_PORT:-3000}"
local shown_password="$ADMIN_PASSWORD"
if [ "$ENV_REUSED" = "1" ] && [ -z "$shown_password" ]; then
shown_password="沿用已有数据库账号;如忘记可执行 jetboard reset"
fi
echo
printf '%s\n' "============================================================"
printf '%s\n' "J-Board Lite standalone 部署完成"
printf '%s\n' "============================================================"
printf '访问地址:%s\n' "${PUBLIC_URL:-http://127.0.0.1:${APP_PORT:-3000}}"
printf '订阅地址:%s\n' "${SUBSCRIPTION_PUBLIC_URL:-${PUBLIC_URL:-http://127.0.0.1:${APP_PORT:-3000}}}"
printf '反代目标:%s\n' "$proxy_target"
printf '管理员邮箱:%s\n' "${ADMIN_EMAIL:-admin@jboard.local}"
printf '管理员密码:%s\n' "$shown_password"
echo
printf '%s\n' "常用命令"
printf ' jetboard # 打开 JetBoard 管理菜单\n'
printf ' jetboard status # 查看服务状态\n'
printf ' jetboard update # 拉取代码、备份数据库并本机构建\n'
printf ' jetboard reset # 重置或创建管理员账号密码\n'
printf ' jetboard logs # 查看服务日志\n'
printf ' jetboard uninstall # 完整卸载 standalone 部署\n'
printf '%s\n' "============================================================"
}
main() {
section "J-Board Lite standalone 一键部署向导"
echo "这个脚本不会安装或使用 Docker。"
echo "它会安装 Node.js、拉取代码、生成 .env、构建 standalone 产物、初始化 SQLite 并注册 systemd 服务。"
install_base_packages
install_node
prepare_repo
load_resource_helpers
configure_env
ensure_service_user
install_dependencies_and_build
write_service
install_jetboard_command
wait_for_app
print_summary
}
main "$@"