Add admin-configurable upload size limits

The 8MB image/file/avatar cap is now a site setting (UploadSettings,
single-row table like SmtpSettings) editable from the Admin Settings tab,
instead of a hardcoded constant. All three upload endpoints read the live
value and interpolate it into their 413 messages. A new GET
/api/uploads/limit endpoint (open to any authenticated user, unlike the
admin-only settings endpoints) lets the composer reject an oversized file
client-side before it ever hits the network, though the server still
enforces the same cap independently.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-15 20:44:05 -06:00
co-authored by Claude Sonnet 5
parent c78d7454b6
commit 62e4760c8a
19 changed files with 435 additions and 17 deletions
+5 -6
View File
@@ -10,11 +10,10 @@ from PIL import Image, UnidentifiedImageError
# production layout with zero new config.
UPLOADS_DIR = pathlib.Path(__file__).resolve().parent.parent.parent / "uploads"
MAX_IMAGE_BYTES = 8 * 1024 * 1024
# Separate named constant (same value for now) so a later size-limit
# redesign for generic file attachments doesn't have to touch image
# behavior.
MAX_FILE_BYTES = MAX_IMAGE_BYTES
# Seed value for the admin-configurable UploadSettings row (see
# app/services/upload_settings_service.py) -- also the fallback `read_capped`
# default for call sites that don't look up the live setting.
DEFAULT_MAX_UPLOAD_BYTES = 8 * 1024 * 1024
_READ_CHUNK_BYTES = 1024 * 1024
_MAX_DIMENSION = 2000
@@ -35,7 +34,7 @@ class InvalidImageError(Exception):
pass
async def read_capped(file, cap: int = MAX_IMAGE_BYTES) -> bytes:
async def read_capped(file, cap: int = DEFAULT_MAX_UPLOAD_BYTES) -> bytes:
"""Reads an UploadFile-like object in chunks, raising as soon as `cap`
is exceeded rather than after buffering the whole (potentially huge)
body first."""