#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ 管理后台部署脚本 - 将构建好的管理后台文件上传到服务器 使用方法: python deploy.py """ import os import sys import shutil import subprocess from pathlib import Path # 配置变量 SERVER_IP = "101.200.208.45" USERNAME = "root" REMOTE_PATH = "/data/www/emotion-museum-admin" # 本地路径 SCRIPT_DIR = Path(__file__).parent.absolute() DIST_DIR = SCRIPT_DIR / "dist" class Colors: """终端颜色""" GREEN = '\033[32m' RED = '\033[31m' YELLOW = '\033[33m' RESET = '\033[0m' def log_info(msg): """打印信息日志""" print(f"{Colors.GREEN}{Colors.RESET} {msg}") def log_error(msg): """打印错误日志""" print(f"{Colors.RED}{Colors.RESET} {msg}") def log_step(msg): """打印步骤日志""" print(f" {msg}") def run_command(cmd, cwd=None, shell=True, capture=True, timeout=60): """执行本地命令""" try: if capture: result = subprocess.run(cmd, cwd=cwd, shell=shell, capture_output=True, text=True, timeout=timeout) return result.returncode == 0, result.stdout, result.stderr else: result = subprocess.run(cmd, cwd=cwd, shell=shell, timeout=timeout) return result.returncode == 0, "", "" except subprocess.TimeoutExpired: return False, "", f"命令执行超时 ({timeout}s): {cmd}" except Exception as e: return False, "", str(e) def run_command_realtime(cmd, cwd=None, shell=True, timeout=120): """执行命令并实时输出(用于大文件上传进度)""" try: result = subprocess.run(cmd, cwd=cwd, shell=shell, timeout=timeout) return result.returncode == 0 except subprocess.TimeoutExpired: log_error(f"命令执行超时 ({timeout}s)") return False except Exception as e: log_error(f"执行失败: {e}") return False def check_npm(): """检查npm是否安装""" success, _, _ = run_command("npm --version") if not success: log_error("错误: 未找到npm命令,请先安装Node.js") sys.exit(1) def check_ssh(): """检查SSH连接""" log_step(" 检查SSH连接...") cmd = f'ssh -o BatchMode=yes -o ConnectTimeout=10 -o StrictHostKeyChecking=no {USERNAME}@{SERVER_IP} "echo ok"' success, stdout, stderr = run_command(cmd, timeout=15) if not success: log_error("SSH连接失败,请检查:") print(f" 1. 服务器IP: {SERVER_IP}") print(f" 2. SSH密钥是否配置正确") print(f" 3. 服务器防火墙是否允许22端口") if "Host key verification failed" in stderr: log_error("主机密钥验证失败,请先手动执行: ssh root@101.200.208.45") sys.exit(1) log_info("SSH连接正常") def clean_dist(): """清理旧的构建文件""" log_step(" 清理旧的构建文件...") if DIST_DIR.exists(): shutil.rmtree(DIST_DIR) def build_project(): """构建项目""" log_step(" 开始构建管理后台项目(生产环境)...") os.chdir(SCRIPT_DIR) env = os.environ.copy() env["NODE_ENV"] = "production" try: result = subprocess.run( "npm run build", shell=True, cwd=SCRIPT_DIR, env=env, timeout=300 ) if result.returncode != 0: log_error("管理后台项目构建失败,请检查代码") sys.exit(1) except Exception as e: log_error(f"构建失败: {e}") sys.exit(1) log_info("管理后台项目构建成功") def verify_dist(): """验证dist目录是否存在""" if not DIST_DIR.exists(): log_error("错误: 构建后dist目录仍不存在,请检查构建配置") sys.exit(1) index_file = DIST_DIR / "index.html" assets_dir = DIST_DIR / "assets" if not index_file.exists(): log_error("错误: dist/index.html 不存在") sys.exit(1) if not assets_dir.exists(): log_error("错误: dist/assets 目录不存在") sys.exit(1) def create_remote_dir(): """创建远程目录""" log_step(" 创建远程目录...") cmd = f'ssh -o BatchMode=yes -o ConnectTimeout=10 -o StrictHostKeyChecking=no {USERNAME}@{SERVER_IP} "mkdir -p {REMOTE_PATH}"' success, _, stderr = run_command(cmd, timeout=15) if not success: log_error(f"创建远程目录失败: {stderr}") sys.exit(1) def upload_files(): """上传文件到服务器""" log_step(" 上传文件到服务器...") print(f" 正在上传到 {SERVER_IP}:{REMOTE_PATH}") # 使用 rsync 替代 scp,支持断点续传和进度显示 assets_dir = DIST_DIR / "assets" # 上传 index.html cmd1 = f'scp -o BatchMode=yes -o ConnectTimeout=10 -o StrictHostKeyChecking=no "{DIST_DIR / "index.html"}" {USERNAME}@{SERVER_IP}:{REMOTE_PATH}/' success1 = run_command_realtime(cmd1, timeout=60) if not success1: log_error("上传 index.html 失败") return False log_info("index.html 上传成功") # 上传 assets 目录(使用 rsync 更高效) cmd2 = f'rsync -avz --progress -e "ssh -o BatchMode=yes -o ConnectTimeout=10 -o StrictHostKeyChecking=no" "{assets_dir}/" {USERNAME}@{SERVER_IP}:{REMOTE_PATH}/assets/' print(f" 正在上传 assets 目录...") success2 = run_command_realtime(cmd2, timeout=300) if not success2: # rsync 不可用则回退到 scp log_info("rsync不可用,使用scp上传...") cmd2_fallback = f'scp -o BatchMode=yes -o ConnectTimeout=10 -o StrictHostKeyChecking=no -r "{assets_dir}" {USERNAME}@{SERVER_IP}:{REMOTE_PATH}/' success2 = run_command_realtime(cmd2_fallback, timeout=300) if not success2: log_error("上传 assets 目录失败") return False log_info("assets 目录上传成功") return True def clean_remote_old_files(): """清理远程服务器上旧的assets目录(全量替换策略)""" log_step(" 清理远程旧文件...") # 直接删除远程 assets 目录,后续会重新上传 cmd = f'ssh -o BatchMode=yes -o ConnectTimeout=10 -o StrictHostKeyChecking=no {USERNAME}@{SERVER_IP} "rm -rf {REMOTE_PATH}/assets"' success, _, stderr = run_command(cmd, timeout=15) if success: log_info("远程旧文件已清理") else: log_info(f"清理远程旧文件跳过(可能目录不存在): {stderr}") def set_permissions(): """设置文件权限""" log_step(" 设置文件权限...") cmd = f'ssh -o BatchMode=yes -o ConnectTimeout=10 -o StrictHostKeyChecking=no {USERNAME}@{SERVER_IP} "find {REMOTE_PATH} -type f -exec chmod 644 {{}} \\; && find {REMOTE_PATH} -type d -exec chmod 755 {{}} \\;"' success, _, stderr = run_command(cmd, timeout=30) if not success: log_error(f"设置权限失败: {stderr}") else: log_info("文件权限设置成功") def deploy(): """执行部署""" print("开始部署管理后台应用到服务器...") # 检查依赖 check_npm() check_ssh() # 清理旧构建 clean_dist() # 构建项目 build_project() # 验证构建结果 verify_dist() # 创建远程目录 create_remote_dir() # 清理远程旧assets目录 clean_remote_old_files() # 上传文件(全量上传) if upload_files(): # 设置权限 set_permissions() log_info("管理后台部署完成!") print(f" 访问地址: http://{SERVER_IP}/emotion-museum-admin/") else: log_error("部署失败,请检查:") print(" 1. 服务器IP地址是否正确") print(" 2. SSH密钥是否配置正确") print(" 3. 服务器目录权限是否正确") sys.exit(1) def main(): """主函数""" deploy() if __name__ == "__main__": main()