mirror of
https://github.com/JetSprow/J-Board-Lite.git
synced 2026-05-01 01:14:10 +05:30
feat: add jetboard deployment manager
This commit is contained in:
@@ -315,6 +315,49 @@ write_env() {
|
||||
rm -f "$tmp"
|
||||
}
|
||||
|
||||
install_jetboard_command() {
|
||||
section "安装 JetBoard 管理命令"
|
||||
|
||||
if [ ! -f "$APP_DIR/scripts/jetboard.sh" ]; then
|
||||
echo "未找到 $APP_DIR/scripts/jetboard.sh,跳过管理命令安装。"
|
||||
return
|
||||
fi
|
||||
|
||||
local config_tmp wrapper_tmp
|
||||
config_tmp="$(mktemp)"
|
||||
wrapper_tmp="$(mktemp)"
|
||||
|
||||
{
|
||||
printf '# JetBoard Docker command configuration\n'
|
||||
printf 'JBOARD_DEPLOY_MODE="docker"\n'
|
||||
printf 'JBOARD_APP_DIR="%s"\n' "$(env_escape "$APP_DIR")"
|
||||
printf 'GH_REPO="%s"\n' "$(env_escape "$GH_REPO")"
|
||||
printf 'BRANCH="%s"\n' "$(env_escape "$BRANCH")"
|
||||
printf 'COMPOSE="docker compose"\n'
|
||||
} > "$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" /etc/jetboard.conf
|
||||
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)"
|
||||
}
|
||||
|
||||
configure_env() {
|
||||
section "生成 .env 配置"
|
||||
|
||||
@@ -416,12 +459,12 @@ 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="沿用已有数据库账号;如忘记请在数据库中重置"
|
||||
shown_password="沿用已有数据库账号;如忘记可执行 jetboard reset"
|
||||
fi
|
||||
|
||||
echo
|
||||
printf '%s\n' "============================================================"
|
||||
printf '%s\n' "J-Board 部署完成"
|
||||
printf '%s\n' "J-Board Lite Docker 部署完成"
|
||||
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}}}"
|
||||
@@ -446,10 +489,12 @@ print_summary() {
|
||||
printf ' 4. 后台 /admin/plans:创建套餐并绑定入站或流媒体服务。\n'
|
||||
echo
|
||||
printf '%s\n' "常用命令"
|
||||
printf ' cd %s\n' "$APP_DIR"
|
||||
printf ' sudo docker compose logs -f app\n'
|
||||
printf ' sudo docker compose ps\n'
|
||||
printf ' sudo ./scripts/upgrade-jboard-panel.sh\n'
|
||||
printf ' jetboard # 打开 JetBoard 管理菜单\n'
|
||||
printf ' jetboard status # 查看 Docker 服务状态\n'
|
||||
printf ' jetboard update # 拉取代码、备份数据库并按机器配置重建\n'
|
||||
printf ' jetboard reset # 重置或创建管理员账号密码\n'
|
||||
printf ' jetboard logs # 查看 app 日志\n'
|
||||
printf ' jetboard uninstall # 完整卸载 Docker 部署\n'
|
||||
printf '%s\n' "============================================================"
|
||||
}
|
||||
|
||||
@@ -462,6 +507,7 @@ main() {
|
||||
prepare_repo
|
||||
load_resource_helpers
|
||||
configure_env
|
||||
install_jetboard_command
|
||||
install_docker
|
||||
start_panel
|
||||
wait_for_app
|
||||
|
||||
603
scripts/install-jetboard-standalone.sh
Executable file
603
scripts/install-jetboard-standalone.sh
Executable file
@@ -0,0 +1,603 @@
|
||||
#!/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 "$@"
|
||||
531
scripts/jetboard.sh
Executable file
531
scripts/jetboard.sh
Executable file
@@ -0,0 +1,531 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
BRAND="JetBoard"
|
||||
CONFIG_FILE="${JETBOARD_CONFIG:-/etc/jetboard.conf}"
|
||||
DEPLOY_MODE="${JBOARD_DEPLOY_MODE:-auto}"
|
||||
SERVICE_NAME="${JBOARD_SERVICE_NAME:-jetboard}"
|
||||
SERVICE_USER="${JBOARD_SERVICE_USER:-jetboard}"
|
||||
APP_DIR="${JBOARD_APP_DIR:-${APP_DIR:-}}"
|
||||
COMPOSE="${COMPOSE:-docker compose}"
|
||||
|
||||
line() {
|
||||
printf '%s\n' "------------------------------------------------------------"
|
||||
}
|
||||
|
||||
section() {
|
||||
echo
|
||||
line
|
||||
printf '%s\n' "$1"
|
||||
line
|
||||
}
|
||||
|
||||
fail() {
|
||||
echo "$1" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
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
|
||||
fail "需要 root 权限。请使用 root 用户运行,或先安装 sudo。"
|
||||
fi
|
||||
}
|
||||
|
||||
load_config() {
|
||||
if [ -r "$CONFIG_FILE" ]; then
|
||||
# shellcheck disable=SC1090
|
||||
. "$CONFIG_FILE"
|
||||
DEPLOY_MODE="${JBOARD_DEPLOY_MODE:-${DEPLOY_MODE:-auto}}"
|
||||
SERVICE_NAME="${JBOARD_SERVICE_NAME:-${SERVICE_NAME:-jetboard}}"
|
||||
SERVICE_USER="${JBOARD_SERVICE_USER:-${SERVICE_USER:-jetboard}}"
|
||||
APP_DIR="${JBOARD_APP_DIR:-${APP_DIR:-}}"
|
||||
COMPOSE="${COMPOSE:-docker compose}"
|
||||
fi
|
||||
}
|
||||
|
||||
resolve_app_dir() {
|
||||
if [ -n "$APP_DIR" ]; then
|
||||
printf '%s' "$APP_DIR"
|
||||
return
|
||||
fi
|
||||
|
||||
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
|
||||
|
||||
printf '%s' "/opt/jboard"
|
||||
}
|
||||
|
||||
detect_deploy_mode() {
|
||||
case "$DEPLOY_MODE" in
|
||||
docker|standalone)
|
||||
return
|
||||
;;
|
||||
auto|'')
|
||||
;;
|
||||
*)
|
||||
echo "未知 JBOARD_DEPLOY_MODE=$DEPLOY_MODE,回退为 auto。" >&2
|
||||
;;
|
||||
esac
|
||||
|
||||
if [ -f "/etc/systemd/system/${SERVICE_NAME}.service" ] && [ -f "$APP_DIR/.next/standalone/server.js" ]; then
|
||||
DEPLOY_MODE="standalone"
|
||||
elif [ -f "$APP_DIR/docker-compose.yml" ]; then
|
||||
DEPLOY_MODE="docker"
|
||||
else
|
||||
DEPLOY_MODE="standalone"
|
||||
fi
|
||||
}
|
||||
|
||||
load_config
|
||||
APP_DIR="$(resolve_app_dir)"
|
||||
[ -d "$APP_DIR" ] || fail "未找到 J-Board Lite 安装目录:$APP_DIR"
|
||||
[ -f "$APP_DIR/package.json" ] || fail "安装目录不是有效的 J-Board Lite 仓库:$APP_DIR"
|
||||
cd "$APP_DIR"
|
||||
detect_deploy_mode
|
||||
|
||||
load_env_for_prompts() {
|
||||
if [ -r .env ]; then
|
||||
set -a
|
||||
# shellcheck disable=SC1091
|
||||
. ./.env
|
||||
set +a
|
||||
elif [ "$(id -u)" -ne 0 ] && command -v sudo >/dev/null 2>&1; then
|
||||
local env_dump
|
||||
env_dump="$(sudo sh -c "set -a; . '$APP_DIR/.env' 2>/dev/null; env" || true)"
|
||||
if [ -n "$env_dump" ]; then
|
||||
while IFS='=' read -r key value; do
|
||||
case "$key" in
|
||||
APP_PORT|SITE_NAME|NEXTAUTH_URL|SUBSCRIPTION_URL|ADMIN_EMAIL|ADMIN_NAME)
|
||||
export "$key=$value"
|
||||
;;
|
||||
esac
|
||||
done <<EOF
|
||||
$env_dump
|
||||
EOF
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
load_standalone_resource_helpers() {
|
||||
if [ -f "$APP_DIR/scripts/lib-standalone-profile.sh" ]; then
|
||||
# shellcheck disable=SC1091
|
||||
. "$APP_DIR/scripts/lib-standalone-profile.sh"
|
||||
fi
|
||||
}
|
||||
|
||||
random_password() {
|
||||
if command -v openssl >/dev/null 2>&1; then
|
||||
openssl rand -hex 12
|
||||
else
|
||||
date +%s | cksum | awk '{print "jb" $1 $2}'
|
||||
fi
|
||||
}
|
||||
|
||||
prompt_value() {
|
||||
local label="$1"
|
||||
local default="$2"
|
||||
local value=""
|
||||
|
||||
if is_interactive; then
|
||||
prompt_print "$label [$default]: "
|
||||
prompt_read value || true
|
||||
fi
|
||||
|
||||
if [ -z "$value" ]; then
|
||||
value="$default"
|
||||
fi
|
||||
printf '%s' "$value"
|
||||
}
|
||||
|
||||
prompt_secret_or_generated() {
|
||||
local label="$1"
|
||||
local default="$2"
|
||||
local value=""
|
||||
|
||||
if is_interactive; then
|
||||
prompt_print "$label [回车自动生成]: "
|
||||
prompt_read value || true
|
||||
fi
|
||||
|
||||
if [ -z "$value" ]; then
|
||||
value="$default"
|
||||
fi
|
||||
printf '%s' "$value"
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
docker_compose() {
|
||||
local -a compose_cmd
|
||||
local -a env_args=()
|
||||
|
||||
read -r -a compose_cmd <<< "$COMPOSE"
|
||||
if [ "${#compose_cmd[@]}" -eq 0 ]; then
|
||||
compose_cmd=(docker compose)
|
||||
fi
|
||||
|
||||
if [ -n "${COMPOSE_PARALLEL_LIMIT:-}" ]; then
|
||||
env_args+=("COMPOSE_PARALLEL_LIMIT=$COMPOSE_PARALLEL_LIMIT")
|
||||
fi
|
||||
|
||||
if [ "$(id -u)" -eq 0 ]; then
|
||||
env "${env_args[@]}" "${compose_cmd[@]}" "$@"
|
||||
else
|
||||
run_as_root env "${env_args[@]}" "${compose_cmd[@]}" "$@"
|
||||
fi
|
||||
}
|
||||
|
||||
standalone_service_ctl() {
|
||||
run_as_root systemctl "$@" "$SERVICE_NAME"
|
||||
}
|
||||
|
||||
standalone_logs() {
|
||||
run_as_root journalctl -u "$SERVICE_NAME" -f
|
||||
}
|
||||
|
||||
standalone_backup_sqlite_database() {
|
||||
run_as_root mkdir -p "$APP_DIR/backups"
|
||||
|
||||
local backup_path="$APP_DIR/backups/jboard-standalone-$(date +%F-%H%M%S).tar.gz"
|
||||
if [ -f "$APP_DIR/storage/jboard.db" ]; then
|
||||
run_as_root tar -C "$APP_DIR/storage" -czf "$backup_path" \
|
||||
jboard.db \
|
||||
$( [ -f "$APP_DIR/storage/jboard.db-wal" ] && printf '%s' "jboard.db-wal" || true ) \
|
||||
$( [ -f "$APP_DIR/storage/jboard.db-shm" ] && printf '%s' "jboard.db-shm" || true )
|
||||
echo "SQLite backup saved: $backup_path"
|
||||
else
|
||||
echo "No existing SQLite database found; skipping database backup."
|
||||
fi
|
||||
}
|
||||
|
||||
prepare_runtime_tree() {
|
||||
[ -f "$APP_DIR/.next/standalone/server.js" ] || fail "未找到 standalone 构建产物,请先执行 jetboard update 或重新安装。"
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
standalone_build_app() {
|
||||
section "$BRAND 构建"
|
||||
load_standalone_resource_helpers
|
||||
|
||||
if command -v jboard_prepare_standalone_build_env >/dev/null 2>&1; then
|
||||
jboard_prepare_standalone_build_env "$APP_DIR"
|
||||
jboard_print_standalone_profile
|
||||
else
|
||||
export NEXT_TELEMETRY_DISABLED=1
|
||||
fi
|
||||
|
||||
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 mkdir -p "$APP_DIR/storage" "$APP_DIR/backups"
|
||||
if id "$SERVICE_USER" >/dev/null 2>&1; then
|
||||
run_as_root chown -R "$SERVICE_USER" "$APP_DIR/storage" "$APP_DIR/backups"
|
||||
fi
|
||||
}
|
||||
|
||||
print_usage() {
|
||||
cat <<EOF
|
||||
$BRAND 管理命令
|
||||
|
||||
当前模式:$DEPLOY_MODE
|
||||
|
||||
用法:
|
||||
jetboard 打开交互菜单
|
||||
jetboard status 查看服务状态
|
||||
jetboard update 按当前部署模式更新
|
||||
jetboard reset 重置或创建管理员账号密码
|
||||
jetboard logs 查看服务日志
|
||||
jetboard restart 重启面板
|
||||
jetboard stop 停止面板
|
||||
jetboard start 启动面板
|
||||
jetboard uninstall 完整卸载当前部署
|
||||
|
||||
可选环境变量:
|
||||
JBOARD_APP_DIR=/opt/jboard
|
||||
JBOARD_DEPLOY_MODE=docker|standalone
|
||||
JBOARD_BUILD_PROFILE=auto|low|normal
|
||||
EOF
|
||||
}
|
||||
|
||||
status_cmd() {
|
||||
section "$BRAND 状态"
|
||||
printf '部署模式:%s\n' "$DEPLOY_MODE"
|
||||
printf '安装目录:%s\n' "$APP_DIR"
|
||||
if git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
||||
printf '当前版本:%s\n' "$(git rev-parse --short HEAD 2>/dev/null || true)"
|
||||
printf '当前分支:%s\n' "$(git branch --show-current 2>/dev/null || true)"
|
||||
fi
|
||||
echo
|
||||
|
||||
if [ "$DEPLOY_MODE" = "docker" ]; then
|
||||
docker_compose ps
|
||||
else
|
||||
printf '服务名称:%s\n\n' "$SERVICE_NAME"
|
||||
run_as_root systemctl --no-pager status "$SERVICE_NAME" || true
|
||||
fi
|
||||
}
|
||||
|
||||
docker_update_cmd() {
|
||||
[ -f "$APP_DIR/scripts/upgrade-jboard-panel.sh" ] || fail "未找到更新脚本:$APP_DIR/scripts/upgrade-jboard-panel.sh"
|
||||
run_as_root env \
|
||||
APP_DIR="$APP_DIR" \
|
||||
COMPOSE="$COMPOSE" \
|
||||
BACKUP="${BACKUP:-1}" \
|
||||
JBOARD_BUILD_PROFILE="${JBOARD_BUILD_PROFILE:-auto}" \
|
||||
bash "$APP_DIR/scripts/upgrade-jboard-panel.sh"
|
||||
}
|
||||
|
||||
standalone_update_cmd() {
|
||||
standalone_backup_sqlite_database
|
||||
|
||||
echo "[1/5] Pulling latest code..."
|
||||
run_in_app_dir git -c safe.directory="$APP_DIR" pull --ff-only
|
||||
|
||||
echo "[2/5] Installing dependencies and building..."
|
||||
standalone_build_app
|
||||
|
||||
echo "[3/5] Restarting service..."
|
||||
standalone_service_ctl restart
|
||||
|
||||
echo "[4/5] Waiting for app..."
|
||||
sleep 5
|
||||
|
||||
echo "[5/5] Service status:"
|
||||
status_cmd
|
||||
}
|
||||
|
||||
update_cmd() {
|
||||
section "$BRAND 更新"
|
||||
if [ "$DEPLOY_MODE" = "docker" ]; then
|
||||
docker_update_cmd
|
||||
else
|
||||
standalone_update_cmd
|
||||
fi
|
||||
}
|
||||
|
||||
reset_admin_cmd() {
|
||||
section "重置管理员账号"
|
||||
load_env_for_prompts
|
||||
|
||||
local email="${1:-${ADMIN_RESET_EMAIL:-}}"
|
||||
local password="${2:-${ADMIN_RESET_PASSWORD:-}}"
|
||||
local name="${3:-${ADMIN_RESET_NAME:-}}"
|
||||
|
||||
email="${email:-$(prompt_value "管理员邮箱" "${ADMIN_EMAIL:-admin@jboard.local}")}"
|
||||
password="${password:-$(prompt_secret_or_generated "新管理员密码" "$(random_password)")}"
|
||||
name="${name:-$(prompt_value "管理员昵称" "${ADMIN_NAME:-Admin}")}"
|
||||
|
||||
[ -n "$email" ] || fail "管理员邮箱不能为空。"
|
||||
[ -n "$password" ] || fail "管理员密码不能为空。"
|
||||
|
||||
if [ "$DEPLOY_MODE" = "docker" ]; then
|
||||
docker_compose --profile setup run --rm \
|
||||
-e "ADMIN_RESET_EMAIL=$email" \
|
||||
-e "ADMIN_RESET_PASSWORD=$password" \
|
||||
-e "ADMIN_RESET_NAME=$name" \
|
||||
init sh -lc 'npm run db:push && npm run admin:reset && chown -R 1001:1001 /app/storage'
|
||||
else
|
||||
run_in_app_dir npm run db:push
|
||||
run_in_app_dir env \
|
||||
ADMIN_RESET_EMAIL="$email" \
|
||||
ADMIN_RESET_PASSWORD="$password" \
|
||||
ADMIN_RESET_NAME="$name" \
|
||||
npm run admin:reset
|
||||
run_as_root chown -R "$SERVICE_USER" "$APP_DIR/storage" || true
|
||||
fi
|
||||
|
||||
echo
|
||||
printf '管理员邮箱:%s\n' "$email"
|
||||
printf '管理员密码:%s\n' "$password"
|
||||
}
|
||||
|
||||
logs_cmd() {
|
||||
if [ "$DEPLOY_MODE" = "docker" ]; then
|
||||
docker_compose logs -f app
|
||||
else
|
||||
standalone_logs
|
||||
fi
|
||||
}
|
||||
|
||||
restart_cmd() {
|
||||
section "$BRAND 重启"
|
||||
if [ "$DEPLOY_MODE" = "docker" ]; then
|
||||
docker_compose up -d app
|
||||
else
|
||||
standalone_service_ctl restart
|
||||
fi
|
||||
status_cmd
|
||||
}
|
||||
|
||||
stop_cmd() {
|
||||
section "$BRAND 停止"
|
||||
if [ "$DEPLOY_MODE" = "docker" ]; then
|
||||
docker_compose stop app
|
||||
else
|
||||
standalone_service_ctl stop
|
||||
fi
|
||||
}
|
||||
|
||||
start_cmd() {
|
||||
section "$BRAND 启动"
|
||||
if [ "$DEPLOY_MODE" = "docker" ]; then
|
||||
docker_compose up -d app
|
||||
else
|
||||
standalone_service_ctl start
|
||||
fi
|
||||
status_cmd
|
||||
}
|
||||
|
||||
uninstall_cmd() {
|
||||
local uninstall_script=""
|
||||
if [ "$DEPLOY_MODE" = "docker" ]; then
|
||||
uninstall_script="$APP_DIR/scripts/uninstall-jboard-panel.sh"
|
||||
else
|
||||
uninstall_script="$APP_DIR/scripts/uninstall-jetboard-standalone.sh"
|
||||
fi
|
||||
[ -f "$uninstall_script" ] || fail "未找到卸载脚本:$uninstall_script"
|
||||
|
||||
local tmp
|
||||
tmp="$(mktemp)"
|
||||
cp "$uninstall_script" "$tmp"
|
||||
chmod +x "$tmp"
|
||||
|
||||
if [ "$DEPLOY_MODE" = "docker" ]; then
|
||||
run_as_root env \
|
||||
JBOARD_APP_DIR="$APP_DIR" \
|
||||
COMPOSE="$COMPOSE" \
|
||||
JETBOARD_CONFIG="$CONFIG_FILE" \
|
||||
bash "$tmp"
|
||||
else
|
||||
run_as_root env \
|
||||
JBOARD_APP_DIR="$APP_DIR" \
|
||||
JBOARD_SERVICE_NAME="$SERVICE_NAME" \
|
||||
JBOARD_SERVICE_USER="$SERVICE_USER" \
|
||||
JETBOARD_CONFIG="$CONFIG_FILE" \
|
||||
bash "$tmp"
|
||||
fi
|
||||
rm -f "$tmp"
|
||||
}
|
||||
|
||||
menu_cmd() {
|
||||
if ! is_interactive; then
|
||||
print_usage
|
||||
return
|
||||
fi
|
||||
|
||||
while true; do
|
||||
section "$BRAND 管理菜单"
|
||||
printf '部署模式:%s\n' "$DEPLOY_MODE"
|
||||
printf '安装目录:%s\n\n' "$APP_DIR"
|
||||
printf ' 1. 查看状态\n'
|
||||
printf ' 2. 更新面板\n'
|
||||
printf ' 3. 重置管理员账号密码\n'
|
||||
printf ' 4. 查看日志\n'
|
||||
printf ' 5. 重启面板\n'
|
||||
printf ' 6. 停止面板\n'
|
||||
printf ' 7. 启动面板\n'
|
||||
printf ' 8. 卸载面板\n'
|
||||
printf ' 0. 退出\n'
|
||||
echo
|
||||
prompt_print "请选择操作 [1]: "
|
||||
|
||||
local choice=""
|
||||
prompt_read choice || true
|
||||
choice="${choice:-1}"
|
||||
|
||||
case "$choice" in
|
||||
1) status_cmd ;;
|
||||
2) update_cmd ;;
|
||||
3) reset_admin_cmd ;;
|
||||
4) logs_cmd ;;
|
||||
5) restart_cmd ;;
|
||||
6) stop_cmd ;;
|
||||
7) start_cmd ;;
|
||||
8) uninstall_cmd; break ;;
|
||||
0|q|Q|exit) break ;;
|
||||
*) echo "未知选项:$choice" >&2 ;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
main() {
|
||||
local command="${1:-menu}"
|
||||
if [ "$#" -gt 0 ]; then
|
||||
shift
|
||||
fi
|
||||
|
||||
case "$command" in
|
||||
menu) menu_cmd ;;
|
||||
status|ps) status_cmd ;;
|
||||
update|upgrade) update_cmd ;;
|
||||
reset|reset-admin) reset_admin_cmd "$@" ;;
|
||||
logs|log) logs_cmd ;;
|
||||
restart) restart_cmd ;;
|
||||
stop) stop_cmd ;;
|
||||
start) start_cmd ;;
|
||||
uninstall|remove) uninstall_cmd ;;
|
||||
help|-h|--help) print_usage ;;
|
||||
*) print_usage; exit 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
main "$@"
|
||||
191
scripts/lib-standalone-profile.sh
Executable file
191
scripts/lib-standalone-profile.sh
Executable file
@@ -0,0 +1,191 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Shared profile detection for non-Docker installs and updates.
|
||||
# Strong machines use the default npm/Next behavior. Small machines trade time
|
||||
# for lower peak CPU and memory usage.
|
||||
|
||||
JBOARD_STANDALONE_PROFILE_RESOLVED=""
|
||||
JBOARD_CPU_COUNT=""
|
||||
JBOARD_HOST_MEM_MB=""
|
||||
JBOARD_APP_DISK_AVAIL_MB=""
|
||||
JBOARD_BUILD_NODE_HEAP_MB=""
|
||||
JBOARD_RUNTIME_NODE_HEAP_MB=""
|
||||
JBOARD_RUNTIME_NODE_OPTIONS=""
|
||||
|
||||
jboard_cpu_count() {
|
||||
local value=""
|
||||
|
||||
if command -v nproc >/dev/null 2>&1; then
|
||||
value="$(nproc 2>/dev/null || true)"
|
||||
elif command -v getconf >/dev/null 2>&1; then
|
||||
value="$(getconf _NPROCESSORS_ONLN 2>/dev/null || true)"
|
||||
elif command -v sysctl >/dev/null 2>&1; then
|
||||
value="$(sysctl -n hw.ncpu 2>/dev/null || true)"
|
||||
fi
|
||||
|
||||
case "$value" in
|
||||
''|*[!0-9]*) echo 1 ;;
|
||||
*) echo "$value" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
jboard_host_mem_mb() {
|
||||
local value=""
|
||||
|
||||
if [ -r /proc/meminfo ]; then
|
||||
value="$(awk '/^MemTotal:/ {print int($2 / 1024)}' /proc/meminfo 2>/dev/null || true)"
|
||||
elif command -v getconf >/dev/null 2>&1; then
|
||||
local pages page_size
|
||||
pages="$(getconf _PHYS_PAGES 2>/dev/null || true)"
|
||||
page_size="$(getconf PAGE_SIZE 2>/dev/null || true)"
|
||||
if [ -n "$pages" ] && [ -n "$page_size" ]; then
|
||||
value="$((pages * page_size / 1024 / 1024))"
|
||||
fi
|
||||
elif command -v sysctl >/dev/null 2>&1; then
|
||||
local bytes
|
||||
bytes="$(sysctl -n hw.memsize 2>/dev/null || true)"
|
||||
if [ -n "$bytes" ]; then
|
||||
value="$((bytes / 1024 / 1024))"
|
||||
fi
|
||||
fi
|
||||
|
||||
case "$value" in
|
||||
''|*[!0-9]*) echo 0 ;;
|
||||
*) echo "$value" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
jboard_path_avail_mb() {
|
||||
local path="$1"
|
||||
local value=""
|
||||
|
||||
if [ -n "$path" ]; then
|
||||
value="$(df -Pm "$path" 2>/dev/null | awk 'NR == 2 {print $4}' || true)"
|
||||
fi
|
||||
|
||||
case "$value" in
|
||||
''|*[!0-9]*) echo 0 ;;
|
||||
*) echo "$value" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
jboard_standalone_build_heap_mb() {
|
||||
local mem_mb="$1"
|
||||
|
||||
if [ -n "${JBOARD_LOW_RESOURCE_NODE_MB:-}" ]; then
|
||||
echo "$JBOARD_LOW_RESOURCE_NODE_MB"
|
||||
return
|
||||
fi
|
||||
|
||||
if [ "$mem_mb" -gt 0 ] && [ "$mem_mb" -le 1200 ]; then
|
||||
echo 640
|
||||
elif [ "$mem_mb" -gt 0 ] && [ "$mem_mb" -le 1700 ]; then
|
||||
echo 768
|
||||
elif [ "$mem_mb" -gt 0 ] && [ "$mem_mb" -le 2400 ]; then
|
||||
echo 1024
|
||||
else
|
||||
echo 1536
|
||||
fi
|
||||
}
|
||||
|
||||
jboard_standalone_runtime_heap_mb() {
|
||||
local mem_mb="$1"
|
||||
|
||||
if [ -n "${JBOARD_RUNTIME_NODE_MB:-}" ]; then
|
||||
echo "$JBOARD_RUNTIME_NODE_MB"
|
||||
return
|
||||
fi
|
||||
|
||||
if [ "$mem_mb" -gt 0 ] && [ "$mem_mb" -le 1200 ]; then
|
||||
echo 512
|
||||
elif [ "$mem_mb" -gt 0 ] && [ "$mem_mb" -le 1700 ]; then
|
||||
echo 768
|
||||
elif [ "$mem_mb" -gt 0 ] && [ "$mem_mb" -le 2400 ]; then
|
||||
echo 1024
|
||||
else
|
||||
echo ""
|
||||
fi
|
||||
}
|
||||
|
||||
jboard_resolve_standalone_profile() {
|
||||
local requested="${JBOARD_BUILD_PROFILE:-auto}"
|
||||
local cpu="$1"
|
||||
local mem_mb="$2"
|
||||
local disk_mb="$3"
|
||||
|
||||
case "$requested" in
|
||||
low|slow|small)
|
||||
echo low
|
||||
return
|
||||
;;
|
||||
normal|fast|strong)
|
||||
echo normal
|
||||
return
|
||||
;;
|
||||
auto|'')
|
||||
;;
|
||||
*)
|
||||
echo "未知 JBOARD_BUILD_PROFILE=$requested,回退为 auto。" >&2
|
||||
;;
|
||||
esac
|
||||
|
||||
if [ "$cpu" -le 1 ]; then
|
||||
echo low
|
||||
elif [ "$mem_mb" -gt 0 ] && [ "$mem_mb" -lt 2048 ]; then
|
||||
echo low
|
||||
elif [ "$disk_mb" -gt 0 ] && [ "$disk_mb" -lt 8192 ]; then
|
||||
echo low
|
||||
else
|
||||
echo normal
|
||||
fi
|
||||
}
|
||||
|
||||
jboard_prepare_standalone_build_env() {
|
||||
local app_dir="${1:-.}"
|
||||
|
||||
JBOARD_CPU_COUNT="$(jboard_cpu_count)"
|
||||
JBOARD_HOST_MEM_MB="$(jboard_host_mem_mb)"
|
||||
JBOARD_APP_DISK_AVAIL_MB="$(jboard_path_avail_mb "$app_dir")"
|
||||
JBOARD_STANDALONE_PROFILE_RESOLVED="$(jboard_resolve_standalone_profile "$JBOARD_CPU_COUNT" "$JBOARD_HOST_MEM_MB" "$JBOARD_APP_DISK_AVAIL_MB")"
|
||||
|
||||
export NEXT_TELEMETRY_DISABLED=1
|
||||
|
||||
if [ "$JBOARD_STANDALONE_PROFILE_RESOLVED" = "low" ]; then
|
||||
JBOARD_BUILD_NODE_HEAP_MB="$(jboard_standalone_build_heap_mb "$JBOARD_HOST_MEM_MB")"
|
||||
JBOARD_RUNTIME_NODE_HEAP_MB="$(jboard_standalone_runtime_heap_mb "$JBOARD_HOST_MEM_MB")"
|
||||
JBOARD_RUNTIME_NODE_OPTIONS="--max-old-space-size=${JBOARD_RUNTIME_NODE_HEAP_MB}"
|
||||
export NPM_CONFIG_JOBS="${NPM_CONFIG_JOBS:-1}"
|
||||
export npm_config_jobs="${npm_config_jobs:-$NPM_CONFIG_JOBS}"
|
||||
export NODE_OPTIONS="${NODE_OPTIONS:---max-old-space-size=${JBOARD_BUILD_NODE_HEAP_MB}}"
|
||||
else
|
||||
JBOARD_BUILD_NODE_HEAP_MB=""
|
||||
JBOARD_RUNTIME_NODE_HEAP_MB=""
|
||||
JBOARD_RUNTIME_NODE_OPTIONS="${JBOARD_RUNTIME_NODE_OPTIONS:-}"
|
||||
export NPM_CONFIG_JOBS="${NPM_CONFIG_JOBS:-}"
|
||||
fi
|
||||
}
|
||||
|
||||
jboard_is_low_resource_standalone() {
|
||||
[ "${JBOARD_STANDALONE_PROFILE_RESOLVED:-}" = "low" ]
|
||||
}
|
||||
|
||||
jboard_print_standalone_profile() {
|
||||
local disk="${JBOARD_APP_DISK_AVAIL_MB:-0}"
|
||||
local disk_text="unknown"
|
||||
|
||||
if [ "$disk" -gt 0 ]; then
|
||||
disk_text="${disk}MB"
|
||||
fi
|
||||
|
||||
if jboard_is_low_resource_standalone; then
|
||||
echo "检测到低资源本机环境:CPU=${JBOARD_CPU_COUNT:-?},内存=${JBOARD_HOST_MEM_MB:-0}MB,可用空间=${disk_text}"
|
||||
echo "启用慢速低占用模式:npm jobs=${NPM_CONFIG_JOBS:-1},构建 Node heap=${JBOARD_BUILD_NODE_HEAP_MB:-?}MB,运行 Node heap=${JBOARD_RUNTIME_NODE_HEAP_MB:-?}MB。"
|
||||
else
|
||||
echo "检测到常规本机环境:CPU=${JBOARD_CPU_COUNT:-?},内存=${JBOARD_HOST_MEM_MB:-0}MB,可用空间=${disk_text}"
|
||||
echo "使用 npm/Next 默认构建策略,不额外限制并发或 Node heap。"
|
||||
fi
|
||||
|
||||
if [ "$disk" -gt 0 ] && [ "$disk" -lt 8192 ]; then
|
||||
echo "提示:可用空间低于 8GB,建议扩容或清理缓存后再构建。"
|
||||
fi
|
||||
}
|
||||
57
scripts/reset-admin.ts
Normal file
57
scripts/reset-admin.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import "dotenv/config";
|
||||
import { PrismaClient } from "@prisma/client";
|
||||
import { PrismaBetterSqlite3 } from "@prisma/adapter-better-sqlite3";
|
||||
import bcrypt from "bcryptjs";
|
||||
|
||||
const adapter = new PrismaBetterSqlite3({
|
||||
url: process.env.DATABASE_URL || "file:./storage/jboard.db",
|
||||
});
|
||||
const prisma = new PrismaClient({ adapter });
|
||||
|
||||
function envValue(key: string, fallback = "") {
|
||||
return process.env[key]?.trim() || fallback;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const email = envValue("ADMIN_RESET_EMAIL", envValue("ADMIN_EMAIL", "admin@jboard.local")).toLowerCase();
|
||||
const password = envValue("ADMIN_RESET_PASSWORD", envValue("ADMIN_PASSWORD"));
|
||||
const name = envValue("ADMIN_RESET_NAME", envValue("ADMIN_NAME", "Admin"));
|
||||
|
||||
if (!email) {
|
||||
throw new Error("ADMIN_RESET_EMAIL is required.");
|
||||
}
|
||||
|
||||
if (!password || password.length < 6) {
|
||||
throw new Error("ADMIN_RESET_PASSWORD must be at least 6 characters.");
|
||||
}
|
||||
|
||||
const hashedPassword = await bcrypt.hash(password, 12);
|
||||
|
||||
await prisma.user.upsert({
|
||||
where: { email },
|
||||
update: {
|
||||
password: hashedPassword,
|
||||
name,
|
||||
role: "ADMIN",
|
||||
status: "ACTIVE",
|
||||
emailVerifiedAt: new Date(),
|
||||
},
|
||||
create: {
|
||||
email,
|
||||
password: hashedPassword,
|
||||
name,
|
||||
role: "ADMIN",
|
||||
status: "ACTIVE",
|
||||
emailVerifiedAt: new Date(),
|
||||
},
|
||||
});
|
||||
|
||||
console.log(`Admin reset completed: ${email}`);
|
||||
}
|
||||
|
||||
main()
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
})
|
||||
.finally(() => prisma.$disconnect());
|
||||
174
scripts/uninstall-jboard-panel.sh
Executable file
174
scripts/uninstall-jboard-panel.sh
Executable file
@@ -0,0 +1,174 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
CONFIG_FILE="${JETBOARD_CONFIG:-/etc/jetboard.conf}"
|
||||
APP_DIR="${JBOARD_APP_DIR:-${APP_DIR:-}}"
|
||||
COMPOSE="${COMPOSE:-docker compose}"
|
||||
if [ -z "${BACKUP_ROOT:-}" ]; then
|
||||
if [ "$(id -u)" -eq 0 ]; then
|
||||
BACKUP_ROOT="/root"
|
||||
else
|
||||
BACKUP_ROOT="${HOME:-/tmp}"
|
||||
fi
|
||||
fi
|
||||
UNINSTALL_CONFIRM="${UNINSTALL_CONFIRM:-}"
|
||||
|
||||
if [ -z "$APP_DIR" ] && [ -r "$CONFIG_FILE" ]; then
|
||||
# shellcheck disable=SC1090
|
||||
. "$CONFIG_FILE"
|
||||
APP_DIR="${JBOARD_APP_DIR:-${APP_DIR:-}}"
|
||||
COMPOSE="${COMPOSE:-docker compose}"
|
||||
fi
|
||||
|
||||
APP_DIR="${APP_DIR:-/opt/jboard}"
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
compose() {
|
||||
if [ "$(id -u)" -eq 0 ]; then
|
||||
$COMPOSE "$@"
|
||||
else
|
||||
run_as_root $COMPOSE "$@"
|
||||
fi
|
||||
}
|
||||
|
||||
line() {
|
||||
printf '%s\n' "------------------------------------------------------------"
|
||||
}
|
||||
|
||||
section() {
|
||||
echo
|
||||
line
|
||||
printf '%s\n' "$1"
|
||||
line
|
||||
}
|
||||
|
||||
confirm_uninstall() {
|
||||
if [ "$UNINSTALL_CONFIRM" = "YES" ]; then
|
||||
return
|
||||
fi
|
||||
|
||||
if ! is_interactive; then
|
||||
echo "非交互卸载请设置 UNINSTALL_CONFIRM=YES。" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
section "确认卸载 J-Board Lite Docker 部署"
|
||||
printf '将停止并删除 Docker Compose 服务和 SQLite volume。\n'
|
||||
printf '将删除安装目录:%s\n' "$APP_DIR"
|
||||
printf '将删除命令:/usr/local/bin/jetboard、/usr/local/bin/JetBoard\n'
|
||||
printf '将删除配置:%s\n' "$CONFIG_FILE"
|
||||
echo "卸载前会尝试备份 .env、backups 和 SQLite 数据库到 ${BACKUP_ROOT}。"
|
||||
echo
|
||||
prompt_print "请输入 YES 确认卸载: "
|
||||
local answer=""
|
||||
prompt_read answer || true
|
||||
if [ "$answer" != "YES" ]; then
|
||||
echo "已取消卸载。"
|
||||
exit 0
|
||||
fi
|
||||
}
|
||||
|
||||
backup_before_remove() {
|
||||
if [ ! -d "$APP_DIR" ]; then
|
||||
echo "安装目录不存在,跳过备份:$APP_DIR"
|
||||
return
|
||||
fi
|
||||
|
||||
local backup_dir backup_file sqlite_backup
|
||||
backup_dir="${BACKUP_ROOT%/}/jboard-docker-uninstall-backup-$(date +%F-%H%M%S)"
|
||||
backup_file="${backup_dir}.tar.gz"
|
||||
sqlite_backup="$backup_dir/sqlite-storage.tar.gz"
|
||||
|
||||
run_as_root mkdir -p "$backup_dir"
|
||||
if [ "$(id -u)" -ne 0 ]; then
|
||||
run_as_root chown "$(id -u):$(id -g)" "$backup_dir"
|
||||
fi
|
||||
if [ -f "$APP_DIR/.env" ]; then
|
||||
run_as_root cp "$APP_DIR/.env" "$backup_dir/.env"
|
||||
fi
|
||||
if [ -d "$APP_DIR/backups" ]; then
|
||||
run_as_root cp -R "$APP_DIR/backups" "$backup_dir/backups"
|
||||
fi
|
||||
|
||||
if [ -f "$APP_DIR/docker-compose.yml" ]; then
|
||||
local backup_cmd='cd /app/storage 2>/dev/null && if [ -f jboard.db ]; then set -- jboard.db; [ -f jboard.db-wal ] && set -- "$@" jboard.db-wal; [ -f jboard.db-shm ] && set -- "$@" jboard.db-shm; tar -czf - "$@"; fi'
|
||||
(
|
||||
cd "$APP_DIR"
|
||||
if compose ps --services --filter status=running 2>/dev/null | grep -qx app; then
|
||||
compose exec -T app sh -lc "$backup_cmd" > "$sqlite_backup" || true
|
||||
fi
|
||||
if [ ! -s "$sqlite_backup" ]; then
|
||||
compose --profile setup run --rm -T --entrypoint sh init -lc "$backup_cmd" > "$sqlite_backup" || true
|
||||
fi
|
||||
)
|
||||
if [ ! -s "$sqlite_backup" ]; then
|
||||
rm -f "$sqlite_backup"
|
||||
echo "未找到可备份的 SQLite volume,跳过数据库备份。"
|
||||
fi
|
||||
fi
|
||||
|
||||
run_as_root tar -C "$(dirname "$backup_dir")" -czf "$backup_file" "$(basename "$backup_dir")"
|
||||
run_as_root rm -rf "$backup_dir"
|
||||
echo "卸载备份已保存:$backup_file"
|
||||
}
|
||||
|
||||
remove_compose_stack() {
|
||||
section "删除 Docker Compose 服务"
|
||||
if [ -f "$APP_DIR/docker-compose.yml" ]; then
|
||||
(
|
||||
cd "$APP_DIR"
|
||||
compose down --volumes --remove-orphans || true
|
||||
)
|
||||
fi
|
||||
}
|
||||
|
||||
remove_files() {
|
||||
section "删除文件"
|
||||
cd /
|
||||
run_as_root rm -f /usr/local/bin/jetboard /usr/local/bin/JetBoard
|
||||
run_as_root rm -f "$CONFIG_FILE"
|
||||
run_as_root rm -rf "$APP_DIR"
|
||||
}
|
||||
|
||||
main() {
|
||||
confirm_uninstall
|
||||
backup_before_remove
|
||||
remove_compose_stack
|
||||
remove_files
|
||||
|
||||
echo
|
||||
echo "J-Board Lite Docker 部署已卸载。Docker 本身不会被删除。"
|
||||
}
|
||||
|
||||
main "$@"
|
||||
158
scripts/uninstall-jetboard-standalone.sh
Executable file
158
scripts/uninstall-jetboard-standalone.sh
Executable file
@@ -0,0 +1,158 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
CONFIG_FILE="${JETBOARD_CONFIG:-/etc/jetboard.conf}"
|
||||
APP_DIR="${JBOARD_APP_DIR:-${APP_DIR:-}}"
|
||||
SERVICE_NAME="${JBOARD_SERVICE_NAME:-jetboard}"
|
||||
SERVICE_USER="${JBOARD_SERVICE_USER:-jetboard}"
|
||||
BACKUP_ROOT="${BACKUP_ROOT:-/root}"
|
||||
UNINSTALL_CONFIRM="${UNINSTALL_CONFIRM:-}"
|
||||
|
||||
if [ -z "$APP_DIR" ] && [ -r "$CONFIG_FILE" ]; then
|
||||
# shellcheck disable=SC1090
|
||||
. "$CONFIG_FILE"
|
||||
APP_DIR="${JBOARD_APP_DIR:-${APP_DIR:-}}"
|
||||
SERVICE_NAME="${JBOARD_SERVICE_NAME:-${SERVICE_NAME:-jetboard}}"
|
||||
SERVICE_USER="${JBOARD_SERVICE_USER:-${SERVICE_USER:-jetboard}}"
|
||||
fi
|
||||
|
||||
APP_DIR="${APP_DIR:-/opt/jboard}"
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
confirm_uninstall() {
|
||||
if [ "$UNINSTALL_CONFIRM" = "YES" ]; then
|
||||
return
|
||||
fi
|
||||
|
||||
if ! is_interactive; then
|
||||
echo "非交互卸载请设置 UNINSTALL_CONFIRM=YES。" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
section "确认卸载 J-Board Lite standalone"
|
||||
printf '将停止并删除服务:%s\n' "$SERVICE_NAME"
|
||||
printf '将删除安装目录:%s\n' "$APP_DIR"
|
||||
printf '将删除命令:/usr/local/bin/jetboard、/usr/local/bin/JetBoard\n'
|
||||
printf '将删除配置:%s\n' "$CONFIG_FILE"
|
||||
echo "卸载前会尝试备份 .env 和 storage 到 ${BACKUP_ROOT}。"
|
||||
echo
|
||||
prompt_print "请输入 YES 确认卸载: "
|
||||
local answer=""
|
||||
prompt_read answer || true
|
||||
if [ "$answer" != "YES" ]; then
|
||||
echo "已取消卸载。"
|
||||
exit 0
|
||||
fi
|
||||
}
|
||||
|
||||
backup_before_remove() {
|
||||
if [ ! -d "$APP_DIR" ]; then
|
||||
echo "安装目录不存在,跳过备份:$APP_DIR"
|
||||
return
|
||||
fi
|
||||
|
||||
local backup_dir backup_file
|
||||
backup_dir="${BACKUP_ROOT%/}/jboard-uninstall-backup-$(date +%F-%H%M%S)"
|
||||
backup_file="${backup_dir}.tar.gz"
|
||||
|
||||
run_as_root mkdir -p "$backup_dir"
|
||||
if [ -f "$APP_DIR/.env" ]; then
|
||||
run_as_root cp "$APP_DIR/.env" "$backup_dir/.env"
|
||||
fi
|
||||
if [ -d "$APP_DIR/storage" ]; then
|
||||
run_as_root cp -R "$APP_DIR/storage" "$backup_dir/storage"
|
||||
fi
|
||||
if [ -d "$APP_DIR/backups" ]; then
|
||||
run_as_root cp -R "$APP_DIR/backups" "$backup_dir/backups"
|
||||
fi
|
||||
|
||||
run_as_root tar -C "$(dirname "$backup_dir")" -czf "$backup_file" "$(basename "$backup_dir")"
|
||||
run_as_root rm -rf "$backup_dir"
|
||||
echo "卸载备份已保存:$backup_file"
|
||||
}
|
||||
|
||||
remove_service() {
|
||||
section "删除 systemd 服务"
|
||||
if command -v systemctl >/dev/null 2>&1; then
|
||||
run_as_root systemctl stop "$SERVICE_NAME" >/dev/null 2>&1 || true
|
||||
run_as_root systemctl disable "$SERVICE_NAME" >/dev/null 2>&1 || true
|
||||
fi
|
||||
run_as_root rm -f "/etc/systemd/system/${SERVICE_NAME}.service"
|
||||
if command -v systemctl >/dev/null 2>&1; then
|
||||
run_as_root systemctl daemon-reload || true
|
||||
run_as_root systemctl reset-failed "$SERVICE_NAME" >/dev/null 2>&1 || true
|
||||
fi
|
||||
}
|
||||
|
||||
remove_files() {
|
||||
section "删除文件"
|
||||
run_as_root rm -f /usr/local/bin/jetboard /usr/local/bin/JetBoard
|
||||
run_as_root rm -f "$CONFIG_FILE"
|
||||
run_as_root rm -rf "$APP_DIR"
|
||||
}
|
||||
|
||||
remove_user() {
|
||||
section "删除运行用户"
|
||||
if id "$SERVICE_USER" >/dev/null 2>&1; then
|
||||
if command -v userdel >/dev/null 2>&1; then
|
||||
run_as_root userdel "$SERVICE_USER" >/dev/null 2>&1 || true
|
||||
elif command -v deluser >/dev/null 2>&1; then
|
||||
run_as_root deluser "$SERVICE_USER" >/dev/null 2>&1 || true
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
main() {
|
||||
confirm_uninstall
|
||||
backup_before_remove
|
||||
remove_service
|
||||
remove_files
|
||||
remove_user
|
||||
|
||||
echo
|
||||
echo "J-Board Lite standalone 已卸载。"
|
||||
}
|
||||
|
||||
main "$@"
|
||||
Reference in New Issue
Block a user