#!/bin/bash # ======================================== # jsite网站管理脚本 # 功能:新建、删除、构建、启动、停止、重启jsite网站 # ======================================== # 颜色定义 RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' NC='\033[0m' # No Color # ======================================== # 默认配置 # ======================================== # 基础配置 DEFAULT_GIT_REPO="http://git.jingrow.com:3000/jsite/jingrow" JSITE_BASE_DIR="/home/jingrow/jsite" USER_NAME="jingrow" TRAEFIK_CONFIG_DIR="/home/jingrow/traefik-docker/conf.d/website" # 默认配置参数 DEFAULT_SITE_URL="example.com" START_PORT=3001 PORT_INCREMENT=1 # 网络配置 PUBLIC_IP="" # 公网IP地址 (用于内网IP不可用时) # ======================================== # 日志函数 # ======================================== log_info() { echo -e "${BLUE}[INFO]${NC} $1" } log_success() { echo -e "${GREEN}[SUCCESS]${NC} $1" } log_warning() { echo -e "${YELLOW}[WARNING]${NC} $1" >&2 } log_error() { echo -e "${RED}[ERROR]${NC} $1" >&2 } # ======================================== # 工具函数 # ======================================== # 检查用户是否存在 check_user() { if ! id "$USER_NAME" &>/dev/null; then log_error "用户 $USER_NAME 不存在,请先创建用户" exit 1 fi } # 检查jsite基础目录 check_base_dir() { if [ ! -d "$JSITE_BASE_DIR" ]; then log_info "创建jsite基础目录: $JSITE_BASE_DIR" mkdir -p "$JSITE_BASE_DIR" chown "$USER_NAME:$USER_NAME" "$JSITE_BASE_DIR" fi } # 检查网站目录是否存在 check_site_exists() { local site_name="$1" [ -d "$JSITE_BASE_DIR/$site_name" ] } # 获取网站状态 get_site_status() { local site_name="$1" if ! check_site_exists "$site_name"; then echo "not_exists" return fi # 检查PM2进程状态 local pm2_status=$(su - "$USER_NAME" -c " export NVM_DIR=\"\$HOME/.nvm\" [ -s \"\$NVM_DIR/nvm.sh\" ] && \. \"\$NVM_DIR/nvm.sh\" pm2 list 2>/dev/null | grep -w '$site_name' | grep -c 'online' || echo '0' ") if [ "$pm2_status" != "0" ]; then echo "running" else echo "stopped" fi } # 智能IP选择:优先使用内网IP,没有内网IP时使用公网IP get_optimal_host_ip() { local host_ip="" # 方法1: 尝试获取内网IP local private_ip=$(ip route get 8.8.8.8 2>/dev/null | awk '{print $7}' | head -1) if [ -n "$private_ip" ] && [ "$private_ip" != "8.8.8.8" ]; then # 测试内网IP是否可达 if ping -c 2 -W 1 "$private_ip" >/dev/null 2>&1; then host_ip="$private_ip" # 注意:这里不输出日志,避免污染返回值 else # 注意:这里不输出日志,避免污染返回值 : fi fi # 方法2: 如果没有内网IP或内网IP不可达,使用公网IP if [ -z "$host_ip" ]; then if [ -n "$PUBLIC_IP" ]; then host_ip="$PUBLIC_IP" # 注意:这里不输出日志,避免污染返回值 else # 尝试自动获取公网IP local auto_public_ip=$(curl -s --max-time 5 ifconfig.me 2>/dev/null || curl -s --max-time 5 ipinfo.io/ip 2>/dev/null) if [ -n "$auto_public_ip" ]; then host_ip="$auto_public_ip" # 注意:这里不输出日志,避免污染返回值 else # 最后回退:使用第一个可用IP host_ip=$(hostname -I | awk '{print $1}') # 注意:这里不输出日志,避免污染返回值 fi fi fi echo "$host_ip" } # 显示帮助信息 show_help() { echo "用法: $0 <命令> <网站名称> [选项]" echo "" echo "命令:" echo " create [git-repo] [domain] 新建网站" echo " delete 删除网站" echo " build 构建网站" echo " start 启动网站" echo " stop 停止网站" echo " restart 重启网站" echo " status 查看网站状态" echo " list 列出所有网站" echo " logs 查看网站日志" echo "" echo "选项:" echo " -h, --help 显示此帮助信息" echo "" echo "示例:" echo " $0 create mysite" echo " $0 create mysite http://git.example.com/myproject" echo " $0 create mysite http://git.example.com/myproject mysite.com" echo " $0 start mysite" echo " $0 stop mysite" echo " $0 restart mysite" echo " $0 status mysite" echo " $0 list" } # 获取可用端口 get_available_port() { local site_name="$1" local port_file="$JSITE_BASE_DIR/site_ports.json" # 如果端口文件不存在,返回起始端口 if [ ! -f "$port_file" ]; then echo "$START_PORT" return 0 fi # 检查网站是否已有分配的端口 if command -v jq &> /dev/null; then local existing_port=$(jq -r ".$site_name // empty" "$port_file" 2>/dev/null) if [ -n "$existing_port" ] && [ "$existing_port" != "null" ] && [ "$existing_port" != "empty" ]; then echo "$existing_port" return 0 fi # 找到最大端口号并加1 local max_port=$(jq -r 'max(.[])' "$port_file" 2>/dev/null || echo "$START_PORT") if [ -n "$max_port" ] && [ "$max_port" != "null" ]; then echo $((max_port + PORT_INCREMENT)) else echo "$START_PORT" fi else # 如果没有jq,使用简单逻辑 echo "$START_PORT" fi } # 保存端口分配 save_port_assignment() { local site_name="$1" local port="$2" local port_file="$JSITE_BASE_DIR/site_ports.json" if command -v jq &> /dev/null; then if [ -f "$port_file" ]; then jq ". + {\"$site_name\": $port}" "$port_file" > "${port_file}.tmp" && mv "${port_file}.tmp" "$port_file" else echo "{\"$site_name\": $port}" > "$port_file" fi else # 简单的JSON格式 if [ -f "$port_file" ]; then grep -v "\"$site_name\"" "$port_file" > "${port_file}.tmp" 2>/dev/null || true sed -i "s/}$/,\"$site_name\": $port}/" "${port_file}.tmp" 2>/dev/null || echo "{\"$site_name\": $port}" > "${port_file}.tmp" mv "${port_file}.tmp" "$port_file" else echo "{\"$site_name\": $port}" > "$port_file" fi fi chown "$USER_NAME:$USER_NAME" "$port_file" 2>/dev/null || true } # 生成Host规则 generate_host_rule() { local domain="$1" # 移除协议前缀 domain=$(echo "$domain" | sed -E 's|^https?://||') # 移除端口号 domain=$(echo "$domain" | sed -E 's|:[0-9]+$||') # 检查是否为一级域名 if [[ "$domain" =~ ^[^.]+\.[^.]+$ ]]; then echo "Host(\`$domain\`) || Host(\`www.$domain\`)" else echo "Host(\`$domain\`)" fi } # 创建traefik网站配置文件 create_traefik_config() { local site_name="$1" local domain="$2" local port="$3" # 检查traefik配置目录是否存在 if [ ! -d "$TRAEFIK_CONFIG_DIR" ]; then log_warning "Traefik配置目录不存在: $TRAEFIK_CONFIG_DIR" log_info "请先确保Traefik已正确安装" return 1 fi local config_file="$TRAEFIK_CONFIG_DIR/$site_name.yml" local host_rule=$(generate_host_rule "$domain") # 获取本机IP地址 local host_ip=$(get_optimal_host_ip) # 创建配置文件 cat > "$config_file" << EOF http: routers: main-https: rule: &host_rule $host_rule entryPoints: - websecure service: main-service tls: certResolver: myresolver main-http-redirect: rule: *host_rule entryPoints: - web middlewares: - redirect-to-https service: noop services: main-service: loadBalancer: servers: - url: "http://$host_ip:$port" noop: loadBalancer: servers: - url: "http://127.0.0.1:65535" # 占位用,无实际后端,仅用于HTTP跳转 middlewares: redirect-to-https: redirectScheme: scheme: https permanent: true EOF chown "$USER_NAME:$USER_NAME" "$config_file" 2>/dev/null || true chmod 644 "$config_file" log_success "Traefik配置文件已创建: $config_file" log_info "域名: $domain -> $host_ip:$port" } # 删除traefik网站配置文件 remove_traefik_config() { local site_name="$1" local config_file="$TRAEFIK_CONFIG_DIR/$site_name.yml" if [ -f "$config_file" ]; then rm -f "$config_file" log_success "Traefik配置文件已删除: $config_file" fi } # 重启traefik服务以加载新配置 restart_traefik() { local traefik_dir="/home/jingrow/traefik-docker" if [ -d "$traefik_dir" ] && [ -f "$traefik_dir/docker-compose.yml" ]; then log_info "重启Traefik服务以加载新配置..." su - "$USER_NAME" -c "cd '$traefik_dir' && docker compose restart" 2>/dev/null || { log_warning "无法重启Traefik服务,请手动重启" } fi } # ======================================== # 核心功能函数 # ======================================== # 新建网站 create_site() { local site_name="$1" local git_repo="${2:-$DEFAULT_GIT_REPO}" local domain="${3:-$DEFAULT_SITE_URL}" if [ -z "$site_name" ]; then log_error "请指定网站名称" return 1 fi log_info "开始创建网站: $site_name" # 检查网站是否已存在 if check_site_exists "$site_name"; then log_error "网站 $site_name 已存在" return 1 fi # 获取并分配端口 local port=$(get_available_port "$site_name") # 克隆项目 log_info "克隆项目: $git_repo" if ! su - "$USER_NAME" -c "cd '$JSITE_BASE_DIR' && git clone '$git_repo' '$site_name'"; then log_error "项目克隆失败" return 1 fi # 检查package.json是否存在 if [ ! -f "$JSITE_BASE_DIR/$site_name/package.json" ]; then log_error "package.json文件不存在,这可能不是一个有效的Node.js项目" rm -rf "$JSITE_BASE_DIR/$site_name" return 1 fi # 保存端口分配 save_port_assignment "$site_name" "$port" # 创建traefik配置文件 if create_traefik_config "$site_name" "$domain" "$port"; then restart_traefik fi log_success "网站 $site_name 创建成功" log_info "项目路径: $JSITE_BASE_DIR/$site_name" log_info "分配端口: $port" log_info "绑定域名: $domain" # 提示下一步操作 log_info "下一步操作:" echo " 1. 构建项目: $0 build $site_name" echo " 2. 启动项目: $0 start $site_name" } # 删除网站 delete_site() { local site_name="$1" if [ -z "$site_name" ]; then log_error "请指定网站名称" return 1 fi if ! check_site_exists "$site_name"; then log_error "网站 $site_name 不存在" return 1 fi log_warning "即将删除网站: $site_name" read -p "确认删除?(y/N): " confirm if [ "$confirm" != "y" ] && [ "$confirm" != "Y" ]; then log_info "取消删除操作" return 0 fi # 停止PM2进程 local status=$(get_site_status "$site_name") if [ "$status" = "running" ]; then log_info "停止运行中的进程..." stop_site "$site_name" fi # 删除PM2配置 su - "$USER_NAME" -c " export NVM_DIR=\"\$HOME/.nvm\" [ -s \"\$NVM_DIR/nvm.sh\" ] && \. \"\$NVM_DIR/nvm.sh\" pm2 delete '$site_name' 2>/dev/null || true " # 删除traefik配置文件 remove_traefik_config "$site_name" # 删除端口分配记录 local port_file="$JSITE_BASE_DIR/site_ports.json" if [ -f "$port_file" ] && command -v jq &> /dev/null; then jq "del(.$site_name)" "$port_file" > "${port_file}.tmp" && mv "${port_file}.tmp" "$port_file" chown "$USER_NAME:$USER_NAME" "$port_file" 2>/dev/null || true fi # 删除项目目录 log_info "删除项目目录: $JSITE_BASE_DIR/$site_name" rm -rf "$JSITE_BASE_DIR/$site_name" # 重启traefik以移除配置 restart_traefik log_success "网站 $site_name 已删除" } # 构建网站 build_site() { local site_name="$1" if [ -z "$site_name" ]; then log_error "请指定网站名称" return 1 fi if ! check_site_exists "$site_name"; then log_error "网站 $site_name 不存在" return 1 fi log_info "构建网站: $site_name" # 安装依赖 log_info "安装依赖..." if ! su - "$USER_NAME" -c " export NVM_DIR=\"\$HOME/.nvm\" [ -s \"\$NVM_DIR/nvm.sh\" ] && \. \"\$NVM_DIR/nvm.sh\" cd '$JSITE_BASE_DIR/$site_name' npm install "; then log_error "依赖安装失败" return 1 fi # 构建项目 log_info "构建项目..." if ! su - "$USER_NAME" -c " export NVM_DIR=\"\$HOME/.nvm\" [ -s \"\$NVM_DIR/nvm.sh\" ] && \. \"\$NVM_DIR/nvm.sh\" cd '$JSITE_BASE_DIR/$site_name' npm run build "; then log_error "项目构建失败" return 1 fi log_success "网站 $site_name 构建完成" } # 启动网站 start_site() { local site_name="$1" if [ -z "$site_name" ]; then log_error "请指定网站名称" return 1 fi if ! check_site_exists "$site_name"; then log_error "网站 $site_name 不存在" return 1 fi local status=$(get_site_status "$site_name") if [ "$status" = "running" ]; then log_warning "网站 $site_name 已在运行中" return 0 fi # 获取分配的端口 local port=$(get_available_port "$site_name") log_info "启动网站: $site_name (端口: $port)" # 检查是否有构建产物 if [ ! -d "$JSITE_BASE_DIR/$site_name/.next" ] && [ ! -d "$JSITE_BASE_DIR/$site_name/dist" ] && [ ! -d "$JSITE_BASE_DIR/$site_name/build" ]; then log_warning "未找到构建产物,建议先执行构建: $0 build $site_name" fi # 使用PM2启动,设置端口环境变量 if ! su - "$USER_NAME" -c " export NVM_DIR=\"\$HOME/.nvm\" [ -s \"\$NVM_DIR/nvm.sh\" ] && \. \"\$NVM_DIR/nvm.sh\" cd '$JSITE_BASE_DIR/$site_name' PORT=$port pm2 start npm --name '$site_name' -- start "; then log_error "网站启动失败" return 1 fi # 等待启动完成 sleep 3 local new_status=$(get_site_status "$site_name") if [ "$new_status" = "running" ]; then log_success "网站 $site_name 启动成功 (端口: $port)" else log_error "网站 $site_name 启动失败" return 1 fi } # 停止网站 stop_site() { local site_name="$1" if [ -z "$site_name" ]; then log_error "请指定网站名称" return 1 fi local status=$(get_site_status "$site_name") if [ "$status" = "not_exists" ]; then log_error "网站 $site_name 不存在" return 1 fi if [ "$status" = "stopped" ]; then log_warning "网站 $site_name 已停止" return 0 fi log_info "停止网站: $site_name" if ! su - "$USER_NAME" -c " export NVM_DIR=\"\$HOME/.nvm\" [ -s \"\$NVM_DIR/nvm.sh\" ] && \. \"\$NVM_DIR/nvm.sh\" pm2 stop '$site_name' "; then log_error "网站停止失败" return 1 fi log_success "网站 $site_name 已停止" } # 重启网站 restart_site() { local site_name="$1" if [ -z "$site_name" ]; then log_error "请指定网站名称" return 1 fi local status=$(get_site_status "$site_name") if [ "$status" = "not_exists" ]; then log_error "网站 $site_name 不存在" return 1 fi log_info "重启网站: $site_name" if [ "$status" = "running" ]; then if ! su - "$USER_NAME" -c " export NVM_DIR=\"\$HOME/.nvm\" [ -s \"\$NVM_DIR/nvm.sh\" ] && \. \"\$NVM_DIR/nvm.sh\" pm2 restart '$site_name' "; then log_error "网站重启失败" return 1 fi else # 如果未运行,则启动 start_site "$site_name" return $? fi log_success "网站 $site_name 重启成功" } # 查看网站状态 show_site_status() { local site_name="$1" if [ -z "$site_name" ]; then log_error "请指定网站名称" return 1 fi local status=$(get_site_status "$site_name") case "$status" in "not_exists") log_error "网站 $site_name 不存在" return 1 ;; "running") log_success "网站 $site_name 正在运行" ;; "stopped") log_warning "网站 $site_name 已停止" ;; esac # 显示PM2详细状态 if [ "$status" != "not_exists" ]; then log_info "PM2状态:" su - "$USER_NAME" -c " export NVM_DIR=\"\$HOME/.nvm\" [ -s \"\$NVM_DIR/nvm.sh\" ] && \. \"\$NVM_DIR/nvm.sh\" pm2 list | grep -E '(App name|$site_name)' || echo '未找到PM2进程' " fi } # 列出所有网站 list_sites() { log_info "jsite网站列表:" if [ ! -d "$JSITE_BASE_DIR" ] || [ -z "$(ls -A "$JSITE_BASE_DIR" 2>/dev/null)" ]; then echo " 暂无网站" return 0 fi for site_dir in "$JSITE_BASE_DIR"/*; do if [ -d "$site_dir" ]; then local site_name=$(basename "$site_dir") local status=$(get_site_status "$site_name") case "$status" in "running") echo " - $site_name (运行中)" ;; "stopped") echo " - $site_name (已停止)" ;; *) echo " - $site_name (未知状态)" ;; esac fi done } # 查看网站日志 show_site_logs() { local site_name="$1" if [ -z "$site_name" ]; then log_error "请指定网站名称" return 1 fi local status=$(get_site_status "$site_name") if [ "$status" = "not_exists" ]; then log_error "网站 $site_name 不存在" return 1 fi log_info "网站 $site_name 的日志:" su - "$USER_NAME" -c " export NVM_DIR=\"\$HOME/.nvm\" [ -s \"\$NVM_DIR/nvm.sh\" ] && \. \"\$NVM_DIR/nvm.sh\" pm2 logs '$site_name' " } # ======================================== # 主程序 # ======================================== main() { # 检查参数 if [ $# -eq 0 ]; then show_help exit 1 fi # 处理帮助选项 case "$1" in -h|--help) show_help exit 0 ;; esac # 检查用户和基础目录 check_user check_base_dir # 解析命令 local command="$1" local site_name="$2" local extra_param="$3" local fourth_param="$4" case "$command" in create) # create命令支持第三个参数作为域名 if [ -n "$fourth_param" ]; then create_site "$site_name" "$extra_param" "$fourth_param" else create_site "$site_name" "$extra_param" fi ;; delete) delete_site "$site_name" ;; build) build_site "$site_name" ;; start) start_site "$site_name" ;; stop) stop_site "$site_name" ;; restart) restart_site "$site_name" ;; status) show_site_status "$site_name" ;; list) list_sites ;; logs) show_site_logs "$site_name" ;; *) log_error "未知命令: $command" echo "" show_help exit 1 ;; esac } # 执行主函数 main "$@"