Files
NodeFlare/install.sh
T

307 lines
10 KiB
Bash
Executable File

#!/bin/sh
set -eu
script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
config_dir=/etc/nodeflare
config_file=$config_dir/config.toml
state_dir=/var/lib/nodeflare
install_dir=/opt/nodeflare
share_dir=$install_dir/share
public_frontend_dir=$share_dir/frontend
admin_frontend_dir=$share_dir/admin
agent_installer_dir=$share_dir/agent
server_binary=$install_dir/nodeflare
service_name=nodeflare
service_file=/etc/systemd/system/nodeflare.service
legacy_current_binary=/usr/local/bin/nodeflare
legacy_server_binary=/usr/local/bin/nodeflare-server
legacy_share_dir=/usr/local/share/nodeflare
legacy_service_file=/etc/systemd/system/nodeflare-server.service
agent_service_file=/etc/systemd/system/nodeflare-agent.service
build_target_dir=""
config_temp=""
tty_state=""
usage() {
printf '%s\n' \
'NodeFlare 面板安装脚本' \
'' \
'用法:' \
' sudo ./install.sh' \
' sudo ./install.sh --uninstall' \
' sudo ./install.sh --uninstall --purge' \
'' \
'首次安装会询问管理员用户名和密码。' \
'其他设置写入 /etc/nodeflare/config.toml,可在安装后编辑。' \
'重复运行会保留已有配置和数据库。' \
'--uninstall 保留配置和数据;只有同时指定 --purge 才彻底删除。'
}
log() {
printf '[NodeFlare] %s\n' "$1"
}
fail() {
printf '[NodeFlare] 错误:%s\n' "$1" >&2
exit 1
}
restore_tty() {
if [ -n "$tty_state" ]; then
stty "$tty_state" < /dev/tty 2>/dev/null || true
tty_state=""
fi
}
cleanup() {
restore_tty
[ -z "$config_temp" ] || rm -f -- "$config_temp"
[ -z "$build_target_dir" ] || rm -rf -- "$build_target_dir"
}
trap cleanup EXIT HUP INT TERM
toml_escape() {
printf '%s' "$1" | sed -e 's/\\/\\\\/g' -e 's/"/\\"/g'
}
prompt_line() {
printf '%s' "$1" > /dev/tty
IFS= read -r prompt_value < /dev/tty || fail "无法读取输入"
}
prompt_secret() {
printf '%s' "$1" > /dev/tty
tty_state=$(stty -g < /dev/tty) || fail "无法读取终端状态"
stty -echo < /dev/tty
if ! IFS= read -r prompt_value < /dev/tty; then
restore_tty
printf '\n' > /dev/tty
fail "无法读取密码"
fi
restore_tty
printf '\n' > /dev/tty
}
prompt_credentials() {
[ -r /dev/tty ] && [ -w /dev/tty ] || fail "首次安装必须在交互式终端中运行"
while :; do
prompt_line "管理员用户名 [admin]: "
admin_username=${prompt_value:-admin}
username_length=$(printf '%s' "$admin_username" | wc -m | tr -d '[:space:]')
case "$admin_username" in
*[!A-Za-z0-9_.-]*)
printf '%s\n' "用户名只能包含字母、数字、点、下划线和连字符。" > /dev/tty
;;
*)
if [ "$username_length" -gt 64 ]; then
printf '%s\n' "用户名不能超过 64 个字符。" > /dev/tty
else
break
fi
;;
esac
done
while :; do
prompt_secret "管理员密码(8-128 个字符): "
admin_password=$prompt_value
password_length=$(printf '%s' "$admin_password" | wc -m | tr -d '[:space:]')
if [ "$password_length" -lt 8 ] || [ "$password_length" -gt 128 ]; then
printf '%s\n' "密码长度必须在 8-128 个字符之间。" > /dev/tty
continue
fi
if LC_ALL=C printf '%s' "$admin_password" | grep -q '[[:cntrl:]]'; then
printf '%s\n' "密码不能包含控制字符。" > /dev/tty
continue
fi
normalized_password=$(printf '%s' "$admin_password" | tr '[:upper:]' '[:lower:]')
case "$normalized_password" in
change_me_with_a_strong_password|change-me-with-a-strong-password)
printf '%s\n' "请不要使用示例占位密码。" > /dev/tty
continue
;;
esac
prompt_secret "再次输入密码: "
[ "$admin_password" = "$prompt_value" ] || {
printf '%s\n' "两次输入的密码不一致。" > /dev/tty
continue
}
break
done
}
write_config() {
escaped_username=$(toml_escape "$admin_username")
escaped_password=$(toml_escape "$admin_password")
config_temp=$(mktemp "$config_dir/.config.toml.XXXXXX")
{
printf '%s\n' '# NodeFlare 服务端配置'
printf '%s\n' '# 修改后运行:systemctl restart nodeflare'
printf '\n'
printf 'database_url = "sqlite:///var/lib/nodeflare/nodeflare.db"\n'
printf 'bind_addr = "127.0.0.1:8080"\n'
printf 'admin_username = "%s"\n' "$escaped_username"
printf 'admin_password = "%s"\n' "$escaped_password"
printf '\n'
printf '%s\n' '# Cloudflare Turnstile:两项留空即禁用'
printf 'turnstile_site_key = ""\n'
printf 'turnstile_secret_key = ""\n'
printf '\n'
printf 'frontend_dir = "/opt/nodeflare/share/frontend"\n'
printf 'admin_frontend_dir = "/opt/nodeflare/share/admin"\n'
printf 'agent_dir = "/opt/nodeflare/share/agent"\n'
printf 'theme_dir = "/var/lib/nodeflare/themes"\n'
printf 'session_ttl_hours = 168\n'
} > "$config_temp"
chown root:root "$config_temp"
chmod 0600 "$config_temp"
mv -f -- "$config_temp" "$config_file"
config_temp=""
admin_password=""
prompt_value=""
}
migrate_config_paths() {
config_temp=$(mktemp "$config_dir/.config.toml.XXXXXX")
sed \
-e 's|^frontend_dir = "/usr/local/share/nodeflare/frontend"$|frontend_dir = "/opt/nodeflare/share/frontend"|' \
-e 's|^admin_frontend_dir = "/usr/local/share/nodeflare/admin"$|admin_frontend_dir = "/opt/nodeflare/share/admin"|' \
-e 's|^agent_dir = "/usr/local/share/nodeflare/agent"$|agent_dir = "/opt/nodeflare/share/agent"|' \
"$config_file" > "$config_temp"
chown root:root "$config_temp"
chmod 0600 "$config_temp"
mv -f -- "$config_temp" "$config_file"
config_temp=""
}
uninstall_server() {
purge=$1
log "停止并移除 NodeFlare 面板服务"
if command -v systemctl >/dev/null 2>&1; then
migrate_legacy_agent_service
systemctl disable --now "$service_name.service" >/dev/null 2>&1 || true
systemctl disable --now nodeflare-server.service >/dev/null 2>&1 || true
fi
rm -f -- "$service_file" "$server_binary" "$legacy_service_file" "$legacy_current_binary" "$legacy_server_binary"
rm -rf -- "$share_dir"
rmdir "$install_dir" 2>/dev/null || true
if command -v systemctl >/dev/null 2>&1; then
systemctl daemon-reload >/dev/null 2>&1 || true
systemctl reset-failed "$service_name.service" >/dev/null 2>&1 || true
systemctl reset-failed nodeflare-server.service >/dev/null 2>&1 || true
fi
if [ "$purge" = true ]; then
rm -rf -- "$config_dir" "$state_dir"
log "已删除配置和数据库"
else
log "已保留配置 $config_file 和数据目录 $state_dir"
log "如需彻底删除,运行:sudo ./install.sh --uninstall --purge"
fi
log "卸载完成"
}
migrate_legacy_agent_service() {
if [ -f "$service_file" ] && grep -Fq '/opt/nodeflare/agent' "$service_file"; then
log "将旧 Agent 服务 nodeflare 迁移为 nodeflare-agent"
systemctl disable --now nodeflare.service >/dev/null 2>&1 || true
if [ ! -e "$agent_service_file" ]; then
mv -- "$service_file" "$agent_service_file"
else
rm -f -- "$service_file"
fi
systemctl daemon-reload
systemctl enable --now nodeflare-agent.service >/dev/null 2>&1 || true
fi
}
mode=install
purge=false
case "$#:${1:-}:${2:-}" in
0::) ;;
1:-h:|1:--help:) usage; exit 0 ;;
1:--uninstall:) mode=uninstall ;;
2:--uninstall:--purge) mode=uninstall; purge=true ;;
*) usage >&2; fail "参数无效" ;;
esac
[ "$(id -u)" -eq 0 ] || fail "请使用 root 权限运行:sudo ./install.sh"
if [ "$mode" = uninstall ]; then
uninstall_server "$purge"
exit 0
fi
[ -d /run/systemd/system ] && command -v systemctl >/dev/null 2>&1 \
|| fail "当前系统未使用 systemd"
migrate_legacy_agent_service
for required_command in bun install mktemp sed grep wc tr id chown chmod mv cp stty; do
command -v "$required_command" >/dev/null 2>&1 || fail "缺少命令:$required_command"
done
new_config=false
if [ ! -f "$config_file" ]; then
new_config=true
prompt_credentials
else
log "保留已有配置:$config_file"
fi
log "构建前端和服务端"
build_target_dir=$(mktemp -d /tmp/nodeflare-build.XXXXXX)
CARGO_TARGET_DIR=$build_target_dir/cargo
export CARGO_TARGET_DIR
sh "$script_dir/scripts/build-frontend.sh"
sh "$script_dir/scripts/build-backend.sh"
install -d -m 0700 -o root -g root "$config_dir"
install -d -m 0750 -o root -g root "$state_dir"
install -d -m 0750 -o root -g root "$state_dir/themes"
chown -R root:root "$state_dir"
install -d -m 0755 -o root -g root "$install_dir"
install -d -m 0755 -o root -g root "$share_dir"
rm -rf -- "$public_frontend_dir" "$admin_frontend_dir" "$agent_installer_dir"
install -d -m 0755 -o root -g root "$public_frontend_dir" "$admin_frontend_dir" "$agent_installer_dir"
cp -a "$script_dir/frontend/dist/." "$public_frontend_dir/"
cp -a "$script_dir/frontend/admin-dist/." "$admin_frontend_dir/"
install -m 0755 "$script_dir/agent/agent.sh" "$agent_installer_dir/agent.sh"
install -m 0755 "$script_dir/agent/install-macos.sh" "$agent_installer_dir/install-macos.sh"
install -m 0755 "$script_dir/agent/install-freebsd.sh" "$agent_installer_dir/install-freebsd.sh"
install -m 0644 "$script_dir/agent/install.ps1" "$agent_installer_dir/install.ps1"
chown -R root:root "$share_dir"
chmod -R u=rwX,go=rX "$share_dir"
systemctl disable --now nodeflare-server.service >/dev/null 2>&1 || true
rm -f -- "$legacy_service_file" "$legacy_current_binary" "$legacy_server_binary"
install -m 0755 "$CARGO_TARGET_DIR/release/nodeflare" "$server_binary"
install -m 0644 "$script_dir/deploy/nodeflare.service" "$service_file"
if [ "$new_config" = true ]; then
write_config
else
migrate_config_paths
chown root:root "$config_file"
chmod 0600 "$config_file"
fi
if ! grep -Fq "$legacy_share_dir/" "$config_file"; then
rm -rf -- "$legacy_share_dir"
fi
systemctl daemon-reload
systemctl enable "$service_name.service" >/dev/null
if ! systemctl restart "$service_name.service"; then
journalctl -u "$service_name.service" -n 30 --no-pager >&2 || true
fail "NodeFlare 服务启动失败"
fi
if ! systemctl is-active --quiet "$service_name.service"; then
journalctl -u "$service_name.service" -n 30 --no-pager >&2 || true
fail "NodeFlare 服务未进入运行状态"
fi
log "安装完成"
printf '%s\n' \
"配置文件:$config_file" \
"程序目录:$install_dir" \
"数据目录:$state_dir" \
'服务状态:systemctl status nodeflare' \
'默认访问:http://127.0.0.1:8080'