Hire-ai/backend/app/routers/resume.py
2026-10-08 02:50:35 +05:30

180 lines
5.8 KiB
Python

from fastapi import APIRouter, Depends, HTTPException, UploadFile, File, status
from fastapi.responses import StreamingResponse
from bson import ObjectId
from datetime import datetime, timezone
import io
from app.database import get_db, get_gridfs
from app.dependencies import get_current_user
from app.services.resume_parser import parse_pdf, parse_docx
router = APIRouter(prefix="/api/resume", tags=["resume"])
ALLOWED_TYPES = {
"application/pdf": "pdf",
"application/vnd.openxmlformats-officedocument.wordprocessingml.document": "docx",
"application/msword": "doc",
}
MAX_SIZE_MB = 10
# ── Upload & Parse ──────────────────────────────────────────────
@router.post("/upload", status_code=status.HTTP_201_CREATED)
async def upload_resume(
file: UploadFile = File(...),
db=Depends(get_db),
gridfs=Depends(get_gridfs),
current_user: dict = Depends(get_current_user),
):
# Validate content type
content_type = file.content_type or ""
if content_type not in ALLOWED_TYPES:
raise HTTPException(
status_code=400,
detail="Only PDF and DOCX files are accepted.",
)
file_bytes = await file.read()
# Validate size
if len(file_bytes) > MAX_SIZE_MB * 1024 * 1024:
raise HTTPException(
status_code=400,
detail=f"File too large. Max size is {MAX_SIZE_MB}MB.",
)
# Parse resume
file_type = ALLOWED_TYPES[content_type]
if file_type == "pdf":
parsed = parse_pdf(file_bytes)
else:
parsed = parse_docx(file_bytes)
# Store file in GridFS
gridfs_id = await gridfs.upload_from_stream(
file.filename,
io.BytesIO(file_bytes),
metadata={"content_type": content_type, "uploaded_by": current_user["google_id"]},
)
# Store metadata in MongoDB
doc = {
"filename": file.filename,
"file_type": file_type,
"content_type": content_type,
"uploaded_by": current_user["google_id"],
"gridfs_id": str(gridfs_id),
"parsed": parsed,
"uploaded_at": datetime.now(timezone.utc),
}
result = await db["resumes"].insert_one(doc)
return {
"id": str(result.inserted_id),
"filename": file.filename,
"file_type": file_type,
"parsed": parsed,
"uploaded_at": doc["uploaded_at"].isoformat(),
"message": "Resume uploaded and parsed successfully",
}
# ── List all resumes for current user ──────────────────────────
@router.get("/")
async def list_resumes(
db=Depends(get_db),
current_user: dict = Depends(get_current_user),
):
cursor = db["resumes"].find(
{"uploaded_by": current_user["google_id"]},
{"parsed.raw_text": 0}, # exclude raw text from list view
).sort("uploaded_at", -1)
resumes = []
async for doc in cursor:
doc["id"] = str(doc.pop("_id"))
doc["uploaded_at"] = doc["uploaded_at"].isoformat() if doc.get("uploaded_at") else ""
resumes.append(doc)
return resumes
# ── Get single resume ──────────────────────────────────────────
@router.get("/{resume_id}")
async def get_resume(
resume_id: str,
db=Depends(get_db),
current_user: dict = Depends(get_current_user),
):
try:
oid = ObjectId(resume_id)
except Exception:
raise HTTPException(status_code=400, detail="Invalid resume ID")
doc = await db["resumes"].find_one(
{"_id": oid, "uploaded_by": current_user["google_id"]}
)
if not doc:
raise HTTPException(status_code=404, detail="Resume not found")
doc["id"] = str(doc.pop("_id"))
doc["uploaded_at"] = doc["uploaded_at"].isoformat() if doc.get("uploaded_at") else ""
return doc
# ── Download original file ─────────────────────────────────────
@router.get("/{resume_id}/download")
async def download_resume(
resume_id: str,
db=Depends(get_db),
gridfs=Depends(get_gridfs),
current_user: dict = Depends(get_current_user),
):
try:
oid = ObjectId(resume_id)
except Exception:
raise HTTPException(status_code=400, detail="Invalid resume ID")
doc = await db["resumes"].find_one(
{"_id": oid, "uploaded_by": current_user["google_id"]}
)
if not doc:
raise HTTPException(status_code=404, detail="Resume not found")
gridfs_id = ObjectId(doc["gridfs_id"])
stream = await gridfs.open_download_stream(gridfs_id)
content = await stream.read()
return StreamingResponse(
io.BytesIO(content),
media_type=doc["content_type"],
headers={"Content-Disposition": f'attachment; filename="{doc["filename"]}"'},
)
# ── Delete resume ──────────────────────────────────────────────
@router.delete("/{resume_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_resume(
resume_id: str,
db=Depends(get_db),
gridfs=Depends(get_gridfs),
current_user: dict = Depends(get_current_user),
):
try:
oid = ObjectId(resume_id)
except Exception:
raise HTTPException(status_code=400, detail="Invalid resume ID")
doc = await db["resumes"].find_one(
{"_id": oid, "uploaded_by": current_user["google_id"]}
)
if not doc:
raise HTTPException(status_code=404, detail="Resume not found")
# Delete from GridFS
try:
await gridfs.delete(ObjectId(doc["gridfs_id"]))
except Exception:
pass # GridFS file may already be gone
await db["resumes"].delete_one({"_id": oid})