refactor(uploads): centralize upload byte-limits in upload_limits.py (#3364) (#3518)

Move every per-route upload byte-limit into src/upload_limits.py as a
validated, env-overridable constant via read_byte_limit_env:

- Add GALLERY_UPLOAD_MAX_BYTES, GALLERY_TRANSFORM_UPLOAD_MAX_BYTES,
  MEMORY_IMPORT_MAX_BYTES, PERSONAL_UPLOAD_MAX_BYTES,
  EMAIL_COMPOSE_UPLOAD_MAX_BYTES, STT_MAX_AUDIO_BYTES, ICS_MAX_BYTES.
- Routes import their constant instead of defining it locally: replaces 4
  raw int(os.getenv(...)) and removes 3 hardcoded literals.
- The 3 previously-hardcoded limits (email compose, STT audio, calendar
  ICS) are now env-overridable with the same ODYSSEUS_*_MAX_BYTES naming.
- Defaults unchanged, so behavior is unchanged unless an env var is set;
  an invalid value now fails fast with a clear message instead of a bare
  int() ValueError.
- Document all env vars in .env.example and the README.

Fixes #3364
This commit is contained in:
Kenny Van de Maele
2026-06-09 01:24:30 +02:00
committed by GitHub
parent a240f28af9
commit 2404b00f18
11 changed files with 174 additions and 22 deletions
+5 -5
View File
@@ -13,7 +13,7 @@ from dateutil.rrule import rrulestr
from core.database import SessionLocal, CalendarCal, CalendarEvent
from src.auth_helpers import require_user
from src.upload_limits import read_upload_limited
from src.upload_limits import read_upload_limited, ICS_MAX_BYTES
logger = logging.getLogger(__name__)
@@ -1170,9 +1170,9 @@ def setup_calendar_routes() -> APIRouter:
finally:
db.close()
# 10 MB hard cap on ICS upload. Loading the whole file into memory is
# unavoidable with python-icalendar, so an unbounded upload would OOM.
_ICS_MAX_BYTES = 10 * 1024 * 1024
# Hard cap on ICS upload (ICS_MAX_BYTES, default 10 MB). Loading the whole
# file into memory is unavoidable with python-icalendar, so an unbounded
# upload would OOM.
@router.post("/import")
async def import_ics(request: Request, file: UploadFile = File(...), calendar_name: str = ""):
@@ -1182,7 +1182,7 @@ def setup_calendar_routes() -> APIRouter:
owner = _require_user(request)
db = SessionLocal()
try:
content = await read_upload_limited(file, _ICS_MAX_BYTES, "ICS file")
content = await read_upload_limited(file, ICS_MAX_BYTES, "ICS file")
try:
cal_data = iCal.from_ical(content)
except Exception as e:
+1 -2
View File
@@ -35,7 +35,7 @@ from fastapi.responses import FileResponse
from src.constants import DATA_DIR
from src.llm_core import llm_call_async
from src.upload_limits import read_upload_limited
from src.upload_limits import read_upload_limited, EMAIL_COMPOSE_UPLOAD_MAX_BYTES
from routes.email_helpers import (
_strip_think, _extract_reply, _apply_email_style_mechanics, require_owner, require_user, _assert_owns_account,
@@ -58,7 +58,6 @@ from routes.email_pollers import _start_poller
logger = logging.getLogger(__name__)
ODYSSEUS_MAIL_ORIGIN = "odysseus-ui"
EMAIL_COMPOSE_UPLOAD_MAX_BYTES = 25 * 1024 * 1024
def _email_tag_owner_aliases(account_id: str | None, owner: str = "") -> list[str]:
+5 -4
View File
@@ -13,7 +13,11 @@ from fastapi import APIRouter, HTTPException, Query, Request
from core.database import SessionLocal, GalleryImage, GalleryAlbum, ModelEndpoint
from core.database import Session as DbSession
from src.auth_helpers import get_current_user, owner_filter, require_privilege
from src.upload_limits import read_upload_limited
from src.upload_limits import (
read_upload_limited,
GALLERY_UPLOAD_MAX_BYTES,
GALLERY_TRANSFORM_UPLOAD_MAX_BYTES,
)
from src.constants import GENERATED_IMAGES_DIR
from routes.gallery_helpers import (
@@ -22,9 +26,6 @@ from routes.gallery_helpers import (
logger = logging.getLogger(__name__)
GALLERY_UPLOAD_MAX_BYTES = int(os.getenv("ODYSSEUS_GALLERY_UPLOAD_MAX_BYTES", str(100 * 1024 * 1024)))
GALLERY_TRANSFORM_UPLOAD_MAX_BYTES = int(os.getenv("ODYSSEUS_GALLERY_TRANSFORM_UPLOAD_MAX_BYTES", str(25 * 1024 * 1024)))
def _current_user_is_admin(request: Request, user: str | None) -> bool:
if not user:
+1 -2
View File
@@ -29,11 +29,10 @@ from src.llm_core import llm_call_async
from services.memory.memory_extractor import audit_memories
from src.auth_helpers import get_current_user, require_user
from src.endpoint_resolver import resolve_endpoint
from src.upload_limits import read_upload_limited
from src.upload_limits import read_upload_limited, MEMORY_IMPORT_MAX_BYTES
logger = logging.getLogger(__name__)
MEMORY_IMPORT_MAX_BYTES = int(os.getenv("ODYSSEUS_MEMORY_IMPORT_MAX_BYTES", str(10 * 1024 * 1024)))
def setup_memory_routes(memory_manager: MemoryManager, session_manager: SessionManager, memory_vector=None):
"""Set up memory-related routes."""
+3 -5
View File
@@ -11,11 +11,9 @@ from src.rag_singleton import get_rag_manager
from src.auth_helpers import require_privilege, require_user
from core.middleware import require_admin
from src.upload_handler import secure_filename
from src.upload_limits import PERSONAL_UPLOAD_MAX_BYTES
UPLOADS_DIR = PERSONAL_UPLOADS_DIR
MAX_PERSONAL_UPLOAD_BYTES = int(
os.getenv("ODYSSEUS_PERSONAL_UPLOAD_MAX_BYTES", str(25 * 1024 * 1024))
)
logger = logging.getLogger(__name__)
@@ -208,8 +206,8 @@ def setup_personal_routes(personal_docs_manager, rag_manager, rag_available):
for upload in files:
try:
file_path, stored_name, safe_name = _unique_personal_upload_path(upload_dir, upload.filename)
content_bytes = await upload.read(MAX_PERSONAL_UPLOAD_BYTES + 1)
if len(content_bytes) > MAX_PERSONAL_UPLOAD_BYTES:
content_bytes = await upload.read(PERSONAL_UPLOAD_MAX_BYTES + 1)
if len(content_bytes) > PERSONAL_UPLOAD_MAX_BYTES:
logger.warning(f"Rejected oversized personal upload: {upload.filename!r}")
total_failed += 1
continue
+1 -3
View File
@@ -4,12 +4,10 @@
from fastapi import APIRouter, HTTPException, UploadFile, File
import logging
from src.upload_limits import read_upload_limited
from src.upload_limits import read_upload_limited, STT_MAX_AUDIO_BYTES
logger = logging.getLogger(__name__)
STT_MAX_AUDIO_BYTES = 25 * 1024 * 1024
def setup_stt_routes(stt_service):
"""Setup STT routes with the provided STT service"""