Github Action 一键部署HTML 静态服务

python 服务脚本

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)

Github Action 脚本

python 复制代码
name: Custom Hexo Deployment via HTTP POST

on:
  push:
    branches:
      - master
  workflow_dispatch: # 允许手动触发

jobs:
  build_and_upload:
    runs-on: ubuntu-latest

    # 移除 permissions 和 environment 配置,因为不再部署到 GitHub Pages
    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '22.14.0'
          cache: 'yarn'

      - name: Install dependencies
        run: yarn install --frozen-lockfile

      - name: Generate static files (Hexo Build)
        run: yarn run build

      - name: Debug Check 'public' directory contents and size
        run: |
          echo "Listing contents of public directory:"
          ls -la public
          echo "Calculating total size of public directory:"
          du -sh public

      - name: Install zip utility
        # 确保系统有 zip 命令
        run: sudo apt-get update && sudo apt-get install -y zip

      - name: Compress contents of 'public' directory to ZIP
        id: compress
        # 切换到 public 目录,压缩所有内容(. 表示当前目录下的所有文件和子目录),
        # 然后将 ZIP 文件保存到上级目录,确保 ZIP 包内不包含顶层 public/ 目录。
        run: |
          cd public
          # 检查目录是否为空,如果为空,此命令会报错,但在 Actions 中默认不会失败,所以我们依赖 ls/du 调试信息
          zip -r ../deployment.zip .

      - name: Upload deployment.zip via POST Request
        id: upload
        # 使用 curl 进行 POST 请求上传文件:
        # -X POST: 指定请求方法
        # -F "file=@deployment.zip": 以 multipart/form-data 格式上传文件,字段名为 'file',文件名是 'deployment.zip'
        # -v: 打印详细输出,便于调试
        # -k: 如果目标是 HTTPS 但证书有问题,可以加上 -k 忽略证书校验
        # --fail: 遇到 HTTP 错误时(如 4xx, 5xx)立即失败
        run: |
          echo "Starting upload to http://192.168.0.1:5000/"
          curl --fail -X POST \
               -F "file=@deployment.zip" \
               http://192.168.0.1:5000/

      - name: Cleanup compressed file (Optional)
        if: always()
        run: rm deployment.zip
相关推荐
星释1 小时前
Rust 练习册 66:密码方块与文本加密
java·前端·rust
IT_陈寒1 小时前
React性能翻倍!90%开发者忽略的5个Hooks最佳实践
前端·人工智能·后端
亿元程序员1 小时前
光图片就300多M,微信小游戏给再大的分包也难啊!
前端
中工钱袋1 小时前
前端请求到底是从哪里发出去的?
前端
じòぴé南冸じょうげん4 小时前
若依框架favicon.ico缓存更新问题解决方案:本地生效但线上未更新
前端·javascript·前端框架·html
狮子座的男孩4 小时前
js基础高级:01、数据类型(typeof、instanceof、===的使用)、数据与变量与内存(定义、赋值与内存关系、引用变量赋值、js调函数传参)
前端·javascript·经验分享·数据类型·数据与变量与内存·赋值与内存关系·引用变量赋值
Cyclo-7 小时前
PDFJS 在React中的引入 使用组件打开文件流PDF
前端·react.js·pdf
椒盐螺丝钉10 小时前
Vue Router应用:组件跳转
前端·javascript·vue.js
顾安r10 小时前
11.20 开源APP
服务器·前端·javascript·python·css3