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 = 500 * 1024 * 1024 # 500 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 def _any_or_404(db: Session, pdf_id: int) -> PDF: """Return PDF if it exists — any authenticated user may read it.""" pdf = db.get(PDF, pdf_id) if not pdf: raise HTTPException(status_code=404, detail="PDF not found.") return pdf def _pdf_out(pdf: PDF, db: Session): """Build PDFOut including owner_username.""" from ..schemas import PDFOut as _PDFOut owner = db.get(User, pdf.user_id) data = { "id": pdf.id, "user_id": pdf.user_id, "owner_username": owner.username if owner else "unknown", "title": pdf.title, "total_pages": pdf.total_pages, "created_at": pdf.created_at, } return _PDFOut.model_validate(data) # ── GET /api/pdfs ───────────────────────────────────────────────────────────── @router.get("", response_model=list[PDFOut]) def list_pdfs( current_user: User = Depends(get_current_user), db: Session = Depends(get_db), ): pdfs = db.query(PDF).order_by(PDF.created_at.desc()).all() return [_pdf_out(p, db) for p in pdfs] # ── 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 500 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=_pdf_out(pdf, db))) 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 — any authenticated user may read.""" pdf = _any_or_404(db, pdf_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", )