From bcab28d97f6e6d8a7c3f9554ad76d080ae39031f Mon Sep 17 00:00:00 2001 From: jingrow Date: Sat, 9 Aug 2025 17:06:42 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0jsite=E7=AE=A1=E7=90=86?= =?UTF-8?q?=E8=84=9A=E6=9C=AC=E7=94=A8=E4=BA=8E=E6=96=B0=E5=BB=BA=E5=88=A0?= =?UTF-8?q?=E9=99=A4=E6=9E=84=E5=BB=BA=E5=90=AF=E5=8A=A8=E9=87=8D=E5=90=AF?= =?UTF-8?q?jsite=E5=AE=9E=E4=BE=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- jsite.sh | 771 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 771 insertions(+) create mode 100644 jsite.sh diff --git a/jsite.sh b/jsite.sh new file mode 100644 index 0000000..eafc4b3 --- /dev/null +++ b/jsite.sh @@ -0,0 +1,771 @@ +#!/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 "$@" \ No newline at end of file