first commit
This commit is contained in:
commit
7a72438fd1
74 changed files with 7548 additions and 0 deletions
180
backend/app/routers/resume.py
Normal file
180
backend/app/routers/resume.py
Normal file
|
|
@ -0,0 +1,180 @@
|
|||
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})
|
||||
Loading…
Add table
Add a link
Reference in a new issue