180 lines
5.8 KiB
Python
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})
|