Files
vue-driven-cloud-storage/install.sh
WanWanYun ff8ba91b8d fix: 修复后端端口更换时脚本异常退出的问题
问题描述:
- 部署时后端端口被占用,用户更换其他端口后脚本异常退出
- 根因: health_check()函数中netstat命令失败时触发set -e导致脚本退出

修复内容:
1. 端口检查添加错误抑制 (2>/dev/null)
2. 添加ss命令作为netstat的fallback
3. Nginx状态检查直接通过目录检测BT Panel环境
4. 修复变量作用域问题 (IS_BT_PANEL仅在局部函数中定义)

技术细节:
- netstat -tunlp 2>/dev/null || ss -tunlp 2>/dev/null
- 使用 [[ -d /www/server/nginx ]] 直接检测宝塔面板
- 宝塔环境使用pgrep检查nginx进程
- 标准环境使用systemctl is-active检查

影响范围:
- health_check() 函数 (install.sh:2588-2612)
- 提升脚本在端口冲突场景下的健壮性
- 兼容宝塔面板和标准Nginx环境
2025-11-13 12:16:14 +08:00

3822 lines
124 KiB
Bash
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/bin/bash
################################################################################
# 玩玩云 (WanWanYun) - 一键部署/卸载/更新脚本
# 项目地址: https://gitee.com/yu-yon/vue-driven-cloud-storage
# 版本: v1.2.0
################################################################################
set -e
# 检查运行模式
MODE="install"
if [[ "$1" == "--uninstall" ]] || [[ "$1" == "-u" ]] || [[ "$1" == "uninstall" ]]; then
MODE="uninstall"
elif [[ "$1" == "--update" ]] || [[ "$1" == "--upgrade" ]] || [[ "$1" == "update" ]]; then
MODE="update"
elif [[ "$1" == "--repair" ]] || [[ "$1" == "--fix" ]] || [[ "$1" == "repair" ]]; then
MODE="repair"
fi
# 颜色定义
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
PURPLE='\033[0;35m'
CYAN='\033[0;36m'
WHITE='\033[1;37m'
NC='\033[0m' # No Color
# 全局变量
PROJECT_NAME="wanwanyun"
PROJECT_DIR="/var/www/${PROJECT_NAME}"
REPO_URL="https://gitee.com/yu-yon/vue-driven-cloud-storage.git"
NODE_VERSION="20"
ADMIN_USERNAME=""
ADMIN_PASSWORD=""
DOMAIN=""
USE_DOMAIN=false
SSL_METHOD=""
HTTP_PORT="80"
HTTPS_PORT="443"
BACKEND_PORT="40001"
################################################################################
# 工具函数
################################################################################
print_banner() {
clear
echo -e "${CYAN}"
echo "╔═══════════════════════════════════════════════════════════════╗"
echo "║ ║"
echo "║ 🌩️ 玩玩云 一键部署脚本 ║"
echo "║ ║"
echo "║ Cloud Storage Platform ║"
echo "║ ║"
echo "╚═══════════════════════════════════════════════════════════════╝"
echo -e "${NC}"
}
print_step() {
echo -e "\n${BLUE}$1${NC}"
}
print_success() {
echo -e "${GREEN}$1${NC}"
}
print_error() {
echo -e "${RED}$1${NC}"
}
print_warning() {
echo -e "${YELLOW}$1${NC}"
}
print_info() {
echo -e "${CYAN} $1${NC}"
}
# 检测操作系统
detect_os() {
if [[ -f /etc/os-release ]]; then
. /etc/os-release
OS=$ID
OS_VERSION=$VERSION_ID
OS_NAME=$NAME
else
print_error "无法检测操作系统"
exit 1
fi
# 统一操作系统标识和包管理器检测
case $OS in
ubuntu)
PKG_MANAGER="apt"
;;
debian)
PKG_MANAGER="apt"
;;
centos)
if [[ "${OS_VERSION%%.*}" -ge 8 ]]; then
PKG_MANAGER="dnf"
else
PKG_MANAGER="yum"
fi
;;
rhel|redhat)
OS="rhel"
if [[ "${OS_VERSION%%.*}" -ge 8 ]]; then
PKG_MANAGER="dnf"
else
PKG_MANAGER="yum"
fi
;;
rocky|rockylinux)
OS="rocky"
PKG_MANAGER="dnf"
;;
almalinux|alma)
OS="almalinux"
PKG_MANAGER="dnf"
;;
fedora)
PKG_MANAGER="dnf"
;;
opensuse|opensuse-leap|opensuse-tumbleweed)
OS="opensuse"
PKG_MANAGER="zypper"
;;
*)
# 自动检测包管理器作为后备方案
print_warning "未识别的操作系统: $OS,尝试自动检测包管理器"
if command -v apt-get &> /dev/null; then
PKG_MANAGER="apt"
print_info "检测到APT包管理器"
elif command -v dnf &> /dev/null; then
PKG_MANAGER="dnf"
print_info "检测到DNF包管理器"
elif command -v yum &> /dev/null; then
PKG_MANAGER="yum"
print_info "检测到YUM包管理器"
elif command -v zypper &> /dev/null; then
PKG_MANAGER="zypper"
print_info "检测到Zypper包管理器"
else
print_error "无法检测到支持的包管理器"
exit 1
fi
;;
esac
}
# 检测系统架构
detect_arch() {
ARCH=$(uname -m)
case $ARCH in
x86_64)
ARCH="amd64"
;;
aarch64)
ARCH="arm64"
;;
*)
print_error "不支持的系统架构: $ARCH"
exit 1
;;
esac
}
# 检测root权限
check_root() {
if [[ $EUID -ne 0 ]]; then
print_error "此脚本需要root权限运行"
print_info "请使用: sudo bash install.sh"
exit 1
fi
}
################################################################################
# 环境检测
################################################################################
system_check() {
print_step "正在检测系统环境..."
# 检测操作系统
detect_os
print_success "操作系统: $OS $OS_VERSION"
# 检测架构
detect_arch
print_success "系统架构: $ARCH"
# 检测内存
TOTAL_MEM=$(free -m | awk '/^Mem:/{print $2}')
if [[ $TOTAL_MEM -lt 512 ]]; then
print_warning "内存不足512MB可能影响性能"
else
print_success "可用内存: ${TOTAL_MEM}MB"
fi
# 检测磁盘空间
DISK_AVAIL=$(df -m / | awk 'NR==2 {print $4}')
if [[ $DISK_AVAIL -lt 2048 ]]; then
print_warning "磁盘空间不足2GB可能影响运行"
else
print_success "可用磁盘: ${DISK_AVAIL}MB"
fi
# 检测网络
if ping -c 1 gitee.com &> /dev/null; then
print_success "网络连接正常"
else
print_error "无法连接到网络"
exit 1
fi
# 检测公网IP
PUBLIC_IP=$(curl -s ifconfig.me || curl -s icanhazip.com || echo "未知")
print_info "公网IP: $PUBLIC_IP"
echo ""
}
################################################################################
# 软件源配置
################################################################################
choose_mirror() {
print_step "选择软件包安装源"
echo ""
echo "请选择软件源:"
echo -e "${GREEN}[1]${NC} 官方源 (国外服务器推荐)"
echo -e "${GREEN}[2]${NC} 阿里云镜像源 (国内服务器推荐,速度更快)"
echo ""
while true; do
read -p "请输入选项 [1-2]: " mirror_choice < /dev/tty
case $mirror_choice in
1)
print_info "使用官方源"
USE_ALIYUN_MIRROR=false
break
;;
2)
print_info "使用阿里云镜像源"
USE_ALIYUN_MIRROR=true
configure_aliyun_mirror
break
;;
*)
print_error "无效选项,请重新选择"
;;
esac
done
echo ""
}
configure_aliyun_mirror() {
print_step "配置阿里云镜像源..."
case $OS in
ubuntu)
# 备份原有源
if [[ ! -f /etc/apt/sources.list.bak ]]; then
cp /etc/apt/sources.list /etc/apt/sources.list.bak
fi
# 配置Ubuntu阿里云源
cat > /etc/apt/sources.list << EOF
deb http://mirrors.aliyun.com/ubuntu/ $(lsb_release -cs) main restricted universe multiverse
deb http://mirrors.aliyun.com/ubuntu/ $(lsb_release -cs)-updates main restricted universe multiverse
deb http://mirrors.aliyun.com/ubuntu/ $(lsb_release -cs)-backports main restricted universe multiverse
deb http://mirrors.aliyun.com/ubuntu/ $(lsb_release -cs)-security main restricted universe multiverse
EOF
print_success "阿里云源配置完成"
;;
debian)
# 备份原有源
if [[ ! -f /etc/apt/sources.list.bak ]]; then
cp /etc/apt/sources.list /etc/apt/sources.list.bak
fi
# 配置Debian阿里云源
cat > /etc/apt/sources.list << EOF
deb http://mirrors.aliyun.com/debian/ $(lsb_release -cs) main contrib non-free non-free-firmware
deb http://mirrors.aliyun.com/debian/ $(lsb_release -cs)-updates main contrib non-free non-free-firmware
deb http://mirrors.aliyun.com/debian/ $(lsb_release -cs)-backports main contrib non-free non-free-firmware
deb http://mirrors.aliyun.com/debian-security $(lsb_release -cs)-security main contrib non-free non-free-firmware
EOF
print_success "阿里云源配置完成"
;;
centos)
# 备份并配置CentOS阿里云源
if [[ -f /etc/yum.repos.d/CentOS-Base.repo ]]; then
if [[ ! -f /etc/yum.repos.d/CentOS-Base.repo.bak ]]; then
cp /etc/yum.repos.d/CentOS-Base.repo /etc/yum.repos.d/CentOS-Base.repo.bak
fi
fi
CENTOS_VERSION="${OS_VERSION%%.*}"
if [[ "$CENTOS_VERSION" == "7" ]]; then
curl -o /etc/yum.repos.d/CentOS-Base.repo http://mirrors.aliyun.com/repo/Centos-7.repo
elif [[ "$CENTOS_VERSION" == "8" ]]; then
curl -o /etc/yum.repos.d/CentOS-Base.repo http://mirrors.aliyun.com/repo/Centos-vault-8.5.2111.repo
sed -i 's/mirrors.cloud.aliyuncs.com/mirrors.aliyun.com/g' /etc/yum.repos.d/CentOS-Base.repo
fi
yum clean all
yum makecache
print_success "阿里云源配置完成"
;;
rhel)
# RHEL使用EPEL和阿里云镜像
print_info "配置RHEL阿里云镜像源..."
yum install -y epel-release
print_success "阿里云源配置完成"
;;
rocky)
# 备份并配置Rocky Linux阿里云源
if [[ -d /etc/yum.repos.d ]]; then
mkdir -p /etc/yum.repos.d/backup
cp /etc/yum.repos.d/*.repo /etc/yum.repos.d/backup/ 2>/dev/null || true
fi
sed -e 's|^mirrorlist=|#mirrorlist=|g' \
-e 's|^#baseurl=http://dl.rockylinux.org/$contentdir|baseurl=https://mirrors.aliyun.com/rockylinux|g' \
-i.bak /etc/yum.repos.d/rocky*.repo
dnf clean all
dnf makecache
print_success "阿里云源配置完成"
;;
almalinux)
# 备份并配置AlmaLinux阿里云源
if [[ -d /etc/yum.repos.d ]]; then
mkdir -p /etc/yum.repos.d/backup
cp /etc/yum.repos.d/*.repo /etc/yum.repos.d/backup/ 2>/dev/null || true
fi
sed -e 's|^mirrorlist=|#mirrorlist=|g' \
-e 's|^# baseurl=https://repo.almalinux.org|baseurl=https://mirrors.aliyun.com|g' \
-i.bak /etc/yum.repos.d/almalinux*.repo
dnf clean all
dnf makecache
print_success "阿里云源配置完成"
;;
fedora)
# 备份并配置Fedora阿里云源
if [[ -d /etc/yum.repos.d ]]; then
mkdir -p /etc/yum.repos.d/backup
cp /etc/yum.repos.d/*.repo /etc/yum.repos.d/backup/ 2>/dev/null || true
fi
sed -e 's|^metalink=|#metalink=|g' \
-e 's|^#baseurl=http://download.example/pub/fedora/linux|baseurl=https://mirrors.aliyun.com/fedora|g' \
-i.bak /etc/yum.repos.d/fedora*.repo /etc/yum.repos.d/fedora-updates*.repo
dnf clean all
dnf makecache
print_success "阿里云源配置完成"
;;
opensuse)
# 配置openSUSE阿里云源
print_info "配置openSUSE阿里云镜像源..."
zypper mr -da
zypper ar -fcg https://mirrors.aliyun.com/opensuse/distribution/leap/\$releasever/repo/oss/ aliyun-oss
zypper ar -fcg https://mirrors.aliyun.com/opensuse/distribution/leap/\$releasever/repo/non-oss/ aliyun-non-oss
zypper ar -fcg https://mirrors.aliyun.com/opensuse/update/leap/\$releasever/oss/ aliyun-update-oss
zypper ar -fcg https://mirrors.aliyun.com/opensuse/update/leap/\$releasever/non-oss/ aliyun-update-non-oss
zypper ref
print_success "阿里云源配置完成"
;;
*)
print_warning "当前系统($OS)暂不支持阿里云镜像源自动配置,使用官方源"
;;
esac
}
################################################################################
check_cpp_compiler() {
print_step "检查C++编译器版本..."
# 检查g++是否已安装
if ! command -v g++ &> /dev/null; then
print_warning "g++未安装,将在依赖安装时自动安装"
return
fi
# 获取g++版本号
GXX_VERSION=$(g++ --version | head -n1 | grep -oP '\d+\.\d+\.\d+' | head -1 | cut -d'.' -f1)
if [[ -z "$GXX_VERSION" ]]; then
print_warning "无法检测g++版本,跳过版本检查"
return
fi
print_info "当前g++版本: $GXX_VERSION.x"
# better-sqlite3 v11+ 需要C++20支持g++ 10+
if [[ $GXX_VERSION -lt 10 ]]; then
print_warning "g++版本过低需要10+以支持C++20正在升级..."
echo ""
case $PKG_MANAGER in
apt)
# Ubuntu/Debian: 使用toolchain PPA
print_info "添加Ubuntu Toolchain PPA..."
add-apt-repository ppa:ubuntu-toolchain-r/test -y || {
print_error "添加PPA失败"
return 1
}
apt-get update
print_info "安装g++-11..."
apt-get install -y g++-11 gcc-11 || {
print_error "g++-11安装失败"
return 1
}
# 设置为默认编译器
update-alternatives --install /usr/bin/g++ g++ /usr/bin/g++-11 100
update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-11 100
# 验证
NEW_VERSION=$(g++ --version | head -n1 | grep -oP '\d+\.\d+\.\d+' | head -1 | cut -d'.' -f1)
print_success "g++已升级到版本: $NEW_VERSION.x"
;;
yum|dnf)
# CentOS/RHEL: 使用devtoolset或gcc-toolset
if [[ "$OS" == "centos" ]] && [[ "$OS_VERSION" == "7" ]]; then
# CentOS 7 使用devtoolset-11
print_info "安装devtoolset-11..."
yum install -y centos-release-scl
yum install -y devtoolset-11-gcc devtoolset-11-gcc-c++
# 启用devtoolset-11
echo "source /opt/rh/devtoolset-11/enable" >> /etc/profile
source /opt/rh/devtoolset-11/enable
print_success "devtoolset-11安装完成"
else
# CentOS 8+ 使用gcc-toolset-11
print_info "安装gcc-toolset-11..."
$PKG_MANAGER install -y gcc-toolset-11-gcc gcc-toolset-11-gcc-c++
# 启用gcc-toolset-11
echo "source /opt/rh/gcc-toolset-11/enable" >> /etc/profile
source /opt/rh/gcc-toolset-11/enable
print_success "gcc-toolset-11安装完成"
fi
;;
zypper)
# OpenSUSE
print_info "升级g++..."
zypper install -y gcc11-c++ || {
print_error "g++升级失败"
return 1
}
# 设置为默认
update-alternatives --install /usr/bin/g++ g++ /usr/bin/g++-11 100
update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-11 100
print_success "g++已升级"
;;
*)
print_warning "不支持的包管理器无法自动升级g++"
print_warning "请手动升级g++到10或更高版本"
return 1
;;
esac
echo ""
else
print_success "g++版本满足要求10+"
echo ""
fi
}
# 安装依赖环境
################################################################################
install_dependencies() {
print_step "正在安装依赖环境..."
case $PKG_MANAGER in
apt)
apt-get update
apt-get install -y curl wget git unzip lsb-release build-essential python3
install_nodejs_apt
install_nginx_apt
;;
yum)
yum install -y curl wget git unzip redhat-lsb-core gcc-c++ make python3
install_nodejs_yum
install_nginx_yum
;;
dnf)
dnf install -y curl wget git unzip redhat-lsb-core gcc-c++ make python3
install_nodejs_dnf
install_nginx_dnf
;;
zypper)
zypper install -y curl wget git unzip lsb-release gcc-c++ make python3
install_nodejs_zypper
install_nginx_zypper
;;
*)
print_error "不支持的包管理器: $PKG_MANAGER"
exit 1
;;
esac
install_pm2
print_success "依赖环境安装完成"
# 检查并升级C++编译器(如果需要)
check_cpp_compiler
echo ""
}
install_nodejs_apt() {
if command -v node &> /dev/null; then
NODE_VER=$(node -v | cut -d'v' -f2 | cut -d'.' -f1)
if [[ $NODE_VER -ge $NODE_VERSION ]]; then
print_success "Node.js 已安装: $(node -v)"
return
fi
fi
print_info "正在安装 Node.js ${NODE_VERSION}.x..."
curl -fsSL https://deb.nodesource.com/setup_${NODE_VERSION}.x | bash -
apt-get install -y nodejs
print_success "Node.js 安装完成: $(node -v)"
}
install_nodejs_yum() {
if command -v node &> /dev/null; then
NODE_VER=$(node -v | cut -d'v' -f2 | cut -d'.' -f1)
if [[ $NODE_VER -ge $NODE_VERSION ]]; then
print_success "Node.js 已安装: $(node -v)"
return
fi
fi
print_info "正在安装 Node.js ${NODE_VERSION}.x..."
curl -fsSL https://rpm.nodesource.com/setup_${NODE_VERSION}.x | bash -
yum install -y nodejs
print_success "Node.js 安装完成: $(node -v)"
}
install_nginx_apt() {
if command -v nginx &> /dev/null; then
print_success "Nginx 已安装: $(nginx -v 2>&1 | cut -d'/' -f2)"
return
fi
print_info "正在安装 Nginx..."
apt-get install -y nginx
systemctl enable nginx
print_success "Nginx 安装完成"
}
install_nginx_yum() {
if command -v nginx &> /dev/null; then
print_success "Nginx 已安装: $(nginx -v 2>&1 | cut -d'/' -f2)"
return
fi
print_info "正在安装 Nginx..."
yum install -y nginx
systemctl enable nginx
print_success "Nginx 安装完成"
}
install_nodejs_dnf() {
if command -v node &> /dev/null; then
NODE_VER=$(node -v | cut -d'v' -f2 | cut -d'.' -f1)
if [[ $NODE_VER -ge $NODE_VERSION ]]; then
print_success "Node.js 已安装: $(node -v)"
return
fi
fi
print_info "正在安装 Node.js ${NODE_VERSION}.x..."
curl -fsSL https://rpm.nodesource.com/setup_${NODE_VERSION}.x | bash -
dnf install -y nodejs
print_success "Node.js 安装完成: $(node -v)"
}
install_nginx_dnf() {
if command -v nginx &> /dev/null; then
print_success "Nginx 已安装: $(nginx -v 2>&1 | cut -d'/' -f2)"
return
fi
print_info "正在安装 Nginx..."
dnf install -y nginx
systemctl enable nginx
print_success "Nginx 安装完成"
}
install_nodejs_zypper() {
if command -v node &> /dev/null; then
NODE_VER=$(node -v | cut -d'v' -f2 | cut -d'.' -f1)
if [[ $NODE_VER -ge $NODE_VERSION ]]; then
print_success "Node.js 已安装: $(node -v)"
return
fi
fi
print_info "正在安装 Node.js ${NODE_VERSION}.x..."
# openSUSE使用官方仓库的Node.js
zypper install -y nodejs${NODE_VERSION}
print_success "Node.js 安装完成: $(node -v)"
}
install_nginx_zypper() {
if command -v nginx &> /dev/null; then
print_success "Nginx 已安装: $(nginx -v 2>&1 | cut -d'/' -f2)"
return
fi
print_info "正在安装 Nginx..."
zypper install -y nginx
systemctl enable nginx
print_success "Nginx 安装完成"
}
install_pm2() {
if command -v pm2 &> /dev/null; then
print_success "PM2 已安装: $(pm2 -v)"
return
fi
print_info "正在安装 PM2..."
npm install -g pm2
pm2 startup
print_success "PM2 安装完成"
}
################################################################################
# 智能端口检测和配置
################################################################################
# 检查端口是否可用(保留用于兼容性)
check_port_available() {
local port=$1
if command -v netstat &> /dev/null; then
if netstat -tuln | grep -q ":${port} "; then
return 1 # 端口被占用
fi
elif command -v ss &> /dev/null; then
if ss -tuln | grep -q ":${port} "; then
return 1 # 端口被占用
fi
fi
return 0 # 端口可用
}
# 智能检测端口状态和占用进程
check_port_status() {
local port=$1
# 1. 检查端口是否被监听
if command -v netstat &> /dev/null; then
if ! netstat -tuln 2>/dev/null | grep -q ":${port} "; then
echo "available"
return 0
fi
elif command -v ss &> /dev/null; then
if ! ss -tuln 2>/dev/null | grep -q ":${port} "; then
echo "available"
return 0
fi
else
echo "available"
return 0
fi
# 2. 端口被占用,检查是什么进程
local process=""
if command -v netstat &> /dev/null; then
process=$(netstat -tulnp 2>/dev/null | grep ":${port} " | awk '{print $7}' | cut -d'/' -f2 | head -1)
fi
if [[ -z "$process" ]] && command -v ss &> /dev/null; then
# 使用sed替代grep -oP以提高兼容性
process=$(ss -tulnp 2>/dev/null | grep ":${port} " | sed -n 's/.*users:(("\([^"]*\)".*/\1/p' | head -1)
fi
# 3. 根据进程返回状态始终返回0以避免set -e导致脚本退出
if [[ -z "$process" ]]; then
# 无法获取进程名(可能权限不足)
echo "occupied"
elif [[ "$process" == "nginx" ]] || [[ "$process" =~ ^nginx: ]]; then
# Nginx占用
echo "nginx"
elif [[ "$process" == "apache2" ]] || [[ "$process" == "httpd" ]] || [[ "$process" =~ apache ]]; then
# Apache占用
echo "apache"
else
# 其他进程
echo "other:$process"
fi
# 始终返回0避免set -e导致脚本退出
return 0
}
# 改进的端口配置函数
configure_ports() {
print_step "智能端口配置"
echo ""
# 全局标志是否共用Nginx端口
SHARE_NGINX=false
# ========== 检测80端口 ==========
port_80_status=$(check_port_status 80)
case $port_80_status in
"available")
print_success "80 端口可用"
HTTP_PORT=80
;;
"nginx")
print_info "检测到 Nginx 已占用 80 端口"
echo ""
echo "🎯 好消息可以通过虚拟主机配置与现有Nginx共用此端口"
echo ""
echo "请选择部署方式:"
echo ""
echo -e "${GREEN}[1]${NC} 共用80端口推荐"
echo " ✅ 需要配置不同的域名"
echo " ✅ 访问: http://your-domain.com"
echo " ✅ 不需要端口号"
echo ""
echo -e "${GREEN}[2]${NC} 使用其他HTTP端口"
echo " 独立端口"
echo " 访问: http://your-domain.com:8080"
echo ""
while true; do
read -p "请选择 [1-2]: " choice < /dev/tty
if [[ "$choice" == "1" ]]; then
HTTP_PORT=80
SHARE_NGINX=true
print_success "将与现有Nginx共用80端口虚拟主机模式"
print_info "提示: 请确保使用不同的域名区分站点"
break
elif [[ "$choice" == "2" ]]; then
# 选择其他端口的逻辑
while true; do
read -p "请输入HTTP端口 [建议: 8080]: " custom_port < /dev/tty
custom_port=${custom_port:-8080}
if [[ ! "$custom_port" =~ ^[0-9]+$ ]] || [[ $custom_port -lt 1024 ]] || [[ $custom_port -gt 65535 ]]; then
print_error "端口范围: 1024-65535"
continue
fi
if ! check_port_available $custom_port; then
print_error "端口 $custom_port 已被占用,请选择其他端口"
continue
fi
HTTP_PORT=$custom_port
print_success "将使用 HTTP 端口: $HTTP_PORT"
break
done
break
else
print_error "无效选项,请重新选择"
fi
done
;;
"apache")
print_warning "检测到 Apache 已占用 80 端口"
echo ""
echo "⚠️ Apache和Nginx不能同时监听同一端口"
echo ""
echo "请选择解决方案:"
echo ""
echo -e "${GREEN}[1]${NC} 停止Apache改用Nginx"
echo " ⚠️ 需要迁移Apache配置"
echo ""
echo -e "${GREEN}[2]${NC} 使用其他HTTP端口推荐"
echo " ✅ 不影响现有Apache服务"
echo ""
while true; do
read -p "请选择 [1-2]: " choice < /dev/tty
if [[ "$choice" == "1" ]]; then
print_info "正在停止Apache..."
systemctl stop apache2 2>/dev/null || systemctl stop httpd 2>/dev/null || true
systemctl disable apache2 2>/dev/null || systemctl disable httpd 2>/dev/null || true
HTTP_PORT=80
print_success "Apache已停止将使用80端口"
break
elif [[ "$choice" == "2" ]]; then
# 选择其他端口
while true; do
read -p "请输入HTTP端口 [建议: 8080]: " custom_port < /dev/tty
custom_port=${custom_port:-8080}
if [[ ! "$custom_port" =~ ^[0-9]+$ ]] || [[ $custom_port -lt 1024 ]] || [[ $custom_port -gt 65535 ]]; then
print_error "端口范围: 1024-65535"
continue
fi
if ! check_port_available $custom_port; then
print_error "端口 $custom_port 已被占用,请选择其他端口"
continue
fi
HTTP_PORT=$custom_port
print_success "将使用 HTTP 端口: $HTTP_PORT"
break
done
break
else
print_error "无效选项,请重新选择"
fi
done
;;
"occupied"|other:*)
process=${port_80_status#other:}
if [[ "$port_80_status" == "occupied" ]]; then
print_warning "80 端口已被占用(无法识别进程)"
else
print_warning "80 端口被进程 ${process} 占用"
fi
echo ""
echo "请选择其他HTTP端口"
while true; do
read -p "请输入HTTP端口 [建议: 8080]: " custom_port < /dev/tty
custom_port=${custom_port:-8080}
if [[ ! "$custom_port" =~ ^[0-9]+$ ]] || [[ $custom_port -lt 1024 ]] || [[ $custom_port -gt 65535 ]]; then
print_error "端口范围: 1024-65535"
continue
fi
if ! check_port_available $custom_port; then
print_error "端口 $custom_port 已被占用,请选择其他端口"
continue
fi
HTTP_PORT=$custom_port
print_success "将使用 HTTP 端口: $HTTP_PORT"
break
done
;;
esac
echo ""
# ========== 检测443端口仅在使用HTTPS时需要==========
if [[ "$USE_DOMAIN" == "true" ]] && [[ "$SSL_METHOD" != "8" ]]; then
port_443_status=$(check_port_status 443)
case $port_443_status in
"available")
print_success "443 端口可用"
HTTPS_PORT=443
;;
"nginx")
print_info "检测到 Nginx 已占用 443 端口"
echo ""
if [[ "$SHARE_NGINX" == "true" ]]; then
# 如果HTTP端口也是共用的默认继续共用
echo "🎯 将继续与现有Nginx共用443端口虚拟主机模式"
HTTPS_PORT=443
print_success "将与现有Nginx共用443端口"
else
echo "请选择部署方式:"
echo ""
echo -e "${GREEN}[1]${NC} 共用443端口"
echo " ✅ 需要配置不同的域名"
echo ""
echo -e "${GREEN}[2]${NC} 使用其他HTTPS端口"
echo " 独立端口如8443"
echo ""
while true; do
read -p "请选择 [1-2]: " choice < /dev/tty
if [[ "$choice" == "1" ]]; then
HTTPS_PORT=443
SHARE_NGINX=true
print_success "将与现有Nginx共用443端口"
break
elif [[ "$choice" == "2" ]]; then
# 选择其他端口
while true; do
read -p "请输入HTTPS端口 [建议: 8443]: " custom_https_port < /dev/tty
custom_https_port=${custom_https_port:-8443}
if [[ ! "$custom_https_port" =~ ^[0-9]+$ ]] || [[ $custom_https_port -lt 1024 ]] || [[ $custom_https_port -gt 65535 ]]; then
print_error "端口范围: 1024-65535"
continue
fi
if ! check_port_available $custom_https_port; then
print_error "端口 $custom_https_port 已被占用,请选择其他端口"
continue
fi
HTTPS_PORT=$custom_https_port
print_success "将使用 HTTPS 端口: $HTTPS_PORT"
break
done
break
else
print_error "无效选项,请重新选择"
fi
done
fi
;;
"apache"|"occupied"|other:*)
# Apache或其他进程占用443需要换端口
if [[ "$port_443_status" == "apache" ]]; then
print_warning "检测到 Apache 已占用 443 端口"
elif [[ "$port_443_status" == "occupied" ]]; then
print_warning "443 端口已被占用"
else
process=${port_443_status#other:}
print_warning "443 端口被进程 ${process} 占用"
fi
echo ""
while true; do
read -p "请输入HTTPS端口 [建议: 8443]: " custom_https_port < /dev/tty
custom_https_port=${custom_https_port:-8443}
if [[ ! "$custom_https_port" =~ ^[0-9]+$ ]] || [[ $custom_https_port -lt 1024 ]] || [[ $custom_https_port -gt 65535 ]]; then
print_error "端口范围: 1024-65535"
continue
fi
if ! check_port_available $custom_https_port; then
print_error "端口 $custom_https_port 已被占用,请选择其他端口"
continue
fi
HTTPS_PORT=$custom_https_port
print_success "将使用 HTTPS 端口: $HTTPS_PORT"
break
done
;;
esac
echo ""
fi
# ========== 检测后端端口 ==========
if ! check_port_available 40001; then
print_warning "检测到 40001 端口已被占用"
echo ""
while true; do
read -p "请输入后端服务端口 [建议: 40002]: " custom_backend_port < /dev/tty
custom_backend_port=${custom_backend_port:-40002}
if [[ ! "$custom_backend_port" =~ ^[0-9]+$ ]] || [[ $custom_backend_port -lt 1024 ]] || [[ $custom_backend_port -gt 65535 ]]; then
print_error "端口范围: 1024-65535"
continue
fi
if ! check_port_available $custom_backend_port; then
print_error "端口 $custom_backend_port 已被占用,请选择其他端口"
continue
fi
BACKEND_PORT=$custom_backend_port
print_success "将使用后端端口: $BACKEND_PORT"
break
done
else
print_success "40001 端口可用"
fi
echo ""
print_info "端口配置摘要:"
echo " - HTTP端口: $HTTP_PORT"
if [[ "$USE_DOMAIN" == "true" ]] && [[ "$SSL_METHOD" != "8" ]]; then
echo " - HTTPS端口: $HTTPS_PORT"
fi
echo " - 后端端口: $BACKEND_PORT"
if [[ "$SHARE_NGINX" == "true" ]]; then
echo " - 模式: 虚拟主机共用端口 ✅"
fi
echo ""
}
################################################################################
# 访问模式选择
################################################################################
choose_access_mode() {
print_step "选择访问模式"
echo ""
echo "请选择访问模式:"
echo -e "${GREEN}[1]${NC} 域名模式 (推荐支持HTTPS)"
echo -e "${GREEN}[2]${NC} IP模式 (仅HTTP适合测试)"
echo ""
while true; do
read -p "请输入选项 [1-2]: " mode_choice < /dev/tty
case $mode_choice in
1)
USE_DOMAIN=true
configure_domain
break
;;
2)
USE_DOMAIN=false
PUBLIC_IP=$(curl -s ifconfig.me || curl -s icanhazip.com || echo "未知")
print_info "将使用 IP 模式访问: http://${PUBLIC_IP}"
echo ""
break
;;
*)
print_error "无效选项,请重新选择"
;;
esac
done
}
configure_domain() {
echo ""
while true; do
read -p "请输入您的域名 (例如: wwy.example.com): " DOMAIN < /dev/tty
if [[ -z "$DOMAIN" ]]; then
print_error "域名不能为空"
continue
fi
# 验证域名格式
if [[ ! "$DOMAIN" =~ ^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$ ]]; then
print_error "域名格式不正确"
continue
fi
# 验证域名解析
print_info "正在验证域名解析..."
DOMAIN_IP=$(dig +short "$DOMAIN" | tail -n1)
PUBLIC_IP=$(curl -s ifconfig.me || curl -s icanhazip.com)
if [[ "$DOMAIN_IP" == "$PUBLIC_IP" ]]; then
print_success "域名已正确解析到当前服务器IP"
break
else
print_warning "域名未解析到当前服务器IP"
print_info "域名解析IP: $DOMAIN_IP"
print_info "当前服务器IP: $PUBLIC_IP"
read -p "是否继续? (y/n): " continue_choice < /dev/tty
if [[ "$continue_choice" == "y" || "$continue_choice" == "Y" ]]; then
break
fi
fi
done
choose_ssl_method
}
################################################################################
# SSL证书配置
################################################################################
choose_ssl_method() {
echo ""
print_step "选择SSL证书部署方式"
echo ""
echo -e "${YELLOW}【推荐方案】${NC}"
echo -e "${GREEN}[1]${NC} Certbot (Let's Encrypt官方工具)"
echo " - 最稳定可靠,支持自动续期"
echo ""
echo -e "${YELLOW}【备选方案】${NC}"
echo -e "${GREEN}[2]${NC} acme.sh + Let's Encrypt"
echo " - 纯Shell脚本更轻量级"
echo -e "${GREEN}[3]${NC} acme.sh + ZeroSSL"
echo " - Let's Encrypt的免费替代品"
echo -e "${GREEN}[4]${NC} acme.sh + Buypass"
echo " - 挪威免费CA有效期180天"
echo ""
echo -e "${YELLOW}【云服务商证书】${NC}"
echo -e "${GREEN}[5]${NC} 阿里云免费证书 (需提供AccessKey)"
echo -e "${GREEN}[6]${NC} 腾讯云免费证书 (需提供SecretKey)"
echo ""
echo -e "${YELLOW}【其他选项】${NC}"
echo -e "${GREEN}[7]${NC} 使用已有证书 (手动上传)"
echo -e "${GREEN}[8]${NC} 暂不配置HTTPS (可后续配置)"
echo ""
while true; do
read -p "请输入选项 [1-8]: " ssl_choice < /dev/tty
case $ssl_choice in
1|2|3|4|5|6|7|8)
SSL_METHOD=$ssl_choice
break
;;
*)
print_error "无效选项,请重新选择"
;;
esac
done
echo ""
}
deploy_ssl() {
if [[ "$USE_DOMAIN" != "true" ]]; then
return 0
fi
case $SSL_METHOD in
1)
deploy_certbot || ssl_fallback "1"
;;
2)
deploy_acme_letsencrypt || ssl_fallback "2"
;;
3)
deploy_acme_zerossl || ssl_fallback "3"
;;
4)
deploy_acme_buypass || ssl_fallback "4"
;;
5)
deploy_aliyun_ssl || ssl_fallback "5"
;;
6)
deploy_tencent_ssl || ssl_fallback "6"
;;
7)
deploy_manual_ssl
;;
8)
print_info "跳过HTTPS配置"
return 0
;;
esac
}
ssl_fallback() {
local failed_method=$1 # 接收失败的方案编号
print_error "SSL证书部署失败"
echo ""
print_warning "建议尝试备选方案:"
echo ""
# 动态显示可用选项(排除已失败的)
local available_options=()
# 方案1: Certbot
if [[ "$failed_method" != "1" ]]; then
echo -e "${GREEN}[1]${NC} Certbot (Let's Encrypt官方工具)"
available_options+=("1")
fi
# 方案2: acme.sh + Let's Encrypt
if [[ "$failed_method" != "2" ]]; then
echo -e "${GREEN}[2]${NC} acme.sh + Let's Encrypt"
available_options+=("2")
fi
# 方案3: acme.sh + ZeroSSL
if [[ "$failed_method" != "3" ]]; then
echo -e "${GREEN}[3]${NC} acme.sh + ZeroSSL"
available_options+=("3")
fi
# 方案4: acme.sh + Buypass
if [[ "$failed_method" != "4" ]]; then
echo -e "${GREEN}[4]${NC} acme.sh + Buypass"
available_options+=("4")
fi
# 方案5: 阿里云(注释掉,未实现)
# if [[ "$failed_method" != "5" ]]; then
# echo -e "${GREEN}[5]${NC} 阿里云免费证书"
# available_options+=("5")
# fi
# 方案6: 腾讯云(注释掉,未实现)
# if [[ "$failed_method" != "6" ]]; then
# echo -e "${GREEN}[6]${NC} 腾讯云免费证书"
# available_options+=("6")
# fi
# 方案8: 不配置HTTPS
echo -e "${GREEN}[8]${NC} 暂不配置HTTPS"
available_options+=("8")
echo ""
echo -e "${YELLOW}提示: 方案 $failed_method 已失败,已从列表中移除${NC}"
echo ""
while true; do
read -p "请选择备选方案: " retry_choice < /dev/tty
# 检查输入是否在可用选项中
if [[ ! " ${available_options[@]} " =~ " ${retry_choice} " ]]; then
print_error "无效选项或该方案已失败"
continue
fi
case $retry_choice in
1)
deploy_certbot && return 0
# 如果再次失败继续调用fallback但排除方案1
ssl_fallback "1"
return $?
;;
2)
deploy_acme_letsencrypt && return 0
# 如果再次失败继续调用fallback但排除方案2
ssl_fallback "2"
return $?
;;
3)
deploy_acme_zerossl && return 0
ssl_fallback "3"
return $?
;;
4)
deploy_acme_buypass && return 0
ssl_fallback "4"
return $?
;;
8)
print_info "跳过HTTPS配置"
SSL_METHOD=8
return 0
;;
esac
done
}
deploy_certbot() {
print_step "使用 Certbot 部署SSL证书..."
# 检查certbot是否已安装
if ! command -v certbot &> /dev/null; then
print_info "正在安装 Certbot..."
# 安装certbot
case $PKG_MANAGER in
apt)
# Ubuntu/Debian: 优先使用snap官方推荐避免Python依赖冲突
if command -v snap &> /dev/null; then
print_info "使用snap安装Certbot官方推荐方式..."
snap install --classic certbot 2>/dev/null || true
ln -sf /snap/bin/certbot /usr/bin/certbot 2>/dev/null || true
# 验证snap安装是否成功
if /snap/bin/certbot --version &> /dev/null; then
print_success "Certbot (snap版) 安装成功"
else
print_warning "snap安装失败尝试apt安装..."
apt-get install -y certbot python3-certbot-nginx
fi
else
print_info "snap不可用使用apt安装..."
apt-get install -y certbot python3-certbot-nginx
fi
;;
yum)
yum install -y certbot python3-certbot-nginx
;;
dnf)
dnf install -y certbot python3-certbot-nginx
;;
zypper)
zypper install -y certbot python3-certbot-nginx
;;
esac
# 最终验证certbot是否可用
if ! command -v certbot &> /dev/null; then
print_error "Certbot安装失败"
return 1
fi
else
print_success "Certbot 已安装: $(certbot --version 2>&1 | head -1)"
fi
# 申请证书使用webroot模式不自动修改Nginx配置
echo ""
print_info "正在申请 Let's Encrypt 证书..."
if certbot certonly --webroot -w "${PROJECT_DIR}/frontend" -d "$DOMAIN" --non-interactive --agree-tos --email "admin@${DOMAIN}"; then
# 将证书复制到Nginx SSL目录
mkdir -p /etc/nginx/ssl
ln -sf "/etc/letsencrypt/live/${DOMAIN}/fullchain.pem" "/etc/nginx/ssl/${DOMAIN}.crt"
ln -sf "/etc/letsencrypt/live/${DOMAIN}/privkey.pem" "/etc/nginx/ssl/${DOMAIN}.key"
# 配置自动续期
systemctl enable certbot.timer 2>/dev/null || true
print_success "Certbot SSL证书申请成功"
return 0
else
print_error "Certbot SSL证书申请失败"
echo ""
print_warning "常见失败原因:"
echo " 1. 域名未正确解析到此服务器"
echo " 2. 防火墙阻止了80端口"
echo " 3. Nginx未正确配置或未启动"
echo " 4. Let's Encrypt速率限制"
echo ""
return 1
fi
}
deploy_acme_letsencrypt() {
print_step "使用 acme.sh + Let's Encrypt 部署SSL证书..."
# 安装acme.sh
if [[ ! -d ~/.acme.sh ]]; then
echo ""
print_info "正在安装 acme.sh..."
# 检测网络环境,选择下载源
if curl -s --connect-timeout 3 https://www.google.com > /dev/null 2>&1; then
# 海外网络 - 使用官方源
print_info "使用官方源安装..."
INSTALL_URL="https://get.acme.sh"
else
# 中国大陆 - 使用Gitee镜像
print_info "检测到国内网络使用Gitee镜像加速..."
INSTALL_URL="https://gitee.com/neilpang/acme.sh/raw/master/acme.sh"
export ACME_USE_GITEE=1
fi
# 改进的安装流程:先下载到临时文件,验证后再执行
TEMP_INSTALL_SCRIPT="/tmp/acme-install-$$.sh"
print_info "正在下载安装脚本..."
if curl -fsSL "$INSTALL_URL" -o "$TEMP_INSTALL_SCRIPT"; then
# 检查下载的文件
if [[ -f "$TEMP_INSTALL_SCRIPT" ]]; then
FILE_SIZE=$(stat -c%s "$TEMP_INSTALL_SCRIPT" 2>/dev/null || stat -f%z "$TEMP_INSTALL_SCRIPT" 2>/dev/null || echo "0")
if [[ $FILE_SIZE -gt 1000 ]]; then
print_success "安装脚本下载成功 (${FILE_SIZE} bytes)"
# 显示脚本前几行以确认内容
print_info "验证脚本内容..."
if head -3 "$TEMP_INSTALL_SCRIPT" | grep -q "acme.sh"; then
print_success "脚本内容验证通过"
else
print_warning "脚本内容可能异常,但继续尝试..."
fi
# 执行安装
print_info "正在执行安装..."
bash "$TEMP_INSTALL_SCRIPT" --install
install_result=$?
# 清理临时文件
rm -f "$TEMP_INSTALL_SCRIPT"
# 重新加载环境变量
source ~/.bashrc 2>/dev/null || source ~/.profile 2>/dev/null || true
# 等待文件系统同步 - 增加等待时间
print_info "等待安装完成..."
sleep 5
else
print_error "下载的文件太小 (${FILE_SIZE} bytes),可能下载不完整"
rm -f "$TEMP_INSTALL_SCRIPT"
return 1
fi
else
print_error "安装脚本下载失败"
return 1
fi
else
print_error "无法下载安装脚本"
return 1
fi
# 验证安装是否真正成功(检查目录是否创建)
if [[ $install_result -eq 0 ]] && [[ -d ~/.acme.sh ]] && [[ -f ~/.acme.sh/acme.sh ]]; then
print_success "acme.sh 安装成功"
else
print_error "acme.sh 安装失败"
echo ""
print_warning "诊断信息:"
echo " - 安装命令退出码: $install_result"
echo " - 目录 ~/.acme.sh 存在: $([ -d ~/.acme.sh ] && echo '是' || echo '否')"
echo " - 文件 ~/.acme.sh/acme.sh 存在: $([ -f ~/.acme.sh/acme.sh ] && echo '是' || echo '否')"
echo " - HOME变量: $HOME"
echo " - 当前用户: $(whoami)"
echo ""
print_info "尝试查找acme.sh安装位置..."
find /root -name "acme.sh" -type f 2>/dev/null | head -5 || echo " 未找到"
echo ""
print_warning "解决方案:"
echo " 1. 检查网络连接"
echo " 2. 查看安装日志: ls -la ~ | grep acme"
echo " 3. 手动安装: export ACME_USE_GITEE=1 && curl https://gitee.com/neilpang/acme.sh/raw/master/acme.sh | sh"
echo " 4. 或访问: https://github.com/acmesh-official/acme.sh/wiki/Install-in-China"
echo ""
return 1
fi
fi
# 确认acme.sh可用
echo ""
print_info "验证 acme.sh 安装..."
# 等待文件系统同步
sleep 2
# 检查安装目录
if [[ ! -d ~/.acme.sh ]]; then
print_error "安装目录不存在: ~/.acme.sh"
return 1
fi
# 检查主脚本文件
if [[ ! -f ~/.acme.sh/acme.sh ]]; then
print_error "主脚本文件不存在: ~/.acme.sh/acme.sh"
print_info "目录内容:"
ls -la ~/.acme.sh/ 2>&1 | head -10 || echo "无法列出目录"
return 1
fi
# 检查脚本是否可执行
if [[ ! -x ~/.acme.sh/acme.sh ]]; then
print_warning "脚本不可执行,正在添加执行权限..."
chmod +x ~/.acme.sh/acme.sh
fi
# 测试脚本是否能运行
if ! ~/.acme.sh/acme.sh --version &> /dev/null; then
print_error "acme.sh 无法运行"
return 1
fi
print_success "acme.sh 验证通过"
# 申请证书
echo ""
print_info "正在申请 Let's Encrypt 证书..."
if ~/.acme.sh/acme.sh --issue -d "$DOMAIN" --nginx; then
print_success "证书申请成功"
else
print_error "证书申请失败"
echo ""
print_warning "常见失败原因:"
echo " 1. 域名未正确解析到此服务器"
echo " 2. Nginx未正确配置"
echo " 3. 80端口被占用或防火墙阻止"
echo ""
return 1
fi
# 安装证书
echo ""
print_info "正在安装证书到Nginx..."
mkdir -p /etc/nginx/ssl
if ~/.acme.sh/acme.sh --install-cert -d "$DOMAIN" \
--key-file /etc/nginx/ssl/${DOMAIN}.key \
--fullchain-file /etc/nginx/ssl/${DOMAIN}.crt \
--reloadcmd "systemctl reload nginx"; then
print_success "证书安装成功"
return 0
else
print_error "证书安装失败"
return 1
fi
}
deploy_acme_zerossl() {
print_step "使用 acme.sh + ZeroSSL 部署SSL证书..."
# 安装acme.sh使用改进的安装逻辑
if [[ ! -d ~/.acme.sh ]]; then
echo ""
print_info "正在安装 acme.sh..."
# 检测网络环境,选择下载源
if curl -s --connect-timeout 3 https://www.google.com > /dev/null 2>&1; then
# 海外网络
print_info "使用官方源安装..."
INSTALL_URL="https://get.acme.sh"
else
# 中国大陆 - 使用Gitee镜像
print_info "检测到国内网络使用Gitee镜像加速..."
INSTALL_URL="https://gitee.com/neilpang/acme.sh/raw/master/acme.sh"
export ACME_USE_GITEE=1
fi
# 改进的安装流程:先下载到临时文件,验证后再执行
TEMP_INSTALL_SCRIPT="/tmp/acme-install-$$.sh"
print_info "正在下载安装脚本..."
if curl -fsSL "$INSTALL_URL" -o "$TEMP_INSTALL_SCRIPT"; then
if [[ -f "$TEMP_INSTALL_SCRIPT" ]]; then
FILE_SIZE=$(stat -c%s "$TEMP_INSTALL_SCRIPT" 2>/dev/null || stat -f%z "$TEMP_INSTALL_SCRIPT" 2>/dev/null || echo "0")
if [[ $FILE_SIZE -gt 1000 ]]; then
print_success "安装脚本下载成功 (${FILE_SIZE} bytes)"
print_info "验证脚本内容..."
if head -3 "$TEMP_INSTALL_SCRIPT" | grep -q "acme.sh"; then
print_success "脚本内容验证通过"
else
print_warning "脚本内容可能异常,但继续尝试..."
fi
print_info "正在执行安装..."
bash "$TEMP_INSTALL_SCRIPT" --install
install_result=$?
rm -f "$TEMP_INSTALL_SCRIPT"
source ~/.bashrc 2>/dev/null || source ~/.profile 2>/dev/null || true
print_info "等待安装完成..."
sleep 5
else
print_error "下载的文件太小 (${FILE_SIZE} bytes),可能下载不完整"
rm -f "$TEMP_INSTALL_SCRIPT"
return 1
fi
else
print_error "安装脚本下载失败"
return 1
fi
else
print_error "无法下载安装脚本"
return 1
fi
# 验证安装
if [[ $install_result -eq 0 ]] && [[ -d ~/.acme.sh ]] && [[ -f ~/.acme.sh/acme.sh ]]; then
print_success "acme.sh 安装成功"
else
print_error "acme.sh 安装失败"
echo ""
print_warning "诊断信息:"
echo " - 安装命令退出码: $install_result"
echo " - 目录 ~/.acme.sh 存在: $([ -d ~/.acme.sh ] && echo '是' || echo '否')"
echo " - 文件 ~/.acme.sh/acme.sh 存在: $([ -f ~/.acme.sh/acme.sh ] && echo '是' || echo '否')"
echo ""
return 1
fi
fi
# 确认acme.sh可用
echo ""
print_info "验证 acme.sh 安装..."
# 等待文件系统同步
sleep 2
# 检查安装目录
if [[ ! -d ~/.acme.sh ]]; then
print_error "安装目录不存在: ~/.acme.sh"
return 1
fi
# 检查主脚本文件
if [[ ! -f ~/.acme.sh/acme.sh ]]; then
print_error "主脚本文件不存在: ~/.acme.sh/acme.sh"
print_info "目录内容:"
ls -la ~/.acme.sh/ 2>&1 | head -10 || echo "无法列出目录"
return 1
fi
# 检查脚本是否可执行
if [[ ! -x ~/.acme.sh/acme.sh ]]; then
print_warning "脚本不可执行,正在添加执行权限..."
chmod +x ~/.acme.sh/acme.sh
fi
# 测试脚本是否能运行
if ! ~/.acme.sh/acme.sh --version &> /dev/null; then
print_error "acme.sh 无法运行"
return 1
fi
print_success "acme.sh 验证通过"
# 申请证书
echo ""
print_info "正在申请 ZeroSSL 证书..."
if ~/.acme.sh/acme.sh --server zerossl --issue -d "$DOMAIN" --nginx; then
print_success "证书申请成功"
else
print_error "证书申请失败"
return 1
fi
# 安装证书
echo ""
print_info "正在安装证书到Nginx..."
mkdir -p /etc/nginx/ssl
if ~/.acme.sh/acme.sh --install-cert -d "$DOMAIN" \
--key-file /etc/nginx/ssl/${DOMAIN}.key \
--fullchain-file /etc/nginx/ssl/${DOMAIN}.crt \
--reloadcmd "systemctl reload nginx"; then
print_success "证书安装成功"
return 0
else
print_error "证书安装失败"
return 1
fi
}
deploy_acme_buypass() {
print_step "使用 acme.sh + Buypass 部署SSL证书..."
# 安装acme.sh使用改进的安装逻辑
if [[ ! -d ~/.acme.sh ]]; then
echo ""
print_info "正在安装 acme.sh..."
# 检测网络环境,选择下载源
if curl -s --connect-timeout 3 https://www.google.com > /dev/null 2>&1; then
# 海外网络
print_info "使用官方源安装..."
INSTALL_URL="https://get.acme.sh"
else
# 中国大陆 - 使用Gitee镜像
print_info "检测到国内网络使用Gitee镜像加速..."
INSTALL_URL="https://gitee.com/neilpang/acme.sh/raw/master/acme.sh"
export ACME_USE_GITEE=1
fi
# 改进的安装流程:先下载到临时文件,验证后再执行
TEMP_INSTALL_SCRIPT="/tmp/acme-install-$$.sh"
print_info "正在下载安装脚本..."
if curl -fsSL "$INSTALL_URL" -o "$TEMP_INSTALL_SCRIPT"; then
if [[ -f "$TEMP_INSTALL_SCRIPT" ]]; then
FILE_SIZE=$(stat -c%s "$TEMP_INSTALL_SCRIPT" 2>/dev/null || stat -f%z "$TEMP_INSTALL_SCRIPT" 2>/dev/null || echo "0")
if [[ $FILE_SIZE -gt 1000 ]]; then
print_success "安装脚本下载成功 (${FILE_SIZE} bytes)"
print_info "验证脚本内容..."
if head -3 "$TEMP_INSTALL_SCRIPT" | grep -q "acme.sh"; then
print_success "脚本内容验证通过"
else
print_warning "脚本内容可能异常,但继续尝试..."
fi
print_info "正在执行安装..."
bash "$TEMP_INSTALL_SCRIPT" --install
install_result=$?
rm -f "$TEMP_INSTALL_SCRIPT"
source ~/.bashrc 2>/dev/null || source ~/.profile 2>/dev/null || true
print_info "等待安装完成..."
sleep 5
else
print_error "下载的文件太小 (${FILE_SIZE} bytes),可能下载不完整"
rm -f "$TEMP_INSTALL_SCRIPT"
return 1
fi
else
print_error "安装脚本下载失败"
return 1
fi
else
print_error "无法下载安装脚本"
return 1
fi
# 验证安装
if [[ $install_result -eq 0 ]] && [[ -d ~/.acme.sh ]] && [[ -f ~/.acme.sh/acme.sh ]]; then
print_success "acme.sh 安装成功"
else
print_error "acme.sh 安装失败"
echo ""
print_warning "诊断信息:"
echo " - 安装命令退出码: $install_result"
echo " - 目录 ~/.acme.sh 存在: $([ -d ~/.acme.sh ] && echo '是' || echo '否')"
echo " - 文件 ~/.acme.sh/acme.sh 存在: $([ -f ~/.acme.sh/acme.sh ] && echo '是' || echo '否')"
echo ""
return 1
fi
fi
# 确认acme.sh可用
echo ""
print_info "验证 acme.sh 安装..."
# 等待文件系统同步
sleep 2
# 检查安装目录
if [[ ! -d ~/.acme.sh ]]; then
print_error "安装目录不存在: ~/.acme.sh"
return 1
fi
# 检查主脚本文件
if [[ ! -f ~/.acme.sh/acme.sh ]]; then
print_error "主脚本文件不存在: ~/.acme.sh/acme.sh"
print_info "目录内容:"
ls -la ~/.acme.sh/ 2>&1 | head -10 || echo "无法列出目录"
return 1
fi
# 检查脚本是否可执行
if [[ ! -x ~/.acme.sh/acme.sh ]]; then
print_warning "脚本不可执行,正在添加执行权限..."
chmod +x ~/.acme.sh/acme.sh
fi
# 测试脚本是否能运行
if ! ~/.acme.sh/acme.sh --version &> /dev/null; then
print_error "acme.sh 无法运行"
return 1
fi
print_success "acme.sh 验证通过"
# 申请证书
echo ""
print_info "正在申请 Buypass 证书..."
if ~/.acme.sh/acme.sh --server buypass --issue -d "$DOMAIN" --nginx; then
print_success "证书申请成功"
else
print_error "证书申请失败"
return 1
fi
# 安装证书
echo ""
print_info "正在安装证书到Nginx..."
mkdir -p /etc/nginx/ssl
if ~/.acme.sh/acme.sh --install-cert -d "$DOMAIN" \
--key-file /etc/nginx/ssl/${DOMAIN}.key \
--fullchain-file /etc/nginx/ssl/${DOMAIN}.crt \
--reloadcmd "systemctl reload nginx"; then
print_success "证书安装成功"
return 0
else
print_error "证书安装失败"
return 1
fi
}
deploy_aliyun_ssl() {
print_step "使用阿里云免费证书..."
print_warning "此功能需要您提供阿里云AccessKey"
echo ""
read -p "阿里云AccessKey ID: " ALIYUN_ACCESS_KEY_ID < /dev/tty
read -p "阿里云AccessKey Secret: " ALIYUN_ACCESS_KEY_SECRET < /dev/tty
# 这里需要调用阿里云API申请证书
# 暂时返回失败,提示用户使用其他方案
print_error "阿里云证书申请功能开发中,请选择其他方案"
return 1
}
deploy_tencent_ssl() {
print_step "使用腾讯云免费证书..."
print_warning "此功能需要您提供腾讯云SecretKey"
echo ""
read -p "腾讯云SecretId: " TENCENT_SECRET_ID < /dev/tty
read -p "腾讯云SecretKey: " TENCENT_SECRET_KEY < /dev/tty
# 这里需要调用腾讯云API申请证书
# 暂时返回失败,提示用户使用其他方案
print_error "腾讯云证书申请功能开发中,请选择其他方案"
return 1
}
deploy_manual_ssl() {
print_step "使用已有证书..."
echo ""
print_info "请将以下文件上传到服务器:"
print_info "- 证书文件: /tmp/ssl_cert.crt"
print_info "- 私钥文件: /tmp/ssl_key.key"
echo ""
read -p "上传完成后按回车继续..." < /dev/tty
if [[ -f /tmp/ssl_cert.crt ]] && [[ -f /tmp/ssl_key.key ]]; then
mkdir -p /etc/nginx/ssl
cp /tmp/ssl_cert.crt /etc/nginx/ssl/${DOMAIN}.crt
cp /tmp/ssl_key.key /etc/nginx/ssl/${DOMAIN}.key
chmod 600 /etc/nginx/ssl/${DOMAIN}.key
print_success "证书文件已复制"
return 0
else
print_error "证书文件未找到"
return 1
fi
}
################################################################################
# 项目部署
################################################################################
create_project_directory() {
print_step "创建项目目录..."
if [[ -d "$PROJECT_DIR" ]]; then
print_warning "项目目录已存在"
read -p "是否删除并重新创建? (y/n): " recreate < /dev/tty
if [[ "$recreate" == "y" || "$recreate" == "Y" ]]; then
rm -rf "$PROJECT_DIR"
else
print_error "部署已取消"
exit 1
fi
fi
mkdir -p "$PROJECT_DIR"
print_success "项目目录已创建: $PROJECT_DIR"
echo ""
}
download_project() {
print_step "正在从Gitee下载项目..."
cd /tmp
if [[ -d "${PROJECT_NAME}" ]]; then
rm -rf "${PROJECT_NAME}"
fi
git clone "$REPO_URL" "${PROJECT_NAME}"
# 复制文件到项目目录
cp -r "/tmp/${PROJECT_NAME}"/* "$PROJECT_DIR/"
# 清理临时文件
rm -rf "/tmp/${PROJECT_NAME}"
print_success "项目下载完成"
echo ""
}
configure_admin_account() {
print_step "配置管理员账号"
echo ""
while true; do
read -p "管理员用户名 [默认: admin]: " ADMIN_USERNAME < /dev/tty
ADMIN_USERNAME=${ADMIN_USERNAME:-admin}
if [[ ${#ADMIN_USERNAME} -lt 3 ]]; then
print_error "用户名至少3个字符"
continue
fi
break
done
while true; do
read -s -p "管理员密码至少6位: " ADMIN_PASSWORD < /dev/tty
echo ""
if [[ ${#ADMIN_PASSWORD} -lt 6 ]]; then
print_error "密码至少6个字符"
continue
fi
read -s -p "确认密码: " ADMIN_PASSWORD_CONFIRM < /dev/tty
echo ""
if [[ "$ADMIN_PASSWORD" != "$ADMIN_PASSWORD_CONFIRM" ]]; then
print_error "两次密码不一致"
continue
fi
break
done
print_success "管理员账号配置完成"
echo ""
}
install_backend_dependencies() {
print_step "安装后端依赖..."
cd "${PROJECT_DIR}/backend"
# 确保Python可用node-gyp需要
if ! command -v python &> /dev/null; then
if command -v python3 &> /dev/null; then
# 创建python软链接指向python3
if [[ "$OS" == "ubuntu" ]] || [[ "$OS" == "debian" ]]; then
ln -sf /usr/bin/python3 /usr/bin/python || true
else
# CentOS/RHEL/其他系统
alternatives --install /usr/bin/python python /usr/bin/python3 1 &> /dev/null || \
ln -sf /usr/bin/python3 /usr/bin/python || true
fi
print_info "已配置Python环境python -> python3"
else
print_warning "未找到Python某些依赖可能需要手动处理"
fi
fi
# 使用国内镜像加速
if [[ "$USE_ALIYUN_MIRROR" == "true" ]]; then
npm config set registry https://registry.npmmirror.com
fi
print_info "正在安装依赖包包含数据库native模块可能需要几分钟..."
# 安装依赖,捕获错误
if PYTHON=python3 npm install --production; then
print_success "后端依赖安装完成"
else
print_error "依赖安装失败"
echo ""
print_warning "可能的解决方案:"
echo " 1. 检查网络连接"
echo " 2. 手动执行: cd ${PROJECT_DIR}/backend && npm install --production"
echo " 3. 查看详细错误日志: ~/.npm/_logs/"
echo ""
# 询问是否继续
read -p "是否忽略错误继续安装?(y/n): " continue_install < /dev/tty
if [[ "$continue_install" != "y" ]] && [[ "$continue_install" != "Y" ]]; then
exit 1
fi
fi
echo ""
}
create_env_file() {
print_step "创建配置文件..."
# 生成随机JWT密钥
JWT_SECRET=$(openssl rand -base64 32)
cat > "${PROJECT_DIR}/backend/.env" << EOF
# 管理员账号
ADMIN_USERNAME=${ADMIN_USERNAME}
ADMIN_PASSWORD=${ADMIN_PASSWORD}
# JWT密钥
JWT_SECRET=${JWT_SECRET}
# 数据库路径
DATABASE_PATH=./data/database.db
# 存储目录
STORAGE_ROOT=./storage
# 服务端口
PORT=${BACKEND_PORT}
# 环境
NODE_ENV=production
# 公开端口nginx监听的端口用于生成分享链接
# 如果使用标准端口(80/443)或未配置,分享链接将不包含端口号
PUBLIC_PORT=${HTTP_PORT}
EOF
print_success "配置文件创建完成"
echo ""
}
create_data_directories() {
print_step "创建数据目录..."
mkdir -p "${PROJECT_DIR}/backend/data"
mkdir -p "${PROJECT_DIR}/backend/storage"
print_success "数据目录创建完成"
echo ""
}
build_upload_tool() {
print_step "下载上传工具..."
cd "${PROJECT_DIR}/upload-tool"
# 检查是否已存在可执行文件并验证大小
if [[ -f "dist/玩玩云上传工具.exe" ]]; then
FILE_SIZE=$(stat -f%z "dist/玩玩云上传工具.exe" 2>/dev/null || stat -c%s "dist/玩玩云上传工具.exe" 2>/dev/null || echo "0")
if [[ $FILE_SIZE -gt 30000000 ]]; then
FILE_SIZE_MB=$(( FILE_SIZE / 1024 / 1024 ))
print_success "上传工具已存在(${FILE_SIZE_MB}MB跳过下载"
echo ""
return 0
else
print_warning "现有文件大小异常(${FILE_SIZE}字节),重新下载..."
rm -f "dist/玩玩云上传工具.exe"
fi
fi
# 创建dist目录
mkdir -p dist
# 下载地址Windows版本
TOOL_DOWNLOAD_URL="http://a.haory.top/e/e82/玩玩云上传工具.exe"
TOOL_FILENAME="玩玩云上传工具.exe"
print_info "正在下载上传工具约43MB可能需要1-2分钟..."
# 尝试下载最多3次重试
DOWNLOAD_SUCCESS=false
for attempt in 1 2 3; do
print_info "尝试下载 ($attempt/3)..."
if command -v wget &> /dev/null; then
# wget: 超时300秒重试3次
if wget --timeout=300 --tries=3 --no-check-certificate -q --show-progress -O "dist/${TOOL_FILENAME}" "$TOOL_DOWNLOAD_URL" 2>&1; then
DOWNLOAD_SUCCESS=true
break
fi
elif command -v curl &> /dev/null; then
# curl: 连接超时60秒总超时300秒
if curl --connect-timeout 60 --max-time 300 -L -# -o "dist/${TOOL_FILENAME}" "$TOOL_DOWNLOAD_URL" 2>&1; then
DOWNLOAD_SUCCESS=true
break
fi
else
print_warning "未找到wget或curl无法下载上传工具"
print_info "用户仍可使用网页上传本地存储或SFTP客户端"
echo ""
return 0
fi
# 如果不是最后一次尝试,等待后重试
if [[ $attempt -lt 3 ]]; then
print_warning "下载失败5秒后重试..."
sleep 5
fi
done
# 验证下载结果
if [[ "$DOWNLOAD_SUCCESS" == "true" ]] && [[ -f "dist/${TOOL_FILENAME}" ]]; then
FILE_SIZE=$(stat -f%z "dist/${TOOL_FILENAME}" 2>/dev/null || stat -c%s "dist/${TOOL_FILENAME}" 2>/dev/null || echo "0")
FILE_SIZE_MB=$(( FILE_SIZE / 1024 / 1024 ))
if [[ $FILE_SIZE -gt 30000000 ]]; then
print_success "上传工具下载完成: ${FILE_SIZE_MB}MB"
echo ""
else
print_error "下载的文件大小异常(${FILE_SIZE}字节),可能下载不完整"
rm -f "dist/${TOOL_FILENAME}"
print_warning "可手动下载: ${TOOL_DOWNLOAD_URL}"
print_info "用户仍可使用网页上传本地存储或SFTP客户端"
echo ""
fi
else
print_error "上传工具下载失败已重试3次"
print_warning "可能的原因:"
echo " 1. 网络连接问题或下载速度过慢"
echo " 2. CDN链接不可访问: ${TOOL_DOWNLOAD_URL}"
echo " 3. 防火墙拦截HTTP连接"
print_info "您可以稍后手动下载并放置到: ${PROJECT_DIR}/upload-tool/dist/"
print_info "用户仍可使用网页上传本地存储或SFTP客户端"
echo ""
fi
}
################################################################################
# Nginx配置 - 分步骤执行
################################################################################
# 步骤1: 先配置HTTP Nginx为SSL证书验证做准备
configure_nginx_http_first() {
print_step "配置基础HTTP Nginx用于SSL证书验证..."
# 总是先配置HTTP模式
local server_name="${DOMAIN:-_}"
# 检测Nginx配置目录结构并创建必要的目录
if [[ -d /www/server/nginx ]]; then
# 宝塔面板 (BT Panel)
NGINX_CONF_DIR="/www/server/panel/vhost/nginx"
NGINX_ENABLED_DIR=""
USE_SYMLINK=false
IS_BT_PANEL=true
# 确保目录存在
mkdir -p ${NGINX_CONF_DIR}
print_info "检测到宝塔面板使用宝塔Nginx配置目录"
elif [[ -d /etc/nginx/sites-available ]] || [[ "$PKG_MANAGER" == "apt" ]]; then
# Debian/Ubuntu: 使用sites-available
NGINX_CONF_DIR="/etc/nginx/sites-available"
NGINX_ENABLED_DIR="/etc/nginx/sites-enabled"
USE_SYMLINK=true
IS_BT_PANEL=false
# 确保目录存在
mkdir -p ${NGINX_CONF_DIR}
mkdir -p ${NGINX_ENABLED_DIR}
else
# CentOS/RHEL: 使用conf.d
NGINX_CONF_DIR="/etc/nginx/conf.d"
NGINX_ENABLED_DIR=""
USE_SYMLINK=false
IS_BT_PANEL=false
# 确保目录存在
mkdir -p ${NGINX_CONF_DIR}
fi
cat > ${NGINX_CONF_DIR}/${PROJECT_NAME}.conf << EOF
server {
listen ${HTTP_PORT};
server_name ${server_name};
# 文件上传大小限制10GB
client_max_body_size 10G;
# 前端静态文件
location / {
root ${PROJECT_DIR}/frontend;
index index.html;
try_files \$uri \$uri/ /index.html;
}
# 后端API
location /api {
proxy_pass http://localhost:${BACKEND_PORT};
proxy_http_version 1.1;
proxy_set_header Upgrade \$http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host \$host;
proxy_cache_bypass \$http_upgrade;
proxy_set_header X-Real-IP \$remote_addr;
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto \$scheme;
# 上传超时设置
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
proxy_connect_timeout 300s;
}
# 分享页面
location /s/ {
proxy_pass http://localhost:${BACKEND_PORT};
proxy_set_header Host \$host;
proxy_set_header X-Real-IP \$remote_addr;
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto \$scheme;
}
# 静态资源
location /libs {
alias ${PROJECT_DIR}/frontend/libs;
expires 30d;
}
# 上传工具下载
location /download-tool {
alias ${PROJECT_DIR}/upload-tool/dist;
}
}
EOF
# 根据系统类型处理配置文件
if [[ "$USE_SYMLINK" == "true" ]]; then
# Debian/Ubuntu: 创建软链接
ln -sf ${NGINX_CONF_DIR}/${PROJECT_NAME}.conf ${NGINX_ENABLED_DIR}/${PROJECT_NAME}.conf
# 删除默认站点
rm -f ${NGINX_ENABLED_DIR}/default
elif [[ "$IS_BT_PANEL" != "true" ]]; then
# CentOS/RHEL (非宝塔): conf.d中的.conf文件会自动加载
rm -f /etc/nginx/conf.d/default.conf
fi
# 宝塔面板:配置文件已自动包含,无需额外操作
# 测试nginx配置
if ! nginx -t; then
print_error "Nginx配置测试失败"
return 1
fi
# 启动或重载Nginx
if [[ "$IS_BT_PANEL" == "true" ]]; then
# 宝塔面板:尝试多种方式
print_info "宝塔环境尝试重载Nginx..."
# 方式1: 使用宝塔命令行工具(如果存在)
if [[ -f /etc/init.d/bt ]]; then
/etc/init.d/bt restart 2>/dev/null
fi
# 方式2: 直接使用nginx命令reload
if [[ -f /www/server/nginx/sbin/nginx ]]; then
/www/server/nginx/sbin/nginx -s reload 2>/dev/null
if [[ $? -eq 0 ]]; then
print_success "已使用nginx -s reload重载配置"
else
# 如果reload失败尝试启动
/www/server/nginx/sbin/nginx 2>/dev/null
if [[ $? -eq 0 ]]; then
print_success "已启动Nginx"
else
print_warning "Nginx reload失败尝试systemctl..."
fi
fi
fi
# 方式3: 尝试systemctl备用
if systemctl is-active --quiet nginx 2>/dev/null; then
systemctl reload nginx 2>/dev/null && print_info "已使用systemctl重载配置"
else
systemctl start nginx 2>/dev/null && print_info "已使用systemctl启动Nginx"
fi
else
# 标准Nginx重启
systemctl restart nginx
fi
# 验证Nginx是否运行
sleep 2
if [[ "$IS_BT_PANEL" == "true" ]]; then
# 宝塔:检查进程
if pgrep -x nginx > /dev/null; then
print_success "Nginx运行正常"
else
print_error "Nginx未运行"
print_warning "请在宝塔面板中手动启动Nginx或运行"
print_warning "/www/server/nginx/sbin/nginx"
return 1
fi
else
# 标准Nginx使用systemctl检查
if ! systemctl is-active --quiet nginx; then
print_error "Nginx启动失败"
return 1
fi
fi
print_success "基础HTTP Nginx配置完成"
echo ""
}
# 步骤2: 根据SSL结果配置最终Nginx
configure_nginx_final() {
print_step "配置最终Nginx..."
# 检查SSL是否成功部署
local ssl_deployed=false
if [[ "$USE_DOMAIN" == "true" ]] && [[ "$SSL_METHOD" != "8" ]]; then
# 检查SSL证书文件是否存在
if [[ -f /etc/nginx/ssl/${DOMAIN}.crt ]] && [[ -f /etc/nginx/ssl/${DOMAIN}.key ]]; then
ssl_deployed=true
print_info "检测到SSL证书配置HTTPS..."
else
print_warning "SSL证书不存在保持HTTP配置"
fi
fi
# 根据SSL状态配置
if [[ "$ssl_deployed" == "true" ]]; then
# 配置HTTPS
configure_nginx_https
else
# 保持HTTP已在第一步配置这里只需确认
print_info "使用HTTP配置"
fi
# 测试nginx配置
if ! nginx -t; then
print_error "Nginx配置测试失败"
return 1
fi
# 重载nginx - 兼容宝塔面板
if [[ "$IS_BT_PANEL" == "true" ]]; then
# 宝塔面板:尝试多种方式
print_info "宝塔环境重载Nginx配置..."
# 方式1: 使用宝塔命令行工具(如果存在)
if [[ -f /etc/init.d/bt ]]; then
/etc/init.d/bt restart 2>/dev/null
fi
# 方式2: 直接使用nginx命令reload最可靠
if [[ -f /www/server/nginx/sbin/nginx ]]; then
/www/server/nginx/sbin/nginx -s reload 2>/dev/null
if [[ $? -eq 0 ]]; then
print_success "已使用nginx -s reload重载配置"
else
# 如果reload失败尝试启动
/www/server/nginx/sbin/nginx 2>/dev/null
if [[ $? -eq 0 ]]; then
print_success "已启动Nginx"
else
print_warning "Nginx reload失败尝试systemctl..."
fi
fi
fi
# 方式3: 尝试systemctl备用
if systemctl is-active --quiet nginx 2>/dev/null; then
systemctl reload nginx 2>/dev/null && print_info "已使用systemctl重载配置"
else
systemctl start nginx 2>/dev/null && print_info "已使用systemctl启动Nginx"
fi
else
# 标准Nginx重载
systemctl reload nginx
fi
print_success "Nginx最终配置完成"
echo ""
}
configure_nginx() {
print_step "配置Nginx..."
if [[ "$USE_DOMAIN" == "true" ]]; then
if [[ "$SSL_METHOD" == "8" ]]; then
# HTTP配置
configure_nginx_http
else
# HTTPS配置
configure_nginx_https
fi
else
# IP模式HTTP配置
configure_nginx_http
fi
# 测试nginx配置
nginx -t
# 重启nginx
systemctl restart nginx
print_success "Nginx配置完成"
echo ""
}
configure_nginx_http() {
local server_name="${DOMAIN:-_}"
# 检测Nginx配置目录结构并创建必要的目录
if [[ -d /www/server/nginx ]]; then
# 宝塔面板
NGINX_CONF_DIR="/www/server/panel/vhost/nginx"
USE_SYMLINK=false
IS_BT_PANEL=true
mkdir -p ${NGINX_CONF_DIR}
elif [[ -d /etc/nginx/sites-available ]] || [[ "$PKG_MANAGER" == "apt" ]]; then
# Debian/Ubuntu
NGINX_CONF_DIR="/etc/nginx/sites-available"
NGINX_ENABLED_DIR="/etc/nginx/sites-enabled"
USE_SYMLINK=true
IS_BT_PANEL=false
mkdir -p ${NGINX_CONF_DIR}
mkdir -p ${NGINX_ENABLED_DIR}
else
# CentOS/RHEL
NGINX_CONF_DIR="/etc/nginx/conf.d"
USE_SYMLINK=false
IS_BT_PANEL=false
mkdir -p ${NGINX_CONF_DIR}
fi
cat > ${NGINX_CONF_DIR}/${PROJECT_NAME}.conf << EOF
server {
listen ${HTTP_PORT};
server_name ${server_name};
# 文件上传大小限制10GB
client_max_body_size 10G;
# 前端静态文件
location / {
root ${PROJECT_DIR}/frontend;
index index.html;
try_files \$uri \$uri/ /index.html;
}
# 后端API
location /api {
proxy_pass http://localhost:${BACKEND_PORT};
proxy_http_version 1.1;
proxy_set_header Upgrade \$http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host \$host;
proxy_cache_bypass \$http_upgrade;
proxy_set_header X-Real-IP \$remote_addr;
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto \$scheme;
# 上传超时设置大文件上传需要更长时间设置为1小时
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
proxy_connect_timeout 300s;
}
# 分享页面(代理到后端处理重定向)
location /s/ {
proxy_pass http://localhost:${BACKEND_PORT};
proxy_set_header Host \$host;
proxy_set_header X-Real-IP \$remote_addr;
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto \$scheme;
}
# 静态资源
location /libs {
alias ${PROJECT_DIR}/frontend/libs;
expires 30d;
}
# 上传工具下载
location /download-tool {
alias ${PROJECT_DIR}/upload-tool/dist;
}
}
EOF
# 根据系统类型处理配置文件
if [[ "$USE_SYMLINK" == "true" ]]; then
# Debian/Ubuntu: 创建软链接
ln -sf ${NGINX_CONF_DIR}/${PROJECT_NAME}.conf ${NGINX_ENABLED_DIR}/${PROJECT_NAME}.conf
# 删除默认站点
rm -f ${NGINX_ENABLED_DIR}/default
elif [[ "$IS_BT_PANEL" != "true" ]]; then
# CentOS/RHEL (非宝塔): conf.d中的.conf文件会自动加载
rm -f /etc/nginx/conf.d/default.conf
fi
}
configure_nginx_https() {
# 检测Nginx配置目录结构并创建必要的目录
if [[ -d /www/server/nginx ]]; then
# 宝塔面板
NGINX_CONF_DIR="/www/server/panel/vhost/nginx"
USE_SYMLINK=false
IS_BT_PANEL=true
mkdir -p ${NGINX_CONF_DIR}
elif [[ -d /etc/nginx/sites-available ]] || [[ "$PKG_MANAGER" == "apt" ]]; then
# Debian/Ubuntu
NGINX_CONF_DIR="/etc/nginx/sites-available"
NGINX_ENABLED_DIR="/etc/nginx/sites-enabled"
USE_SYMLINK=true
IS_BT_PANEL=false
mkdir -p ${NGINX_CONF_DIR}
mkdir -p ${NGINX_ENABLED_DIR}
else
# CentOS/RHEL
NGINX_CONF_DIR="/etc/nginx/conf.d"
USE_SYMLINK=false
IS_BT_PANEL=false
mkdir -p ${NGINX_CONF_DIR}
fi
cat > ${NGINX_CONF_DIR}/${PROJECT_NAME}.conf << EOF
server {
listen ${HTTP_PORT};
server_name ${DOMAIN};
return 301 https://\$server_name:\${HTTPS_PORT}\$request_uri;
}
server {
listen ${HTTPS_PORT} ssl http2;
server_name ${DOMAIN};
ssl_certificate /etc/nginx/ssl/${DOMAIN}.crt;
ssl_certificate_key /etc/nginx/ssl/${DOMAIN}.key;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;
# 文件上传大小限制10GB
client_max_body_size 10G;
# 前端静态文件
location / {
root ${PROJECT_DIR}/frontend;
index index.html;
try_files \$uri \$uri/ /index.html;
}
# 后端API
location /api {
proxy_pass http://localhost:${BACKEND_PORT};
proxy_http_version 1.1;
proxy_set_header Upgrade \$http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host \$host;
proxy_cache_bypass \$http_upgrade;
proxy_set_header X-Real-IP \$remote_addr;
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto \$scheme;
# 上传超时设置大文件上传需要更长时间设置为1小时
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
proxy_connect_timeout 300s;
}
# 分享页面(代理到后端处理重定向)
location /s/ {
proxy_pass http://localhost:${BACKEND_PORT};
proxy_set_header Host \$host;
proxy_set_header X-Real-IP \$remote_addr;
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto \$scheme;
}
# 静态资源
location /libs {
alias ${PROJECT_DIR}/frontend/libs;
expires 30d;
}
# 上传工具下载
location /download-tool {
alias ${PROJECT_DIR}/upload-tool/dist;
}
}
EOF
# 根据系统类型处理配置文件
if [[ "$USE_SYMLINK" == "true" ]]; then
# Debian/Ubuntu: 创建软链接
ln -sf ${NGINX_CONF_DIR}/${PROJECT_NAME}.conf ${NGINX_ENABLED_DIR}/${PROJECT_NAME}.conf
# 删除默认站点
rm -f ${NGINX_ENABLED_DIR}/default
elif [[ "$IS_BT_PANEL" != "true" ]]; then
# CentOS/RHEL (非宝塔): conf.d中的.conf文件会自动加载
rm -f /etc/nginx/conf.d/default.conf
fi
}
start_backend_service() {
print_step "启动后端服务..."
cd "${PROJECT_DIR}/backend"
# 使用PM2启动
pm2 start server.js --name ${PROJECT_NAME}-backend
pm2 save
print_success "后端服务已启动"
echo ""
}
################################################################################
# 健康检查
################################################################################
health_check() {
print_step "正在进行健康检查..."
sleep 3
# 检查后端服务
if pm2 status | grep -q "${PROJECT_NAME}-backend.*online"; then
print_success "后端服务运行正常"
else
print_error "后端服务启动失败"
print_info "查看日志: pm2 logs ${PROJECT_NAME}-backend"
return 1
fi
# 检查端口
if netstat -tunlp 2>/dev/null | grep -q ":${BACKEND_PORT}" || ss -tunlp 2>/dev/null | grep -q ":${BACKEND_PORT}"; then
print_success "后端端口监听正常 (${BACKEND_PORT})"
else
print_error "后端端口监听异常"
return 1
fi
# 检查Nginx
if [[ -d /www/server/nginx ]]; then
# 宝塔面板:检查进程
if pgrep -x nginx > /dev/null; then
print_success "Nginx服务运行正常"
else
print_error "Nginx服务异常"
return 1
fi
else
# 标准Nginx使用systemctl检查
if systemctl is-active --quiet nginx; then
print_success "Nginx服务运行正常"
else
print_error "Nginx服务异常"
return 1
fi
fi
# 检查数据库
if [[ -f "${PROJECT_DIR}/backend/ftp-manager.db" ]]; then
print_success "数据库初始化成功"
else
print_warning "数据库文件不存在"
fi
# 检查存储目录
if [[ -d "${PROJECT_DIR}/backend/storage" ]]; then
print_success "文件存储目录就绪"
else
print_warning "存储目录不存在"
fi
echo ""
return 0
}
################################################################################
# 完成提示
################################################################################
print_completion() {
clear
echo -e "${GREEN}"
echo "╔═══════════════════════════════════════════════════════════════╗"
echo "║ ║"
echo "║ 🎉 部署成功! ║"
echo "║ ║"
echo "╚═══════════════════════════════════════════════════════════════╝"
echo -e "${NC}"
echo ""
# 访问地址
if [[ "$USE_DOMAIN" == "true" ]]; then
if [[ "$SSL_METHOD" == "8" ]]; then
if [[ "$HTTP_PORT" == "80" ]]; then
echo -e "${CYAN}访问地址:${NC} http://${DOMAIN}"
else
echo -e "${CYAN}访问地址:${NC} http://${DOMAIN}:${HTTP_PORT}"
fi
else
if [[ "$HTTPS_PORT" == "443" ]]; then
echo -e "${CYAN}访问地址:${NC} https://${DOMAIN}"
else
echo -e "${CYAN}访问地址:${NC} https://${DOMAIN}:${HTTPS_PORT}"
fi
fi
else
PUBLIC_IP=$(curl -s ifconfig.me || curl -s icanhazip.com || echo "服务器IP")
if [[ "$HTTP_PORT" == "80" ]]; then
echo -e "${CYAN}访问地址:${NC} http://${PUBLIC_IP}"
else
echo -e "${CYAN}访问地址:${NC} http://${PUBLIC_IP}:${HTTP_PORT}"
fi
fi
echo -e "${CYAN}管理员账号:${NC} ${ADMIN_USERNAME}"
echo -e "${CYAN}管理员密码:${NC} ********"
echo ""
# 端口信息
if [[ "$HTTP_PORT" != "80" ]] || [[ "$HTTPS_PORT" != "443" ]] || [[ "$BACKEND_PORT" != "40001" ]]; then
echo -e "${YELLOW}端口配置:${NC}"
echo " HTTP端口: $HTTP_PORT"
if [[ "$USE_DOMAIN" == "true" ]] && [[ "$SSL_METHOD" != "8" ]]; then
echo " HTTPS端口: $HTTPS_PORT"
fi
echo " 后端端口: $BACKEND_PORT"
echo ""
fi
# 常用命令
echo -e "${YELLOW}常用命令:${NC}"
echo " 查看服务状态: pm2 status"
echo " 查看日志: pm2 logs ${PROJECT_NAME}-backend"
echo " 重启服务: pm2 restart ${PROJECT_NAME}-backend"
echo " 停止服务: pm2 stop ${PROJECT_NAME}-backend"
echo ""
# 配置文件位置
echo -e "${YELLOW}配置文件位置:${NC}"
echo " 后端配置: ${PROJECT_DIR}/backend/.env"
echo " Nginx配置: /etc/nginx/sites-enabled/${PROJECT_NAME}.conf"
echo " 数据库: ${PROJECT_DIR}/backend/data/database.db"
echo " 文件存储: ${PROJECT_DIR}/backend/storage"
echo ""
# SSL续期提示
if [[ "$USE_DOMAIN" == "true" ]] && [[ "$SSL_METHOD" != "8" ]]; then
echo -e "${YELLOW}SSL证书:${NC}"
case $SSL_METHOD in
1)
echo " 自动续期: 已配置Certbot自动续期"
;;
2|3|4)
echo " 自动续期: 已配置acme.sh自动续期"
;;
esac
echo ""
fi
echo -e "${GREEN}祝您使用愉快!${NC}"
echo ""
}
################################################################################
# 卸载功能
################################################################################
print_uninstall_banner() {
clear
echo -e "${RED}"
echo "╔═══════════════════════════════════════════════════════════════╗"
echo "║ ║"
echo "║ ⚠️ 玩玩云 卸载模式 ║"
echo "║ ║"
echo "║ Uninstall Mode ║"
echo "║ ║"
echo "╚═══════════════════════════════════════════════════════════════╝"
echo -e "${NC}"
}
confirm_uninstall() {
print_uninstall_banner
echo -e "${YELLOW}"
echo "本脚本将执行以下操作:"
echo ""
echo "【将要删除】"
echo " ✓ PM2 进程: ${PROJECT_NAME}-backend"
echo " ✓ 项目目录: ${PROJECT_DIR}"
echo " ✓ Nginx 配置: /etc/nginx/sites-enabled/${PROJECT_NAME}.conf"
echo " ✓ 数据库文件: ${PROJECT_DIR}/backend/data/"
echo " ✓ 用户文件: ${PROJECT_DIR}/backend/storage/"
echo ""
echo "【将会保留】"
echo " ✓ Node.js"
echo " ✓ Nginx (程序本身)"
echo " ✓ PM2 (程序本身)"
echo " ✓ 编译工具 (build-essential等)"
echo -e "${NC}"
echo ""
print_warning "此操作不可逆,所有数据将被永久删除!"
echo ""
read -p "确定要卸载吗? (yes/no): " confirm < /dev/tty
if [[ "$confirm" != "yes" ]]; then
print_info "已取消卸载"
exit 0
fi
echo ""
read -p "请再次确认 (yes/no): " confirm2 < /dev/tty
if [[ "$confirm2" != "yes" ]]; then
print_info "已取消卸载"
exit 0
fi
echo ""
}
uninstall_backup_data() {
print_step "备份用户数据..."
if [[ ! -d "$PROJECT_DIR" ]]; then
print_info "项目目录不存在,跳过备份"
return
fi
BACKUP_DIR="/root/${PROJECT_NAME}-backup-$(date +%Y%m%d-%H%M%S)"
echo ""
read -p "是否备份用户数据? (y/n): " backup_choice < /dev/tty
if [[ "$backup_choice" == "y" || "$backup_choice" == "Y" ]]; then
mkdir -p "$BACKUP_DIR"
# 备份数据库
if [[ -d "${PROJECT_DIR}/backend/data" ]]; then
cp -r "${PROJECT_DIR}/backend/data" "$BACKUP_DIR/"
print_success "数据库已备份"
fi
# 备份用户文件
if [[ -d "${PROJECT_DIR}/backend/storage" ]]; then
cp -r "${PROJECT_DIR}/backend/storage" "$BACKUP_DIR/"
print_success "用户文件已备份"
fi
# 备份配置文件
if [[ -f "${PROJECT_DIR}/backend/.env" ]]; then
cp "${PROJECT_DIR}/backend/.env" "$BACKUP_DIR/"
print_success "配置文件已备份"
fi
print_success "备份已保存到: $BACKUP_DIR"
echo ""
else
print_warning "跳过备份,数据将被永久删除"
echo ""
fi
}
uninstall_stop_pm2() {
print_step "停止PM2进程..."
if command -v pm2 &> /dev/null; then
if pm2 list | grep -q "${PROJECT_NAME}-backend"; then
pm2 delete ${PROJECT_NAME}-backend
pm2 save --force
print_success "PM2进程已停止并删除"
else
print_info "PM2进程不存在跳过"
fi
else
print_info "PM2未安装跳过"
fi
}
uninstall_nginx_config() {
print_step "删除Nginx配置..."
local need_reload=false
# 删除sites-enabled软链接
if [[ -L /etc/nginx/sites-enabled/${PROJECT_NAME}.conf ]]; then
rm -f /etc/nginx/sites-enabled/${PROJECT_NAME}.conf
print_success "删除 sites-enabled 配置"
need_reload=true
fi
# 删除sites-available配置文件
if [[ -f /etc/nginx/sites-available/${PROJECT_NAME}.conf ]]; then
rm -f /etc/nginx/sites-available/${PROJECT_NAME}.conf
print_success "删除 sites-available 配置"
fi
# 测试并重载nginx
if [[ "$need_reload" == true ]] && command -v nginx &> /dev/null; then
if nginx -t &> /dev/null; then
systemctl reload nginx
print_success "Nginx配置已重载"
else
print_warning "Nginx配置测试失败请手动检查"
fi
fi
}
uninstall_ssl_certificates() {
print_step "清理SSL证书..."
# 删除nginx SSL证书目录下的项目证书
local cert_removed=false
if [[ -d /etc/nginx/ssl ]]; then
# 使用find查找包含项目名或域名的证书
find /etc/nginx/ssl -name "*${PROJECT_NAME}*" -type f -delete 2>/dev/null && cert_removed=true
fi
if [[ "$cert_removed" == true ]]; then
print_success "已删除Nginx SSL证书"
else
print_info "未发现Nginx SSL证书"
fi
# 提示acme.sh证书
if [[ -d ~/.acme.sh ]]; then
if ~/.acme.sh/acme.sh --list 2>/dev/null | grep -q "Main_Domain"; then
print_info "检测到acme.sh证书"
print_warning "如需删除,请手动运行: ~/.acme.sh/acme.sh --remove -d <your-domain>"
fi
fi
}
uninstall_project_directory() {
print_step "删除项目目录..."
if [[ -d "$PROJECT_DIR" ]]; then
# 统计大小
SIZE=$(du -sh "$PROJECT_DIR" 2>/dev/null | cut -f1 || echo "未知")
print_info "项目目录大小: $SIZE"
# 删除目录
rm -rf "$PROJECT_DIR"
print_success "项目目录已删除: $PROJECT_DIR"
else
print_info "项目目录不存在: $PROJECT_DIR"
fi
}
uninstall_temp_files() {
print_step "清理临时文件..."
local cleaned=false
# 清理/tmp下的临时文件
if [[ -d "/tmp/${PROJECT_NAME}" ]]; then
rm -rf "/tmp/${PROJECT_NAME}"
print_success "删除临时目录"
cleaned=true
fi
# 清理npm缓存
if command -v npm &> /dev/null; then
npm cache clean --force &> /dev/null || true
print_success "清理npm缓存"
cleaned=true
fi
if [[ "$cleaned" == false ]]; then
print_info "无需清理"
fi
}
uninstall_check_residual() {
print_step "检查残留文件..."
local residual_found=false
# 检查项目目录
if [[ -d "$PROJECT_DIR" ]]; then
print_warning "残留: 项目目录 $PROJECT_DIR"
residual_found=true
fi
# 检查Nginx配置
if [[ -f /etc/nginx/sites-enabled/${PROJECT_NAME}.conf ]] || [[ -f /etc/nginx/sites-available/${PROJECT_NAME}.conf ]]; then
print_warning "残留: Nginx配置文件"
residual_found=true
fi
# 检查PM2进程
if command -v pm2 &> /dev/null; then
if pm2 list | grep -q "${PROJECT_NAME}"; then
print_warning "残留: PM2进程"
residual_found=true
fi
fi
if [[ "$residual_found" == false ]]; then
print_success "未发现残留文件"
fi
}
print_uninstall_completion() {
clear
echo -e "${GREEN}"
echo "╔═══════════════════════════════════════════════════════════════╗"
echo "║ ║"
echo "║ ✓ 卸载完成! ║"
echo "║ ║"
echo "╚═══════════════════════════════════════════════════════════════╝"
echo -e "${NC}"
echo ""
echo -e "${CYAN}已删除内容:${NC}"
echo " ✓ PM2进程"
echo " ✓ 项目目录"
echo " ✓ Nginx配置"
echo " ✓ 数据库和用户文件"
echo ""
echo -e "${CYAN}保留的环境:${NC}"
echo " ✓ Node.js $(node -v 2>/dev/null || echo '(未安装)')"
echo " ✓ Nginx $(nginx -v 2>&1 | cut -d'/' -f2 || echo '(未安装)')"
echo " ✓ PM2 $(pm2 -v 2>/dev/null || echo '(未安装)')"
echo " ✓ 编译工具 (build-essential 等)"
echo ""
if [[ -n "$BACKUP_DIR" ]] && [[ -d "$BACKUP_DIR" ]]; then
echo -e "${YELLOW}备份位置:${NC}"
echo " $BACKUP_DIR"
echo ""
fi
echo -e "${GREEN}感谢使用玩玩云!${NC}"
echo ""
}
################################################################################
# 更新功能
################################################################################
print_update_banner() {
clear
echo -e "${BLUE}"
echo "╔═══════════════════════════════════════════════════════════════╗"
echo "║ ║"
echo "║ 🔄 玩玩云 更新模式 ║"
echo "║ ║"
echo "║ Update Mode ║"
echo "║ ║"
echo "╚═══════════════════════════════════════════════════════════════╝"
echo -e "${NC}"
}
confirm_update() {
print_update_banner
echo -e "${YELLOW}"
echo "本脚本将执行以下操作:"
echo ""
echo "【将要更新】"
echo " ✓ 从Gitee拉取最新代码"
echo " ✓ 更新后端依赖npm install"
echo " ✓ 重启后端服务"
echo ""
echo "【将会保留】"
echo " ✓ 数据库文件(用户数据)"
echo " ✓ 用户上传的文件"
echo " ✓ .env 配置文件"
echo " ✓ Nginx 配置"
echo -e "${NC}"
echo ""
print_info "建议在更新前备份重要数据"
echo ""
read -p "确定要更新吗? (y/n): " confirm < /dev/tty
if [[ "$confirm" != "y" && "$confirm" != "Y" ]]; then
print_info "已取消更新"
exit 0
fi
echo ""
}
update_check_project() {
print_step "检查项目是否已安装..."
if [[ ! -d "$PROJECT_DIR" ]]; then
print_error "项目未安装: $PROJECT_DIR"
print_info "请先运行安装命令"
exit 1
fi
if [[ ! -f "${PROJECT_DIR}/backend/server.js" ]]; then
print_error "项目目录不完整"
exit 1
fi
print_success "项目已安装: $PROJECT_DIR"
}
update_backup_important_files() {
print_step "备份重要文件..."
TEMP_BACKUP="/tmp/${PROJECT_NAME}-update-backup-$(date +%Y%m%d-%H%M%S)"
mkdir -p "$TEMP_BACKUP"
# 备份数据库
if [[ -d "${PROJECT_DIR}/backend/data" ]]; then
cp -r "${PROJECT_DIR}/backend/data" "$TEMP_BACKUP/"
print_success "数据库已备份"
fi
# 备份用户文件
if [[ -d "${PROJECT_DIR}/backend/storage" ]]; then
cp -r "${PROJECT_DIR}/backend/storage" "$TEMP_BACKUP/"
print_success "用户文件已备份"
fi
# 备份配置文件
if [[ -f "${PROJECT_DIR}/backend/.env" ]]; then
cp "${PROJECT_DIR}/backend/.env" "$TEMP_BACKUP/"
print_success "配置文件已备份"
fi
print_success "备份完成: $TEMP_BACKUP"
echo ""
}
update_stop_services() {
print_step "停止服务..."
if command -v pm2 &> /dev/null; then
if pm2 list | grep -q "${PROJECT_NAME}-backend"; then
pm2 stop ${PROJECT_NAME}-backend
print_success "后端服务已停止"
fi
fi
}
update_pull_latest_code() {
print_step "正在从Gitee拉取最新代码..."
cd /tmp
if [[ -d "${PROJECT_NAME}-update" ]]; then
rm -rf "${PROJECT_NAME}-update"
fi
# 克隆最新代码
git clone "$REPO_URL" "${PROJECT_NAME}-update"
# 更新前端文件
print_info "更新前端文件..."
if [[ -d "/tmp/${PROJECT_NAME}-update/frontend" ]]; then
rm -rf "${PROJECT_DIR}/frontend"
cp -r "/tmp/${PROJECT_NAME}-update/frontend" "${PROJECT_DIR}/"
fi
# 更新上传工具
if [[ -d "/tmp/${PROJECT_NAME}-update/upload-tool" ]]; then
rm -rf "${PROJECT_DIR}/upload-tool"
cp -r "/tmp/${PROJECT_NAME}-update/upload-tool" "${PROJECT_DIR}/"
fi
# 更新后端代码文件(但不覆盖 data、storage、.env
print_info "更新后端代码..."
if [[ -d "/tmp/${PROJECT_NAME}-update/backend" ]]; then
cd "/tmp/${PROJECT_NAME}-update/backend"
# 复制后端文件,但排除 data、storage、.env、node_modules
for item in *; do
if [[ "$item" != "data" ]] && [[ "$item" != "storage" ]] && [[ "$item" != ".env" ]] && [[ "$item" != "node_modules" ]]; then
rm -rf "${PROJECT_DIR}/backend/$item"
cp -r "$item" "${PROJECT_DIR}/backend/"
fi
done
fi
# 确保备份的重要文件存在
print_info "确保数据完整性..."
# 如果 data 目录不存在,从备份恢复
if [[ ! -d "${PROJECT_DIR}/backend/data" ]] && [[ -d "$TEMP_BACKUP/data" ]]; then
print_warning "检测到 data 目录丢失,正在从备份恢复..."
cp -r "$TEMP_BACKUP/data" "${PROJECT_DIR}/backend/"
print_success "数据库已恢复"
fi
# 如果 storage 目录不存在,从备份恢复
if [[ ! -d "${PROJECT_DIR}/backend/storage" ]] && [[ -d "$TEMP_BACKUP/storage" ]]; then
print_warning "检测到 storage 目录丢失,正在从备份恢复..."
cp -r "$TEMP_BACKUP/storage" "${PROJECT_DIR}/backend/"
print_success "用户文件已恢复"
fi
# 如果 .env 文件不存在,从备份恢复
if [[ ! -f "${PROJECT_DIR}/backend/.env" ]] && [[ -f "$TEMP_BACKUP/.env" ]]; then
print_warning "检测到 .env 文件丢失,正在从备份恢复..."
cp "$TEMP_BACKUP/.env" "${PROJECT_DIR}/backend/"
print_success "配置文件已恢复"
fi
# 清理临时文件
rm -rf "/tmp/${PROJECT_NAME}-update"
rm -rf "$TEMP_BACKUP"
print_success "代码更新完成"
echo ""
}
update_install_dependencies() {
print_step "更新后端依赖..."
cd "${PROJECT_DIR}/backend"
# 确保Python可用node-gyp需要
if ! command -v python &> /dev/null; then
if command -v python3 &> /dev/null; then
if [[ "$OS" == "ubuntu" ]] || [[ "$OS" == "debian" ]]; then
ln -sf /usr/bin/python3 /usr/bin/python || true
else
alternatives --install /usr/bin/python python /usr/bin/python3 1 &> /dev/null || \
ln -sf /usr/bin/python3 /usr/bin/python || true
fi
print_info "已配置Python环境"
fi
fi
# 使用国内镜像加速(如果之前选择了)
if command -v npm &> /dev/null; then
current_registry=$(npm config get registry)
if [[ "$current_registry" =~ "npmmirror" ]] || [[ "$current_registry" =~ "taobao" ]]; then
print_info "检测到使用国内镜像源"
fi
fi
# 清理旧的node_modules
# 检查并升级C++编译器(如果需要)
check_cpp_compiler
if [[ -d "node_modules" ]]; then
print_info "清理旧依赖..."
rm -rf node_modules package-lock.json
fi
print_info "正在重新安装依赖(可能需要几分钟)..."
if PYTHON=python3 npm install --production; then
print_success "依赖更新完成"
else
print_error "依赖更新失败"
print_warning "请检查错误日志: ~/.npm/_logs/"
fi
echo ""
}
update_migrate_database() {
print_step "迁移数据库配置..."
cd "${PROJECT_DIR}/backend"
# 检查是否需要升级上传限制从100MB升级到10GB
if command -v sqlite3 &> /dev/null; then
if [[ -f "ftp-manager.db" ]]; then
CURRENT_LIMIT=$(sqlite3 ftp-manager.db "SELECT value FROM system_settings WHERE key = 'max_upload_size';" 2>/dev/null || echo "")
if [[ "$CURRENT_LIMIT" == "104857600" ]]; then
print_info "检测到旧的上传限制(100MB)正在升级到10GB..."
sqlite3 ftp-manager.db "UPDATE system_settings SET value = '10737418240' WHERE key = 'max_upload_size';"
print_success "上传限制已升级: 100MB → 10GB"
elif [[ "$CURRENT_LIMIT" == "10737418240" ]]; then
print_success "上传限制已是最新: 10GB"
elif [[ -n "$CURRENT_LIMIT" ]]; then
print_info "当前上传限制: ${CURRENT_LIMIT} 字节"
else
print_info "数据库配置正常"
fi
fi
else
print_warning "sqlite3未安装跳过数据库迁移检查"
fi
echo ""
}
update_restart_services() {
print_step "重启服务..."
cd "${PROJECT_DIR}/backend"
if command -v pm2 &> /dev/null; then
pm2 restart ${PROJECT_NAME}-backend
pm2 save
print_success "后端服务已重启"
fi
# 重载Nginx
if systemctl is-active --quiet nginx; then
systemctl reload nginx
print_success "Nginx已重载"
fi
echo ""
}
update_check_version() {
print_step "检查更新后的版本..."
# 检查package.json版本
if [[ -f "${PROJECT_DIR}/backend/package.json" ]]; then
VERSION=$(grep '"version"' "${PROJECT_DIR}/backend/package.json" | head -1 | awk -F'"' '{print $4}')
if [[ -n "$VERSION" ]]; then
print_success "当前版本: v$VERSION"
fi
fi
echo ""
}
print_update_completion() {
clear
echo -e "${GREEN}"
echo "╔═══════════════════════════════════════════════════════════════╗"
echo "║ ║"
echo "║ 🎉 更新成功! ║"
echo "║ ║"
echo "╚═══════════════════════════════════════════════════════════════╝"
echo -e "${NC}"
echo ""
echo -e "${CYAN}更新内容:${NC}"
echo " ✓ 代码已更新到最新版本"
echo " ✓ 依赖已更新"
echo " ✓ 服务已重启"
echo ""
echo -e "${CYAN}保留的数据:${NC}"
echo " ✓ 数据库(用户、分享链接等)"
echo " ✓ 用户文件storage目录"
echo " ✓ 配置文件(.env"
echo ""
echo -e "${YELLOW}常用命令:${NC}"
echo " 查看服务状态: pm2 status"
echo " 查看日志: pm2 logs ${PROJECT_NAME}-backend"
echo " 重启服务: pm2 restart ${PROJECT_NAME}-backend"
echo ""
echo -e "${GREEN}更新完成,祝您使用愉快!${NC}"
echo ""
}
update_main() {
# 检查root权限
check_root
# 确认更新
confirm_update
# 检查项目
update_check_project
# 备份重要文件
update_backup_important_files
# 停止服务
update_stop_services
# 拉取最新代码
update_pull_latest_code
# 更新依赖
update_install_dependencies
# 迁移数据库配置
update_migrate_database
# 重启服务
update_restart_services
# 健康检查
if ! health_check; then
print_error "健康检查未通过,请检查日志"
print_info "查看日志: pm2 logs ${PROJECT_NAME}-backend"
exit 1
fi
# 检查版本
update_check_version
# 完成提示
print_update_completion
}
################################################################################
# 主流程
################################################################################
main() {
print_banner
# 检查root权限
check_root
# 如果没有通过命令行参数指定模式,则显示交互式选择
if [[ "$MODE" == "install" ]] && [[ "$1" != "--skip-mode-select" ]]; then
# 检测是否可以使用交互式输入
if [[ -t 0 ]] || [[ -c /dev/tty ]]; then
print_step "请选择操作模式"
echo ""
echo -e "${GREEN}[1]${NC} 安装/部署 玩玩云"
echo -e "${BLUE}[2]${NC} 更新/升级 玩玩云"
echo -e "${YELLOW}[3]${NC} 修复/重新配置 玩玩云"
echo -e "${RED}[4]${NC} 卸载 玩玩云"
echo -e "${GRAY}[0]${NC} 退出脚本"
echo ""
while true; do
read -p "请输入选项 [0-4]: " mode_choice < /dev/tty
case $mode_choice in
1)
print_success "已选择: 安装模式"
echo ""
break
;;
2)
print_info "切换到更新模式..."
echo ""
update_main
exit 0
;;
3)
print_info "切换到修复模式..."
echo ""
repair_main
exit 0
;;
4)
print_info "切换到卸载模式..."
echo ""
uninstall_main
exit 0
;;
0)
print_info "正在退出脚本..."
echo ""
exit 0
;;
*)
print_error "无效选项,请重新选择"
;;
esac
done
else
# 管道执行时的提示
print_info "检测到通过管道执行脚本"
print_info "默认进入安装模式"
print_warning "如需其他操作,请下载脚本后运行"
echo ""
echo -e "${YELLOW}提示:${NC}"
echo " 安装: wget https://gitee.com/yu-yon/vue-driven-cloud-storage/raw/master/install.sh && bash install.sh"
echo " 更新: wget https://gitee.com/yu-yon/vue-driven-cloud-storage/raw/master/install.sh && bash install.sh --update"
echo " 卸载: wget https://gitee.com/yu-yon/vue-driven-cloud-storage/raw/master/install.sh && bash install.sh --uninstall"
echo " 修复: wget https://gitee.com/yu-yon/vue-driven-cloud-storage/raw/master/install.sh && bash install.sh --repair"
echo ""
sleep 2
fi
fi
# 系统检测
system_check
# 选择软件源
choose_mirror
# 安装依赖
install_dependencies
# 选择访问模式
choose_access_mode
# 端口配置
configure_ports
# 配置管理员账号
configure_admin_account
# 创建项目目录
create_project_directory
# 下载项目
download_project
# 安装后端依赖
install_backend_dependencies
# 创建配置文件
create_env_file
# 创建数据目录
create_data_directories
# 打包上传工具
build_upload_tool
# 先配置基础HTTP NginxSSL证书申请需要
configure_nginx_http_first
# 部署SSL证书需要HTTP server block进行验证
deploy_ssl
# 根据SSL结果配置最终Nginx
configure_nginx_final
# 启动后端服务
start_backend_service
# 健康检查
if ! health_check; then
print_error "健康检查未通过,请检查日志"
exit 1
fi
# 完成提示
print_completion
}
uninstall_main() {
# 检查root权限
check_root
# 确认卸载
confirm_uninstall
# 备份数据
uninstall_backup_data
# 停止PM2进程
uninstall_stop_pm2
# 删除Nginx配置
uninstall_nginx_config
# 清理SSL证书
uninstall_ssl_certificates
# 删除项目目录
uninstall_project_directory
# 清理临时文件
uninstall_temp_files
# 检查残留
uninstall_check_residual
# 完成提示
print_uninstall_completion
}
################################################################################
# 修复功能
################################################################################
print_repair_banner() {
clear
echo -e "${BLUE}"
echo "╔═══════════════════════════════════════════════════════════════╗"
echo "║ ║"
echo "║ 🔧 玩玩云 修复模式 ║"
echo "║ ║"
echo "║ Repair Mode ║"
echo "║ ║"
echo "╚═══════════════════════════════════════════════════════════════╝"
echo -e "${NC}"
}
confirm_repair() {
print_repair_banner
echo -e "${YELLOW}"
echo "本脚本将执行以下操作:"
echo ""
echo "【将会重新配置】"
echo " ✓ 重新生成Nginx配置应用最新配置"
echo " ✓ 重启后端服务"
echo " ✓ 重载Nginx服务"
echo ""
echo "【将会保留】"
echo " ✓ 数据库文件(用户数据)"
echo " ✓ 用户上传的文件"
echo " ✓ .env 配置文件"
echo " ✓ SSL证书"
echo -e "${NC}"
echo ""
print_info "适用场景: 更新配置、修复nginx配置错误、重新应用设置"
echo ""
read -p "确定要执行修复吗? (y/n): " confirm < /dev/tty
if [[ "$confirm" != "y" && "$confirm" != "Y" ]]; then
print_info "已取消修复"
exit 0
fi
echo ""
}
repair_check_project() {
print_step "检查项目是否已安装..."
if [[ ! -d "$PROJECT_DIR" ]]; then
print_error "项目未安装: $PROJECT_DIR"
print_info "请先运行安装命令: bash install.sh"
exit 1
fi
if [[ ! -f "${PROJECT_DIR}/backend/server.js" ]]; then
print_error "项目目录不完整"
exit 1
fi
print_success "项目已安装: $PROJECT_DIR"
echo ""
}
repair_load_existing_config() {
print_step "读取现有配置..."
# 从.env读取端口配置
if [[ -f "${PROJECT_DIR}/backend/.env" ]]; then
BACKEND_PORT=$(grep "^PORT=" "${PROJECT_DIR}/backend/.env" | cut -d'=' -f2 || echo "40001")
print_success "后端端口: $BACKEND_PORT"
else
print_warning ".env文件不存在使用默认端口40001"
BACKEND_PORT="40001"
fi
# 检查现有nginx配置
if [[ -f "/etc/nginx/sites-enabled/${PROJECT_NAME}.conf" ]]; then
# 尝试从现有配置读取端口
EXISTING_HTTP_PORT=$(grep "listen" "/etc/nginx/sites-enabled/${PROJECT_NAME}.conf" | grep -v "ssl" | head -1 | awk '{print $2}' | tr -d ';' || echo "80")
HTTP_PORT=${EXISTING_HTTP_PORT:-80}
# 检查是否有HTTPS配置
if grep -q "listen.*ssl" "/etc/nginx/sites-enabled/${PROJECT_NAME}.conf"; then
EXISTING_HTTPS_PORT=$(grep "listen.*ssl" "/etc/nginx/sites-enabled/${PROJECT_NAME}.conf" | head -1 | awk '{print $2}' | tr -d ';' || echo "443")
HTTPS_PORT=${EXISTING_HTTPS_PORT:-443}
SSL_METHOD="existing"
print_success "检测到HTTPS配置端口: $HTTPS_PORT"
else
SSL_METHOD="8"
print_info "未检测到HTTPS配置"
fi
# 检查是否有域名
SERVER_NAME=$(grep "server_name" "/etc/nginx/sites-enabled/${PROJECT_NAME}.conf" | head -1 | awk '{print $2}' | tr -d ';' || echo "_")
if [[ "$SERVER_NAME" != "_" ]] && [[ "$SERVER_NAME" != "localhost" ]]; then
DOMAIN="$SERVER_NAME"
USE_DOMAIN=true
print_success "检测到域名: $DOMAIN"
else
USE_DOMAIN=false
print_info "使用IP模式"
fi
print_success "HTTP端口: $HTTP_PORT"
else
print_warning "未找到现有nginx配置将使用默认配置"
HTTP_PORT="80"
HTTPS_PORT="443"
SSL_METHOD="8"
USE_DOMAIN=false
fi
echo ""
}
repair_regenerate_nginx_config() {
print_step "重新生成Nginx配置..."
# 清理旧的备份文件避免nginx读取到错误配置
rm -f /etc/nginx/sites-enabled/${PROJECT_NAME}.conf.backup.* 2>/dev/null || true
rm -f /etc/nginx/sites-available/${PROJECT_NAME}.conf.backup.* 2>/dev/null || true
# 备份当前配置到 /root/
if [[ -f "/etc/nginx/sites-enabled/${PROJECT_NAME}.conf" ]]; then
cp "/etc/nginx/sites-enabled/${PROJECT_NAME}.conf" "/root/nginx-backup-${PROJECT_NAME}.conf.$(date +%Y%m%d%H%M%S)"
print_success "已备份现有配置到 /root/"
fi
# 调用现有的configure_nginx函数
configure_nginx
print_success "Nginx配置已重新生成"
echo ""
}
repair_restart_services() {
print_step "重启服务..."
# 重启后端
if command -v pm2 &> /dev/null; then
if pm2 list | grep -q "${PROJECT_NAME}-backend"; then
pm2 restart ${PROJECT_NAME}-backend
print_success "后端服务已重启"
else
print_warning "后端服务未运行,尝试启动..."
cd "${PROJECT_DIR}/backend"
pm2 start server.js --name ${PROJECT_NAME}-backend
pm2 save
print_success "后端服务已启动"
fi
fi
# 重载Nginx
if systemctl is-active --quiet nginx; then
systemctl reload nginx
print_success "Nginx已重载"
else
print_warning "Nginx未运行尝试启动..."
systemctl start nginx
print_success "Nginx已启动"
fi
echo ""
}
repair_verify_services() {
print_step "验证服务状态..."
# 等待服务启动
sleep 3
# 检查后端
if pm2 status | grep -q "${PROJECT_NAME}-backend.*online"; then
print_success "后端服务运行正常"
else
print_error "后端服务状态异常"
print_info "查看日志: pm2 logs ${PROJECT_NAME}-backend"
fi
# 检查Nginx
if systemctl is-active --quiet nginx; then
print_success "Nginx服务运行正常"
else
print_error "Nginx服务异常"
fi
# 检查端口
if netstat -tunlp 2>/dev/null | grep -q ":${BACKEND_PORT}" || ss -tunlp 2>/dev/null | grep -q ":${BACKEND_PORT}"; then
print_success "后端端口监听正常 (${BACKEND_PORT})"
else
print_warning "后端端口监听异常"
fi
echo ""
}
print_repair_completion() {
clear
echo -e "${GREEN}"
echo "╔═══════════════════════════════════════════════════════════════╗"
echo "║ ║"
echo "║ ✓ 修复完成! ║"
echo "║ ║"
echo "╚═══════════════════════════════════════════════════════════════╝"
echo -e "${NC}"
echo ""
echo -e "${CYAN}修复内容:${NC}"
echo " ✓ Nginx配置已更新到最新版本"
echo " ✓ 服务已重启"
echo ""
echo -e "${CYAN}保留的数据:${NC}"
echo " ✓ 数据库(用户、分享链接等)"
echo " ✓ 用户文件storage目录"
echo " ✓ 配置文件(.env"
echo ""
# 显示访问地址
if [[ "$USE_DOMAIN" == "true" ]]; then
if [[ "$SSL_METHOD" == "8" ]] || [[ "$SSL_METHOD" == "" ]]; then
if [[ "$HTTP_PORT" == "80" ]]; then
echo -e "${CYAN}访问地址:${NC} http://${DOMAIN}"
else
echo -e "${CYAN}访问地址:${NC} http://${DOMAIN}:${HTTP_PORT}"
fi
else
if [[ "$HTTPS_PORT" == "443" ]]; then
echo -e "${CYAN}访问地址:${NC} https://${DOMAIN}"
else
echo -e "${CYAN}访问地址:${NC} https://${DOMAIN}:${HTTPS_PORT}"
fi
fi
else
PUBLIC_IP=$(curl -s ifconfig.me || curl -s icanhazip.com || echo "服务器IP")
if [[ "$HTTP_PORT" == "80" ]]; then
echo -e "${CYAN}访问地址:${NC} http://${PUBLIC_IP}"
else
echo -e "${CYAN}访问地址:${NC} http://${PUBLIC_IP}:${HTTP_PORT}"
fi
fi
echo ""
echo -e "${YELLOW}常用命令:${NC}"
echo " 查看服务状态: pm2 status"
echo " 查看日志: pm2 logs ${PROJECT_NAME}-backend"
echo " 重启服务: pm2 restart ${PROJECT_NAME}-backend"
echo ""
echo -e "${GREEN}修复完成,请测试功能是否正常!${NC}"
echo ""
}
repair_main() {
# 检查root权限
check_root
# 检测操作系统
detect_os
# 确认修复
confirm_repair
# 检查项目
repair_check_project
# 读取现有配置
repair_load_existing_config
# 重新生成nginx配置
repair_regenerate_nginx_config
# 重启服务
repair_restart_services
# 验证服务
repair_verify_services
# 健康检查
if ! health_check; then
print_warning "部分健康检查未通过,请查看日志"
print_info "查看日志: pm2 logs ${PROJECT_NAME}-backend"
fi
# 完成提示
print_repair_completion
}
# 执行主流程
if [[ "$MODE" == "uninstall" ]]; then
uninstall_main
elif [[ "$MODE" == "update" ]]; then
update_main
elif [[ "$MODE" == "repair" ]]; then
repair_main
else
main
fi