mirror of
https://github.com/langgenius/dify.git
synced 2026-02-12 13:00:45 -05:00
- Deleted `AppAssetFileResource` class and its associated file upload logic. - Removed the `create_file` method from `AppAssetService` to streamline asset management. - Updated `AppAssetBatchUploadResource` for improved readability by condensing method calls.
322 lines
12 KiB
Python
322 lines
12 KiB
Python
from flask import request
|
|
from flask_restx import Resource
|
|
from pydantic import BaseModel, Field, field_validator
|
|
|
|
from controllers.console import console_ns
|
|
from controllers.console.app.error import (
|
|
AppAssetNodeNotFoundError,
|
|
AppAssetPathConflictError,
|
|
)
|
|
from controllers.console.app.wraps import get_app_model
|
|
from controllers.console.wraps import account_initialization_required, setup_required
|
|
from core.app.entities.app_asset_entities import BatchUploadNode
|
|
from libs.login import current_account_with_tenant, login_required
|
|
from models import App
|
|
from models.model import AppMode
|
|
from services.app_asset_service import AppAssetService
|
|
from services.errors.app_asset import (
|
|
AppAssetNodeNotFoundError as ServiceNodeNotFoundError,
|
|
)
|
|
from services.errors.app_asset import (
|
|
AppAssetParentNotFoundError,
|
|
)
|
|
from services.errors.app_asset import (
|
|
AppAssetPathConflictError as ServicePathConflictError,
|
|
)
|
|
|
|
DEFAULT_REF_TEMPLATE_SWAGGER_2_0 = "#/definitions/{model}"
|
|
|
|
|
|
class CreateFolderPayload(BaseModel):
|
|
name: str = Field(..., min_length=1, max_length=255)
|
|
parent_id: str | None = None
|
|
|
|
|
|
class CreateFilePayload(BaseModel):
|
|
name: str = Field(..., min_length=1, max_length=255)
|
|
parent_id: str | None = None
|
|
|
|
@field_validator("name", mode="before")
|
|
@classmethod
|
|
def strip_name(cls, v: str) -> str:
|
|
return v.strip() if isinstance(v, str) else v
|
|
|
|
@field_validator("parent_id", mode="before")
|
|
@classmethod
|
|
def empty_to_none(cls, v: str | None) -> str | None:
|
|
return v or None
|
|
|
|
|
|
class GetUploadUrlPayload(BaseModel):
|
|
name: str = Field(..., min_length=1, max_length=255)
|
|
size: int = Field(..., ge=0)
|
|
parent_id: str | None = None
|
|
|
|
@field_validator("name", mode="before")
|
|
@classmethod
|
|
def strip_name(cls, v: str) -> str:
|
|
return v.strip() if isinstance(v, str) else v
|
|
|
|
@field_validator("parent_id", mode="before")
|
|
@classmethod
|
|
def empty_to_none(cls, v: str | None) -> str | None:
|
|
return v or None
|
|
|
|
|
|
class BatchUploadPayload(BaseModel):
|
|
children: list[BatchUploadNode] = Field(..., min_length=1)
|
|
|
|
|
|
class UpdateFileContentPayload(BaseModel):
|
|
content: str
|
|
|
|
|
|
class RenameNodePayload(BaseModel):
|
|
name: str = Field(..., min_length=1, max_length=255)
|
|
|
|
|
|
class MoveNodePayload(BaseModel):
|
|
parent_id: str | None = None
|
|
|
|
|
|
class ReorderNodePayload(BaseModel):
|
|
after_node_id: str | None = Field(default=None, description="Place after this node, None for first position")
|
|
|
|
|
|
def reg(cls: type[BaseModel]) -> None:
|
|
console_ns.schema_model(cls.__name__, cls.model_json_schema(ref_template=DEFAULT_REF_TEMPLATE_SWAGGER_2_0))
|
|
|
|
|
|
reg(CreateFolderPayload)
|
|
reg(CreateFilePayload)
|
|
reg(GetUploadUrlPayload)
|
|
reg(BatchUploadNode)
|
|
reg(BatchUploadPayload)
|
|
reg(UpdateFileContentPayload)
|
|
reg(RenameNodePayload)
|
|
reg(MoveNodePayload)
|
|
reg(ReorderNodePayload)
|
|
|
|
|
|
@console_ns.route("/apps/<string:app_id>/assets/tree")
|
|
class AppAssetTreeResource(Resource):
|
|
@setup_required
|
|
@login_required
|
|
@account_initialization_required
|
|
@get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW])
|
|
def get(self, app_model: App):
|
|
current_user, _ = current_account_with_tenant()
|
|
tree = AppAssetService.get_asset_tree(app_model, current_user.id)
|
|
return {"children": [view.model_dump() for view in tree.transform()]}
|
|
|
|
|
|
@console_ns.route("/apps/<string:app_id>/assets/folders")
|
|
class AppAssetFolderResource(Resource):
|
|
@console_ns.expect(console_ns.models[CreateFolderPayload.__name__])
|
|
@setup_required
|
|
@login_required
|
|
@account_initialization_required
|
|
@get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW])
|
|
def post(self, app_model: App):
|
|
current_user, _ = current_account_with_tenant()
|
|
payload = CreateFolderPayload.model_validate(console_ns.payload or {})
|
|
|
|
try:
|
|
node = AppAssetService.create_folder(app_model, current_user.id, payload.name, payload.parent_id)
|
|
return node.model_dump(), 201
|
|
except AppAssetParentNotFoundError:
|
|
raise AppAssetNodeNotFoundError()
|
|
except ServicePathConflictError:
|
|
raise AppAssetPathConflictError()
|
|
|
|
|
|
@console_ns.route("/apps/<string:app_id>/assets/files/<string:node_id>")
|
|
class AppAssetFileDetailResource(Resource):
|
|
@setup_required
|
|
@login_required
|
|
@account_initialization_required
|
|
@get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW])
|
|
def get(self, app_model: App, node_id: str):
|
|
current_user, _ = current_account_with_tenant()
|
|
try:
|
|
content = AppAssetService.get_file_content(app_model, current_user.id, node_id)
|
|
return {"content": content.decode("utf-8", errors="replace")}
|
|
except ServiceNodeNotFoundError:
|
|
raise AppAssetNodeNotFoundError()
|
|
|
|
@console_ns.expect(console_ns.models[UpdateFileContentPayload.__name__])
|
|
@setup_required
|
|
@login_required
|
|
@account_initialization_required
|
|
@get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW])
|
|
def put(self, app_model: App, node_id: str):
|
|
current_user, _ = current_account_with_tenant()
|
|
|
|
file = request.files.get("file")
|
|
if file:
|
|
content = file.read()
|
|
else:
|
|
payload = UpdateFileContentPayload.model_validate(console_ns.payload or {})
|
|
content = payload.content.encode("utf-8")
|
|
|
|
try:
|
|
node = AppAssetService.update_file_content(app_model, current_user.id, node_id, content)
|
|
return node.model_dump()
|
|
except ServiceNodeNotFoundError:
|
|
raise AppAssetNodeNotFoundError()
|
|
|
|
|
|
@console_ns.route("/apps/<string:app_id>/assets/nodes/<string:node_id>")
|
|
class AppAssetNodeResource(Resource):
|
|
@setup_required
|
|
@login_required
|
|
@account_initialization_required
|
|
@get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW])
|
|
def delete(self, app_model: App, node_id: str):
|
|
current_user, _ = current_account_with_tenant()
|
|
try:
|
|
AppAssetService.delete_node(app_model, current_user.id, node_id)
|
|
return {"result": "success"}, 200
|
|
except ServiceNodeNotFoundError:
|
|
raise AppAssetNodeNotFoundError()
|
|
|
|
|
|
@console_ns.route("/apps/<string:app_id>/assets/nodes/<string:node_id>/rename")
|
|
class AppAssetNodeRenameResource(Resource):
|
|
@console_ns.expect(console_ns.models[RenameNodePayload.__name__])
|
|
@setup_required
|
|
@login_required
|
|
@account_initialization_required
|
|
@get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW])
|
|
def post(self, app_model: App, node_id: str):
|
|
current_user, _ = current_account_with_tenant()
|
|
payload = RenameNodePayload.model_validate(console_ns.payload or {})
|
|
|
|
try:
|
|
node = AppAssetService.rename_node(app_model, current_user.id, node_id, payload.name)
|
|
return node.model_dump()
|
|
except ServiceNodeNotFoundError:
|
|
raise AppAssetNodeNotFoundError()
|
|
except ServicePathConflictError:
|
|
raise AppAssetPathConflictError()
|
|
|
|
|
|
@console_ns.route("/apps/<string:app_id>/assets/nodes/<string:node_id>/move")
|
|
class AppAssetNodeMoveResource(Resource):
|
|
@console_ns.expect(console_ns.models[MoveNodePayload.__name__])
|
|
@setup_required
|
|
@login_required
|
|
@account_initialization_required
|
|
@get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW])
|
|
def post(self, app_model: App, node_id: str):
|
|
current_user, _ = current_account_with_tenant()
|
|
payload = MoveNodePayload.model_validate(console_ns.payload or {})
|
|
|
|
try:
|
|
node = AppAssetService.move_node(app_model, current_user.id, node_id, payload.parent_id)
|
|
return node.model_dump()
|
|
except ServiceNodeNotFoundError:
|
|
raise AppAssetNodeNotFoundError()
|
|
except AppAssetParentNotFoundError:
|
|
raise AppAssetNodeNotFoundError()
|
|
except ServicePathConflictError:
|
|
raise AppAssetPathConflictError()
|
|
|
|
|
|
@console_ns.route("/apps/<string:app_id>/assets/nodes/<string:node_id>/reorder")
|
|
class AppAssetNodeReorderResource(Resource):
|
|
@console_ns.expect(console_ns.models[ReorderNodePayload.__name__])
|
|
@setup_required
|
|
@login_required
|
|
@account_initialization_required
|
|
@get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW])
|
|
def post(self, app_model: App, node_id: str):
|
|
current_user, _ = current_account_with_tenant()
|
|
payload = ReorderNodePayload.model_validate(console_ns.payload or {})
|
|
|
|
try:
|
|
node = AppAssetService.reorder_node(app_model, current_user.id, node_id, payload.after_node_id)
|
|
return node.model_dump()
|
|
except ServiceNodeNotFoundError:
|
|
raise AppAssetNodeNotFoundError()
|
|
|
|
|
|
@console_ns.route("/apps/<string:app_id>/assets/files/<string:node_id>/download-url")
|
|
class AppAssetFileDownloadUrlResource(Resource):
|
|
@setup_required
|
|
@login_required
|
|
@account_initialization_required
|
|
@get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW])
|
|
def get(self, app_model: App, node_id: str):
|
|
current_user, _ = current_account_with_tenant()
|
|
try:
|
|
download_url = AppAssetService.get_file_download_url(app_model, current_user.id, node_id)
|
|
return {"download_url": download_url}
|
|
except ServiceNodeNotFoundError:
|
|
raise AppAssetNodeNotFoundError()
|
|
|
|
|
|
@console_ns.route("/apps/<string:app_id>/assets/files/upload")
|
|
class AppAssetFileUploadUrlResource(Resource):
|
|
@console_ns.expect(console_ns.models[GetUploadUrlPayload.__name__])
|
|
@setup_required
|
|
@login_required
|
|
@account_initialization_required
|
|
@get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW])
|
|
def post(self, app_model: App):
|
|
current_user, _ = current_account_with_tenant()
|
|
payload = GetUploadUrlPayload.model_validate(console_ns.payload or {})
|
|
|
|
try:
|
|
node, upload_url = AppAssetService.get_file_upload_url(
|
|
app_model, current_user.id, payload.name, payload.size, payload.parent_id
|
|
)
|
|
return {"node": node.model_dump(), "upload_url": upload_url}, 201
|
|
except AppAssetParentNotFoundError:
|
|
raise AppAssetNodeNotFoundError()
|
|
except ServicePathConflictError:
|
|
raise AppAssetPathConflictError()
|
|
|
|
|
|
@console_ns.route("/apps/<string:app_id>/assets/batch-upload")
|
|
class AppAssetBatchUploadResource(Resource):
|
|
@console_ns.expect(console_ns.models[BatchUploadPayload.__name__])
|
|
@setup_required
|
|
@login_required
|
|
@account_initialization_required
|
|
@get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW])
|
|
def post(self, app_model: App):
|
|
"""
|
|
Create nodes from tree structure and return upload URLs.
|
|
|
|
Input:
|
|
{
|
|
"children": [
|
|
{"name": "folder1", "node_type": "folder", "children": [
|
|
{"name": "file1.txt", "node_type": "file", "size": 1024}
|
|
]},
|
|
{"name": "root.txt", "node_type": "file", "size": 512}
|
|
]
|
|
}
|
|
|
|
Output:
|
|
{
|
|
"children": [
|
|
{"id": "xxx", "name": "folder1", "node_type": "folder", "children": [
|
|
{"id": "yyy", "name": "file1.txt", "node_type": "file", "size": 1024, "upload_url": "..."}
|
|
]},
|
|
{"id": "zzz", "name": "root.txt", "node_type": "file", "size": 512, "upload_url": "..."}
|
|
]
|
|
}
|
|
"""
|
|
current_user, _ = current_account_with_tenant()
|
|
payload = BatchUploadPayload.model_validate(console_ns.payload or {})
|
|
|
|
try:
|
|
result_children = AppAssetService.batch_create_from_tree(app_model, current_user.id, payload.children)
|
|
return {"children": [child.model_dump() for child in result_children]}, 201
|
|
except AppAssetParentNotFoundError:
|
|
raise AppAssetNodeNotFoundError()
|
|
except ServicePathConflictError:
|
|
raise AppAssetPathConflictError()
|