Kamixitong/deploy.py
2025-11-22 20:32:49 +08:00

340 lines
10 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 -*-
"""
KaMiXiTong 部署脚本
用于生产环境的一键部署
"""
import os
import sys
import subprocess
import argparse
from pathlib import Path
def check_python_version():
"""检查Python版本"""
if sys.version_info < (3, 6):
print("❌ 错误: 需要Python 3.6或更高版本")
print(f"当前版本: {sys.version}")
sys.exit(1)
print(f"✅ Python版本检查通过: {sys.version}")
def create_directories():
"""创建必要的目录"""
directories = [
'logs',
'static/uploads',
'static/css',
'static/js'
]
for directory in directories:
Path(directory).mkdir(parents=True, exist_ok=True)
print("✅ 目录结构创建完成")
def check_env_file():
"""检查环境配置文件"""
env_file = Path('.env')
if not env_file.exists():
print("⚠️ 未找到 .env 文件,正在创建默认配置...")
try:
with open('.env.example', 'r', encoding='utf-8') as src:
with open('.env', 'w', encoding='utf-8') as dst:
dst.write(src.read())
print("✅ 已创建默认 .env 文件")
except FileNotFoundError:
print("❌ 未找到 .env.example 文件")
return False
else:
print("✅ 环境配置文件已存在")
return True
def install_dependencies():
"""安装依赖"""
print("📦 正在安装依赖包...")
try:
subprocess.check_call([sys.executable, '-m', 'pip', 'install', '-r', 'requirements.txt'])
# 检查并安装生产环境依赖
try:
subprocess.check_call([sys.executable, '-m', 'pip', 'install', 'gunicorn'])
print("✅ Gunicorn安装完成")
except subprocess.CalledProcessError:
print("⚠️ Gunicorn安装失败")
print("✅ 依赖安装完成")
return True
except subprocess.CalledProcessError:
print("❌ 依赖安装失败")
return False
def init_database():
"""初始化数据库"""
print("🗄️ 正在初始化数据库...")
# 检查使用哪种数据库
env_file = Path('.env')
if env_file.exists():
with open(env_file, 'r', encoding='utf-8') as f:
content = f.read()
if 'DATABASE_URL=sqlite' in content:
init_script = 'init_db_sqlite.py'
else:
init_script = 'setup_mysql.py'
else:
# 默认使用SQLite
init_script = 'init_db_sqlite.py'
try:
if os.path.exists(init_script):
subprocess.check_call([sys.executable, init_script])
print("✅ 数据库初始化完成")
return True
else:
print(f"⚠️ 数据库初始化脚本 {init_script} 不存在")
return False
except subprocess.CalledProcessError as e:
print(f"❌ 数据库初始化失败: {e}")
return False
def setup_systemd_service():
"""配置Systemd服务"""
print("⚙️ 正在配置Systemd服务...")
# 获取当前路径
app_path = os.path.abspath('.')
venv_path = os.path.join(app_path, 'venv', 'bin', 'gunicorn')
# 如果没有虚拟环境使用系统gunicorn
if not os.path.exists(venv_path):
venv_path = 'gunicorn'
# 从环境变量获取域名
frontend_domain = os.environ.get('FRONTEND_DOMAIN') or os.environ.get('DOMAIN_NAME') or os.environ.get('SERVER_NAME') or 'your-domain.com'
# 确保域名不包含协议部分
if frontend_domain.startswith(('http://', 'https://')):
frontend_domain = frontend_domain.split('://', 1)[1]
service_content = f"""[Unit]
Description=KaMiXiTong Service
After=network.target
[Service]
User=www-data
Group=www-data
WorkingDirectory={app_path}
Environment="FLASK_ENV=production"
Environment="DOMAIN_NAME={frontend_domain}"
ExecStart={venv_path} -w 4 -b 0.0.0.0:5088 run:app
Restart=always
RestartSec=10
[Install]
WantedBy=multi-user.target
"""
try:
# 写入服务文件
with open('/tmp/kamaxitong.service', 'w') as f:
f.write(service_content)
print("✅ Systemd服务配置文件已生成")
print("💡 请使用以下命令安装服务:")
print(" sudo cp /tmp/kamaxitong.service /etc/systemd/system/")
print(" sudo systemctl daemon-reload")
print(" sudo systemctl enable kamaxitong.service")
print(" sudo systemctl start kamaxitong.service")
return True
except Exception as e:
print(f"❌ Systemd服务配置失败: {e}")
return False
def setup_nginx_config():
"""配置Nginx"""
print("⚙️ 正在生成Nginx配置...")
# 从环境变量获取域名
frontend_domain = os.environ.get('FRONTEND_DOMAIN') or os.environ.get('DOMAIN_NAME') or os.environ.get('SERVER_NAME') or ''
if frontend_domain:
# 确保域名不包含协议部分
if frontend_domain.startswith(('http://', 'https://')):
server_name = frontend_domain.split('://', 1)[1]
else:
server_name = frontend_domain
else:
server_name = 'your-domain.com'
nginx_config = f"""server {{
listen 80;
server_name {server_name};
server_tokens off;
# ACME挑战目录用于Let's Encrypt证书
location /.well-known/acme-challenge/ {{
root /var/www/certbot;
}}
# 重定向到HTTPS
location / {{
return 301 https://$server_name$request_uri;
}}
}}
server {{
listen 443 ssl http2;
server_name {server_name};
server_tokens off;
# SSL证书配置请替换为实际路径
# ssl_certificate /etc/letsencrypt/live/{server_name}/fullchain.pem;
# ssl_certificate_key /etc/letsencrypt/live/{server_name}/privkey.pem;
# SSL安全配置
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-RSA-AES256-GCM-SHA512:DHE-RSA-AES256-GCM-SHA512:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-SHA384;
ssl_prefer_server_ciphers off;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
# 安全头配置
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "no-referrer-when-downgrade" always;
add_header Content-Security-Policy "default-src 'self' http: https: data: blob: 'unsafe-inline'" always;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
# Gzip压缩
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_proxied expired no-cache no-store private;
gzip_types text/plain text/css text/xml text/javascript application/x-javascript application/xml+rss;
# 代理到Flask应用
location / {{
proxy_pass http://127.0.0.1:5088;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $server_name;
proxy_redirect off;
# 超时设置
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
}}
# 静态文件直接由Nginx处理
location /static/ {{
alias /var/www/kamaxitong/static/;
expires 1y;
add_header Cache-Control "public, immutable";
add_header Access-Control-Allow-Origin "*";
}}
# 上传文件处理
location /static/uploads/ {{
alias /var/www/kamaxitong/static/uploads/;
expires 1d;
add_header Cache-Control "public";
}}
# ACME挑战目录用于Let's Encrypt证书
location /.well-known/acme-challenge/ {{
root /var/www/certbot;
}}
# 日志配置
access_log /var/log/nginx/kamaxitong.access.log;
error_log /var/log/nginx/kamaxitong.error.log;
# 错误页面
error_page 500 502 503 504 /50x.html;
location = /50x.html {{
root /usr/share/nginx/html;
}}
}}"""
try:
# 写入配置文件
with open('/tmp/kamaxitong_nginx.conf', 'w') as f:
f.write(nginx_config)
print("✅ Nginx配置文件已生成")
print("💡 请使用以下命令安装配置:")
print(" sudo cp /tmp/kamaxitong_nginx.conf /etc/nginx/sites-available/kamaxitong")
print(" sudo ln -s /etc/nginx/sites-available/kamaxitong /etc/nginx/sites-enabled/")
print(" sudo nginx -t")
print(" sudo systemctl restart nginx")
return True
except Exception as e:
print(f"❌ Nginx配置生成失败: {e}")
return False
def main():
"""主函数"""
parser = argparse.ArgumentParser(description='KaMiXiTong 部署脚本')
parser.add_argument('--skip-install', action='store_true', help='跳过依赖安装')
parser.add_argument('--skip-db', action='store_true', help='跳过数据库初始化')
parser.add_argument('--setup-service', action='store_true', help='配置Systemd服务')
parser.add_argument('--setup-nginx', action='store_true', help='生成Nginx配置')
args = parser.parse_args()
print("=" * 50)
print(" KaMiXiTong 生产环境部署脚本")
print("=" * 50)
# 检查Python版本
check_python_version()
# 创建目录
create_directories()
# 检查环境配置
if not check_env_file():
sys.exit(1)
# 安装依赖
if not args.skip_install:
if not install_dependencies():
sys.exit(1)
else:
print("⏭️ 跳过依赖安装")
# 初始化数据库
if not args.skip_db:
if not init_database():
sys.exit(1)
else:
print("⏭️ 跳过数据库初始化")
# 配置Systemd服务
if args.setup_service:
setup_systemd_service()
# 配置Nginx
if args.setup_nginx:
setup_nginx_config()
print("\n" + "=" * 50)
print("✅ 部署准备完成!")
print("=" * 50)
if args.setup_service:
print("💡 已生成Systemd服务配置文件请按提示安装")
if args.setup_nginx:
print("💡 已生成Nginx配置文件请按提示安装")
print("\n📌 下一步操作:")
print("1. 编辑 .env 文件,配置数据库和其他参数")
print("2. 配置SSL证书推荐")
print("3. 启动服务")
print("4. 访问你的域名")
if __name__ == '__main__':
main()