feat: organizer Android app and token-gated upload API
CI / lint-and-test (push) Failing after 1m18s
CI / lint-and-test (push) Failing after 1m18s
Replace Syncthing as the product upload path with a share-target app and POST /api/jobs. Web UI live-polls job progress. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+25
-45
@@ -3,9 +3,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import re
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
from urllib.parse import quote
|
||||
|
||||
from fastapi import FastAPI, Form, HTTPException, Query, Request
|
||||
@@ -13,40 +11,35 @@ from fastapi.responses import FileResponse, HTMLResponse, RedirectResponse
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from fastapi.templating import Jinja2Templates
|
||||
|
||||
from . import config, images, pipeline, remix
|
||||
from . import api, config, images, jobs, pipeline, remix
|
||||
from .logging_config import configure_logging
|
||||
|
||||
configure_logging()
|
||||
logger = logging.getLogger("livef12.web")
|
||||
|
||||
app = FastAPI(title=config.SITE_TITLE)
|
||||
app.include_router(api.router)
|
||||
|
||||
BASE_DIR = Path(__file__).resolve().parent
|
||||
templates = Jinja2Templates(directory=str(BASE_DIR / "templates"))
|
||||
templates.env.globals["img_attrs"] = images.img_attrs
|
||||
templates.env.globals["PAIR_SIZES"] = images.PAIR_SIZES
|
||||
templates.env.globals["GRID_SIZES"] = images.GRID_SIZES
|
||||
templates.env.globals["status_label"] = jobs.status_label
|
||||
templates.env.globals["stage_label"] = jobs.stage_label
|
||||
templates.env.globals["ACTIVE_STATUSES"] = jobs.ACTIVE_STATUSES
|
||||
app.mount("/static", StaticFiles(directory=str(BASE_DIR / "static")), name="static")
|
||||
|
||||
# Job id = sanitized source stem (may include dots).
|
||||
_JOB_ID_RE = re.compile(r"^[A-Za-z0-9._-]+$")
|
||||
|
||||
|
||||
def _validate_job_id(job_id: str) -> str:
|
||||
raw = job_id or ""
|
||||
if not raw.strip() or "/" in raw or "\\" in raw or ".." in raw:
|
||||
raise HTTPException(status_code=400, detail="Ungueltige Job-ID")
|
||||
stem = pipeline.sanitize_stem(raw)
|
||||
if not _JOB_ID_RE.match(stem):
|
||||
raise HTTPException(status_code=400, detail="Ungueltige Job-ID")
|
||||
return stem
|
||||
try:
|
||||
return jobs.validate_job_id(job_id)
|
||||
except jobs.JobIdError as exc:
|
||||
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
||||
|
||||
|
||||
def _job_exists(job_id: str) -> str:
|
||||
stem = _validate_job_id(job_id)
|
||||
if pipeline.read_status(stem) is None and not pipeline.job_paths(stem).original.exists():
|
||||
raise HTTPException(status_code=404, detail="Job nicht gefunden")
|
||||
return stem
|
||||
return jobs.require_job_id(job_id)
|
||||
|
||||
|
||||
def _safe_job_file(job_id: str, filename: str) -> Path:
|
||||
@@ -54,9 +47,9 @@ def _safe_job_file(job_id: str, filename: str) -> Path:
|
||||
stem = _validate_job_id(job_id)
|
||||
name = Path(filename).name
|
||||
if name != filename or "/" in filename or "\\" in filename or name.startswith("."):
|
||||
raise HTTPException(status_code=400, detail="Ungueltiger Pfad")
|
||||
raise HTTPException(status_code=400, detail="Ungültiger Pfad")
|
||||
if not name.startswith(f"{stem}_"):
|
||||
raise HTTPException(status_code=400, detail="Ungueltiger Pfad")
|
||||
raise HTTPException(status_code=400, detail="Ungültiger Pfad")
|
||||
|
||||
for directory in (config.VARIANTS_DIR, config.INTERMEDIATES_DIR):
|
||||
candidate = (directory / name).resolve()
|
||||
@@ -67,30 +60,17 @@ def _safe_job_file(job_id: str, filename: str) -> Path:
|
||||
raise HTTPException(status_code=404, detail="Datei nicht gefunden")
|
||||
|
||||
|
||||
def list_jobs() -> list[dict[str, Any]]:
|
||||
jobs = []
|
||||
for job_id in pipeline.list_job_ids():
|
||||
data = pipeline.read_meta(job_id) or {}
|
||||
variants = data.get("variants") or []
|
||||
jobs.append(
|
||||
{
|
||||
"job_id": job_id,
|
||||
"status": data.get("status", "unknown"),
|
||||
"created_at": data.get("created_at") or "",
|
||||
"variant_count": len(variants),
|
||||
"thumbnail": variants[-1].get("file") if variants else None,
|
||||
}
|
||||
)
|
||||
jobs.sort(key=lambda j: j["created_at"] or j["job_id"], reverse=True)
|
||||
return jobs
|
||||
|
||||
|
||||
@app.get("/", response_class=HTMLResponse)
|
||||
def index(request: Request) -> HTMLResponse:
|
||||
job_list = jobs.list_jobs()
|
||||
return templates.TemplateResponse(
|
||||
request,
|
||||
"index.html",
|
||||
{"jobs": list_jobs(), "site_title": config.SITE_TITLE},
|
||||
{
|
||||
"jobs": job_list,
|
||||
"site_title": config.SITE_TITLE,
|
||||
"has_active": any(j["status"] in jobs.ACTIVE_STATUSES for j in job_list),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -100,11 +80,7 @@ def job_detail(request: Request, job_id: str) -> HTMLResponse:
|
||||
paths = pipeline.job_paths(stem)
|
||||
status = pipeline.read_status(stem) or {}
|
||||
manifest = pipeline.read_manifest(stem) or {"variants": []}
|
||||
intermediates = (
|
||||
sorted(p.name for p in paths.intermediates.glob(f"{stem}_*.png") if p.is_file())
|
||||
if paths.intermediates.exists()
|
||||
else []
|
||||
)
|
||||
intermediates = jobs.list_intermediates(stem)
|
||||
|
||||
return templates.TemplateResponse(
|
||||
request,
|
||||
@@ -118,6 +94,7 @@ def job_detail(request: Request, job_id: str) -> HTMLResponse:
|
||||
"rembg_name": paths.rembg.name if paths.rembg.exists() else None,
|
||||
"has_rembg": paths.rembg.exists(),
|
||||
"intermediates": intermediates,
|
||||
"live": (status.get("status") or "") in jobs.ACTIVE_STATUSES,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -134,7 +111,7 @@ def job_file(
|
||||
if w not in images.SRCSET_WIDTHS:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"Ungueltige Breite (erlaubt: {', '.join(map(str, images.SRCSET_WIDTHS))})",
|
||||
detail=f"Ungültige Breite (erlaubt: {', '.join(map(str, images.SRCSET_WIDTHS))})",
|
||||
)
|
||||
try:
|
||||
cached = images.get_or_create_resized(path, w)
|
||||
@@ -154,7 +131,10 @@ def remix_form(
|
||||
stem = _job_exists(job_id)
|
||||
paths = pipeline.job_paths(stem)
|
||||
if not paths.rembg.exists():
|
||||
raise HTTPException(status_code=409, detail="Job hat noch kein Rembg-Ergebnis, Remix noch nicht moeglich.")
|
||||
raise HTTPException(
|
||||
status_code=409,
|
||||
detail="Job hat noch kein Rembg-Ergebnis, Remix noch nicht möglich.",
|
||||
)
|
||||
|
||||
assets = pipeline.load_assets()
|
||||
options = remix.build_remix_options(assets)
|
||||
|
||||
Reference in New Issue
Block a user