first commit

This commit is contained in:
Anurag Jangid 2026-10-08 02:50:35 +05:30
commit 7a72438fd1
74 changed files with 7548 additions and 0 deletions

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