Spaces:
Running
Running
| """Author RAG Chatbot SaaS β Document Ingestion Validator. | |
| This module is the single entry point for all pre-ingestion file validation. | |
| It wraps the low-level checks in file_utils and raises typed ingestion exceptions. | |
| Validation order (MUST run BEFORE any processing starts): | |
| 1. File existence check | |
| 2. Empty file check | |
| 3. Size limit check (UPLOAD_MAX_FILE_SIZE_MB) | |
| 4. MIME type check by magic bytes (never trust file extension alone) | |
| RULE: Call validate_file() before any parsing, chunking, or embedding. | |
| RULE: Raise typed exceptions β never return False or None on failure. | |
| """ | |
| import os | |
| from pathlib import Path | |
| import structlog | |
| from app.config import get_settings | |
| from app.exceptions.ingestion import ( | |
| EmptyFileError, | |
| FileTooLargeError, | |
| ParseError, | |
| UnsupportedFormatError, | |
| ) | |
| from app.utils.file_utils import ( | |
| compute_sha256, | |
| detect_mime_type, | |
| get_file_extension_from_mime, | |
| validate_upload, | |
| ) | |
| logger = structlog.get_logger(__name__) | |
| cfg = get_settings() | |
| def validate_file(file_path: str) -> str: | |
| """Run all pre-ingestion validation checks on an uploaded file. | |
| This is the primary validation entry point used by the ingestion pipeline. | |
| Delegates to file_utils.validate_upload for the actual checks. | |
| Args: | |
| file_path: Absolute path to the uploaded/saved file. | |
| Returns: | |
| Detected extension label ('pdf', 'epub', 'docx', 'txt'). | |
| Raises: | |
| EmptyFileError: File has no content. | |
| FileTooLargeError: File exceeds UPLOAD_MAX_FILE_SIZE_MB. | |
| UnsupportedFormatError: File MIME type is not supported. | |
| ParseError: File does not exist or cannot be read. | |
| """ | |
| if not os.path.exists(file_path): | |
| raise ParseError(filename=Path(file_path).name, reason="File not found on disk") | |
| size_bytes = os.path.getsize(file_path) | |
| logger.debug("Validating upload", path=file_path, size_bytes=size_bytes) | |
| # Delegates all checks to the centralized file_utils implementation | |
| extension = validate_upload(file_path, size_bytes) | |
| logger.info("File validation passed", path=file_path, extension=extension) | |
| return extension | |
| def check_for_corruption(file_path: str, extension: str) -> None: | |
| """Attempt a lightweight read of the file to detect obvious corruption. | |
| This is a best-effort check β does not guarantee the file is fully valid. | |
| Real corruption is caught during the parse stage with a ParseError. | |
| Args: | |
| file_path: Absolute path to the file. | |
| extension: Validated extension label. | |
| Raises: | |
| ParseError: If the file cannot be opened or is obviously corrupted. | |
| """ | |
| try: | |
| with open(file_path, "rb") as f: | |
| header = f.read(512) | |
| if len(header) == 0: | |
| raise EmptyFileError() | |
| except EmptyFileError: | |
| raise | |
| except Exception as e: | |
| raise ParseError(filename=Path(file_path).name, reason=f"File unreadable: {e}") | |
| # Re-export core utilities for callers that import directly from this module | |
| __all__ = [ | |
| "validate_file", | |
| "check_for_corruption", | |
| "compute_sha256", | |
| "detect_mime_type", | |
| ] | |