import os import uuid from pathlib import Path from fastapi import APIRouter, Depends, HTTPException, UploadFile, status from fastapi.responses import FileResponse from sqlalchemy.orm import Session from ..database import get_db from ..dependencies import get_current_user from ..models import PDF, User from ..schemas import PDFOut, PDFUploadResult router = APIRouter(prefix="/pdfs", tags=["pdfs"]) UPLOAD_DIR = Path(os.getenv("UPLOAD_DIR", "/uploads")) MAX_FILE_SIZE = 50 * 1024 * 1024 # 50 MB def _user_dir(user_id: int) -> Path: d = UPLOAD_DIR / str(user_id) d.mkdir(parents=True, exist_ok=True) return d def _own_or_404(db: Session, pdf_id: int, user_id: int) -> PDF: pdf = db.get(PDF, pdf_id) if not pdf or pdf.user_id != user_id: raise HTTPException(status_code=404, detail="PDF not found.") return pdf # ── GET /api/pdfs ───────────────────────────────────────────────────────────── @router.get("", response_model=list[PDFOut]) def list_pdfs( current_user: User = Depends(get_current_user), db: Session = Depends(get_db), ): return ( db.query(PDF) .filter(PDF.user_id == current_user.id) .order_by(PDF.created_at.desc()) .all() ) # ── POST /api/pdfs/upload ───────────────────────────────────────────────────── @router.post("/upload", response_model=list[PDFUploadResult], status_code=status.HTTP_201_CREATED) async def upload_pdfs( files: list[UploadFile], current_user: User = Depends(get_current_user), db: Session = Depends(get_db), ): """Accept one or more PDF files in a single multipart request.""" results: list[PDFUploadResult] = [] for file in files: # ── Validate MIME type ──────────────────────────────────────────────── if file.content_type not in ("application/pdf", "application/octet-stream"): results.append(PDFUploadResult(filename=file.filename or "", success=False, error="Not a PDF file.")) continue # ── Read & check magic bytes (PDF header: %PDF) ─────────────────────── header = await file.read(5) if not header.startswith(b"%PDF-"): results.append(PDFUploadResult(filename=file.filename or "", success=False, error="File is not a valid PDF.")) continue # ── Size guard ──────────────────────────────────────────────────────── body = header + await file.read() if len(body) > MAX_FILE_SIZE: results.append(PDFUploadResult(filename=file.filename or "", success=False, error="File exceeds 50 MB limit.")) continue # ── Save to disk with UUID filename (prevents path traversal) ───────── safe_name = f"{uuid.uuid4()}.pdf" dest = _user_dir(current_user.id) / safe_name dest.write_bytes(body) # ── Persist metadata ────────────────────────────────────────────────── original_title = Path(file.filename or "Untitled").stem or "Untitled" pdf = PDF( user_id=current_user.id, title=original_title, file_path=str(dest), ) db.add(pdf) db.commit() db.refresh(pdf) results.append(PDFUploadResult(filename=file.filename or "", success=True, pdf=PDFOut.model_validate(pdf))) return results # ── DELETE /api/pdfs/{pdf_id} ───────────────────────────────────────────────── @router.delete("/{pdf_id}", status_code=status.HTTP_204_NO_CONTENT) def delete_pdf( pdf_id: int, current_user: User = Depends(get_current_user), db: Session = Depends(get_db), ): pdf = _own_or_404(db, pdf_id, current_user.id) file_path = Path(pdf.file_path) db.delete(pdf) db.commit() if file_path.exists(): file_path.unlink() # ── GET /api/pdfs/{pdf_id}/file ─────────────────────────────────────────────── @router.get("/{pdf_id}/file") def serve_pdf( pdf_id: int, current_user: User = Depends(get_current_user), db: Session = Depends(get_db), ): """Serve the raw PDF bytes — only to the owning user.""" pdf = _own_or_404(db, pdf_id, current_user.id) file_path = Path(pdf.file_path) if not file_path.exists(): raise HTTPException(status_code=404, detail="File not found on disk.") return FileResponse( path=str(file_path), media_type="application/pdf", filename=f"{pdf.title}.pdf", )