340 lines
10 KiB
Python
340 lines
10 KiB
Python
#!/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() |