feat(app_asset): add batch upload and file upload URL generation

- Introduced `GetUploadUrlPayload` and `BatchUploadPayload` models for handling file uploads.
- Implemented `AppAssetFileUploadUrlResource` for generating pre-signed upload URLs.
- Added `AppAssetBatchUploadResource` to support batch creation of asset nodes from a tree structure.
- Enhanced `AppAssetService` with methods for obtaining upload URLs and batch creation of assets.
- Removed checksum handling from file creation to streamline the process.
This commit is contained in:
Harry 2026-01-23 14:34:16 +08:00
parent a522327662
commit 225c33633a
5 changed files with 403 additions and 191 deletions

View File

@ -10,6 +10,7 @@ from controllers.console.app.error import (
)
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
@ -47,6 +48,26 @@ class CreateFilePayload(BaseModel):
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
@ -69,6 +90,9 @@ def reg(cls: type[BaseModel]) -> None:
reg(CreateFolderPayload)
reg(CreateFilePayload)
reg(GetUploadUrlPayload)
reg(BatchUploadNode)
reg(BatchUploadPayload)
reg(UpdateFileContentPayload)
reg(RenameNodePayload)
reg(MoveNodePayload)
@ -256,3 +280,70 @@ class AppAssetFileDownloadUrlResource(Resource):
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()

View File

@ -20,16 +20,13 @@ class AppAssetNode(BaseModel):
order: int = Field(default=0, description="Sort order within parent folder, lower values first")
extension: str = Field(default="", description="File extension without dot, empty for folders")
size: int = Field(default=0, description="File size in bytes, 0 for folders")
checksum: str = Field(default="", description="SHA-256 checksum of file content, empty for folders")
@classmethod
def create_folder(cls, node_id: str, name: str, parent_id: str | None = None) -> AppAssetNode:
return cls(id=node_id, node_type=AssetNodeType.FOLDER, name=name, parent_id=parent_id)
@classmethod
def create_file(
cls, node_id: str, name: str, parent_id: str | None = None, size: int = 0, checksum: str = ""
) -> AppAssetNode:
def create_file(cls, node_id: str, name: str, parent_id: str | None = None, size: int = 0) -> AppAssetNode:
return cls(
id=node_id,
node_type=AssetNodeType.FILE,
@ -37,7 +34,6 @@ class AppAssetNode(BaseModel):
parent_id=parent_id,
extension=name.rsplit(".", 1)[-1] if "." in name else "",
size=size,
checksum=checksum,
)
@ -48,10 +44,39 @@ class AppAssetNodeView(BaseModel):
path: str = Field(description="Full path from root, e.g. '/folder/file.txt'")
extension: str = Field(default="", description="File extension without dot")
size: int = Field(default=0, description="File size in bytes")
checksum: str = Field(default="", description="SHA-256 checksum of file content")
children: list[AppAssetNodeView] = Field(default_factory=list, description="Child nodes for folders")
class BatchUploadNode(BaseModel):
"""Structure for batch upload_url tree nodes, used for both input and output."""
name: str
node_type: AssetNodeType
size: int = 0
children: list[BatchUploadNode] = []
id: str | None = None
upload_url: str | None = None
def to_app_asset_nodes(self, parent_id: str | None = None) -> list[AppAssetNode]:
"""
Generate IDs and convert to AppAssetNode list.
Mutates self to set id field.
"""
from uuid import uuid4
self.id = str(uuid4())
nodes: list[AppAssetNode] = []
if self.node_type == AssetNodeType.FOLDER:
nodes.append(AppAssetNode.create_folder(self.id, self.name, parent_id))
for child in self.children:
nodes.extend(child.to_app_asset_nodes(self.id))
else:
nodes.append(AppAssetNode.create_file(self.id, self.name, parent_id, self.size))
return nodes
class TreeNodeNotFoundError(Exception):
"""Tree internal: node not found"""
@ -192,12 +217,11 @@ class AppAssetFileTree(BaseModel):
self.nodes.append(node)
return node
def update(self, node_id: str, size: int, checksum: str) -> AppAssetNode:
def update(self, node_id: str, size: int) -> AppAssetNode:
node = self.get(node_id)
if not node or node.node_type != AssetNodeType.FILE:
raise TreeNodeNotFoundError(node_id)
node.size = size
node.checksum = checksum
return node
def rename(self, node_id: str, new_name: str) -> AppAssetNode:
@ -284,7 +308,6 @@ class AppAssetFileTree(BaseModel):
path=path,
extension=node.extension,
size=node.size,
checksum=node.checksum,
children=child_views,
)

View File

@ -1,6 +1,5 @@
from __future__ import annotations
import hashlib
import io
import zipfile
from collections.abc import Callable
@ -77,8 +76,7 @@ class SourceZipExtractor:
parent_path = file.path.rsplit("/", 1)[0] if "/" in file.path else None
parent_id = path_to_node_id.get(parent_path) if parent_path else None
checksum = hashlib.sha256(file.content).hexdigest()
node = AppAssetNode.create_file(node_id, name, parent_id, len(file.content), checksum)
node = AppAssetNode.create_file(node_id, name, parent_id, len(file.content))
tree.add(node)
storage_key = storage_key_fn(tenant_id, app_id, node_id)

View File

@ -1,4 +1,3 @@
import hashlib
import logging
from uuid import uuid4
@ -8,6 +7,7 @@ from core.app.entities.app_asset_entities import (
AppAssetFileTree,
AppAssetNode,
AssetNodeType,
BatchUploadNode,
TreeNodeNotFoundError,
TreeParentNotFoundError,
TreePathConflictError,
@ -34,9 +34,14 @@ logger = logging.getLogger(__name__)
class AppAssetService:
MAX_PREVIEW_CONTENT_SIZE = 5 * 1024 * 1024 # 1MB
MAX_PREVIEW_CONTENT_SIZE = 5 * 1024 * 1024 # 5MB
_PRESIGN_CACHE_TTL_BUFFER_SECONDS = 300
_PRESIGN_CACHE_MIN_TTL_SECONDS = 60
_LOCK_TIMEOUT_SECONDS = 60
@staticmethod
def _lock(app_id: str):
return redis_client.lock(f"app_asset:lock:{app_id}", timeout=AppAssetService._LOCK_TIMEOUT_SECONDS)
@staticmethod
def _draft_download_cache_key(storage_key: str) -> str:
@ -198,25 +203,27 @@ class AppAssetService:
name: str,
parent_id: str | None = None,
) -> AppAssetNode:
with Session(db.engine, expire_on_commit=False) as session:
assets = AppAssetService.get_or_create_assets(session, app_model, account_id)
tree = assets.asset_tree
with AppAssetService._lock(app_model.id):
with Session(db.engine, expire_on_commit=False) as session:
assets = AppAssetService.get_or_create_assets(session, app_model, account_id)
tree = assets.asset_tree
node = AppAssetNode.create_folder(str(uuid4()), name, parent_id)
node = AppAssetNode.create_folder(str(uuid4()), name, parent_id)
try:
tree.add(node)
except TreeParentNotFoundError as e:
raise AppAssetParentNotFoundError(str(e)) from e
except TreePathConflictError as e:
raise AppAssetPathConflictError(str(e)) from e
try:
tree.add(node)
except TreeParentNotFoundError as e:
raise AppAssetParentNotFoundError(str(e)) from e
except TreePathConflictError as e:
raise AppAssetPathConflictError(str(e)) from e
assets.asset_tree = tree
assets.updated_by = account_id
session.commit()
assets.asset_tree = tree
assets.updated_by = account_id
session.commit()
return node
return node
# FIXME(Mairuis): migrate to get_file_upload_url / get_file_upload_urls API
@staticmethod
def create_file(
app_model: App,
@ -225,37 +232,37 @@ class AppAssetService:
content: bytes,
parent_id: str | None = None,
) -> AppAssetNode:
with Session(db.engine, expire_on_commit=False) as session:
assets = AppAssetService.get_or_create_assets(session, app_model, account_id)
tree = assets.asset_tree
with AppAssetService._lock(app_model.id):
with Session(db.engine, expire_on_commit=False) as session:
assets = AppAssetService.get_or_create_assets(session, app_model, account_id)
tree = assets.asset_tree
node_id = str(uuid4())
checksum = hashlib.sha256(content).hexdigest()
node = AppAssetNode.create_file(node_id, name, parent_id, len(content), checksum)
node_id = str(uuid4())
node = AppAssetNode.create_file(node_id, name, parent_id, len(content))
try:
tree.add(node)
except TreeParentNotFoundError as e:
raise AppAssetParentNotFoundError(str(e)) from e
except TreePathConflictError as e:
raise AppAssetPathConflictError(str(e)) from e
try:
tree.add(node)
except TreeParentNotFoundError as e:
raise AppAssetParentNotFoundError(str(e)) from e
except TreePathConflictError as e:
raise AppAssetPathConflictError(str(e)) from e
storage_key = AssetPaths.draft_file(app_model.tenant_id, app_model.id, node_id)
storage.save(storage_key, content)
storage_key = AssetPaths.draft_file(app_model.tenant_id, app_model.id, node_id)
storage.save(storage_key, content)
assets.asset_tree = tree
assets.updated_by = account_id
session.commit()
assets.asset_tree = tree
assets.updated_by = account_id
session.commit()
cache_key = AppAssetService._draft_storage_key_for_node(
app_model.tenant_id,
app_model.id,
assets.id,
node,
)
AppAssetService._clear_draft_download_cache([cache_key])
cache_key = AppAssetService._draft_storage_key_for_node(
app_model.tenant_id,
app_model.id,
assets.id,
node,
)
AppAssetService._clear_draft_download_cache([cache_key])
return node
return node
@staticmethod
def get_file_content(app_model: App, account_id: str, node_id: str) -> bytes:
@ -274,6 +281,7 @@ class AppAssetService:
storage_key = AssetPaths.draft_file(app_model.tenant_id, app_model.id, node_id)
return storage.load_once(storage_key)
# FIXME(Mairuis): migrate to presigned upload API
@staticmethod
def update_file_content(
app_model: App,
@ -281,105 +289,23 @@ class AppAssetService:
node_id: str,
content: bytes,
) -> AppAssetNode:
with Session(db.engine, expire_on_commit=False) as session:
assets = AppAssetService.get_or_create_assets(session, app_model, account_id)
tree = assets.asset_tree
with AppAssetService._lock(app_model.id):
with Session(db.engine, expire_on_commit=False) as session:
assets = AppAssetService.get_or_create_assets(session, app_model, account_id)
tree = assets.asset_tree
checksum = hashlib.sha256(content).hexdigest()
try:
node = tree.update(node_id, len(content))
except TreeNodeNotFoundError as e:
raise AppAssetNodeNotFoundError(str(e)) from e
try:
node = tree.update(node_id, len(content), checksum)
except TreeNodeNotFoundError as e:
raise AppAssetNodeNotFoundError(str(e)) from e
storage_key = AssetPaths.draft_file(app_model.tenant_id, app_model.id, node_id)
storage.save(storage_key, content)
storage_key = AssetPaths.draft_file(app_model.tenant_id, app_model.id, node_id)
storage.save(storage_key, content)
assets.asset_tree = tree
assets.updated_by = account_id
session.commit()
assets.asset_tree = tree
assets.updated_by = account_id
session.commit()
cache_key = AppAssetService._draft_storage_key_for_node(
app_model.tenant_id,
app_model.id,
assets.id,
node,
)
AppAssetService._clear_draft_download_cache([cache_key])
return node
@staticmethod
def rename_node(
app_model: App,
account_id: str,
node_id: str,
new_name: str,
) -> AppAssetNode:
with Session(db.engine, expire_on_commit=False) as session:
assets = AppAssetService.get_or_create_assets(session, app_model, account_id)
tree = assets.asset_tree
old_node = tree.get(node_id)
old_extension = old_node.extension if old_node else None
try:
node = tree.rename(node_id, new_name)
except TreeNodeNotFoundError as e:
raise AppAssetNodeNotFoundError(str(e)) from e
except TreePathConflictError as e:
raise AppAssetPathConflictError(str(e)) from e
assets.asset_tree = tree
assets.updated_by = account_id
session.commit()
if node.node_type == AssetNodeType.FILE:
cache_keys: list[str] = []
if old_extension is not None:
old_storage_key = (
AssetPaths.build_resolved_file(app_model.tenant_id, app_model.id, assets.id, node.id)
if old_extension == "md"
else AssetPaths.draft_file(app_model.tenant_id, app_model.id, node.id)
)
cache_keys.append(old_storage_key)
cache_keys.append(
AppAssetService._draft_storage_key_for_node(
app_model.tenant_id,
app_model.id,
assets.id,
node,
)
)
AppAssetService._clear_draft_download_cache(list(set(cache_keys)))
return node
@staticmethod
def move_node(
app_model: App,
account_id: str,
node_id: str,
new_parent_id: str | None,
) -> AppAssetNode:
with Session(db.engine, expire_on_commit=False) as session:
assets = AppAssetService.get_or_create_assets(session, app_model, account_id)
tree = assets.asset_tree
try:
node = tree.move(node_id, new_parent_id)
except TreeNodeNotFoundError as e:
raise AppAssetNodeNotFoundError(str(e)) from e
except TreeParentNotFoundError as e:
raise AppAssetParentNotFoundError(str(e)) from e
except TreePathConflictError as e:
raise AppAssetPathConflictError(str(e)) from e
assets.asset_tree = tree
assets.updated_by = account_id
session.commit()
if node.node_type == AssetNodeType.FILE:
cache_key = AppAssetService._draft_storage_key_for_node(
app_model.tenant_id,
app_model.id,
@ -388,7 +314,90 @@ class AppAssetService:
)
AppAssetService._clear_draft_download_cache([cache_key])
return node
return node
@staticmethod
def rename_node(
app_model: App,
account_id: str,
node_id: str,
new_name: str,
) -> AppAssetNode:
with AppAssetService._lock(app_model.id):
with Session(db.engine, expire_on_commit=False) as session:
assets = AppAssetService.get_or_create_assets(session, app_model, account_id)
tree = assets.asset_tree
old_node = tree.get(node_id)
old_extension = old_node.extension if old_node else None
try:
node = tree.rename(node_id, new_name)
except TreeNodeNotFoundError as e:
raise AppAssetNodeNotFoundError(str(e)) from e
except TreePathConflictError as e:
raise AppAssetPathConflictError(str(e)) from e
assets.asset_tree = tree
assets.updated_by = account_id
session.commit()
if node.node_type == AssetNodeType.FILE:
cache_keys: list[str] = []
if old_extension is not None:
old_storage_key = (
AssetPaths.build_resolved_file(app_model.tenant_id, app_model.id, assets.id, node.id)
if old_extension == "md"
else AssetPaths.draft_file(app_model.tenant_id, app_model.id, node.id)
)
cache_keys.append(old_storage_key)
cache_keys.append(
AppAssetService._draft_storage_key_for_node(
app_model.tenant_id,
app_model.id,
assets.id,
node,
)
)
AppAssetService._clear_draft_download_cache(list(set(cache_keys)))
return node
@staticmethod
def move_node(
app_model: App,
account_id: str,
node_id: str,
new_parent_id: str | None,
) -> AppAssetNode:
with AppAssetService._lock(app_model.id):
with Session(db.engine, expire_on_commit=False) as session:
assets = AppAssetService.get_or_create_assets(session, app_model, account_id)
tree = assets.asset_tree
try:
node = tree.move(node_id, new_parent_id)
except TreeNodeNotFoundError as e:
raise AppAssetNodeNotFoundError(str(e)) from e
except TreeParentNotFoundError as e:
raise AppAssetParentNotFoundError(str(e)) from e
except TreePathConflictError as e:
raise AppAssetPathConflictError(str(e)) from e
assets.asset_tree = tree
assets.updated_by = account_id
session.commit()
if node.node_type == AssetNodeType.FILE:
cache_key = AppAssetService._draft_storage_key_for_node(
app_model.tenant_id,
app_model.id,
assets.id,
node,
)
AppAssetService._clear_draft_download_cache([cache_key])
return node
@staticmethod
def reorder_node(
@ -397,52 +406,54 @@ class AppAssetService:
node_id: str,
after_node_id: str | None,
) -> AppAssetNode:
with Session(db.engine, expire_on_commit=False) as session:
assets = AppAssetService.get_or_create_assets(session, app_model, account_id=account_id)
tree = assets.asset_tree
with AppAssetService._lock(app_model.id):
with Session(db.engine, expire_on_commit=False) as session:
assets = AppAssetService.get_or_create_assets(session, app_model, account_id=account_id)
tree = assets.asset_tree
try:
node = tree.reorder(node_id, after_node_id)
except TreeNodeNotFoundError as e:
raise AppAssetNodeNotFoundError(str(e)) from e
try:
node = tree.reorder(node_id, after_node_id)
except TreeNodeNotFoundError as e:
raise AppAssetNodeNotFoundError(str(e)) from e
assets.asset_tree = tree
assets.updated_by = account_id
session.commit()
assets.asset_tree = tree
assets.updated_by = account_id
session.commit()
return node
return node
@staticmethod
def delete_node(app_model: App, account_id: str, node_id: str) -> None:
with Session(db.engine) as session:
assets = AppAssetService.get_or_create_assets(session, app_model, account_id)
tree = assets.asset_tree
with AppAssetService._lock(app_model.id):
with Session(db.engine) as session:
assets = AppAssetService.get_or_create_assets(session, app_model, account_id)
tree = assets.asset_tree
target_ids = [node_id] + tree.get_descendant_ids(node_id)
target_nodes = [tree.get(nid) for nid in target_ids]
cache_keys = [
AppAssetService._draft_storage_key_for_node(app_model.tenant_id, app_model.id, assets.id, node)
for node in target_nodes
if node is not None and node.node_type == AssetNodeType.FILE
]
target_ids = [node_id] + tree.get_descendant_ids(node_id)
target_nodes = [tree.get(nid) for nid in target_ids]
cache_keys = [
AppAssetService._draft_storage_key_for_node(app_model.tenant_id, app_model.id, assets.id, node)
for node in target_nodes
if node is not None and node.node_type == AssetNodeType.FILE
]
try:
removed_ids = tree.remove(node_id)
except TreeNodeNotFoundError as e:
raise AppAssetNodeNotFoundError(str(e)) from e
for nid in removed_ids:
storage_key = AssetPaths.draft_file(app_model.tenant_id, app_model.id, nid)
try:
storage.delete(storage_key)
except Exception:
logger.warning("Failed to delete storage file %s", storage_key, exc_info=True)
removed_ids = tree.remove(node_id)
except TreeNodeNotFoundError as e:
raise AppAssetNodeNotFoundError(str(e)) from e
assets.asset_tree = tree
assets.updated_by = account_id
session.commit()
for nid in removed_ids:
storage_key = AssetPaths.draft_file(app_model.tenant_id, app_model.id, nid)
try:
storage.delete(storage_key)
except Exception:
logger.warning("Failed to delete storage file %s", storage_key, exc_info=True)
AppAssetService._clear_draft_download_cache(cache_keys)
assets.asset_tree = tree
assets.updated_by = account_id
session.commit()
AppAssetService._clear_draft_download_cache(cache_keys)
@staticmethod
def publish(session: Session, app_model: App, account_id: str, workflow_id: str) -> AppAssets:
@ -528,10 +539,99 @@ class AppAssetService:
account_id: str,
new_tree: AppAssetFileTree,
) -> AppAssets:
with Session(db.engine, expire_on_commit=False) as session:
assets = AppAssetService.get_or_create_assets(session, app_model, account_id)
assets.asset_tree = new_tree
assets.updated_by = account_id
session.commit()
with AppAssetService._lock(app_model.id):
with Session(db.engine, expire_on_commit=False) as session:
assets = AppAssetService.get_or_create_assets(session, app_model, account_id)
assets.asset_tree = new_tree
assets.updated_by = account_id
session.commit()
return assets
return assets
@staticmethod
def get_file_upload_url(
app_model: App,
account_id: str,
name: str,
size: int,
parent_id: str | None = None,
expires_in: int = 3600,
) -> tuple[AppAssetNode, str]:
"""
Create a file node with metadata and return a pre-signed upload URL.
The file metadata is saved immediately. If the user doesn't upload,
the download will fail when the file is accessed.
Returns:
tuple of (node, upload_url)
"""
with AppAssetService._lock(app_model.id):
with Session(db.engine, expire_on_commit=False) as session:
assets = AppAssetService.get_or_create_assets(session, app_model, account_id)
tree = assets.asset_tree
node_id = str(uuid4())
node = AppAssetNode.create_file(node_id, name, parent_id, size)
try:
tree.add(node)
except TreeParentNotFoundError as e:
raise AppAssetParentNotFoundError(str(e)) from e
except TreePathConflictError as e:
raise AppAssetPathConflictError(str(e)) from e
assets.asset_tree = tree
assets.updated_by = account_id
session.commit()
storage_key = AssetPaths.draft_file(app_model.tenant_id, app_model.id, node_id)
presign_storage = FilePresignStorage(storage.storage_runner)
upload_url = presign_storage.get_upload_url(storage_key, expires_in)
return node, upload_url
@staticmethod
def batch_create_from_tree(
app_model: App,
account_id: str,
input_children: list[BatchUploadNode],
expires_in: int = 3600,
) -> list[BatchUploadNode]:
if not input_children:
return []
new_nodes: list[AppAssetNode] = []
for child in input_children:
new_nodes.extend(child.to_app_asset_nodes(None))
with AppAssetService._lock(app_model.id):
with Session(db.engine, expire_on_commit=False) as session:
assets = AppAssetService.get_or_create_assets(session, app_model, account_id)
tree = assets.asset_tree
try:
for node in new_nodes:
tree.add(node)
except TreeParentNotFoundError as e:
raise AppAssetParentNotFoundError(str(e)) from e
except TreePathConflictError as e:
raise AppAssetPathConflictError(str(e)) from e
assets.asset_tree = tree
assets.updated_by = account_id
session.commit()
presign_storage = FilePresignStorage(storage.storage_runner)
def fill_urls(node: BatchUploadNode) -> None:
if node.node_type == AssetNodeType.FILE and node.id:
storage_key = AssetPaths.draft_file(app_model.tenant_id, app_model.id, node.id)
node.upload_url = presign_storage.get_upload_url(storage_key, expires_in)
for child in node.children:
fill_urls(child)
for child in input_children:
fill_urls(child)
return input_children

View File

@ -195,7 +195,7 @@ class SandboxProviderService:
)
if not system_configed:
raise ValueError(
f"No system default provider configured for tenant {tenant_id} and provider type {tenant_configed.provider_type}"
f"No system default provider configured for provider type {tenant_configed.provider_type}"
)
return SandboxProviderEntity(
id=tenant_configed.id,