feat: add authenticated artifact streaming and UI visual previews

This commit is contained in:
Space-Banane
2026-05-27 17:50:21 +02:00
parent 10355bf11a
commit 8fe6ad2d75
6 changed files with 184 additions and 57 deletions

View File

@@ -6,6 +6,7 @@ from pathlib import Path
from typing import Any
from fastapi import Depends, FastAPI, Header, HTTPException, Query, WebSocket, WebSocketDisconnect
from fastapi.responses import FileResponse
from fastapi.responses import HTMLResponse, JSONResponse
from pydantic import BaseModel, Field
@@ -86,7 +87,13 @@ def create_app(config: AppConfig | None = None) -> FastAPI:
async def _on_startup() -> None:
ws_hub.set_loop(asyncio.get_running_loop())
def _extract_token(authorization: str | None, x_screenjob_token: str | None) -> str:
def _extract_token(
authorization: str | None,
x_screenjob_token: str | None,
query_token: str | None,
) -> str:
if query_token:
return query_token.strip()
if x_screenjob_token:
return x_screenjob_token.strip()
if authorization:
@@ -99,9 +106,10 @@ def create_app(config: AppConfig | None = None) -> FastAPI:
def require_token(
authorization: str | None = Header(default=None),
x_screenjob_token: str | None = Header(default=None),
token: str | None = Query(default=None),
) -> None:
token = _extract_token(authorization, x_screenjob_token)
if not token or not secrets.compare_digest(token, app_config.screenjob_token):
resolved = _extract_token(authorization, x_screenjob_token, token)
if not resolved or not secrets.compare_digest(resolved, app_config.screenjob_token):
raise HTTPException(status_code=401, detail="Unauthorized")
@app.post("/api/jobs")
@@ -130,6 +138,13 @@ def create_app(config: AppConfig | None = None) -> FastAPI:
raise HTTPException(status_code=404, detail="Job not found")
return job
@app.get("/api/jobs/{job_id}/status")
def get_job_status(job_id: str, _: None = Depends(require_token)) -> dict[str, Any]:
job = manager.get_job(job_id)
if job is None:
raise HTTPException(status_code=404, detail="Job not found")
return job
@app.get("/api/jobs/{job_id}/events")
def get_job_events(
job_id: str,
@@ -149,6 +164,28 @@ def create_app(config: AppConfig | None = None) -> FastAPI:
accepted = manager.cancel_job(job_id)
return {"job_id": job_id, "cancel_requested": bool(accepted)}
@app.get("/api/jobs/{job_id}/artifact")
def get_job_artifact(
job_id: str,
path: str = Query(..., min_length=1),
_: None = Depends(require_token),
) -> FileResponse:
job = manager.get_job(job_id)
if job is None:
raise HTTPException(status_code=404, detail="Job not found")
artifacts_dir_raw = str(job.get("artifacts_dir") or "").strip()
if not artifacts_dir_raw:
raise HTTPException(status_code=404, detail="Artifacts not available yet")
artifacts_dir = Path(artifacts_dir_raw).resolve()
requested = Path(path).resolve()
try:
requested.relative_to(artifacts_dir)
except ValueError as exc:
raise HTTPException(status_code=400, detail="Artifact path is outside job artifacts directory") from exc
if not requested.exists() or not requested.is_file():
raise HTTPException(status_code=404, detail="Artifact not found")
return FileResponse(str(requested))
@app.get("/api/stats")
def stats(_: None = Depends(require_token)) -> dict[str, Any]:
return manager.stats()