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:
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 "$@"
|
||||
Reference in New Issue
Block a user