Files
dify/api/controllers/files/storage_proxy.py
Harry f52fb919d1 refactor(storage): remove signer, using general file storage
- Removed unused app asset download and upload endpoints, along with sandbox archive and file download endpoints.
- Updated imports in the file controller to reflect the removal of these endpoints.
- Simplified the generator.py file by consolidating the code context field definition.
- Enhanced the storage layer with a unified presign wrapper for better handling of presigned URLs.
2026-01-29 23:01:12 +08:00

103 lines
3.2 KiB
Python

"""Unified file proxy controller for storage operations.
This controller handles file download and upload operations when the underlying
storage backend doesn't support presigned URLs. It verifies signed proxy URLs
generated by FilePresignStorage and streams files to/from storage.
Endpoints:
GET /files/storage/{filename}/download - Download a file
PUT /files/storage/{filename}/upload - Upload a file
"""
from urllib.parse import quote, unquote
from flask import Response, request
from flask_restx import Resource
from pydantic import BaseModel, Field
from werkzeug.exceptions import Forbidden, NotFound
from controllers.files import files_ns
from extensions.ext_storage import storage
from extensions.storage.file_presign_storage import FilePresignStorage
DEFAULT_REF_TEMPLATE_SWAGGER_2_0 = "#/definitions/{model}"
class StorageProxyQuery(BaseModel):
"""Query parameters for storage proxy URLs."""
timestamp: str = Field(..., description="Unix timestamp used in the signature")
nonce: str = Field(..., description="Random string for signature")
sign: str = Field(..., description="HMAC signature")
files_ns.schema_model(
StorageProxyQuery.__name__,
StorageProxyQuery.model_json_schema(ref_template=DEFAULT_REF_TEMPLATE_SWAGGER_2_0),
)
@files_ns.route("/storage/<path:filename>/download")
class StorageFileDownloadApi(Resource):
"""Handle file downloads through the proxy."""
def get(self, filename: str):
"""Download a file from storage.
Verifies the signed URL and streams the file content back to the client.
"""
filename = unquote(filename)
args = StorageProxyQuery.model_validate(request.args.to_dict(flat=True))
if not FilePresignStorage.verify_signature(
filename=filename,
operation="download",
timestamp=args.timestamp,
nonce=args.nonce,
sign=args.sign,
):
raise Forbidden("Invalid or expired download link")
try:
generator = storage.load_stream(filename)
except FileNotFoundError:
raise NotFound("File not found")
encoded_filename = quote(filename.split("/")[-1])
return Response(
generator,
mimetype="application/octet-stream",
direct_passthrough=True,
headers={
"Content-Disposition": f"attachment; filename*=UTF-8''{encoded_filename}",
},
)
@files_ns.route("/storage/<path:filename>/upload")
class StorageFileUploadApi(Resource):
"""Handle file uploads through the proxy."""
def put(self, filename: str):
"""Upload a file to storage.
Verifies the signed URL and saves the request body to storage.
"""
filename = unquote(filename)
args = StorageProxyQuery.model_validate(request.args.to_dict(flat=True))
if not FilePresignStorage.verify_signature(
filename=filename,
operation="upload",
timestamp=args.timestamp,
nonce=args.nonce,
sign=args.sign,
):
raise Forbidden("Invalid or expired upload link")
content = request.get_data()
storage.save(filename, content)
return Response(status=204)