From cc1ea166c86671eb5e88bce136ad732b24b6b2b8 Mon Sep 17 00:00:00 2001 From: root Date: Thu, 28 May 2026 15:40:02 +0000 Subject: [PATCH] update --- backend/database.py | 23 ++++++++++++++++++----- backend/generator.py | 38 +++++++++++++++++++++++--------------- backend/models.py | 2 -- backend/paths.py | 17 +++++++++++++++++ backend/routes.py | 35 +++++++++++++++++------------------ 5 files changed, 75 insertions(+), 40 deletions(-) create mode 100644 backend/paths.py diff --git a/backend/database.py b/backend/database.py index a13cd60..3bd6f84 100644 --- a/backend/database.py +++ b/backend/database.py @@ -1,5 +1,6 @@ import aiosqlite -from config import DB_PATH +from config import DB_PATH, STORAGE_DIR +from paths import final_paths CREATE_GUIDES = """ CREATE TABLE IF NOT EXISTS guides ( @@ -10,8 +11,6 @@ CREATE TABLE IF NOT EXISTS guides ( status TEXT NOT NULL DEFAULT 'queued', progress TEXT, error_msg TEXT, - html_path TEXT, - pdf_path TEXT, created_at TEXT NOT NULL, updated_at TEXT NOT NULL ) @@ -68,6 +67,20 @@ async def init_db(): "WHERE status IN ('queued', 'generating')" ) await db.commit() + await _migrate_uuid_filenames(db) + + +async def _migrate_uuid_filenames(db: aiosqlite.Connection) -> None: + cursor = await db.execute("SELECT id, topic, format FROM guides WHERE status = 'done'") + rows = await cursor.fetchall() + for guide_id, topic, format_name in rows: + final_html, final_pdf = final_paths(topic, format_name) + old_html = STORAGE_DIR / "html" / f"{guide_id}.html" + old_pdf = STORAGE_DIR / "pdf" / f"{guide_id}.pdf" + if old_html.exists() and not final_html.exists(): + old_html.rename(final_html) + if old_pdf.exists() and not final_pdf.exists(): + old_pdf.rename(final_pdf) async def close_db(): @@ -85,8 +98,8 @@ def _row_to_dict(row, cursor): async def create_guide(guide: dict) -> dict: db = await get_db() await db.execute( - """INSERT INTO guides (id, topic, format, instructions, status, progress, html_path, pdf_path, created_at, updated_at) - VALUES (:id, :topic, :format, :instructions, :status, :progress, :html_path, :pdf_path, :created_at, :updated_at)""", + """INSERT INTO guides (id, topic, format, instructions, status, progress, created_at, updated_at) + VALUES (:id, :topic, :format, :instructions, :status, :progress, :created_at, :updated_at)""", guide, ) await db.commit() diff --git a/backend/generator.py b/backend/generator.py index 78724c5..d00d134 100644 --- a/backend/generator.py +++ b/backend/generator.py @@ -1,6 +1,7 @@ import asyncio import json import re +import shutil import subprocess import tempfile import uuid @@ -22,6 +23,7 @@ from database import ( list_bausteine, update_baustein, ) +from paths import final_paths, temp_paths _semaphore = asyncio.Semaphore(MAX_CONCURRENT_GENERATIONS) _active_processes: dict[str, asyncio.subprocess.Process] = {} @@ -174,8 +176,7 @@ async def generate_guide(guide_id: str, topic: str, format_name: str, instructio now = datetime.now(timezone.utc).isoformat() await update_guide(guide_id, status="generating", progress="Recherche…", updated_at=now) - html_path = STORAGE_DIR / "html" / f"{guide_id}.html" - pdf_path = STORAGE_DIR / "pdf" / f"{guide_id}.pdf" + html_path, pdf_path = final_paths(topic, format_name) try: if guide_id in _cancelled: @@ -241,12 +242,7 @@ async def generate_guide(guide_id: str, topic: str, format_name: str, instructio now = datetime.now(timezone.utc).isoformat() await update_guide( - guide_id, - status="done", - progress=None, - html_path=str(html_path), - pdf_path=str(pdf_path), - updated_at=now, + guide_id, status="done", progress=None, updated_at=now, ) except asyncio.TimeoutError: @@ -263,16 +259,23 @@ async def rework_guide(guide_id: str, topic: str, format_name: str, instructions now = datetime.now(timezone.utc).isoformat() await update_guide(guide_id, status="generating", progress="Überarbeite…", updated_at=now) - html_path = STORAGE_DIR / "html" / f"{guide_id}.html" - pdf_path = STORAGE_DIR / "pdf" / f"{guide_id}.pdf" + final_html, final_pdf = final_paths(topic, format_name) + tmp_html, tmp_pdf = temp_paths(guide_id) + try: if guide_id in _cancelled: return + if not final_html.exists(): + await _fail(guide_id, "Original-HTML nicht gefunden") + return + + shutil.copy2(final_html, tmp_html) + current_step = "Überarbeitung" current_timeout = AGENT_TIMEOUT - rework_prompt = _build_rework_prompt(topic, format_name, html_path, instructions) + rework_prompt = _build_rework_prompt(topic, format_name, tmp_html, instructions) returncode, stdout, stderr = await _run_claude(guide_id, rework_prompt, AGENT_TIMEOUT) if guide_id in _cancelled: @@ -281,20 +284,23 @@ async def rework_guide(guide_id: str, topic: str, format_name: str, instructions await _fail(guide_id, f"Rework-Fehler: {stderr[:1000]}") return - if not html_path.exists(): + if not tmp_html.exists(): await _fail(guide_id, "HTML-Datei wurde nicht erstellt") return await _set_progress(guide_id, "Rendere PDF…") - ok, err = await _render_pdf(html_path, pdf_path) + ok, err = await _render_pdf(tmp_html, tmp_pdf) if not ok: await _fail(guide_id, f"WeasyPrint-Fehler: {err}") return + # Atomar: Temp → Final umbenennen + tmp_html.replace(final_html) + tmp_pdf.replace(final_pdf) + now = datetime.now(timezone.utc).isoformat() await update_guide( - guide_id, status="done", progress=None, - html_path=str(html_path), pdf_path=str(pdf_path), updated_at=now, + guide_id, status="done", progress=None, updated_at=now, ) except asyncio.TimeoutError: @@ -304,6 +310,8 @@ async def rework_guide(guide_id: str, topic: str, format_name: str, instructions finally: _active_processes.pop(guide_id, None) _cancelled.discard(guide_id) + tmp_html.unlink(missing_ok=True) + tmp_pdf.unlink(missing_ok=True) async def _fail(guide_id: str, msg: str) -> None: diff --git a/backend/models.py b/backend/models.py index 7f4be73..48feaf0 100644 --- a/backend/models.py +++ b/backend/models.py @@ -28,8 +28,6 @@ class GuideResponse(BaseModel): status: str progress: str | None = None error_msg: str | None = None - html_path: str | None = None - pdf_path: str | None = None created_at: str updated_at: str diff --git a/backend/paths.py b/backend/paths.py new file mode 100644 index 0000000..d561583 --- /dev/null +++ b/backend/paths.py @@ -0,0 +1,17 @@ +from pathlib import Path + +from config import STORAGE_DIR + + +def safe_basename(topic: str, format_name: str) -> str: + clean = topic.replace("/", "_").replace("\x00", "") + return f"{clean} - {format_name}" + + +def final_paths(topic: str, format_name: str) -> tuple[Path, Path]: + base = safe_basename(topic, format_name) + return STORAGE_DIR / "html" / f"{base}.html", STORAGE_DIR / "pdf" / f"{base}.pdf" + + +def temp_paths(guide_id: str) -> tuple[Path, Path]: + return STORAGE_DIR / "html" / f"{guide_id}.tmp.html", STORAGE_DIR / "pdf" / f"{guide_id}.tmp.pdf" diff --git a/backend/routes.py b/backend/routes.py index e3ffcd8..53ce0f0 100644 --- a/backend/routes.py +++ b/backend/routes.py @@ -1,12 +1,11 @@ import asyncio import uuid from datetime import datetime, timezone -from pathlib import Path from fastapi import APIRouter, HTTPException from fastapi.responses import FileResponse -from config import FORMAT_META, STORAGE_DIR +from config import FORMAT_META from database import ( create_guide, delete_guide, get_guide, list_guides, create_baustein as db_create_baustein, list_bausteine, get_baustein, delete_baustein as db_delete_baustein, @@ -17,6 +16,7 @@ from models import ( GuideCreateRequest, GuideReworkRequest, GuideResponse, BausteinCreateRequest, BausteinResponse, SuggestionResponse, ) +from paths import final_paths router = APIRouter(prefix="/api") @@ -36,8 +36,6 @@ async def create(req: GuideCreateRequest): "instructions": req.instructions.strip(), "status": "queued", "progress": None, - "html_path": None, - "pdf_path": None, "created_at": now, "updated_at": now, } @@ -64,12 +62,12 @@ async def download_html(guide_id: str): guide = await get_guide(guide_id) if guide is None: raise HTTPException(404, "Guide nicht gefunden") - if guide["status"] != "done" or not guide["html_path"]: + if guide["status"] != "done": raise HTTPException(404, "HTML nicht verfügbar") - path = Path(guide["html_path"]) - if not path.exists(): + html_path, _ = final_paths(guide["topic"], guide["format"]) + if not html_path.exists(): raise HTTPException(404, "Datei nicht gefunden") - return FileResponse(path, filename=f"{guide['topic']}-{guide['format']}.html", media_type="text/html") + return FileResponse(html_path, filename=html_path.name, media_type="text/html") @router.post("/guides/{guide_id}/rework") @@ -96,12 +94,12 @@ async def download_pdf(guide_id: str): guide = await get_guide(guide_id) if guide is None: raise HTTPException(404, "Guide nicht gefunden") - if guide["status"] != "done" or not guide["pdf_path"]: + if guide["status"] != "done": raise HTTPException(404, "PDF nicht verfügbar") - path = Path(guide["pdf_path"]) - if not path.exists(): + _, pdf_path = final_paths(guide["topic"], guide["format"]) + if not pdf_path.exists(): raise HTTPException(404, "Datei nicht gefunden") - return FileResponse(path, filename=f"{guide['topic']}-{guide['format']}.pdf", media_type="application/pdf") + return FileResponse(pdf_path, filename=pdf_path.name, media_type="application/pdf") @router.delete("/guides/{guide_id}") @@ -109,10 +107,9 @@ async def remove(guide_id: str): guide = await get_guide(guide_id) if guide is None: raise HTTPException(404, "Guide nicht gefunden") - if guide["html_path"]: - Path(guide["html_path"]).unlink(missing_ok=True) - if guide["pdf_path"]: - Path(guide["pdf_path"]).unlink(missing_ok=True) + html_path, pdf_path = final_paths(guide["topic"], guide["format"]) + html_path.unlink(missing_ok=True) + pdf_path.unlink(missing_ok=True) await delete_guide(guide_id) return {"ok": True} @@ -165,8 +162,10 @@ async def trigger_suggestions(topic: str): guides = await list_guides() html_paths = [] for g in guides: - if g["topic"] == topic and g["status"] == "done" and g["html_path"]: - html_paths.append(Path(g["html_path"])) + if g["topic"] == topic and g["status"] == "done": + html_path, _ = final_paths(g["topic"], g["format"]) + if html_path.exists(): + html_paths.append(html_path) asyncio.create_task(generate_suggestions(topic, html_paths)) return {"ok": True}