#!/bin/bash set -e # 解决 macOS 下 tr 可能出现的非法字节序列问题 export LANG=en_US.UTF-8 export LC_ALL=C # GitHub repo used for release downloads REPO="Sagit-chu/flux-panel" # 固定版本号(Release 构建时自动填充,留空则获取最新版) PINNED_VERSION="" # 镜像加速配置(可由面板传入或交互式询问) PROXY_ENABLED="${PROXY_ENABLED:-}" PROXY_URL="${PROXY_URL:-}" # 镜像加速 maybe_proxy_url() { local url="$1" if [[ "$PROXY_ENABLED" == "false" ]]; then echo "$url" return fi local proxy="${PROXY_URL:-gcode.hostcentral.cc}" if [[ "$proxy" == https://* || "$proxy" == http://* ]]; then proxy="${proxy%/}" else proxy="https://${proxy%/}" fi echo "${proxy}/${url}" } ask_proxy_config() { if [[ -n "$PROXY_ENABLED" ]]; then return fi if [[ -n "$PROXY_URL" ]]; then PROXY_ENABLED="true" return fi echo "" echo "===============================================" echo " GitHub 加速配置" echo "===============================================" if ! read -r -p "是否开启 GitHub 加速? (Y/n): " proxy_choice; then proxy_choice="" fi case "$proxy_choice" in n|N) PROXY_ENABLED="false" echo "已关闭加速,将直连 GitHub" ;; *) PROXY_ENABLED="true" if ! read -r -p "加速地址 (默认 gcode.hostcentral.cc): " input_url; then input_url="" fi PROXY_URL="${input_url:-gcode.hostcentral.cc}" echo "已开启加速: $PROXY_URL" ;; esac echo "===============================================" } resolve_latest_release_tag() { local effective_url tag api_tag latest_url api_url latest_url="https://github.com/${REPO}/releases/latest" api_url="https://api.github.com/repos/${REPO}/releases/latest" effective_url=$(curl -fsSL -o /dev/null -w '%{url_effective}' -L "$(maybe_proxy_url "$latest_url")" 2>/dev/null || true) tag="${effective_url##*/}" if [[ -n "$tag" && "$tag" != "latest" ]]; then echo "$tag" return 0 fi api_tag=$(curl -fsSL "$(maybe_proxy_url "$api_url")" 2>/dev/null | grep -m1 '"tag_name"' | sed -E 's/.*"tag_name"[[:space:]]*:[[:space:]]*"([^"]+)".*/\1/' || true) if [[ -n "$api_tag" ]]; then echo "$api_tag" return 0 fi return 1 } resolve_version() { if [[ -n "${VERSION:-}" ]]; then echo "$VERSION" return 0 fi if [[ -n "${FLUX_VERSION:-}" ]]; then echo "$FLUX_VERSION" return 0 fi if [[ -n "${PINNED_VERSION:-}" ]]; then echo "$PINNED_VERSION" return 0 fi if resolve_latest_release_tag; then return 0 fi echo "❌ 无法获取最新版本号。你可以手动指定版本,例如:VERSION=<版本号> ./panel_install.sh" >&2 return 1 } # 根据版本号设置 compose 下载地址 set_compose_urls_by_version() { local version="$1" DOCKER_COMPOSEV4_URL=$(maybe_proxy_url "https://github.com/${REPO}/releases/download/${version}/docker-compose-v4.yml") DOCKER_COMPOSEV6_URL=$(maybe_proxy_url "https://github.com/${REPO}/releases/download/${version}/docker-compose-v6.yml") } ensure_compose_urls_initialized() { if [[ -n "${DOCKER_COMPOSEV4_URL:-}" && -n "${DOCKER_COMPOSEV6_URL:-}" ]]; then return 0 fi RESOLVED_VERSION=$(resolve_version) || return 1 set_compose_urls_by_version "$RESOLVED_VERSION" } # 根据IPv6支持情况选择docker-compose URL get_docker_compose_url() { if check_ipv6_support > /dev/null 2>&1; then echo "$DOCKER_COMPOSEV6_URL" else echo "$DOCKER_COMPOSEV4_URL" fi } # 检查 docker-compose 或 docker compose 命令 check_docker() { if command -v docker-compose &> /dev/null; then DOCKER_CMD="docker-compose" elif command -v docker &> /dev/null; then if docker compose version &> /dev/null; then DOCKER_CMD="docker compose" else echo "错误:检测到 docker,但不支持 'docker compose' 命令。请安装 docker-compose 或更新 docker 版本。" exit 1 fi else echo "错误:未检测到 docker 或 docker-compose 命令。请先安装 Docker。" exit 1 fi echo "检测到 Docker 命令:$DOCKER_CMD" } # 检测系统是否支持 IPv6 check_ipv6_support() { echo "🔍 检测 IPv6 支持..." # 检查是否有 IPv6 地址(排除 link-local 地址) if ip -6 addr show | grep -v "scope link" | grep -q "inet6"; then echo "✅ 检测到系统支持 IPv6" return 0 elif ifconfig 2>/dev/null | grep -v "fe80:" | grep -q "inet6"; then echo "✅ 检测到系统支持 IPv6" return 0 else echo "⚠️ 未检测到 IPv6 支持" return 1 fi } # 配置 Docker 启用 IPv6 configure_docker_ipv6() { echo "🔧 配置 Docker IPv6 支持..." # 检查操作系统类型 OS_TYPE=$(uname -s) if [[ "$OS_TYPE" == "Darwin" ]]; then # macOS 上 Docker Desktop 已默认支持 IPv6 echo "✅ macOS Docker Desktop 默认支持 IPv6" return 0 fi # Docker daemon 配置文件路径 DOCKER_CONFIG="/etc/docker/daemon.json" # 检查是否需要 sudo if [[ $EUID -ne 0 ]]; then SUDO_CMD="sudo" else SUDO_CMD="" fi # 检查 Docker 配置文件 if [ -f "$DOCKER_CONFIG" ]; then # 检查是否已经配置了 IPv6 if grep -q '"ipv6"' "$DOCKER_CONFIG"; then echo "✅ Docker 已配置 IPv6 支持" else echo "📝 更新 Docker 配置以启用 IPv6..." # 备份原配置 $SUDO_CMD cp "$DOCKER_CONFIG" "${DOCKER_CONFIG}.backup" # 使用 jq 或 sed 添加 IPv6 配置 if command -v jq &> /dev/null; then $SUDO_CMD jq '. + {"ipv6": true, "fixed-cidr-v6": "fd00::/80"}' "$DOCKER_CONFIG" > /tmp/daemon.json && $SUDO_CMD mv /tmp/daemon.json "$DOCKER_CONFIG" else # 如果没有 jq,使用 sed $SUDO_CMD sed -i 's/^{$/{\n "ipv6": true,\n "fixed-cidr-v6": "fd00::\/80",/' "$DOCKER_CONFIG" fi echo "🔄 重启 Docker 服务..." if command -v systemctl &> /dev/null; then $SUDO_CMD systemctl restart docker elif command -v service &> /dev/null; then $SUDO_CMD service docker restart else echo "⚠️ 请手动重启 Docker 服务" fi sleep 5 fi else # 创建新的配置文件 echo "📝 创建 Docker 配置文件..." $SUDO_CMD mkdir -p /etc/docker echo '{ "ipv6": true, "fixed-cidr-v6": "fd00::/80" }' | $SUDO_CMD tee "$DOCKER_CONFIG" > /dev/null echo "🔄 重启 Docker 服务..." if command -v systemctl &> /dev/null; then $SUDO_CMD systemctl restart docker elif command -v service &> /dev/null; then $SUDO_CMD service docker restart else echo "⚠️ 请手动重启 Docker 服务" fi sleep 5 fi } # 显示菜单 show_menu() { echo "===============================================" echo " 面板管理脚本" echo "===============================================" echo "请选择操作:" echo "1. 安装面板" echo "2. 更新面板" echo "3. 卸载面板" echo "4. 迁移到 PostgreSQL" echo "5. 退出" echo "===============================================" } generate_random() { LC_ALL=C tr -dc 'A-Za-z0-9' "$tmp_file" else printf '%s=%s\n' "$key" "$value" > "$tmp_file" fi mv "$tmp_file" "$file" } get_env_var() { local key="$1" local file="${2:-.env}" if [[ ! -f "$file" ]]; then return 0 fi grep -m1 "^${key}=" "$file" | cut -d= -f2- } get_current_db_type() { local db_type database_url db_type=$(get_env_var "DB_TYPE") database_url=$(get_env_var "DATABASE_URL") if [[ "$db_type" == "sqlite" ]]; then echo "sqlite" elif [[ "$db_type" == "postgres" || "$database_url" == postgres://* || "$database_url" == postgresql://* ]]; then echo "postgres" else echo "sqlite" fi } wait_for_postgres_healthy() { local pg_health echo "🔍 检查 PostgreSQL 服务状态..." for i in {1..90}; do if docker ps --format "{{.Names}}" | grep -q "^flux-panel-postgres$"; then pg_health=$(docker inspect -f '{{.State.Health.Status}}' flux-panel-postgres 2>/dev/null || echo "unknown") if [[ "$pg_health" == "healthy" ]]; then echo "✅ PostgreSQL 服务健康检查通过" return 0 elif [[ "$pg_health" == "unhealthy" ]]; then echo "⚠️ PostgreSQL 健康状态:$pg_health" fi else pg_health="not_running" fi if [ $i -eq 90 ]; then echo "❌ PostgreSQL 启动超时(90秒)" echo "🔍 当前状态:$(docker inspect -f '{{.State.Health.Status}}' flux-panel-postgres 2>/dev/null || echo '容器不存在')" return 1 fi if [ $((i % 15)) -eq 1 ]; then echo "⏳ 等待 PostgreSQL 启动... ($i/90) 状态:${pg_health:-unknown}" fi sleep 1 done } wait_for_backend_healthy() { local backend_health echo "🔍 检查后端服务状态..." for i in {1..90}; do if docker ps --format "{{.Names}}" | grep -q "^flux-panel-backend$"; then backend_health=$(docker inspect -f '{{.State.Health.Status}}' flux-panel-backend 2>/dev/null || echo "unknown") if [[ "$backend_health" == "healthy" ]]; then echo "✅ 后端服务健康检查通过" return 0 elif [[ "$backend_health" == "unhealthy" ]]; then echo "⚠️ 后端健康状态:$backend_health" fi else backend_health="not_running" fi if [ $i -eq 90 ]; then echo "❌ 后端服务启动超时(90秒)" echo "🔍 当前状态:$(docker inspect -f '{{.State.Health.Status}}' flux-panel-backend 2>/dev/null || echo '容器不存在')" return 1 fi if [ $((i % 15)) -eq 1 ]; then echo "⏳ 等待后端服务启动... ($i/90) 状态:${backend_health:-unknown}" fi sleep 1 done } # 删除脚本自身 delete_self() { echo "" echo "🗑️ 操作已完成,正在清理脚本文件..." SCRIPT_PATH="$(readlink -f "$0" 2>/dev/null || realpath "$0" 2>/dev/null || echo "$0")" sleep 1 rm -f "$SCRIPT_PATH" && echo "✅ 脚本文件已删除" || echo "❌ 删除脚本文件失败" } # 获取用户输入的配置参数 get_config_params() { echo "🔧 请输入配置参数:" read -p "前端端口(默认 6366): " FRONTEND_PORT FRONTEND_PORT=${FRONTEND_PORT:-6366} read -p "后端端口(默认 6365): " BACKEND_PORT BACKEND_PORT=${BACKEND_PORT:-6365} echo "请选择数据库类型:" echo "1. SQLite(默认)" echo "2. PostgreSQL" read -p "数据库类型(1/2,默认 1): " DB_CHOICE case "$DB_CHOICE" in 2) DB_TYPE="postgres" ;; ""|1) DB_TYPE="sqlite" ;; *) echo "⚠️ 输入无效,默认使用 SQLite" DB_TYPE="sqlite" ;; esac POSTGRES_DB="flux_panel" POSTGRES_USER="flux_panel" POSTGRES_PASSWORD=$(generate_random) if [[ "$DB_TYPE" == "postgres" ]]; then DATABASE_URL="postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB}?sslmode=disable" else DATABASE_URL="" fi # 生成JWT密钥 JWT_SECRET=$(generate_random) } # 安装功能 install_panel() { echo "🚀 开始安装面板..." ask_proxy_config ensure_compose_urls_initialized || return 1 check_docker get_config_params echo "🔽 下载必要文件..." DOCKER_COMPOSE_URL=$(get_docker_compose_url) echo "📡 选择配置文件:$(basename "$DOCKER_COMPOSE_URL")" curl -L -o docker-compose.yml "$DOCKER_COMPOSE_URL" echo "✅ 文件准备完成" # 自动检测并配置 IPv6 支持 if check_ipv6_support; then echo "🚀 系统支持 IPv6,自动启用 IPv6 配置..." configure_docker_ipv6 fi cat > .env </dev/null || true docker stop -t 10 vite-frontend 2>/dev/null || true # 等待 WAL 文件同步 echo "⏳ 等待数据同步..." sleep 5 # 然后再完全停止 $DOCKER_CMD down echo "⬇️ 拉取最新镜像..." if [[ "$CURRENT_DB_TYPE" == "postgres" ]]; then $DOCKER_CMD pull backend frontend postgres else $DOCKER_CMD pull backend frontend fi echo "🚀 启动更新后的服务..." if [[ "$CURRENT_DB_TYPE" == "postgres" ]]; then $DOCKER_CMD up -d postgres wait_for_postgres_healthy $DOCKER_CMD up -d backend frontend else $DOCKER_CMD up -d backend frontend fi # 等待服务启动 echo "⏳ 等待服务启动..." if ! wait_for_backend_healthy; then echo "🛑 更新终止" return 1 fi echo "✅ 更新完成" } migrate_to_postgres() { local current_db_type postgres_db postgres_user postgres_password database_url echo "🔄 开始迁移 SQLite -> PostgreSQL..." check_docker if [[ ! -f ".env" ]]; then echo "❌ 未找到 .env 文件,请先安装面板" return 1 fi if [[ ! -f "docker-compose.yml" ]]; then echo "⚠️ 未找到 docker-compose.yml 文件,正在下载..." ask_proxy_config ensure_compose_urls_initialized || return 1 DOCKER_COMPOSE_URL=$(get_docker_compose_url) echo "📡 选择配置文件:$(basename "$DOCKER_COMPOSE_URL")" curl -L -o docker-compose.yml "$DOCKER_COMPOSE_URL" echo "✅ docker-compose.yml 下载完成" fi current_db_type=$(get_current_db_type) if [[ "$current_db_type" == "postgres" ]]; then echo "ℹ️ 当前已使用 PostgreSQL,无需迁移" return 0 fi postgres_db=$(get_env_var "POSTGRES_DB") postgres_user=$(get_env_var "POSTGRES_USER") postgres_password=$(get_env_var "POSTGRES_PASSWORD") postgres_db=${postgres_db:-flux_panel} postgres_user=${postgres_user:-flux_panel} postgres_password=${postgres_password:-$(generate_random)} upsert_env_var ".env" "POSTGRES_DB" "$postgres_db" upsert_env_var ".env" "POSTGRES_USER" "$postgres_user" upsert_env_var ".env" "POSTGRES_PASSWORD" "$postgres_password" echo "🛑 停止当前服务..." docker stop -t 30 flux-panel-backend 2>/dev/null || true docker stop -t 10 vite-frontend 2>/dev/null || true echo "⏳ 等待数据同步..." sleep 5 $DOCKER_CMD down echo "💾 备份 SQLite 数据到当前目录..." if ! docker run --rm -v sqlite_data:/data -v "$(pwd)":/backup alpine sh -c "cp /data/gost.db /backup/gost.db.bak"; then echo "❌ SQLite 备份失败,迁移终止" return 1 fi echo "🚀 启动 PostgreSQL..." $DOCKER_CMD up -d postgres if ! wait_for_postgres_healthy; then echo "🛑 PostgreSQL 未就绪,迁移终止" return 1 fi echo "🔄 执行 pgloader 迁移..." if ! docker run --rm --network gost-network -v sqlite_data:/sqlite dimitri/pgloader:latest pgloader /sqlite/gost.db "postgresql://${postgres_user}:${postgres_password}@postgres:5432/${postgres_db}"; then echo "❌ pgloader 迁移失败,迁移终止(如报 28P01,可执行 docker volume rm postgres_data 后重试)" return 1 fi database_url="postgresql://${postgres_user}:${postgres_password}@postgres:5432/${postgres_db}?sslmode=disable" upsert_env_var ".env" "DB_TYPE" "postgres" upsert_env_var ".env" "DATABASE_URL" "$database_url" echo "🚀 启动迁移后的服务..." $DOCKER_CMD up -d postgres backend frontend echo "⏳ 等待服务启动..." if ! wait_for_backend_healthy; then echo "🛑 迁移后服务启动失败" return 1 fi echo "✅ SQLite -> PostgreSQL 迁移完成" } # 卸载功能 uninstall_panel() { echo "🗑️ 开始卸载面板..." check_docker if [[ ! -f "docker-compose.yml" ]]; then echo "⚠️ 未找到 docker-compose.yml 文件,正在下载以完成卸载..." ask_proxy_config ensure_compose_urls_initialized || return 1 DOCKER_COMPOSE_URL=$(get_docker_compose_url) echo "📡 选择配置文件:$(basename "$DOCKER_COMPOSE_URL")" curl -L -o docker-compose.yml "$DOCKER_COMPOSE_URL" echo "✅ docker-compose.yml 下载完成" fi read -p "确认卸载面板吗?此操作将停止并删除所有容器和数据 (y/N): " confirm if [[ "$confirm" != "y" && "$confirm" != "Y" ]]; then echo "❌ 取消卸载" return 0 fi echo "🛑 停止并删除容器、镜像、卷..." $DOCKER_CMD down --rmi all --volumes --remove-orphans echo "🧹 删除配置文件..." rm -f docker-compose.yml .env echo "✅ 卸载完成" } # 主逻辑 main() { # 显示交互式菜单 while true; do show_menu read -p "请输入选项 (1-5): " choice case $choice in 1) install_panel delete_self exit 0 ;; 2) update_panel delete_self exit 0 ;; 3) uninstall_panel delete_self exit 0 ;; 4) migrate_to_postgres delete_self exit 0 ;; 5) echo "👋 退出脚本" delete_self exit 0 ;; *) echo "❌ 无效选项,请输入 1-5" echo "" ;; esac done } # 执行主函数 main