import shutil from pathlib import Path from fastapi import HTTPException, UploadFile from config import ACCEPTED_EXTENSIONS, MAX_UPLOAD_BYTES, TEMP_UPLOADS # Leading "magic bytes" per accepted file type. Used to reject files whose real # content does not match their extension. _MAGIC = { ".pdf": [b"%PDF"], ".png": [b"\x89PNG\r\n\x1a\n"], ".jpg": [b"\xff\xd8\xff"], ".jpeg": [b"\xff\xd8\xff"], } def ensure_dirs(): TEMP_UPLOADS.mkdir(parents=True, exist_ok=True) def validate_upload(doc_type: str, filename: str) -> str: if doc_type not in ACCEPTED_EXTENSIONS: raise HTTPException(status_code=400, detail=f"Invalid doc_type: {doc_type}") ext = Path(filename).suffix.lower() if ext not in ACCEPTED_EXTENSIONS[doc_type]: raise HTTPException( status_code=400, detail=f"Invalid extension {ext} for {doc_type}. Accepted: {sorted(ACCEPTED_EXTENSIONS[doc_type])}", ) return ext def _check_magic(ext: str, head: bytes) -> bool: signatures = _MAGIC.get(ext) if not signatures: return True return any(head.startswith(sig) for sig in signatures) async def save_upload(doc_id: str, file: UploadFile) -> Path: ensure_dirs() dest_dir = TEMP_UPLOADS / doc_id dest_dir.mkdir(parents=True, exist_ok=True) # Never trust the client filename for the path; only use its extension. ext = Path(file.filename or "upload.bin").suffix.lower() dest = dest_dir / f"original{ext}" written = 0 first_chunk = True with dest.open("wb") as f: while True: chunk = await file.read(1024 * 1024) if not chunk: break if first_chunk: if not _check_magic(ext, chunk): f.close() shutil.rmtree(dest_dir, ignore_errors=True) raise HTTPException( status_code=400, detail="File content does not match its extension.", ) first_chunk = False written += len(chunk) if written > MAX_UPLOAD_BYTES: f.close() shutil.rmtree(dest_dir, ignore_errors=True) raise HTTPException( status_code=413, detail=f"File too large. Max {MAX_UPLOAD_BYTES // (1024 * 1024)}MB.", ) f.write(chunk) if written == 0: shutil.rmtree(dest_dir, ignore_errors=True) raise HTTPException(status_code=400, detail="Empty file.") return dest def get_upload_path(doc_id: str) -> Path | None: dest_dir = TEMP_UPLOADS / doc_id if not dest_dir.exists(): return None files = list(dest_dir.glob("original.*")) return files[0] if files else None def delete_upload(doc_id: str): dest_dir = TEMP_UPLOADS / doc_id if dest_dir.exists(): shutil.rmtree(dest_dir, ignore_errors=True)