Files
dify/api/controllers/files/upload.py
2025-08-25 09:27:09 +08:00

130 lines
4.9 KiB
Python

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()