mirror of
https://github.com/langgenius/dify.git
synced 2026-04-20 10:47:21 +08:00
Port the complete infrastructure for agent sandbox execution and skill system: Sandbox & Virtual Environment (core/sandbox/, core/virtual_environment/): - Sandbox entity with lifecycle management (ready/failed/cancelled states) - SandboxBuilder with fluent API for configuring providers - 5 VM providers: Local, SSH, Docker, E2B, AWS CodeInterpreter - VirtualEnvironment base with command execution, file transfer, transport layers - Channel transport: pipe, queue, socket implementations - Bash session management and DifyCli binary integration - Storage: archive storage, file storage, noop storage, presign storage - Initializers: DifyCli, AppAssets, DraftAppAssets, Skills - Inspector: file browser, archive/runtime source, script utils - Security: encryption utils, debug helpers Skill & App Assets (core/skill/, core/app_assets/, core/app_bundle/): - Skill entity and manager - App asset accessor, builder pipeline (file, skill builders) - App bundle source zip extractor - Storage and converter utilities API Endpoints: - CLI API blueprint (controllers/cli_api/) for sandbox callback - Sandbox provider management (workspace/sandbox_providers) - Sandbox file browser (console/sandbox_files) - App asset management (console/app/app_asset) - Skill management (console/app/skills) - Storage file endpoints (controllers/files/storage_files) Services: - Sandbox service, provider service, file service - App asset service, app bundle service Config: - CliApiConfig, CreatorsPlatformConfig, CollaborationConfig - FILES_API_URL for sandbox file access Note: Controller route registration temporarily commented out (marked TODO) pending resolution of deep dependency chains (socketio, workflow_comment, command node, etc.). Core sandbox modules are fully ported and syntax-validated. 110 files changed, 10,549 insertions. Made-with: Cursor
81 lines
2.7 KiB
Python
81 lines
2.7 KiB
Python
"""Token-based file proxy controller for storage operations.
|
|
|
|
This controller handles file download and upload operations using opaque UUID tokens.
|
|
The token maps to the real storage key in Redis, so the actual storage path is never
|
|
exposed in the URL.
|
|
|
|
Routes:
|
|
GET /files/storage-files/{token} - Download a file
|
|
PUT /files/storage-files/{token} - Upload a file
|
|
|
|
The operation type (download/upload) is determined by the ticket stored in Redis,
|
|
not by the HTTP method. This ensures a download ticket cannot be used for upload
|
|
and vice versa.
|
|
"""
|
|
|
|
from urllib.parse import quote
|
|
|
|
from flask import Response, request
|
|
from flask_restx import Resource
|
|
from werkzeug.exceptions import Forbidden, NotFound, RequestEntityTooLarge
|
|
|
|
from controllers.files import files_ns
|
|
from extensions.ext_storage import storage
|
|
from services.storage_ticket_service import StorageTicketService
|
|
|
|
|
|
@files_ns.route("/storage-files/<string:token>")
|
|
class StorageFilesApi(Resource):
|
|
"""Handle file operations through token-based URLs."""
|
|
|
|
def get(self, token: str):
|
|
"""Download a file using a token.
|
|
|
|
The ticket must have op="download", otherwise returns 403.
|
|
"""
|
|
ticket = StorageTicketService.get_ticket(token)
|
|
if ticket is None:
|
|
raise Forbidden("Invalid or expired token")
|
|
|
|
if ticket.op != "download":
|
|
raise Forbidden("This token is not valid for download")
|
|
|
|
try:
|
|
generator = storage.load_stream(ticket.storage_key)
|
|
except FileNotFoundError:
|
|
raise NotFound("File not found")
|
|
|
|
filename = ticket.filename or ticket.storage_key.rsplit("/", 1)[-1]
|
|
encoded_filename = quote(filename)
|
|
|
|
return Response(
|
|
generator,
|
|
mimetype="application/octet-stream",
|
|
direct_passthrough=True,
|
|
headers={
|
|
"Content-Disposition": f"attachment; filename*=UTF-8''{encoded_filename}",
|
|
},
|
|
)
|
|
|
|
def put(self, token: str):
|
|
"""Upload a file using a token.
|
|
|
|
The ticket must have op="upload", otherwise returns 403.
|
|
If the request body exceeds max_bytes, returns 413.
|
|
"""
|
|
ticket = StorageTicketService.get_ticket(token)
|
|
if ticket is None:
|
|
raise Forbidden("Invalid or expired token")
|
|
|
|
if ticket.op != "upload":
|
|
raise Forbidden("This token is not valid for upload")
|
|
|
|
content = request.get_data()
|
|
|
|
if ticket.max_bytes is not None and len(content) > ticket.max_bytes:
|
|
raise RequestEntityTooLarge(f"Upload exceeds maximum size of {ticket.max_bytes} bytes")
|
|
|
|
storage.save(ticket.storage_key, content)
|
|
|
|
return Response(status=204)
|