mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-06-15 09:15:29 -04:00
fix(upload): configure chat attachment size limit (#2439)
This commit is contained in:
@@ -147,6 +147,11 @@ SEARXNG_INSTANCE=http://localhost:8080
|
||||
# if you intentionally want scheduled scripts to run remotely.
|
||||
# ODYSSEUS_SCRIPT_HOST=localhost
|
||||
|
||||
# Chat / agent attachment size cap in bytes (default: 10 MB).
|
||||
# Raise this for local installs that need larger PDFs or text documents.
|
||||
# Example: 52428800 = 50 MB.
|
||||
# ODYSSEUS_CHAT_UPLOAD_MAX_BYTES=10485760
|
||||
|
||||
# ============================================================
|
||||
# GPU support (Docker Compose)
|
||||
# ============================================================
|
||||
|
||||
@@ -396,6 +396,7 @@ Key settings:
|
||||
| `CHROMADB_HOST` | `localhost` | ChromaDB host for vector memory. Docker overrides this to `chromadb`. |
|
||||
| `CHROMADB_PORT` | `8100` | ChromaDB port for manual host runs. Docker overrides this to `8000`. |
|
||||
| `EMBEDDING_URL` | -- | OpenAI-compatible embeddings endpoint |
|
||||
| `ODYSSEUS_CHAT_UPLOAD_MAX_BYTES` | `10485760` | Chat/agent attachment cap in bytes. Raise for larger local PDFs or text documents. |
|
||||
|
||||
### Built-in MCP servers (optional setup)
|
||||
|
||||
|
||||
@@ -59,6 +59,7 @@ services:
|
||||
- ODYSSEUS_INPROCESS_POLLERS=${ODYSSEUS_INPROCESS_POLLERS:-1}
|
||||
- ODYSSEUS_INPROCESS_TASKS=${ODYSSEUS_INPROCESS_TASKS:-1}
|
||||
- ODYSSEUS_SCRIPT_HOST=${ODYSSEUS_SCRIPT_HOST:-localhost}
|
||||
- ODYSSEUS_CHAT_UPLOAD_MAX_BYTES=${ODYSSEUS_CHAT_UPLOAD_MAX_BYTES:-10485760}
|
||||
- DATA_BRAVE_API_KEY=${DATA_BRAVE_API_KEY:-}
|
||||
- GOOGLE_API_KEY=${GOOGLE_API_KEY:-}
|
||||
- GOOGLE_PSE_CX=${GOOGLE_PSE_CX:-}
|
||||
|
||||
@@ -58,6 +58,7 @@ services:
|
||||
- ODYSSEUS_INPROCESS_POLLERS=${ODYSSEUS_INPROCESS_POLLERS:-1}
|
||||
- ODYSSEUS_INPROCESS_TASKS=${ODYSSEUS_INPROCESS_TASKS:-1}
|
||||
- ODYSSEUS_SCRIPT_HOST=${ODYSSEUS_SCRIPT_HOST:-localhost}
|
||||
- ODYSSEUS_CHAT_UPLOAD_MAX_BYTES=${ODYSSEUS_CHAT_UPLOAD_MAX_BYTES:-10485760}
|
||||
- DATA_BRAVE_API_KEY=${DATA_BRAVE_API_KEY:-}
|
||||
- GOOGLE_API_KEY=${GOOGLE_API_KEY:-}
|
||||
- GOOGLE_PSE_CX=${GOOGLE_PSE_CX:-}
|
||||
|
||||
@@ -47,6 +47,7 @@ services:
|
||||
- ODYSSEUS_INPROCESS_POLLERS=${ODYSSEUS_INPROCESS_POLLERS:-1}
|
||||
- ODYSSEUS_INPROCESS_TASKS=${ODYSSEUS_INPROCESS_TASKS:-1}
|
||||
- ODYSSEUS_SCRIPT_HOST=${ODYSSEUS_SCRIPT_HOST:-localhost}
|
||||
- ODYSSEUS_CHAT_UPLOAD_MAX_BYTES=${ODYSSEUS_CHAT_UPLOAD_MAX_BYTES:-10485760}
|
||||
- DATA_BRAVE_API_KEY=${DATA_BRAVE_API_KEY:-}
|
||||
- GOOGLE_API_KEY=${GOOGLE_API_KEY:-}
|
||||
- GOOGLE_PSE_CX=${GOOGLE_PSE_CX:-}
|
||||
|
||||
+5
-2
@@ -13,6 +13,8 @@ from fastapi import HTTPException
|
||||
from fastapi import UploadFile
|
||||
from typing import List, Optional
|
||||
|
||||
from src.upload_limits import format_byte_limit, get_chat_upload_max_bytes
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -201,12 +203,13 @@ def validate_file_upload(file: UploadFile) -> UploadFile:
|
||||
}
|
||||
)
|
||||
|
||||
if file_size > 10 * 1024 * 1024:
|
||||
upload_limit = get_chat_upload_max_bytes()
|
||||
if file_size > upload_limit:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail={
|
||||
"error": "FILE_TOO_LARGE",
|
||||
"message": "File size exceeds 10MB limit"
|
||||
"message": f"File size exceeds {format_byte_limit(upload_limit)} limit"
|
||||
}
|
||||
)
|
||||
except IOError as e:
|
||||
|
||||
@@ -12,6 +12,10 @@ import threading
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Dict, Any, Optional
|
||||
from fastapi import HTTPException, UploadFile
|
||||
|
||||
from src.upload_limits import format_byte_limit, get_chat_upload_max_bytes
|
||||
|
||||
|
||||
def secure_filename(filename: str) -> str:
|
||||
"""Sanitize a filename (replaces werkzeug.utils.secure_filename)."""
|
||||
import unicodedata
|
||||
@@ -73,7 +77,7 @@ class UploadHandler:
|
||||
def __init__(self, base_dir: str, upload_dir: str):
|
||||
self.base_dir = base_dir
|
||||
self.upload_dir = upload_dir
|
||||
self.max_upload_size = 10 * 1024 * 1024 # 10MB
|
||||
self.max_upload_size = get_chat_upload_max_bytes()
|
||||
self.max_concurrent_uploads = 3
|
||||
self.cleanup_days = 30
|
||||
# Per-IP per-minute cap. save_upload() counts EACH file, and the chat
|
||||
@@ -518,7 +522,7 @@ class UploadHandler:
|
||||
if file_size > self.max_upload_size:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"File size exceeds {self.max_upload_size/1024/1024}MB limit"
|
||||
detail=f"File size exceeds {format_byte_limit(self.max_upload_size)} limit"
|
||||
)
|
||||
|
||||
# Get original filename and sanitize it
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
"""Small helpers for route-local upload size caps."""
|
||||
|
||||
import os
|
||||
|
||||
from fastapi import HTTPException, UploadFile
|
||||
|
||||
DEFAULT_CHAT_UPLOAD_MAX_BYTES = 10 * 1024 * 1024
|
||||
CHAT_UPLOAD_MAX_BYTES_ENV = "ODYSSEUS_CHAT_UPLOAD_MAX_BYTES"
|
||||
|
||||
|
||||
def format_byte_limit(limit: int) -> str:
|
||||
if limit % (1024 * 1024) == 0:
|
||||
@@ -11,6 +16,23 @@ def format_byte_limit(limit: int) -> str:
|
||||
return f"{limit} bytes"
|
||||
|
||||
|
||||
def read_byte_limit_env(name: str, default: int) -> int:
|
||||
raw = os.getenv(name)
|
||||
if raw is None or not raw.strip():
|
||||
return default
|
||||
try:
|
||||
limit = int(raw)
|
||||
except ValueError as exc:
|
||||
raise ValueError(f"{name} must be an integer byte count") from exc
|
||||
if limit < 1:
|
||||
raise ValueError(f"{name} must be greater than 0")
|
||||
return limit
|
||||
|
||||
|
||||
def get_chat_upload_max_bytes() -> int:
|
||||
return read_byte_limit_env(CHAT_UPLOAD_MAX_BYTES_ENV, DEFAULT_CHAT_UPLOAD_MAX_BYTES)
|
||||
|
||||
|
||||
async def read_upload_limited(upload: UploadFile, limit: int, label: str = "Upload") -> bytes:
|
||||
"""Read an UploadFile with a hard byte cap."""
|
||||
data = await upload.read(limit + 1)
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
import io
|
||||
|
||||
import pytest
|
||||
from fastapi import HTTPException, UploadFile
|
||||
|
||||
from src.chat_helpers import validate_file_upload
|
||||
from src.upload_handler import UploadHandler
|
||||
from src.upload_limits import (
|
||||
DEFAULT_CHAT_UPLOAD_MAX_BYTES,
|
||||
get_chat_upload_max_bytes,
|
||||
read_byte_limit_env,
|
||||
)
|
||||
|
||||
|
||||
def _upload(name: str, data: bytes) -> UploadFile:
|
||||
return UploadFile(filename=name, file=io.BytesIO(data))
|
||||
|
||||
|
||||
def test_chat_upload_limit_defaults_to_10mb(monkeypatch):
|
||||
monkeypatch.delenv("ODYSSEUS_CHAT_UPLOAD_MAX_BYTES", raising=False)
|
||||
|
||||
assert get_chat_upload_max_bytes() == DEFAULT_CHAT_UPLOAD_MAX_BYTES
|
||||
|
||||
|
||||
def test_chat_upload_limit_uses_env_bytes(monkeypatch):
|
||||
monkeypatch.setenv("ODYSSEUS_CHAT_UPLOAD_MAX_BYTES", "12345")
|
||||
|
||||
assert get_chat_upload_max_bytes() == 12345
|
||||
|
||||
|
||||
def test_chat_upload_limit_rejects_invalid_env(monkeypatch):
|
||||
monkeypatch.setenv("ODYSSEUS_CHAT_UPLOAD_MAX_BYTES", "not-bytes")
|
||||
|
||||
with pytest.raises(ValueError, match="ODYSSEUS_CHAT_UPLOAD_MAX_BYTES"):
|
||||
get_chat_upload_max_bytes()
|
||||
|
||||
|
||||
def test_read_byte_limit_env_rejects_non_positive(monkeypatch):
|
||||
monkeypatch.setenv("ODYSSEUS_CHAT_UPLOAD_MAX_BYTES", "0")
|
||||
|
||||
with pytest.raises(ValueError, match="greater than 0"):
|
||||
read_byte_limit_env("ODYSSEUS_CHAT_UPLOAD_MAX_BYTES", 10)
|
||||
|
||||
|
||||
def test_validate_file_upload_uses_configured_chat_limit(monkeypatch):
|
||||
monkeypatch.setenv("ODYSSEUS_CHAT_UPLOAD_MAX_BYTES", "4")
|
||||
|
||||
with pytest.raises(HTTPException) as exc:
|
||||
validate_file_upload(_upload("too-large.txt", b"abcde"))
|
||||
|
||||
assert exc.value.status_code == 400
|
||||
assert exc.value.detail["error"] == "FILE_TOO_LARGE"
|
||||
assert exc.value.detail["message"] == "File size exceeds 4 bytes limit"
|
||||
|
||||
|
||||
def test_upload_handler_uses_configured_chat_limit(monkeypatch, tmp_path):
|
||||
monkeypatch.setenv("ODYSSEUS_CHAT_UPLOAD_MAX_BYTES", "4")
|
||||
handler = UploadHandler(base_dir=str(tmp_path), upload_dir=str(tmp_path / "uploads"))
|
||||
|
||||
with pytest.raises(HTTPException) as exc:
|
||||
handler.save_upload(_upload("too-large.txt", b"abcde"), client_ip="127.0.0.1")
|
||||
|
||||
assert exc.value.status_code == 400
|
||||
assert exc.value.detail == "File size exceeds 4 bytes limit"
|
||||
Reference in New Issue
Block a user