from mimetypes import guess_extension from typing import Optional from flask_restx import Resource, reqparse from flask_restx.api import HTTPStatus from werkzeug.datastructures import FileStorage from werkzeug.exceptions import Forbidden import services from controllers.common.errors import ( FileTooLargeError, UnsupportedFileTypeError, ) from controllers.console.wraps import setup_required from controllers.files import files_ns from controllers.inner_api.plugin.wraps import get_user from core.file.helpers import verify_plugin_file_signature from core.tools.tool_file_manager import ToolFileManager from fields.file_fields import build_file_model # Define parser for both documentation and validation upload_parser = reqparse.RequestParser() upload_parser.add_argument("file", location="files", type=FileStorage, required=True, help="File to upload") upload_parser.add_argument( "timestamp", type=str, required=True, location="args", help="Unix timestamp for signature verification" ) upload_parser.add_argument( "nonce", type=str, required=True, location="args", help="Random string for signature verification" ) upload_parser.add_argument( "sign", type=str, required=True, location="args", help="HMAC signature for request validation" ) upload_parser.add_argument("tenant_id", type=str, required=True, location="args", help="Tenant identifier") upload_parser.add_argument("user_id", type=str, required=False, location="args", help="User identifier") @files_ns.route("/upload/for-plugin") class PluginUploadFileApi(Resource): @setup_required @files_ns.expect(upload_parser) @files_ns.doc("upload_plugin_file") @files_ns.doc(description="Upload a file for plugin usage with signature verification") @files_ns.doc( responses={ 201: "File uploaded successfully", 400: "Invalid request parameters", 403: "Forbidden - Invalid signature or missing parameters", 413: "File too large", 415: "Unsupported file type", } ) @files_ns.marshal_with(build_file_model(files_ns), code=HTTPStatus.CREATED) def post(self): """Upload a file for plugin usage. Accepts a file upload with signature verification for security. The file must be accompanied by valid timestamp, nonce, and signature parameters. Returns: dict: File metadata including ID, URLs, and properties int: HTTP status code (201 for success) Raises: Forbidden: Invalid signature or missing required parameters FileTooLargeError: File exceeds size limit UnsupportedFileTypeError: File type not supported """ # Parse and validate all arguments args = upload_parser.parse_args() file: FileStorage = args["file"] timestamp: str = args["timestamp"] nonce: str = args["nonce"] sign: str = args["sign"] tenant_id: str = args["tenant_id"] user_id: Optional[str] = args.get("user_id") user = get_user(tenant_id, user_id) filename: Optional[str] = file.filename mimetype: Optional[str] = file.mimetype if not filename or not mimetype: raise Forbidden("Invalid request.") if not verify_plugin_file_signature( filename=filename, mimetype=mimetype, tenant_id=tenant_id, user_id=user_id, timestamp=timestamp, nonce=nonce, sign=sign, ): raise Forbidden("Invalid request.") try: tool_file = ToolFileManager().create_file_by_raw( user_id=user.id, tenant_id=tenant_id, file_binary=file.read(), mimetype=mimetype, filename=filename, conversation_id=None, ) extension = guess_extension(tool_file.mimetype) or ".bin" preview_url = ToolFileManager.sign_file(tool_file_id=tool_file.id, extension=extension) # Create a dictionary with all the necessary attributes result = { "id": tool_file.id, "user_id": tool_file.user_id, "tenant_id": tool_file.tenant_id, "conversation_id": tool_file.conversation_id, "file_key": tool_file.file_key, "mimetype": tool_file.mimetype, "original_url": tool_file.original_url, "name": tool_file.name, "size": tool_file.size, "mime_type": mimetype, "extension": extension, "preview_url": preview_url, } return result, 201 except services.errors.file.FileTooLargeError as file_too_large_error: raise FileTooLargeError(file_too_large_error.description) except services.errors.file.UnsupportedFileTypeError: raise UnsupportedFileTypeError()