Files
LMS/backend/app/routers/pdfs.py
T

155 lines
5.7 KiB
Python

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",
)