diff --git a/ai-converter/Dockerfile b/ai-converter/Dockerfile index 70b4c21..a009c29 100644 --- a/ai-converter/Dockerfile +++ b/ai-converter/Dockerfile @@ -4,13 +4,11 @@ FROM python:3.12-slim WORKDIR /app -COPY requirements.txt ./ -RUN pip install --no-cache-dir -r requirements.txt \ - && useradd --system --no-create-home converter +RUN useradd --system --no-create-home converter COPY app.py ./ USER converter EXPOSE 8090 -CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8090"] +CMD ["python", "app.py"] diff --git a/ai-converter/app.py b/ai-converter/app.py index 0bd4ff6..05cf9bd 100644 --- a/ai-converter/app.py +++ b/ai-converter/app.py @@ -7,17 +7,15 @@ features that would otherwise produce a silently corrupted AI document. from __future__ import annotations +import json import math import re from datetime import datetime, timezone +from http import HTTPStatus +from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer from typing import Iterable from xml.etree import ElementTree as ET -from fastapi import FastAPI, HTTPException, Request -from fastapi.responses import Response - -app = FastAPI(title="WordCloud AI Converter", version="0.1.0") - MAX_SVG_BYTES = 25 * 1024 * 1024 SVG_NS = "{http://www.w3.org/2000/svg}" NUMBER = r"[-+]?(?:\d*\.\d+|\d+\.?)(?:[eE][-+]?\d+)?" @@ -25,36 +23,83 @@ PATH_TOKENS = re.compile(rf"[AaCcHhLlMmQqSsTtVvZz]|{NUMBER}") TRANSFORM = re.compile(rf"([A-Za-z]+)\s*\(([^)]*)\)") -@app.get("/health") -def health() -> dict[str, str]: - return {"status": "ok"} - - -@app.post("/convert") -async def convert(request: Request) -> Response: - raw = await request.body() +def convert_svg(raw: bytes) -> bytes: if not raw: - raise HTTPException(status_code=400, detail="SVG 内容不能为空") + raise RequestError(HTTPStatus.BAD_REQUEST, "SVG 内容不能为空") if len(raw) > MAX_SVG_BYTES: - raise HTTPException(status_code=413, detail="SVG 文件超过 25 MB 限制") + raise RequestError(HTTPStatus.REQUEST_ENTITY_TOO_LARGE, "SVG 文件超过 25 MB 限制") if b" None: + if self.path == "/health": + self.send_bytes(HTTPStatus.OK, b'{"status":"ok"}', "application/json") + return + self.send_error(HTTPStatus.NOT_FOUND) + + def do_POST(self) -> None: + if self.path != "/convert": + self.send_error(HTTPStatus.NOT_FOUND) + return + try: + length = int(self.headers.get("Content-Length", "")) + if length < 0: + raise ValueError + except ValueError: + self.send_json_error(HTTPStatus.BAD_REQUEST, "缺少或无效 Content-Length") + return + if length > MAX_SVG_BYTES: + self.send_json_error(HTTPStatus.REQUEST_ENTITY_TOO_LARGE, "SVG 文件超过 25 MB 限制") + return + raw = self.rfile.read(length) + try: + ai = convert_svg(raw) + except RequestError as exc: + self.send_json_error(exc.status, exc.detail) + return + self.send_bytes( + HTTPStatus.OK, + ai, + "application/postscript", + {"Content-Disposition": 'attachment; filename="wordcloud.ai"'}, + ) + + def send_json_error(self, status: HTTPStatus, detail: str) -> None: + body = json.dumps({"detail": detail}, ensure_ascii=False).encode("utf-8") + self.send_bytes(status, body, "application/json; charset=utf-8") + + def send_bytes(self, status: HTTPStatus, body: bytes, content_type: str, headers: dict[str, str] | None = None) -> None: + self.send_response(status) + self.send_header("Content-Type", content_type) + self.send_header("Content-Length", str(len(body))) + for key, value in (headers or {}).items(): + self.send_header(key, value) + self.end_headers() + self.wfile.write(body) + + def log_message(self, _format: str, *_args: object) -> None: + # The reverse proxy already logs requests; avoid echoing SVG payloads. + return class UnsupportedSvg(ValueError): pass @@ -410,3 +455,7 @@ def format_number(value: float) -> str: def n(value: float) -> str: return format_number(value) + + +if __name__ == "__main__": + ThreadingHTTPServer(("0.0.0.0", 8090), ConverterHandler).serve_forever() diff --git a/ai-converter/requirements.txt b/ai-converter/requirements.txt deleted file mode 100644 index a4c245d..0000000 --- a/ai-converter/requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -fastapi>=0.115,<1 -uvicorn[standard]>=0.30,<1