|
5 | 5 | import asyncio |
6 | 6 | import contextlib |
7 | 7 | import logging |
8 | | -from pathlib import Path |
9 | 8 | from typing import TYPE_CHECKING, Any |
10 | 9 |
|
11 | | -from fastapi import APIRouter, HTTPException, Request, status |
12 | | -from fastapi.responses import FileResponse, HTMLResponse, JSONResponse, StreamingResponse |
| 10 | +from fastapi import APIRouter, HTTPException, Request, Response, status |
| 11 | +from fastapi.responses import HTMLResponse, JSONResponse, StreamingResponse |
13 | 12 |
|
14 | 13 | from api.config import get_settings |
15 | 14 | from api.middleware import limiter |
|
19 | 18 | from api.shared import templates |
20 | 19 | from core.output_formats import OutputFormat |
21 | 20 | from core.progress import ProgressStage |
| 21 | +from storage.factory import get_storage |
22 | 22 |
|
23 | 23 | if TYPE_CHECKING: |
24 | 24 | from collections.abc import AsyncGenerator |
@@ -299,47 +299,40 @@ async def api_ingest_get( |
299 | 299 | @router.get("/api/download/file/{ingest_id}", response_model=None) |
300 | 300 | async def download_ingest( |
301 | 301 | ingest_id: UUID, |
302 | | -) -> FileResponse | JSONResponse: # noqa: FA100 |
| 302 | +) -> Response | JSONResponse: # noqa: FA100 |
303 | 303 | """Download the text file produced for an ingest ID. |
304 | 304 |
|
| 305 | + Uses the configured storage backend (local filesystem or GCS) to |
| 306 | + retrieve the digest content. |
| 307 | +
|
305 | 308 | Parameters |
306 | 309 | ---------- |
307 | 310 | ingest_id : UUID |
308 | 311 | Identifier that the ingest step emitted. |
309 | 312 |
|
310 | 313 | Returns |
311 | 314 | ------- |
312 | | - FileResponse |
| 315 | + Response |
313 | 316 | Streamed response with media type ``text/plain``. |
314 | 317 |
|
315 | 318 | Raises |
316 | 319 | ------ |
317 | 320 | HTTPException |
318 | | - 404 if digest directory is missing or contains no ``.txt`` file. |
319 | | - 403 if there is a permission error reading the file. |
| 321 | + 404 if the digest does not exist in storage. |
320 | 322 |
|
321 | 323 | """ |
322 | | - tmp_base = Path(settings.local_storage_path) |
323 | | - directory = (tmp_base / str(ingest_id)).resolve() |
324 | | - |
325 | | - if not str(directory).startswith(str(tmp_base.resolve())): |
326 | | - raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=f"Invalid ingest ID: {ingest_id!r}") |
| 324 | + storage = get_storage() |
| 325 | + digest_id = str(ingest_id) |
327 | 326 |
|
328 | | - if not directory.is_dir(): |
| 327 | + if not storage.digest_exists(digest_id): |
329 | 328 | raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"Digest {ingest_id!r} not found") |
330 | 329 |
|
331 | | - try: |
332 | | - first_txt_file = next(directory.glob("*.txt")) |
333 | | - except StopIteration as exc: |
334 | | - raise HTTPException( |
335 | | - status_code=status.HTTP_404_NOT_FOUND, |
336 | | - detail=f"No .txt file found for digest {ingest_id!r}", |
337 | | - ) from exc |
| 330 | + content_bytes = storage.get_digest_bytes(digest_id) |
| 331 | + if content_bytes is None: |
| 332 | + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"No content for digest {ingest_id!r}") |
338 | 333 |
|
339 | | - try: |
340 | | - return FileResponse(path=first_txt_file, media_type="text/plain", filename=first_txt_file.name) |
341 | | - except PermissionError as exc: |
342 | | - raise HTTPException( |
343 | | - status_code=status.HTTP_403_FORBIDDEN, |
344 | | - detail=f"Permission denied for {first_txt_file}", |
345 | | - ) from exc |
| 334 | + return Response( |
| 335 | + content=content_bytes, |
| 336 | + media_type="text/plain", |
| 337 | + headers={"Content-Disposition": f'attachment; filename="digest-{ingest_id}.txt"'}, |
| 338 | + ) |
0 commit comments