feat(gateway): proxy artifact downloads via public doc endpoints
This commit is contained in:
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user