From 704ba550d4e962c39fd4a351dcf8bdf027e09d87 Mon Sep 17 00:00:00 2001 From: Pier-Jean Malandrino Date: Wed, 8 Apr 2026 10:26:51 +0200 Subject: [PATCH] feat: make document max size configurable via MAX_FILE_SIZE_MB env var Replace hardcoded 5 MB upload limit with a configurable setting. Backend exposes the value via /api/health, frontend reads it dynamically for validation and UI messages. Closes #48 --- .env.example | 3 +++ document-parser/api/documents.py | 11 +++++++---- document-parser/infra/settings.py | 4 ++++ document-parser/main.py | 2 ++ document-parser/services/document_service.py | 6 +++--- document-parser/tests/test_api_endpoints.py | 6 ++++++ document-parser/tests/test_settings.py | 17 +++++++++++++++++ frontend/src/app/App.vue | 4 +++- frontend/src/features/document/store.ts | 9 +++++---- .../src/features/document/ui/DocumentUpload.vue | 11 +++++++---- .../src/features/feature-flags/store.test.ts | 14 ++++++++++++++ frontend/src/features/feature-flags/store.ts | 5 ++++- frontend/src/shared/i18n.ts | 12 ++++++------ 13 files changed, 81 insertions(+), 23 deletions(-) diff --git a/.env.example b/.env.example index 3534f11..832eb86 100644 --- a/.env.example +++ b/.env.example @@ -15,6 +15,9 @@ # Max parallel analysis jobs (default: 3) # MAX_CONCURRENT_ANALYSES=3 +# Max upload file size in MB (default: 50, 0 = unlimited) +# MAX_FILE_SIZE_MB=50 + # Max pages per PDF (default: 0 = unlimited). Set to 20 for HF Spaces. # MAX_PAGE_COUNT=0 diff --git a/document-parser/api/documents.py b/document-parser/api/documents.py index f36fa81..516ffcc 100644 --- a/document-parser/api/documents.py +++ b/document-parser/api/documents.py @@ -8,6 +8,7 @@ from fastapi import APIRouter, HTTPException, Query, UploadFile from fastapi.responses import Response from api.schemas import DocumentResponse +from infra.settings import settings from services import document_service logger = logging.getLogger(__name__) @@ -34,16 +35,18 @@ async def upload(file: UploadFile) -> DocumentResponse: raise HTTPException(status_code=400, detail="No filename provided") # Reject early if Content-Length exceeds limit (before reading body) - if file.size and file.size > document_service.MAX_FILE_SIZE: - raise HTTPException(status_code=413, detail="File too large (max 5 MB)") + _max = document_service.MAX_FILE_SIZE + _detail = f"File too large (max {settings.max_file_size_mb} MB)" + if _max > 0 and file.size and file.size > _max: + raise HTTPException(status_code=413, detail=_detail) # Read in chunks to avoid holding the full upload in a single allocation chunks: list[bytes] = [] total = 0 while chunk := await file.read(_READ_CHUNK_SIZE): total += len(chunk) - if total > document_service.MAX_FILE_SIZE: - raise HTTPException(status_code=413, detail="File too large (max 5 MB)") + if _max > 0 and total > _max: + raise HTTPException(status_code=413, detail=_detail) chunks.append(chunk) content = b"".join(chunks) diff --git a/document-parser/infra/settings.py b/document-parser/infra/settings.py index 2ad8538..3fba8f2 100644 --- a/document-parser/infra/settings.py +++ b/document-parser/infra/settings.py @@ -20,6 +20,7 @@ class Settings: default_table_mode: str = "accurate" # "accurate" or "fast" max_page_count: int = 0 # 0 = unlimited (upload validation) max_file_size: int = 0 # 0 = unlimited (Docling-level, bytes) + max_file_size_mb: int = 50 # upload limit in MB (0 = unlimited) batch_page_size: int = 0 # 0 = disabled, > 0 = pages per batch upload_dir: str = "./uploads" db_path: str = "./data/docling_studio.db" @@ -43,6 +44,8 @@ class Settings: errors.append(f"max_page_count must be >= 0 (got {self.max_page_count})") if self.max_file_size < 0: errors.append(f"max_file_size must be >= 0 (got {self.max_file_size})") + if self.max_file_size_mb < 0: + errors.append(f"max_file_size_mb must be >= 0 (got {self.max_file_size_mb})") if self.batch_page_size < 0: errors.append(f"batch_page_size must be >= 0 (got {self.batch_page_size})") if self.default_table_mode not in ("accurate", "fast"): @@ -81,6 +84,7 @@ class Settings: default_table_mode=os.environ.get("DEFAULT_TABLE_MODE", "accurate"), max_page_count=int(os.environ.get("MAX_PAGE_COUNT", "0")), max_file_size=int(os.environ.get("MAX_FILE_SIZE", "0")), + max_file_size_mb=int(os.environ.get("MAX_FILE_SIZE_MB", "50")), batch_page_size=int(os.environ.get("BATCH_PAGE_SIZE", "0")), upload_dir=os.environ.get("UPLOAD_DIR", "./uploads"), db_path=os.environ.get("DB_PATH", "./data/docling_studio.db"), diff --git a/document-parser/main.py b/document-parser/main.py index 2976228..7c5defd 100644 --- a/document-parser/main.py +++ b/document-parser/main.py @@ -122,4 +122,6 @@ async def health() -> dict[str, str | int]: } if settings.max_page_count > 0: result["maxPageCount"] = settings.max_page_count + if settings.max_file_size_mb > 0: + result["maxFileSizeMb"] = settings.max_file_size_mb return result diff --git a/document-parser/services/document_service.py b/document-parser/services/document_service.py index a0d9479..d0a7854 100644 --- a/document-parser/services/document_service.py +++ b/document-parser/services/document_service.py @@ -16,7 +16,7 @@ from persistence import analysis_repo, document_repo logger = logging.getLogger(__name__) UPLOAD_DIR = settings.upload_dir -MAX_FILE_SIZE = 5 * 1024 * 1024 # 5 MB +MAX_FILE_SIZE = settings.max_file_size_mb * 1024 * 1024 if settings.max_file_size_mb > 0 else 0 MAX_PAGE_COUNT = settings.max_page_count # 0 = unlimited @@ -32,8 +32,8 @@ async def upload(filename: str, content_type: str, file_content: bytes) -> Docum Writes the file in fixed-size chunks to keep peak memory usage low. """ - if len(file_content) > MAX_FILE_SIZE: - raise ValueError("File too large (max 5 MB)") + if MAX_FILE_SIZE > 0 and len(file_content) > MAX_FILE_SIZE: + raise ValueError(f"File too large (max {settings.max_file_size_mb} MB)") if not file_content[:4].startswith(_PDF_MAGIC): raise ValueError("Invalid file: not a PDF document") diff --git a/document-parser/tests/test_api_endpoints.py b/document-parser/tests/test_api_endpoints.py index 11b7c4e..40196f1 100644 --- a/document-parser/tests/test_api_endpoints.py +++ b/document-parser/tests/test_api_endpoints.py @@ -33,6 +33,12 @@ class TestHealthEndpoint: assert "engine" in data assert "database" in data + def test_health_exposes_max_file_size_mb(self, client): + resp = client.get("/api/health") + data = resp.json() + assert "maxFileSizeMb" in data + assert data["maxFileSizeMb"] == 50 + class TestDocumentEndpoints: @patch("services.document_service.find_all", new_callable=AsyncMock) diff --git a/document-parser/tests/test_settings.py b/document-parser/tests/test_settings.py index 20dafad..4e8e2e6 100644 --- a/document-parser/tests/test_settings.py +++ b/document-parser/tests/test_settings.py @@ -17,6 +17,7 @@ class TestSettingsDefaults: assert s.document_timeout == 120.0 assert s.lock_timeout == 300 assert s.max_page_count == 0 + assert s.max_file_size_mb == 50 assert s.batch_page_size == 0 assert s.upload_dir == "./uploads" assert s.db_path == "./data/docling_studio.db" @@ -68,6 +69,20 @@ class TestSettingsValidation: with pytest.raises(ValueError, match="max_file_size must be >= 0"): Settings(max_file_size=-1) + def test_negative_max_file_size_mb_rejected(self): + import pytest + + with pytest.raises(ValueError, match="max_file_size_mb must be >= 0"): + Settings(max_file_size_mb=-1) + + def test_zero_max_file_size_mb_accepted(self): + s = Settings(max_file_size_mb=0) + assert s.max_file_size_mb == 0 + + def test_positive_max_file_size_mb_accepted(self): + s = Settings(max_file_size_mb=100) + assert s.max_file_size_mb == 100 + def test_negative_batch_page_size_rejected(self): import pytest @@ -129,6 +144,7 @@ class TestSettingsFromEnv: monkeypatch.setenv("DOCUMENT_TIMEOUT", "60.0") monkeypatch.setenv("LOCK_TIMEOUT", "600") monkeypatch.setenv("MAX_PAGE_COUNT", "20") + monkeypatch.setenv("MAX_FILE_SIZE_MB", "100") monkeypatch.setenv("BATCH_PAGE_SIZE", "15") monkeypatch.setenv("UPLOAD_DIR", "/data/uploads") monkeypatch.setenv("DB_PATH", "/data/test.db") @@ -145,6 +161,7 @@ class TestSettingsFromEnv: assert s.lock_timeout == 600 assert s.document_timeout == 60.0 assert s.max_page_count == 20 + assert s.max_file_size_mb == 100 assert s.batch_page_size == 15 assert s.upload_dir == "/data/uploads" assert s.db_path == "/data/test.db" diff --git a/frontend/src/app/App.vue b/frontend/src/app/App.vue index a75d8be..1733b98 100644 --- a/frontend/src/app/App.vue +++ b/frontend/src/app/App.vue @@ -30,7 +30,7 @@