feat: ensure unique names for asset nodes during creation and batch upload

This commit is contained in:
Harry 2026-02-05 14:42:42 +08:00
parent 6b0e6b2785
commit a750d87ae4
2 changed files with 80 additions and 9 deletions

View File

@ -1,5 +1,6 @@
from __future__ import annotations
import os
from collections import defaultdict
from collections.abc import Generator
from enum import StrEnum
@ -59,12 +60,12 @@ class BatchUploadNode(BaseModel):
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.
Generate IDs when missing and convert to AppAssetNode list.
Mutates self to set id field when it is not set.
"""
from uuid import uuid4
self.id = str(uuid4())
self.id = self.id or str(uuid4())
nodes: list[AppAssetNode] = []
if self.node_type == AssetNodeType.FOLDER:
@ -114,6 +115,37 @@ class AppAssetFileTree(BaseModel):
nodes: list[AppAssetNode] = Field(default_factory=list, description="Flat list of all nodes in the tree")
def ensure_unique_name(
self,
parent_id: str | None,
name: str,
*,
is_file: bool,
extra_taken: set[str] | None = None,
) -> str:
"""
Return a sibling-unique name by appending numeric suffixes when needed.
The suffix format is " <n>" (e.g. "report 1", "report 2"). For files,
the suffix is inserted before the extension.
"""
taken = extra_taken or set()
if not self.has_child_named(parent_id, name) and name not in taken:
return name
suffix_index = 1
while True:
candidate = self._apply_name_suffix(name, suffix_index, is_file=is_file)
if not self.has_child_named(parent_id, candidate) and candidate not in taken:
return candidate
suffix_index += 1
@staticmethod
def _apply_name_suffix(name: str, suffix_index: int, *, is_file: bool) -> str:
if not is_file:
return f"{name} {suffix_index}"
stem, extension = os.path.splitext(name)
return f"{stem} {suffix_index}{extension}"
def get(self, node_id: str) -> AppAssetNode | None:
return next((n for n in self.nodes if n.id == node_id), None)

View File

@ -187,7 +187,12 @@ class AppAssetService:
assets = AppAssetService.get_or_create_assets(session, app_model, account_id)
tree = assets.asset_tree
node = AppAssetNode.create_folder(str(uuid4()), name, parent_id)
unique_name = tree.ensure_unique_name(
parent_id,
name,
is_file=False,
)
node = AppAssetNode.create_folder(str(uuid4()), unique_name, parent_id)
try:
tree.add(node)
@ -408,6 +413,9 @@ class AppAssetService:
The file metadata is saved immediately. If the user doesn't upload,
the download will fail when the file is accessed.
If a sibling with the same name exists, a numeric suffix is appended
to make the name unique (e.g. "report 1.txt").
Returns:
tuple of (node, upload_url)
"""
@ -416,8 +424,13 @@ class AppAssetService:
assets = AppAssetService.get_or_create_assets(session, app_model, account_id)
tree = assets.asset_tree
unique_name = tree.ensure_unique_name(
parent_id,
name,
is_file=True,
)
node_id = str(uuid4())
node = AppAssetNode.create_file(node_id, name, parent_id, size)
node = AppAssetNode.create_file(node_id, unique_name, parent_id, size)
try:
tree.add(node)
@ -452,15 +465,41 @@ class AppAssetService:
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
def assign_ids_and_unique_names(
nodes: list[BatchUploadNode],
parent_id: str | None,
taken_by_parent: dict[str | None, set[str]],
) -> None:
for node in nodes:
if node.id is None:
node.id = str(uuid4())
if parent_id not in taken_by_parent:
taken_by_parent[parent_id] = {
child.name for child in tree.get_children(parent_id)
}
taken = taken_by_parent[parent_id]
unique_name = tree.ensure_unique_name(
parent_id,
node.name,
is_file=node.node_type == AssetNodeType.FILE,
extra_taken=taken,
)
node.name = unique_name
taken.add(unique_name)
if node.node_type == AssetNodeType.FOLDER:
assign_ids_and_unique_names(node.children, node.id, taken_by_parent)
assign_ids_and_unique_names(input_children, None, {})
new_nodes: list[AppAssetNode] = []
for child in input_children:
new_nodes.extend(child.to_app_asset_nodes(None))
try:
for node in new_nodes:
tree.add(node)