Files
DP/AdminTool/auto_deploy_core.py

541 lines
17 KiB
Python
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.
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Core 应用自动发布脚本
完全自动化构建、打包、上传服务器、更新数据库
使用方法:
python auto_deploy_core.py --version v1.0.6
python auto_deploy_core.py --version v1.0.6 --deploy # 部署到服务器
python auto_deploy_core.py --version v1.0.6 --deploy --update-db # 部署并更新数据库
"""
import os
import sys
import shutil
import subprocess
import argparse
import zipfile
import json
from pathlib import Path
from datetime import datetime
import paramiko
import pymysql
# ==================== 配置区域 ====================
PROJECT_ROOT = Path(__file__).parent.parent.absolute() # 上一级目录
DESIGNER_DIR = PROJECT_ROOT / "Designer"
DIST_CORE_DIR = DESIGNER_DIR / "dist" / "Designer" # Core 构建输出目录
DIST_SHELL_DIR = DESIGNER_DIR / "dist" / "Shell" # Shell 构建输出目录
SERVER_DIR = PROJECT_ROOT / "Server"
ARCHIVES_DIR = SERVER_DIR / "archives"
CONFIG_FILE = Path(__file__).parent / "deploy_config.json"
# 颜色输出Windows 兼容)
try:
import colorama
colorama.init()
GREEN = '\033[92m'
YELLOW = '\033[93m'
RED = '\033[91m'
BLUE = '\033[94m'
RESET = '\033[0m'
except ImportError:
GREEN = YELLOW = RED = BLUE = RESET = ''
# ==================== 工具函数 ====================
def print_step(step_num, total_steps, message):
"""打印步骤信息"""
print(f"\n{BLUE}{'='*60}{RESET}")
print(f"{GREEN}[步骤 {step_num}/{total_steps}] {message}{RESET}")
print(f"{BLUE}{'='*60}{RESET}\n")
def print_success(message):
"""打印成功信息"""
print(f"{GREEN}{message}{RESET}")
def print_warning(message):
"""打印警告信息"""
print(f"{YELLOW}{message}{RESET}")
def print_error(message):
"""打印错误信息"""
print(f"{RED}{message}{RESET}")
def run_command(command, cwd=None, shell=True, check=True):
"""运行命令并返回结果"""
print(f" 执行: {command}")
try:
result = subprocess.run(
command,
cwd=cwd,
shell=shell,
check=check,
capture_output=True,
text=True,
encoding='utf-8',
errors='replace'
)
if result.stdout:
print(f" 输出: {result.stdout.strip()}")
return result
except subprocess.CalledProcessError as e:
print_error(f"命令执行失败: {e}")
if e.stderr:
print(f" 错误: {e.stderr}")
if check:
sys.exit(1)
return None
# ==================== 发布步骤 ====================
def load_config():
"""加载部署配置"""
if not CONFIG_FILE.exists():
return None
try:
with open(CONFIG_FILE, 'r', encoding='utf-8') as f:
return json.load(f)
except Exception as e:
print_warning(f"加载配置文件失败: {e}")
return None
def save_config(config):
"""保存部署配置"""
try:
with open(CONFIG_FILE, 'w', encoding='utf-8') as f:
json.dump(config, f, ensure_ascii=False, indent=2)
print_success(f"配置已保存: {CONFIG_FILE}")
except Exception as e:
print_error(f"保存配置失败: {e}")
def step1_build_frontend():
"""步骤 1: 构建前端Shell + Core"""
print_step(1, 8, "构建前端Shell + Core")
# 检查 package.json
package_json = DESIGNER_DIR / "package.json"
if not package_json.exists():
print_error(f"未找到 package.json: {package_json}")
sys.exit(1)
# 运行构建命令
os.chdir(DESIGNER_DIR)
print(" 正在构建 Core...")
run_command("npm run build:core", cwd=DESIGNER_DIR)
print(" 正在构建 Shell...")
run_command("npm run build", cwd=DESIGNER_DIR)
# 验证输出目录
if not DIST_CORE_DIR.exists():
print_error(f"Core 构建输出目录不存在: {DIST_CORE_DIR}")
sys.exit(1)
if not DIST_SHELL_DIR.exists():
print_error(f"Shell 构建输出目录不存在: {DIST_SHELL_DIR}")
sys.exit(1)
print_success(f"构建完成")
print(f" Core: {DIST_CORE_DIR}")
print(f" Shell: {DIST_SHELL_DIR}")
def step2_package_shell_zip(version):
"""步骤 2: 打包 Shell 为 ZIP供 CEP 扩展下载)"""
print_step(2, 8, "打包 Shell 为 ZIP")
# 生成 ZIP 文件名
zip_filename = f"shell-{version}.zip"
zip_path = DESIGNER_DIR / "dist" / zip_filename
# 如果文件已存在,先删除
if zip_path.exists():
print_warning(f"ZIP 文件已存在,删除: {zip_path}")
zip_path.unlink()
# 创建 ZIP
print(f" 创建 ZIP: {zip_path}")
with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) as zipf:
for root, dirs, files in os.walk(DIST_SHELL_DIR):
for file in files:
file_path = Path(root) / file
arcname = file_path.relative_to(DIST_SHELL_DIR)
zipf.write(file_path, arcname)
# 显示文件大小
size_mb = zip_path.stat().st_size / (1024 * 1024)
print_success(f"Shell 打包完成: {zip_path}")
print(f" 文件大小: {size_mb:.2f} MB")
return zip_path
def step3_upload_to_server(version, config):
"""步骤 3: 上传到服务器"""
print_step(3, 8, "上传到服务器")
if not config:
print_warning("未配置服务器信息,跳过上传")
return
# 连接 SSH
ssh = paramiko.SSHClient()
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
try:
print(f" 连接服务器: {config['host']}")
ssh.connect(
hostname=config['host'],
port=int(config.get('port', 22)),
username=config['username'],
password=config.get('password', ''),
timeout=30
)
print_success("SSH 连接成功")
sftp = ssh.open_sftp()
remote_path = config['remote_path']
# 1. 创建远程目录
print(" 创建远程目录...")
commands = [
f"mkdir -p {remote_path}/shell",
f"mkdir -p {remote_path}/core/{version}",
f"mkdir -p {remote_path}/downloads"
]
for cmd in commands:
stdin, stdout, stderr = ssh.exec_command(cmd)
stdout.channel.recv_exit_status()
print_success("目录创建完成")
# 2. 上传 Shell在线登录页
print(" 上传 Shell在线登录页...")
upload_directory(sftp, DIST_SHELL_DIR, f"{remote_path}/shell")
print_success("Shell 上传完成")
# 3. 上传 Core
print(" 上传 Core核心应用...")
upload_directory(sftp, DIST_CORE_DIR, f"{remote_path}/core/{version}")
print_success("Core 上传完成")
# 4. 上传 Shell.zip
print(" 上传 Shell.zipCEP 扩展下载)...")
shell_zip = DESIGNER_DIR / "dist" / f"shell-{version}.zip"
if shell_zip.exists():
remote_zip = f"{remote_path}/downloads/shell-{version}.zip"
sftp.put(str(shell_zip), remote_zip)
print_success(f"Shell.zip 上传完成: {remote_zip}")
sftp.close()
ssh.close()
print_success("所有文件上传完成")
print(f"\n访问地址:")
print(f" Shell: https://{config['host']}/shell/")
print(f" Core: https://{config['host']}/core/{version}/")
print(f" 下载: https://{config['host']}/downloads/shell-{version}.zip")
except Exception as e:
print_error(f"上传失败: {e}")
raise
finally:
ssh.close()
def upload_directory(sftp, local_dir, remote_dir):
"""递归上传目录"""
if not os.path.exists(local_dir):
raise Exception(f"本地目录不存在: {local_dir}")
# 创建远程目录
try:
sftp.stat(remote_dir)
except IOError:
sftp.mkdir(remote_dir)
# 递归上传文件
for item in os.listdir(local_dir):
local_path = os.path.join(local_dir, item)
remote_path = remote_dir + '/' + item
if os.path.isfile(local_path):
print(f"{item}")
sftp.put(local_path, remote_path)
elif os.path.isdir(local_path):
upload_directory(sftp, local_path, remote_path)
def step4_update_mysql(version, config):
"""步骤 4: 更新 MySQL 数据库 (通过 SSH 执行 Docker 命令)"""
print_step(4, 8, "更新 MySQL 数据库")
# 注意:我们不再直接连接 MySQL而是通过 SSH 发送 docker exec 命令
# 这样用户不需要暴露 MySQL 端口,更安全
if not config:
print_warning("未配置服务器信息,跳过数据库更新")
return
try:
# 连接 SSH
ssh = paramiko.SSHClient()
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
print(f" 连接服务器: {config['host']}")
ssh.connect(
hostname=config['host'],
port=int(config.get('port', 22)),
username=config['username'],
password=config.get('password', ''),
timeout=30
)
print_success("SSH 连接成功")
# 构造 SQL 语句
# 更新 'default' 分组为最新版本 (更健壮,不依赖 ID=1)
sql = f"UPDATE plugin_groups SET current_version_file = 'core-v{version}.zip' WHERE name = 'default';"
# 构造 Docker 命令
# 假设容器名为 designercep_db用户 designer_user密码 DesignerPass123!,库名 designer_db
# 这些应该与 docker-compose.yml 保持一致
db_user = "designer_user"
db_pass = "DesignerPass123!"
db_name = "designer_db"
container_name = "designercep_db"
print(f" 执行远程 Docker SQL 命令...")
print(f" SQL: {sql}")
docker_cmd = f'docker exec -i {container_name} mysql -u{db_user} -p{db_pass} {db_name} -e "{sql}"'
stdin, stdout, stderr = ssh.exec_command(docker_cmd)
exit_status = stdout.channel.recv_exit_status()
if exit_status == 0:
print_success("数据库更新命令执行成功")
print(f" 输出: {stdout.read().decode().strip()}")
else:
print_error(f"数据库更新失败 (Exit code: {exit_status})")
print(f" 错误: {stderr.read().decode().strip()}")
raise Exception("Docker exec failed")
ssh.close()
except Exception as e:
print_error(f"数据库更新失败: {e}")
print_warning("您需要手动执行 SQL (进入容器执行):")
print(f" {sql}")
# 不抛出异常,以免打断流程,因为这步失败通常不影响文件上传
def step5_clean_cache():
"""步骤 5: 清除客户端缓存(测试用)"""
print_step(5, 8, "清除客户端缓存(可选)")
cache_dir = Path.home() / "AppData" / "Roaming" / "DesignerCache"
if not cache_dir.exists():
print_warning(f"缓存目录不存在: {cache_dir}")
return
try:
shutil.rmtree(cache_dir)
print_success(f"缓存已清除: {cache_dir}")
except Exception as e:
print_warning(f"清除缓存失败: {e}")
print(" 您可以手动删除缓存目录")
def step6_setup_config():
"""步骤 6: 配置服务器信息(首次运行)"""
print_step(6, 8, "配置服务器信息")
config = load_config()
if config:
print_warning("配置文件已存在,跳过配置")
print(f" 配置文件: {CONFIG_FILE}")
return config
print("请输入服务器配置信息:")
print()
config = {}
# SSH 配置
config['host'] = input(" 服务器地址: ").strip()
config['port'] = input(" SSH 端口 [22]: ").strip() or "22"
config['username'] = input(" SSH 用户名: ").strip()
config['password'] = input(" SSH 密码: ").strip()
config['remote_path'] = input(" 远程路径 [/var/www/DesignerCEP/Server/static]: ").strip() \
or "/var/www/DesignerCEP/Server/static"
print()
# MySQL 配置
use_mysql = input(" 是否配置 MySQL (y/n) [y]: ").strip().lower()
if use_mysql != 'n':
config['mysql'] = {}
config['mysql']['host'] = input(" MySQL 地址: ").strip()
config['mysql']['port'] = input(" MySQL 端口 [3306]: ").strip() or "3306"
config['mysql']['username'] = input(" MySQL 用户名: ").strip()
config['mysql']['password'] = input(" MySQL 密码: ").strip()
config['mysql']['database'] = input(" 数据库名: ").strip()
config['mysql']['table'] = input(" 表名 [plugin_groups]: ").strip() or "plugin_groups"
# 保存配置
save_config(config)
return config
def step7_summary(version, deployed):
"""步骤 7: 发布总结"""
print_step(7, 8, "发布总结")
print(f"""
{GREEN}{'='*60}
DesignerCEP 发布完成!
{'='*60}{RESET}
[发布信息]
版本号: {version}
构建时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
部署状态: {'已部署到服务器' if deployed else '仅本地构建'}
[构建产物]
Core: {DIST_CORE_DIR}
Shell: {DIST_SHELL_DIR}
Shell.zip: {DESIGNER_DIR / 'dist' / f'shell-{version}.zip'}
[后续步骤]
1. {'' if deployed else ''} 验证服务器文件
2. {'' if deployed else ''} 确认数据库版本
3. ○ 测试客户端更新功能
4. ○ 通知用户更新
[测试方法]
1. 删除客户端缓存(已自动执行)
2. 重启 Photoshop 插件
3. 登录账号,检查是否下载新版本
4. 验证功能是否正常
{GREEN}{'='*60}{RESET}
""")
# ==================== 主函数 ====================
def main():
parser = argparse.ArgumentParser(
description='DesignerCEP 自动发布脚本Shell + Core',
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
示例:
# 仅构建(不部署)
python auto_deploy_core.py --version 1.0.6
# 构建并部署到服务器
python auto_deploy_core.py --version 1.0.6 --deploy
# 构建、部署并更新数据库
python auto_deploy_core.py --version 1.0.6 --deploy --update-db
# 首次运行,配置服务器信息
python auto_deploy_core.py --version 1.0.6 --setup
"""
)
parser.add_argument(
'--version', '-v',
required=True,
help='版本号,例如: 1.0.6(不要加 v'
)
parser.add_argument(
'--deploy', '-d',
action='store_true',
help='部署到服务器'
)
parser.add_argument(
'--update-db',
action='store_true',
help='自动更新 MySQL 数据库'
)
parser.add_argument(
'--setup',
action='store_true',
help='配置服务器信息(首次运行)'
)
parser.add_argument(
'--skip-clean',
action='store_true',
help='跳过清除缓存步骤'
)
args = parser.parse_args()
version = args.version
print(f"""
{BLUE}{'='*60}
DesignerCEP 自动发布脚本
{'='*60}{RESET}
[配置信息]
版本号: {version}
项目根目录: {PROJECT_ROOT}
Designer: {DESIGNER_DIR}
部署到服务器: {'' if args.deploy else ''}
更新数据库: {'' if args.update_db else ''}
清除缓存: {'' if args.skip_clean else ''}
{BLUE}{'='*60}{RESET}
""")
try:
# 加载配置
config = None
if args.deploy or args.setup:
config = load_config()
if not config or args.setup:
config = step6_setup_config()
# 执行发布步骤
step1_build_frontend()
step2_package_shell_zip(version)
if args.deploy:
if not config:
print_error("未找到配置文件,请先运行 --setup 配置服务器")
sys.exit(1)
step3_upload_to_server(version, config)
if args.update_db:
step4_update_mysql(version, config)
else:
print_step(4, 8, "更新数据库(已跳过)")
print_warning("数据库未自动更新,请手动执行:")
print(f" UPDATE plugin_groups SET current_version = '{version}';")
else:
print_step(3, 8, "上传到服务器(已跳过)")
print_step(4, 8, "更新数据库(已跳过)")
if not args.skip_clean:
step5_clean_cache()
else:
print_step(5, 8, "清除缓存(已跳过)")
step7_summary(version, args.deploy)
except KeyboardInterrupt:
print_error("\n用户中断")
sys.exit(1)
except Exception as e:
print_error(f"发布失败: {e}")
import traceback
traceback.print_exc()
sys.exit(1)
if __name__ == "__main__":
main()