Arag / app /services /file_validator.py
AuthorBot
Restructure project for HF Spaces deployment
772f852
Raw
History Blame Contribute Delete
3.14 kB
"""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",
]