From 69adfe6abb055482725cf2371a74ae4fc4197cd0 Mon Sep 17 00:00:00 2001 From: Ferdinand Urban Date: Thu, 23 Apr 2026 12:24:42 +0000 Subject: [PATCH] fix: disk leak, upload limit, thread safety, pydantic defaults - #4: background thread deletes export ZIPs older than 1h - #5: 50 MB per-file upload limit via read(MAX+1) guard - #6: replace _jobs .update() with atomic key assignments - #7: ExportRequest mutable dict fields use Field(default_factory=dict) Co-Authored-By: Claude Sonnet 4.6 --- server.py | 39 +++++++++++++++++++++++++++++---------- 1 file changed, 29 insertions(+), 10 deletions(-) diff --git a/server.py b/server.py index 2b17f77..6c5b4a4 100644 --- a/server.py +++ b/server.py @@ -8,13 +8,13 @@ import threading import zipfile from datetime import date from time import time -from typing import List +from typing import List, Optional from dotenv import load_dotenv from fastapi import FastAPI, File, Form, HTTPException, Request, UploadFile from fastapi.responses import FileResponse, JSONResponse, Response, StreamingResponse from fastapi.middleware.cors import CORSMiddleware -from pydantic import BaseModel +from pydantic import BaseModel, Field import secrets import uvicorn from PIL import Image @@ -116,9 +116,9 @@ class ExportRequest(BaseModel): brightness: float = 1.0 contrast: float = 1.0 saturation: float = 1.0 - text_watermark: dict = {} + text_watermark: dict = Field(default_factory=dict) image_watermark_path: str = "" - image_watermark_settings: dict = {} + image_watermark_settings: dict = Field(default_factory=dict) @app.get("/") @@ -128,6 +128,7 @@ def serve_frontend(): UPLOAD_ALLOWED_EXTENSIONS = {".jpg", ".jpeg", ".png", ".webp", ".heic", ".heif"} _HEIC_EXTS = {".heic", ".heif"} +MAX_UPLOAD_BYTES = 50 * 1024 * 1024 # 50 MB per file @app.post("/upload") @@ -143,7 +144,9 @@ async def upload_files(files: List[UploadFile] = File(...), folder: str = Form(" ext = os.path.splitext(file.filename or "")[1].lower() if ext not in UPLOAD_ALLOWED_EXTENSIONS: continue - raw = await file.read() + raw = await file.read(MAX_UPLOAD_BYTES + 1) + if len(raw) > MAX_UPLOAD_BYTES: + continue if ext in _HEIC_EXTS and _HEIF_SUPPORTED: # Convert HEIC/HEIF → JPEG so cv2 and browsers can handle it safe_name = os.path.splitext(os.path.basename(file.filename))[0] + ".jpg" @@ -243,11 +246,9 @@ _PHASE_LABELS = { def _run_analyze_job(job_id: str, req: AnalyzeRequest): try: def on_progress(done, total, phase): - _jobs[job_id].update({ - "done": done, - "total": total, - "phase": _PHASE_LABELS.get(phase, phase), - }) + _jobs[job_id]["done"] = done + _jobs[job_id]["total"] = total + _jobs[job_id]["phase"] = _PHASE_LABELS.get(phase, phase) api_key = os.getenv("ANTHROPIC_API_KEY") if req.use_ai else None results = analyze_folder( @@ -352,6 +353,24 @@ def move_files(req: MoveRequest): _zip_store: dict = {} # zip_id -> path +_ZIP_TTL = 3600 # ZIPs older than 1 h are deleted automatically + + +def _cleanup_zips(): + while True: + import time as _time + _time.sleep(_ZIP_TTL) + cutoff = time() - _ZIP_TTL + for zip_id, path in list(_zip_store.items()): + try: + if os.path.getmtime(path) < cutoff: + os.unlink(path) + _zip_store.pop(zip_id, None) + except OSError: + _zip_store.pop(zip_id, None) + + +threading.Thread(target=_cleanup_zips, daemon=True).start() def _run_export_job(job_id: str, req: ExportRequest):