Private
Public Access
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:
@@ -16,6 +16,7 @@ from app.schemas.admin import (
|
||||
)
|
||||
from app.schemas.site_invite import SiteInviteCreate, SiteInviteRead
|
||||
from app.schemas.smtp_settings import SmtpSettingsRead, SmtpSettingsUpdate
|
||||
from app.schemas.upload_settings import UploadSettingsRead, UploadSettingsUpdate
|
||||
from app.schemas.webhook import EventSubscriptionAdminRead, WebhookIncomingAdminRead
|
||||
from app.services.admin_service import (
|
||||
CannotActOnSelfError,
|
||||
@@ -40,6 +41,7 @@ from app.services.site_invite_service import (
|
||||
revoke_site_invite,
|
||||
)
|
||||
from app.services.smtp_settings_service import get_smtp_settings, upsert_smtp_settings
|
||||
from app.services.upload_settings_service import get_upload_settings, update_upload_settings
|
||||
from app.services.webhook_service import (
|
||||
list_all_event_subscriptions_admin,
|
||||
list_all_incoming_webhooks_admin,
|
||||
@@ -379,3 +381,24 @@ async def test_smtp_settings_endpoint(
|
||||
raise HTTPException(status_code=400, detail="SMTP is not configured yet")
|
||||
except Exception as exc:
|
||||
raise HTTPException(status_code=502, detail=f"Failed to send test email: {exc}")
|
||||
|
||||
|
||||
@router.get("/settings/uploads", response_model=UploadSettingsRead)
|
||||
async def get_upload_settings_endpoint(
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
require_site_admin(current_user)
|
||||
cfg = await get_upload_settings(db)
|
||||
return UploadSettingsRead(max_upload_bytes=cfg.max_upload_bytes, updated_at=cfg.updated_at)
|
||||
|
||||
|
||||
@router.put("/settings/uploads", response_model=UploadSettingsRead)
|
||||
async def update_upload_settings_endpoint(
|
||||
data: UploadSettingsUpdate,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
require_site_admin(current_user)
|
||||
cfg = await update_upload_settings(db, max_upload_bytes=data.max_upload_bytes)
|
||||
return UploadSettingsRead(max_upload_bytes=cfg.max_upload_bytes, updated_at=cfg.updated_at)
|
||||
|
||||
@@ -20,6 +20,7 @@ from app.services.password_service import (
|
||||
request_password_reset,
|
||||
validate_reset_token,
|
||||
)
|
||||
from app.services.upload_settings_service import format_mb, get_upload_settings
|
||||
from app.storage import (
|
||||
ALLOWED_IMAGE_CONTENT_TYPES,
|
||||
InvalidImageError,
|
||||
@@ -89,10 +90,14 @@ async def upload_avatar(
|
||||
if file.content_type not in ALLOWED_IMAGE_CONTENT_TYPES:
|
||||
raise HTTPException(status_code=400, detail="Unsupported image type")
|
||||
|
||||
upload_settings = await get_upload_settings(db)
|
||||
try:
|
||||
data = await read_capped(file)
|
||||
data = await read_capped(file, cap=upload_settings.max_upload_bytes)
|
||||
except UploadTooLargeError:
|
||||
raise HTTPException(status_code=413, detail="Image exceeds 8 MB limit")
|
||||
raise HTTPException(
|
||||
status_code=413,
|
||||
detail=f"Image exceeds {format_mb(upload_settings.max_upload_bytes)} limit",
|
||||
)
|
||||
|
||||
try:
|
||||
data, ext = process_image(
|
||||
|
||||
@@ -35,6 +35,7 @@ from app.schemas.webhook import (
|
||||
WebhookIncomingRead,
|
||||
)
|
||||
from app.services.message_service import get_reactions_for_messages, list_recent_messages
|
||||
from app.services.upload_settings_service import format_mb, get_upload_settings
|
||||
from app.services.room_service import (
|
||||
AlreadyMemberError,
|
||||
CannotRemoveOwnerError,
|
||||
@@ -73,7 +74,6 @@ from app.services.webhook_service import (
|
||||
from app.services.ssrf import UnsafeWebhookUrlError
|
||||
from app.storage import (
|
||||
ALLOWED_IMAGE_CONTENT_TYPES,
|
||||
MAX_FILE_BYTES,
|
||||
UPLOADS_DIR,
|
||||
InvalidImageError,
|
||||
UploadTooLargeError,
|
||||
@@ -337,10 +337,14 @@ async def upload_room_image_endpoint(
|
||||
if file.content_type not in ALLOWED_IMAGE_CONTENT_TYPES:
|
||||
raise HTTPException(status_code=400, detail="Unsupported image type")
|
||||
|
||||
upload_settings = await get_upload_settings(db)
|
||||
try:
|
||||
data = await read_capped(file)
|
||||
data = await read_capped(file, cap=upload_settings.max_upload_bytes)
|
||||
except UploadTooLargeError:
|
||||
raise HTTPException(status_code=413, detail="Image exceeds 8 MB limit")
|
||||
raise HTTPException(
|
||||
status_code=413,
|
||||
detail=f"Image exceeds {format_mb(upload_settings.max_upload_bytes)} limit",
|
||||
)
|
||||
|
||||
try:
|
||||
data, ext = process_image(data, file.content_type)
|
||||
@@ -388,10 +392,14 @@ async def upload_room_file_endpoint(
|
||||
):
|
||||
await require_room_member(room_id, current_user, db)
|
||||
|
||||
upload_settings = await get_upload_settings(db)
|
||||
try:
|
||||
data = await read_capped(file, cap=MAX_FILE_BYTES)
|
||||
data = await read_capped(file, cap=upload_settings.max_upload_bytes)
|
||||
except UploadTooLargeError:
|
||||
raise HTTPException(status_code=413, detail="File exceeds 8 MB limit")
|
||||
raise HTTPException(
|
||||
status_code=413,
|
||||
detail=f"File exceeds {format_mb(upload_settings.max_upload_bytes)} limit",
|
||||
)
|
||||
|
||||
original_filename = file.filename or "file"
|
||||
ext = pathlib.Path(original_filename).suffix
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
from fastapi import APIRouter, Depends
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.database import get_db
|
||||
from app.dependencies import get_current_user
|
||||
from app.models import User
|
||||
from app.schemas.upload_settings import UploadSettingsRead
|
||||
from app.services.upload_settings_service import get_upload_settings
|
||||
|
||||
router = APIRouter(prefix="/api/uploads", tags=["uploads"])
|
||||
|
||||
|
||||
@router.get("/limit", response_model=UploadSettingsRead)
|
||||
async def get_upload_limit_endpoint(
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""Any authenticated user (not just site admins -- unlike
|
||||
/api/admin/settings/uploads) can read the current cap, so the composer
|
||||
can validate client-side before uploading."""
|
||||
cfg = await get_upload_settings(db)
|
||||
return UploadSettingsRead(max_upload_bytes=cfg.max_upload_bytes, updated_at=cfg.updated_at)
|
||||
Reference in New Issue
Block a user