mirror of
https://github.com/JetSprow/J-Board-Lite.git
synced 2026-05-01 01:14:10 +05:30
532 lines
13 KiB
Bash
Executable File
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 "$@"
|