diff --git a/pkg/api/http/controller/group.py b/pkg/api/http/controller/group.py index 8ab4f4d9..0665a1d6 100644 --- a/pkg/api/http/controller/group.py +++ b/pkg/api/http/controller/group.py @@ -9,6 +9,9 @@ from quart.typing import RouteCallable from ....core import app +# Maximum file upload size limit (10MB) +MAX_FILE_SIZE = 10 * 1024 * 1024 # 10MB + preregistered_groups: list[type[RouterGroup]] = [] """Pre-registered list of RouterGroup""" diff --git a/pkg/api/http/controller/groups/files.py b/pkg/api/http/controller/groups/files.py index c90d172e..05877e14 100644 --- a/pkg/api/http/controller/groups/files.py +++ b/pkg/api/http/controller/groups/files.py @@ -31,19 +31,41 @@ class FilesRouterGroup(group.RouterGroup): @self.route('/documents', methods=['POST'], auth_type=group.AuthType.USER_TOKEN) async def _() -> quart.Response: request = quart.request + + # Check file size limit before reading the file + content_length = request.content_length + if content_length and content_length > group.MAX_FILE_SIZE: + return self.fail(400, 'File size exceeds 10MB limit. Please split large files into smaller parts.') + # get file bytes from 'file' - file = (await request.files)['file'] + files = await request.files + if 'file' not in files: + return self.fail(400, 'No file provided in request') + + file = files['file'] assert isinstance(file, quart.datastructures.FileStorage) file_bytes = await asyncio.to_thread(file.stream.read) - extension = file.filename.split('.')[-1] - file_name = file.filename.split('.')[0] + + # Double-check actual file size after reading + if len(file_bytes) > group.MAX_FILE_SIZE: + return self.fail(400, 'File size exceeds 10MB limit. Please split large files into smaller parts.') + + # Split filename and extension properly + if '.' in file.filename: + file_name, extension = file.filename.rsplit('.', 1) + else: + file_name = file.filename + extension = '' # check if file name contains '/' or '\' if '/' in file_name or '\\' in file_name: return self.fail(400, 'File name contains invalid characters') - file_key = file_name + '_' + str(uuid.uuid4())[:8] + '.' + extension + file_key = file_name + '_' + str(uuid.uuid4())[:8] + if extension: + file_key += '.' + extension + # save file to storage await self.ap.storage_mgr.storage_provider.save(file_key, file_bytes) return self.success( diff --git a/pkg/api/http/controller/main.py b/pkg/api/http/controller/main.py index 4f6d30af..ca12f4bb 100644 --- a/pkg/api/http/controller/main.py +++ b/pkg/api/http/controller/main.py @@ -5,6 +5,7 @@ import os import quart import quart_cors +from werkzeug.exceptions import RequestEntityTooLarge from ....core import app, entities as core_entities from ....utils import importutil @@ -35,7 +36,20 @@ class HTTPController: self.quart_app = quart.Quart(__name__) quart_cors.cors(self.quart_app, allow_origin='*') + # Set maximum content length to prevent large file uploads + self.quart_app.config['MAX_CONTENT_LENGTH'] = group.MAX_FILE_SIZE + async def initialize(self) -> None: + # Register custom error handler for file size limit + @self.quart_app.errorhandler(RequestEntityTooLarge) + async def handle_request_entity_too_large(e): + return quart.jsonify( + { + 'code': 400, + 'msg': 'File size exceeds 10MB limit. Please split large files into smaller parts.', + } + ), 400 + await self.register_routes() async def run(self) -> None: diff --git a/web/src/app/home/knowledge/components/kb-docs/FileUploadZone.tsx b/web/src/app/home/knowledge/components/kb-docs/FileUploadZone.tsx index ff61a5b4..a4c9d61b 100644 --- a/web/src/app/home/knowledge/components/kb-docs/FileUploadZone.tsx +++ b/web/src/app/home/knowledge/components/kb-docs/FileUploadZone.tsx @@ -23,6 +23,13 @@ export default function FileUploadZone({ async (file: File) => { if (isUploading) return; + // Check file size (10MB limit) + const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB + if (file.size > MAX_FILE_SIZE) { + toast.error(t('knowledge.documentsTab.fileSizeExceeded')); + return; + } + setIsUploading(true); const toastId = toast.loading(t('knowledge.documentsTab.uploadingFile')); @@ -46,7 +53,7 @@ export default function FileUploadZone({ setIsUploading(false); } }, - [kbId, isUploading, onUploadSuccess, onUploadError], + [kbId, isUploading, onUploadSuccess, onUploadError, t], ); const handleDragOver = useCallback((e: React.DragEvent) => { diff --git a/web/src/i18n/locales/en-US.ts b/web/src/i18n/locales/en-US.ts index d61cfb68..0646f418 100644 --- a/web/src/i18n/locales/en-US.ts +++ b/web/src/i18n/locales/en-US.ts @@ -503,6 +503,8 @@ const enUS = { uploadSuccess: 'File uploaded successfully!', uploadError: 'File upload failed, please try again', uploadingFile: 'Uploading file...', + fileSizeExceeded: + 'File size exceeds 10MB limit. Please split into smaller files.', actions: 'Actions', delete: 'Delete File', fileDeleteSuccess: 'File deleted successfully', diff --git a/web/src/i18n/locales/ja-JP.ts b/web/src/i18n/locales/ja-JP.ts index 7ecf74ac..696a538f 100644 --- a/web/src/i18n/locales/ja-JP.ts +++ b/web/src/i18n/locales/ja-JP.ts @@ -506,6 +506,8 @@ const jaJP = { uploadSuccess: 'ファイルのアップロードに成功しました!', uploadError: 'ファイルのアップロードに失敗しました。再度お試しください', uploadingFile: 'ファイルをアップロード中...', + fileSizeExceeded: + 'ファイルサイズが10MBの制限を超えています。より小さいファイルに分割してください。', actions: 'アクション', delete: 'ドキュメントを削除', fileDeleteSuccess: 'ドキュメントの削除に成功しました', diff --git a/web/src/i18n/locales/zh-Hans.ts b/web/src/i18n/locales/zh-Hans.ts index e22f7f44..2d3627b1 100644 --- a/web/src/i18n/locales/zh-Hans.ts +++ b/web/src/i18n/locales/zh-Hans.ts @@ -483,6 +483,7 @@ const zhHans = { uploadSuccess: '文件上传成功!', uploadError: '文件上传失败,请重试', uploadingFile: '上传文件中...', + fileSizeExceeded: '文件大小超过 10MB 限制,请分割成较小的文件后上传', actions: '操作', delete: '删除文件', fileDeleteSuccess: '文件删除成功', diff --git a/web/src/i18n/locales/zh-Hant.ts b/web/src/i18n/locales/zh-Hant.ts index 444b1398..68a1fb56 100644 --- a/web/src/i18n/locales/zh-Hant.ts +++ b/web/src/i18n/locales/zh-Hant.ts @@ -480,6 +480,7 @@ const zhHant = { uploadSuccess: '文檔上傳成功!', uploadError: '文檔上傳失敗,請重試', uploadingFile: '上傳文檔中...', + fileSizeExceeded: '檔案大小超過 10MB 限制,請分割成較小的檔案後上傳', actions: '操作', delete: '刪除文檔', fileDeleteSuccess: '文檔刪除成功',