Files
peanut d40ecdde19 fix: 优化 web-admin 部署脚本,解决 SSH 卡死和旧文件清理问题
- 所有 SSH/SCP 命令添加 BatchMode=yes 和超时限制
- 新增 SSH 预检步骤,失败立即退出
- 清理旧文件改为先删后传策略,避免 find -printf 兼容性问题
- 使用 rsync 上传 assets,fallback 到 scp
- 文件/目录权限分别设置为 644/755

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-10 23:49:55 +08:00

263 lines
7.8 KiB
Python

#!/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()