python
复制代码
import os
import zipfile
import shutil # 新增: 用于递归删除目录
from flask import Flask, request, redirect, url_for, render_template_string
from werkzeug.utils import secure_filename
# --- 配置 ---
UPLOAD_FOLDER = './uploads' # 文件上传的原始目录
# ⚠️ 目标目录已更改为网站根目录,请确保运行此服务的用户有写入权限
EXTRACT_FOLDER = '/var/www/hexo.xyz' # 文件解压的目标目录
ALLOWED_EXTENSIONS = {'zip'}
HOST = '0.0.0.0'
PORT = 5000
# 确保目录存在
os.makedirs(UPLOAD_FOLDER, exist_ok=True)
# 确保目标网站目录存在
os.makedirs(EXTRACT_FOLDER, exist_ok=True)
app = Flask(__name__)
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024 # 限制文件大小为 16MB
# --- 辅助函数 ---
def allowed_file(filename):
"""检查文件后缀是否在允许列表中"""
return '.' in filename and \
filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
def handle_unzip(zip_path, extract_target):
"""
处理 zip 文件解压的核心逻辑。
此版本会在解压前清空目标目录,并将内容直接解压到目标目录。
"""
try:
# 1. 清空目标目录
print(f"正在清空目标目录: {extract_target}")
for item in os.listdir(extract_target):
item_path = os.path.join(extract_target, item)
if os.path.isdir(item_path):
# 递归删除子目录
shutil.rmtree(item_path)
else:
# 删除文件
os.remove(item_path)
# 2. 直接解压到目标目录
final_target = extract_target # 直接解压到 /var/www/xiaochungu.xyz
with zipfile.ZipFile(zip_path, 'r') as zip_ref:
# 将 ZIP 文件中的所有内容直接解压到 final_target
zip_ref.extractall(final_target)
base_name = os.path.splitext(os.path.basename(zip_path))[0]
print(f"成功部署文件: {zip_path} 到 {final_target}")
# ⚠️ 可选:解压完成后删除原始 zip 文件
# os.remove(zip_path)
return True, f"文件 {base_name}.zip 的内容已成功部署到网站根目录 {final_target} (已清空原目录内容)"
except zipfile.BadZipFile:
return False, "错误:上传的文件不是一个有效的 ZIP 文件。"
except Exception as e:
return False, f"解压过程中发生错误: {e}。请确保服务运行用户拥有写入和删除 {extract_target} 目录内容的权限。"
# --- Flask 路由 ---
@app.route('/', methods=['GET', 'POST'])
def upload_file():
if request.method == 'POST':
# 检查请求中是否有文件部分
if 'file' not in request.files:
# 客户端(GitHub Action)的字段名必须是 'file'
return render_template_string(HTML_ERROR, message="请求中缺少 'file' 字段。请检查上传客户端的配置。")
file = request.files['file']
# 如果用户没有选择文件
if file.filename == '':
return redirect(request.url)
if file and allowed_file(file.filename):
# 1. 保存上传文件
filename = secure_filename(file.filename)
upload_path = os.path.join(app.config['UPLOAD_FOLDER'], filename)
file.save(upload_path)
# 2. 执行自动解压/部署逻辑
success, message = handle_unzip(upload_path, EXTRACT_FOLDER)
if success:
# 针对 GitHub Actions 的 POST 请求返回简洁的成功信息
if 'User-Agent' in request.headers and 'curl' in request.headers['User-Agent']:
return message, 200
return render_template_string(HTML_SUCCESS, message=message)
else:
# 针对 GitHub Actions 的 POST 请求返回错误信息和 500 状态码
if 'User-Agent' in request.headers and 'curl' in request.headers['User-Agent']:
return message, 500
return render_template_string(HTML_ERROR, message=message)
# 如果文件类型不被允许
return render_template_string(HTML_ERROR, message="只允许上传 ZIP 文件。")
# GET 请求:显示上传表单
return render_template_string(HTML_FORM)
# --- 模板 HTML ---
HTML_FORM = """
<!doctype html>
<title>ZIP 文件上传服务</title>
<h1>ZIP 文件部署服务</h1>
<p>文件将上传到 `{{ config['UPLOAD_FOLDER'] }}` 并部署到网站根目录 `{{ EXTRACT_FOLDER }}`</p>
<form method=post enctype=multipart/form-data>
<input type=file name=file>
<input type=submit value=上传>
</form>
"""
HTML_SUCCESS = """
<!doctype html>
<title>部署成功</title>
<h1 style="color: green;">部署成功</h1>
<p>{{ message }}</p>
<p><a href="/">继续部署</a></p>
"""
HTML_ERROR = """
<!doctype html>
<title>部署失败</title>
<h1 style="color: red;">部署失败</h1>
<p>{{ message }}</p>
<p><a href="/">返回上传页面</a></p>
"""
if __name__ == '__main__':
# ⚠️ 在生产环境中,请使用 Gunicorn/uWSGI 等 WSGI 服务器运行 Flask,
# 而不是使用 app.run()
print(f"服务启动于 http://{HOST}:{PORT}")
print(f"ZIP 文件上传目录: {os.path.abspath(UPLOAD_FOLDER)}")
print(f"文件部署目标目录: {os.path.abspath(EXTRACT_FOLDER)}")
# 🚨 注意:运行此服务需要足够的权限来写入和清空 /var/www/hexo.xyz 目录。
# 如果遇到权限问题,请确保使用 sudo 或一个有权限的用户运行服务。
app.run(host=HOST, port=PORT, debug=False)