jshells/jsite.sh

878 lines
23 KiB
Bash
Raw Blame History

This file contains ambiguous Unicode characters

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
# ========================================
# 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"
TRAEFIK_CONFIG_DIR="/home/jingrow/traefik-docker/conf.d/website"
# 默认配置参数
SITE_NAME="jingrow"
START_PORT=3001
PORT_INCREMENT=1
# 网络配置
PUBLIC_IP="" # 公网IP地址 (用于内网IP不可用时)
# .env文件参数
SITE_URL="starrbud.com"
REVALIDATE_TOKEN="535bc122f3e364c"
SERVER_URL="https://admin.jingrow.com"
API_KEY="535bc122f3e364c"
API_SECRET="8629a3b12fc1cc2"
# ========================================
# 日志函数
# ========================================
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 "jingrow" &>/dev/null; then
log_error "用户 jingrow 不存在,请先创建用户"
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 "jingrow:jingrow" "$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 - "jingrow" -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 新建网站"
echo " delete <name> 删除网站"
echo " build <name> 构建网站"
echo " start <name> 启动网站"
echo " stop <name> 停止网站"
echo " restart <name> 重启网站"
echo " status <name> 查看网站状态"
echo " list 列出所有网站"
echo " logs <name> 查看网站日志"
echo ""
echo "create命令选项:"
echo " --site-name NAME 网站名称 (必需)"
echo " --git-repo URL Git仓库地址 (可选)"
echo " --site-url URL 网站URL (可选)"
echo ""
echo "全局选项:"
echo " -h, --help 显示此帮助信息"
echo ""
echo "示例:"
echo " $0 create --site-name mysite"
echo " $0 create --site-name mysite --git-repo http://git.example.com/project"
echo " $0 create --site-name mysite --site-url mysite.com"
echo " $0 create --site-name mysite --git-repo http://git.example.com/project --site-url 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 "jingrow:jingrow" "$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
}
# 创建.env文件
create_env_file() {
local site_name="$1"
local port="$2"
log_info "创建.env文件..."
cd "$JSITE_BASE_DIR/$site_name"
# 智能IP选择优先使用内网IP没有内网IP时使用公网IP与traefik配置保持一致
local host_ip=$(get_optimal_host_ip)
# 输出IP选择结果
if [[ "$host_ip" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
log_success "使用IP地址: $host_ip"
else
log_warning "IP地址格式异常: $host_ip"
return 1
fi
# 构建PUBLIC_SITE_URL优先使用内网IP没有内网IP时使用公网IP
local public_site_url="http://$host_ip:$port"
# 检查.env文件是否已存在
if [ -f "$JSITE_BASE_DIR/$site_name/.env" ]; then
log_warning ".env文件已存在将会覆盖..."
fi
# 创建.env文件
cat > "$JSITE_BASE_DIR/$site_name/.env" << EOF
# 网站配置
PUBLIC_SITE_URL=$public_site_url
# 重新验证令牌
REVALIDATE_TOKEN=$REVALIDATE_TOKEN
# 项目配置
JINGROW_SITE_NAME=$site_name
# 服务器配置
JINGROW_SERVER_URL=$SERVER_URL
JINGROW_API_KEY=$API_KEY
JINGROW_API_SECRET=$API_SECRET
EOF
# 设置文件权限
chown "jingrow:jingrow" "$JSITE_BASE_DIR/$site_name/.env"
chmod 600 "$JSITE_BASE_DIR/$site_name/.env"
log_success ".env文件创建完成 (端口: $port, PUBLIC_SITE_URL: $public_site_url)"
}
# 创建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 "jingrow:jingrow" "$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 - "jingrow" -c "cd '$traefik_dir' && docker compose restart" 2>/dev/null || {
log_warning "无法重启Traefik服务请手动重启"
}
fi
}
# ========================================
# 核心功能函数
# ========================================
# 解析create命令的参数
parse_create_args() {
# 重置变量
CREATE_SITE_NAME=""
CREATE_GIT_REPO="$DEFAULT_GIT_REPO"
CREATE_DOMAIN="$SITE_URL"
# 解析参数
while [[ $# -gt 0 ]]; do
case $1 in
--site-name)
CREATE_SITE_NAME="$2"
shift 2
;;
--git-repo)
CREATE_GIT_REPO="$2"
shift 2
;;
--site-url)
CREATE_DOMAIN="$2"
shift 2
;;
-h|--help)
show_help
exit 0
;;
*)
log_error "未知参数: $1"
echo "使用 $0 create --help 查看帮助"
exit 1
;;
esac
done
# 验证必需参数
if [ -z "$CREATE_SITE_NAME" ]; then
log_error "必需参数: --site-name"
echo "使用: $0 create --site-name <网站名称>"
exit 1
fi
}
# 新建网站
create_site() {
local site_name="$CREATE_SITE_NAME"
local git_repo="$CREATE_GIT_REPO"
local domain="$CREATE_DOMAIN"
log_info "开始创建网站: $site_name"
log_info "Git仓库: $git_repo"
log_info "域名: $domain"
# 检查网站是否已存在
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 - "jingrow" -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"
# 创建.env文件
create_env_file "$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 - "jingrow" -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 "jingrow:jingrow" "$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 - "jingrow" -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 - "jingrow" -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 - "jingrow" -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 - "jingrow" -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 - "jingrow" -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 - "jingrow" -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 - "jingrow" -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"
shift # 移除命令,剩下的都是参数
case "$command" in
create)
parse_create_args "$@"
create_site
;;
delete)
local site_name="$1"
delete_site "$site_name"
;;
build)
local site_name="$1"
build_site "$site_name"
;;
start)
local site_name="$1"
start_site "$site_name"
;;
stop)
local site_name="$1"
stop_site "$site_name"
;;
restart)
local site_name="$1"
restart_site "$site_name"
;;
status)
local site_name="$1"
show_site_status "$site_name"
;;
list)
list_sites
;;
logs)
local site_name="$1"
show_site_logs "$site_name"
;;
*)
log_error "未知命令: $command"
echo ""
show_help
exit 1
;;
esac
}
# 执行主函数
main "$@"