refactor(app-asset): remove deprecated file download resource and streamline download URL handling with pre-signed storage

This commit is contained in:
Harry
2026-01-15 19:27:52 +08:00
parent cd0724b827
commit d8bafb0d1c
5 changed files with 135 additions and 87 deletions

View File

@ -1,7 +1,6 @@
from flask import Response, request
from flask import request
from flask_restx import Resource
from pydantic import BaseModel, Field, field_validator
from werkzeug.exceptions import Forbidden
from controllers.console import console_ns
from controllers.console.app.error import (
@ -273,33 +272,3 @@ class AppAssetFileDownloadUrlResource(Resource):
return {"download_url": download_url}
except ServiceNodeNotFoundError:
raise AppAssetNodeNotFoundError()
@console_ns.route("/apps/<string:app_id>/assets/files/<string:node_id>/download")
class AppAssetFileDownloadResource(Resource):
@setup_required
@get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW])
def get(self, app_model: App, node_id: str):
timestamp = request.args.get("timestamp", "")
nonce = request.args.get("nonce", "")
sign = request.args.get("sign", "")
if not AppAssetService.verify_download_signature(
app_id=app_model.id,
node_id=node_id,
timestamp=timestamp,
nonce=nonce,
sign=sign,
):
raise Forbidden("Invalid or expired download link")
try:
content, filename = AppAssetService.get_file_for_download(app_model, node_id)
except ServiceNodeNotFoundError:
raise AppAssetNodeNotFoundError()
return Response(
content,
mimetype="application/octet-stream",
headers={"Content-Disposition": f'attachment; filename="{filename}"'},
)

View File

@ -14,7 +14,7 @@ api = ExternalApi(
files_ns = Namespace("files", description="File operations", path="/")
from . import image_preview, tool_files, upload
from . import image_preview, storage_download, tool_files, upload
api.add_namespace(files_ns)
@ -23,6 +23,7 @@ __all__ = [
"bp",
"files_ns",
"image_preview",
"storage_download",
"tool_files",
"upload",
]

View File

@ -0,0 +1,56 @@
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 StorageDownloadQuery(BaseModel):
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(
StorageDownloadQuery.__name__,
StorageDownloadQuery.model_json_schema(ref_template=DEFAULT_REF_TEMPLATE_SWAGGER_2_0),
)
@files_ns.route("/storage/<path:filename>/download")
class StorageFileDownloadApi(Resource):
def get(self, filename: str):
filename = unquote(filename)
args = StorageDownloadQuery.model_validate(request.args.to_dict(flat=True))
if not FilePresignStorage.verify_signature(
filename=filename,
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}",
},
)