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//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//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//assets/files/") 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//assets/nodes/") 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//assets/nodes//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//assets/nodes//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//assets/nodes//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//assets/files//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//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//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()