mirror of
https://github.com/langgenius/dify.git
synced 2026-03-29 18:09:57 +08:00
The previous nested folder upload flow bypassed the backend batch-upload contract when parentId was set. Instead of creating the whole metadata tree in one backend operation, the frontend recursively called createFolder/getFileUploadUrl for each node. That introduced two regressions for uploads into subfolders: - consistency regression: mid-sequence failures could leave partially created folder trees under the destination folder - performance regression: metadata creation degraded from a single batch request to O(files + folders) round-trips before file bytes were uploaded This change moves nested uploads back to the original batch semantics: - add optional parent_id support to app asset batch-upload payload - create the whole nested tree under the target parent in AppAssetService.batch_create_from_tree - pass parentId through useBatchUpload instead of using per-node createFolder/getFileUploadUrl calls - remove the now-unnecessary useBatchUploadOperation wrapper - add a backend unit test covering batch tree creation under an existing parent folder After this change, both root uploads and subfolder uploads use the same single-request metadata creation path, preserving atomic tree creation semantics and avoiding avoidable metadata round-trips.
87 lines
2.8 KiB
Python
87 lines
2.8 KiB
Python
from types import SimpleNamespace
|
|
|
|
from core.app.entities.app_asset_entities import AppAssetFileTree, AppAssetNode, AssetNodeType, BatchUploadNode
|
|
from core.app_assets.storage import AssetPaths
|
|
from services import app_asset_service
|
|
from services.app_asset_service import AppAssetService
|
|
|
|
|
|
class DummyLock:
|
|
def __enter__(self):
|
|
return self
|
|
|
|
def __exit__(self, exc_type, exc, tb):
|
|
return False
|
|
|
|
|
|
class DummySession:
|
|
committed: bool
|
|
|
|
def __init__(self) -> None:
|
|
self.committed = False
|
|
|
|
def __enter__(self):
|
|
return self
|
|
|
|
def __exit__(self, exc_type, exc, tb):
|
|
return False
|
|
|
|
def commit(self) -> None:
|
|
self.committed = True
|
|
|
|
|
|
class DummyStorage:
|
|
keys: list[str]
|
|
|
|
def __init__(self) -> None:
|
|
self.keys = []
|
|
|
|
def get_upload_url(self, key: str, expires_in: int) -> str:
|
|
self.keys.append(key)
|
|
return f"https://upload.local/{key}?expires_in={expires_in}"
|
|
|
|
|
|
def test_batch_create_from_tree_creates_nodes_under_parent(monkeypatch):
|
|
session = DummySession()
|
|
storage = DummyStorage()
|
|
tenant_id = "11111111-1111-4111-8111-111111111111"
|
|
app_id = "22222222-2222-4222-8222-222222222222"
|
|
parent_folder = AppAssetNode.create_folder("33333333-3333-4333-8333-333333333333", "existing-parent")
|
|
assets = SimpleNamespace(asset_tree=AppAssetFileTree(nodes=[parent_folder]), updated_by=None)
|
|
app_model = SimpleNamespace(id=app_id, tenant_id=tenant_id)
|
|
|
|
monkeypatch.setattr(AppAssetService, "_lock", staticmethod(lambda _app_id: DummyLock()))
|
|
monkeypatch.setattr(app_asset_service, "db", SimpleNamespace(engine=object()))
|
|
monkeypatch.setattr(app_asset_service, "Session", lambda *args, **kwargs: session)
|
|
monkeypatch.setattr(
|
|
AppAssetService,
|
|
"get_or_create_assets",
|
|
staticmethod(lambda _session, _app_model, _account_id: assets),
|
|
)
|
|
monkeypatch.setattr(AppAssetService, "get_storage", staticmethod(lambda: storage))
|
|
|
|
result = AppAssetService.batch_create_from_tree(
|
|
app_model,
|
|
"account-1",
|
|
[
|
|
BatchUploadNode(
|
|
name="docs",
|
|
node_type=AssetNodeType.FOLDER,
|
|
children=[
|
|
BatchUploadNode(name="guide.md", node_type=AssetNodeType.FILE, size=12),
|
|
],
|
|
),
|
|
],
|
|
parent_id=parent_folder.id,
|
|
)
|
|
|
|
created_folder = next(node for node in assets.asset_tree.nodes if node.name == "docs")
|
|
created_file = next(node for node in assets.asset_tree.nodes if node.name == "guide.md")
|
|
|
|
assert created_folder.parent_id == parent_folder.id
|
|
assert created_file.parent_id == created_folder.id
|
|
assert created_file.id == result[0].children[0].id
|
|
assert assets.updated_by == "account-1"
|
|
assert session.committed is True
|
|
assert storage.keys == [AssetPaths.draft(tenant_id, app_id, created_file.id)]
|