feat(gateway): proxy artifact downloads via public doc endpoints

This commit is contained in:
NODA1 System
2026-02-21 17:22:06 +01:00
parent cca16254e5
commit 088ca07137
2 changed files with 77 additions and 9 deletions

View File

@@ -9,11 +9,16 @@ Endpoints:
- POST /api/doc/update - Update existing document text (versioned)
- POST /api/doc/publish - Publish physical file version via artifact registry
- GET /api/doc/versions/{doc_id} - List document versions
- GET /api/doc/artifacts/{artifact_id}/versions/{version_id}/download - Download via gateway proxy
"""
import logging
import os
import re
from typing import Optional, Dict, Any
from fastapi import APIRouter, HTTPException, UploadFile, File, Form
from fastapi.responses import Response
from pydantic import BaseModel
import httpx
from services.doc_service import (
doc_service,
@@ -34,6 +39,8 @@ from services.doc_service import (
logger = logging.getLogger(__name__)
router = APIRouter()
ARTIFACT_REGISTRY_URL = os.getenv("ARTIFACT_REGISTRY_URL", "http://artifact-registry:9220").rstrip("/")
DOC_DOWNLOAD_TIMEOUT_SECONDS = float(os.getenv("DOC_DOWNLOAD_TIMEOUT_SECONDS", "60"))
# ========================================
@@ -402,3 +409,57 @@ async def get_document_context(session_id: str):
except Exception as e:
logger.error(f"Get document context error: {e}", exc_info=True)
raise HTTPException(status_code=500, detail=str(e))
@router.get("/api/doc/artifacts/{artifact_id}/versions/{version_id}/download")
async def download_artifact_version_via_gateway(
artifact_id: str,
version_id: str,
filename: Optional[str] = None,
inline: bool = False,
):
"""
Proxy download for artifact version to avoid exposing internal MinIO host to browser clients.
"""
aid = (artifact_id or "").strip()
vid = (version_id or "").strip()
if not aid or not vid:
raise HTTPException(status_code=400, detail="artifact_id and version_id are required")
try:
async with httpx.AsyncClient(timeout=DOC_DOWNLOAD_TIMEOUT_SECONDS) as client:
meta_resp = await client.get(
f"{ARTIFACT_REGISTRY_URL}/artifacts/{aid}/versions/{vid}/download"
)
if meta_resp.status_code >= 400:
detail = ""
try:
detail = meta_resp.json().get("detail") # type: ignore[assignment]
except Exception:
detail = meta_resp.text[:200]
raise HTTPException(status_code=meta_resp.status_code, detail=detail or "Version download info failed")
meta = meta_resp.json()
signed_url = (meta.get("url") or "").strip()
if not signed_url:
raise HTTPException(status_code=502, detail="artifact-registry returned empty download URL")
file_resp = await client.get(signed_url)
if file_resp.status_code >= 400:
raise HTTPException(status_code=502, detail=f"Artifact storage download failed: {file_resp.status_code}")
mime = (meta.get("mime") or file_resp.headers.get("content-type") or "application/octet-stream").strip()
storage_key = str(meta.get("storage_key") or "")
inferred_name = storage_key.rsplit("/", 1)[-1] if "/" in storage_key else storage_key
out_name = (filename or inferred_name or f"{aid}_{vid}.bin").strip()
out_name = re.sub(r"[^A-Za-z0-9._-]+", "_", out_name).strip("._") or f"{aid}_{vid}.bin"
disposition = "inline" if inline else "attachment"
headers = {
"Content-Disposition": f'{disposition}; filename="{out_name}"',
"Cache-Control": "private, max-age=60",
}
return Response(content=file_resp.content, media_type=mime, headers=headers)
except HTTPException:
raise
except Exception as e:
logger.error(f"Artifact version proxy download failed: aid={aid}, vid={vid}, err={e}", exc_info=True)
raise HTTPException(status_code=500, detail="Artifact proxy download failed")