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})