""" Validation utilities for file upload pipeline. """ import os from fastapi import UploadFile, HTTPException STRUCTURED_EXTENSIONS = {"csv", "xlsx"} UNSTRUCTURED_EXTENSIONS = {"pdf", "txt", "doc", "docx", "ppt", "pptx", "jpg", "jpeg", "png"} ALLOWED_EXTENSIONS = STRUCTURED_EXTENSIONS | UNSTRUCTURED_EXTENSIONS MIME_TYPE_MAP = { "csv": ["text/csv", "application/csv", "text/plain"], "xlsx": ["application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"], "pdf": ["application/pdf"], "txt": ["text/plain"], "doc": ["application/msword"], "docx": ["application/vnd.openxmlformats-officedocument.wordprocessingml.document"], "ppt": ["application/vnd.ms-powerpoint"], "pptx": ["application/vnd.openxmlformats-officedocument.presentationml.presentation"], "jpg": ["image/jpeg"], "jpeg": ["image/jpeg"], "png": ["image/png"], } def get_file_extension(filename: str) -> str: """Extract and normalize file extension.""" if not filename or "." not in filename: return "" return filename.rsplit(".", 1)[-1].lower() def validate_file_extension(filename: str) -> str: """ Validate that file has an allowed extension. Returns the extension if valid, raises HTTPException otherwise. """ ext = get_file_extension(filename) if not ext: raise HTTPException( status_code=400, detail="File must have a valid extension." ) if ext not in ALLOWED_EXTENSIONS: raise HTTPException( status_code=400, detail=f"Unsupported file type: .{ext}. Allowed types: {', '.join(sorted(ALLOWED_EXTENSIONS))}" ) return ext def validate_file_size(file: UploadFile) -> None: """ Validate that file is not empty. Raises HTTPException if file size is 0. """ file.file.seek(0, os.SEEK_END) size = file.file.tell() file.file.seek(0) if size == 0: raise HTTPException( status_code=400, detail="File is empty. Please upload a valid file." ) def validate_mime_type(file: UploadFile, extension: str) -> None: """ Light MIME type sanity check. Raises HTTPException if MIME type doesn't match extension. """ content_type = file.content_type or "" expected_mimes = MIME_TYPE_MAP.get(extension, []) if not expected_mimes: return if content_type and content_type not in expected_mimes: if content_type != "application/octet-stream": raise HTTPException( status_code=400, detail=f"MIME type mismatch. Expected one of {expected_mimes} for .{extension}, got {content_type}" ) async def validate_upload_file(file: UploadFile) -> str: """ Run all validations on uploaded file. Returns the validated file extension. """ if not file or not file.filename: raise HTTPException( status_code=400, detail="No file provided or file has no name." ) extension = validate_file_extension(file.filename) validate_file_size(file) validate_mime_type(file, extension) return extension