mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-06-04 20:05:16 +02:00
90 lines
3.1 KiB
Python
90 lines
3.1 KiB
Python
|
|
"""HTTP routes for document file storage (metadata listing + original download)."""
|
||
|
|
|
||
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
from urllib.parse import quote
|
||
|
|
|
||
|
|
from fastapi import APIRouter, Depends, HTTPException
|
||
|
|
from fastapi.responses import StreamingResponse
|
||
|
|
from sqlalchemy import select
|
||
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||
|
|
|
||
|
|
from app.db import Document, Permission, User, get_async_session
|
||
|
|
from app.file_storage.persistence.enums import DocumentFileKind
|
||
|
|
from app.file_storage.schemas import DocumentFileRead
|
||
|
|
from app.file_storage.service import (
|
||
|
|
get_document_file,
|
||
|
|
list_document_files,
|
||
|
|
open_document_file_stream,
|
||
|
|
)
|
||
|
|
from app.users import current_active_user
|
||
|
|
from app.utils.rbac import check_permission
|
||
|
|
|
||
|
|
router = APIRouter()
|
||
|
|
|
||
|
|
|
||
|
|
async def _load_readable_document(
|
||
|
|
*, document_id: int, session: AsyncSession, user: User
|
||
|
|
) -> Document:
|
||
|
|
"""Load a document the user may read, or raise 404/403."""
|
||
|
|
document = (
|
||
|
|
await session.execute(select(Document).where(Document.id == document_id))
|
||
|
|
).scalar_one_or_none()
|
||
|
|
if document is None:
|
||
|
|
raise HTTPException(status_code=404, detail="Document not found")
|
||
|
|
|
||
|
|
await check_permission(
|
||
|
|
session,
|
||
|
|
user,
|
||
|
|
document.search_space_id,
|
||
|
|
Permission.DOCUMENTS_READ.value,
|
||
|
|
"You don't have permission to read documents in this search space",
|
||
|
|
)
|
||
|
|
return document
|
||
|
|
|
||
|
|
|
||
|
|
def _content_disposition(filename: str) -> str:
|
||
|
|
"""Build an attachment header safe for arbitrary filenames (RFC 5987)."""
|
||
|
|
fallback = filename.encode("ascii", "ignore").decode("ascii") or "download"
|
||
|
|
fallback = fallback.replace('"', "")
|
||
|
|
return f"attachment; filename=\"{fallback}\"; filename*=UTF-8''{quote(filename)}"
|
||
|
|
|
||
|
|
|
||
|
|
@router.get(
|
||
|
|
"/documents/{document_id}/files",
|
||
|
|
response_model=list[DocumentFileRead],
|
||
|
|
)
|
||
|
|
async def read_document_files(
|
||
|
|
document_id: int,
|
||
|
|
session: AsyncSession = Depends(get_async_session),
|
||
|
|
user: User = Depends(current_active_user),
|
||
|
|
) -> list[DocumentFileRead]:
|
||
|
|
"""Return metadata for every stored file of a document (gates the UI)."""
|
||
|
|
await _load_readable_document(document_id=document_id, session=session, user=user)
|
||
|
|
records = await list_document_files(session, document_id=document_id)
|
||
|
|
return [DocumentFileRead.model_validate(r) for r in records]
|
||
|
|
|
||
|
|
|
||
|
|
@router.get("/documents/{document_id}/download-original")
|
||
|
|
async def download_original_document_file(
|
||
|
|
document_id: int,
|
||
|
|
session: AsyncSession = Depends(get_async_session),
|
||
|
|
user: User = Depends(current_active_user),
|
||
|
|
) -> StreamingResponse:
|
||
|
|
"""Stream the document's original uploaded file."""
|
||
|
|
await _load_readable_document(document_id=document_id, session=session, user=user)
|
||
|
|
|
||
|
|
record = await get_document_file(
|
||
|
|
session, document_id=document_id, kind=DocumentFileKind.ORIGINAL
|
||
|
|
)
|
||
|
|
if record is None:
|
||
|
|
raise HTTPException(
|
||
|
|
status_code=404, detail="No original file stored for this document"
|
||
|
|
)
|
||
|
|
|
||
|
|
return StreamingResponse(
|
||
|
|
open_document_file_stream(record),
|
||
|
|
media_type=record.mime_type or "application/octet-stream",
|
||
|
|
headers={"Content-Disposition": _content_disposition(record.original_filename)},
|
||
|
|
)
|