You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

upload.py 4.8KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128
  1. from mimetypes import guess_extension
  2. from flask_restx import Resource, reqparse
  3. from flask_restx.api import HTTPStatus
  4. from werkzeug.datastructures import FileStorage
  5. from werkzeug.exceptions import Forbidden
  6. import services
  7. from controllers.common.errors import (
  8. FileTooLargeError,
  9. UnsupportedFileTypeError,
  10. )
  11. from controllers.console.wraps import setup_required
  12. from controllers.files import files_ns
  13. from controllers.inner_api.plugin.wraps import get_user
  14. from core.file.helpers import verify_plugin_file_signature
  15. from core.tools.tool_file_manager import ToolFileManager
  16. from fields.file_fields import build_file_model
  17. # Define parser for both documentation and validation
  18. upload_parser = reqparse.RequestParser()
  19. upload_parser.add_argument("file", location="files", type=FileStorage, required=True, help="File to upload")
  20. upload_parser.add_argument(
  21. "timestamp", type=str, required=True, location="args", help="Unix timestamp for signature verification"
  22. )
  23. upload_parser.add_argument(
  24. "nonce", type=str, required=True, location="args", help="Random string for signature verification"
  25. )
  26. upload_parser.add_argument(
  27. "sign", type=str, required=True, location="args", help="HMAC signature for request validation"
  28. )
  29. upload_parser.add_argument("tenant_id", type=str, required=True, location="args", help="Tenant identifier")
  30. upload_parser.add_argument("user_id", type=str, required=False, location="args", help="User identifier")
  31. @files_ns.route("/upload/for-plugin")
  32. class PluginUploadFileApi(Resource):
  33. @setup_required
  34. @files_ns.expect(upload_parser)
  35. @files_ns.doc("upload_plugin_file")
  36. @files_ns.doc(description="Upload a file for plugin usage with signature verification")
  37. @files_ns.doc(
  38. responses={
  39. 201: "File uploaded successfully",
  40. 400: "Invalid request parameters",
  41. 403: "Forbidden - Invalid signature or missing parameters",
  42. 413: "File too large",
  43. 415: "Unsupported file type",
  44. }
  45. )
  46. @files_ns.marshal_with(build_file_model(files_ns), code=HTTPStatus.CREATED)
  47. def post(self):
  48. """Upload a file for plugin usage.
  49. Accepts a file upload with signature verification for security.
  50. The file must be accompanied by valid timestamp, nonce, and signature parameters.
  51. Returns:
  52. dict: File metadata including ID, URLs, and properties
  53. int: HTTP status code (201 for success)
  54. Raises:
  55. Forbidden: Invalid signature or missing required parameters
  56. FileTooLargeError: File exceeds size limit
  57. UnsupportedFileTypeError: File type not supported
  58. """
  59. # Parse and validate all arguments
  60. args = upload_parser.parse_args()
  61. file: FileStorage = args["file"]
  62. timestamp: str = args["timestamp"]
  63. nonce: str = args["nonce"]
  64. sign: str = args["sign"]
  65. tenant_id: str = args["tenant_id"]
  66. user_id: str | None = args.get("user_id")
  67. user = get_user(tenant_id, user_id)
  68. filename: str | None = file.filename
  69. mimetype: str | None = file.mimetype
  70. if not filename or not mimetype:
  71. raise Forbidden("Invalid request.")
  72. if not verify_plugin_file_signature(
  73. filename=filename,
  74. mimetype=mimetype,
  75. tenant_id=tenant_id,
  76. user_id=user.id,
  77. timestamp=timestamp,
  78. nonce=nonce,
  79. sign=sign,
  80. ):
  81. raise Forbidden("Invalid request.")
  82. try:
  83. tool_file = ToolFileManager().create_file_by_raw(
  84. user_id=user.id,
  85. tenant_id=tenant_id,
  86. file_binary=file.read(),
  87. mimetype=mimetype,
  88. filename=filename,
  89. conversation_id=None,
  90. )
  91. extension = guess_extension(tool_file.mimetype) or ".bin"
  92. preview_url = ToolFileManager.sign_file(tool_file_id=tool_file.id, extension=extension)
  93. # Create a dictionary with all the necessary attributes
  94. result = {
  95. "id": tool_file.id,
  96. "user_id": tool_file.user_id,
  97. "tenant_id": tool_file.tenant_id,
  98. "conversation_id": tool_file.conversation_id,
  99. "file_key": tool_file.file_key,
  100. "mimetype": tool_file.mimetype,
  101. "original_url": tool_file.original_url,
  102. "name": tool_file.name,
  103. "size": tool_file.size,
  104. "mime_type": mimetype,
  105. "extension": extension,
  106. "preview_url": preview_url,
  107. }
  108. return result, 201
  109. except services.errors.file.FileTooLargeError as file_too_large_error:
  110. raise FileTooLargeError(file_too_large_error.description)
  111. except services.errors.file.UnsupportedFileTypeError:
  112. raise UnsupportedFileTypeError()