mirror of
https://git.victorphan.net/basketballcantho/ten-project.git
synced 2026-08-05 21:23:11 +07:00
155 lines
5.7 KiB
Python
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",
|
|
)
|