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

532 lines
13 KiB
Bash
Executable File

#!/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 "$@"