fix: allows_same_name_different_parent

This commit is contained in:
tao.chen
2026-08-14 18:49:27 +08:00
parent 5d49ff5e34
commit 5c5dec9a4a
4 changed files with 82 additions and 19 deletions
+19 -13
View File
@@ -371,16 +371,9 @@ async def create_script_record(
"parent directory not found",
)
display_parent = parent
child_path = f"{display_parent}/{name}" if display_parent else name
relative_path = user_relative_path(context, child_path)
logger.debug(relative_path)
# The Jupyter-side filename is the sanitized script name; extension and
# path separators are enforced by safe_script_name(). The script_id ULID
# remains the Scripts PK only. We still do a Scripts-only conflict check
# on the user-supplied name so two scripts cannot claim the same display
# name within the same workspace.
# remains the Scripts PK only.
user_id = context.user.user_id
script_id = new_ulid() # PK only — kept as ULID
jupyter_basename = _jupyter_path(script_type, name) # name is already passed through safe_script_name
@@ -398,17 +391,33 @@ async def create_script_record(
jupyter_path = f"{user_id}/{parent_segment}/{jupyter_basename}"
else:
jupyter_path = f"{user_id}/{jupyter_basename}"
# StorageObjects relative_path mirrors the Jupyter layout under the
# workspace/user prefix. Compute it before the conflict check so we can
# scope duplicates by the full path, not just the display name.
relative_path = user_relative_path(
context, f"{parent}/{jupyter_basename}" if parent else jupyter_basename
)
logger.debug(relative_path)
name_clash = await session.scalar(
select(Scripts.script_id).where(
select(Scripts.script_id)
.join(
StorageObjects,
StorageObjects.storage_object_id == Scripts.current_object_id,
)
.where(
Scripts.workspace_id == context.workspace.workspace_id,
Scripts.owner_user_id == context.user.user_id,
Scripts.script_name == name,
Scripts.status == "active",
StorageObjects.relative_path == relative_path,
)
)
if name_clash is not None:
raise HTTPException(
status.HTTP_409_CONFLICT,
"a script with the same name already exists in this workspace",
"a script with the same name already exists at this path",
)
# Push the file directly to the workspace's live Jupyter instance.
@@ -475,9 +484,6 @@ async def create_script_record(
object_key = await _resolve_unique_object_key(session, object_key)
object_key_hash = hashlib.sha256(object_key.encode("utf-8")).digest()
bucket_name = settings.s3_workspace_bucket
relative_path = user_relative_path(
context, f"{parent}/{jupyter_basename}" if parent else jupyter_basename
)
mime_type = mimetypes.guess_type(jupyter_basename)[0]
storage_object = StorageObjects(
storage_object_id=object_id,
+4 -4
View File
@@ -48,7 +48,7 @@ from common.storage.schemas import (
DownloadUrlRequest,
ServerObjectRequest,
)
from common.storage import build_storage_uri
from common.storage import actual_bucket_name, build_storage_uri
# ── shared low-level helpers (module-private) ────────────────────────────
@@ -590,7 +590,7 @@ async def soft_delete_object(
"storage_object_id": storage_object_id,
"object_status": item.object_status,
"trash_key": item.trash_key,
"trash_bucket": settings.s3_trash_bucket,
"trash_bucket": actual_bucket_name("trash"),
}
}
if item.storage_backend == settings.storage_backend and item.bucket_name and item.object_key:
@@ -598,7 +598,7 @@ async def soft_delete_object(
try:
object_stores = request.app.state.object_stores
data = await object_stores[item.bucket_name].get(item.object_key)
await object_stores[settings.s3_trash_bucket].put(trash_key, data)
await object_stores[actual_bucket_name("trash")].put(trash_key, data)
await object_stores[item.bucket_name].delete(item.object_key)
except Exception as exc:
raise HTTPException(
@@ -614,6 +614,6 @@ async def soft_delete_object(
"storage_object_id": storage_object_id,
"object_status": item.object_status,
"trash_key": item.trash_key,
"trash_bucket": settings.s3_trash_bucket,
"trash_bucket": actual_bucket_name("trash"),
}
}
+2 -2
View File
@@ -479,7 +479,7 @@ async def restore_object(
try:
# Cross-backend copy: get from trash, put back to source bucket.
object_stores = request.app.state.object_stores
data = await object_stores[settings.s3_trash_bucket].get(item.trash_key)
data = await object_stores[actual_bucket_name("trash")].get(item.trash_key)
await object_stores[item.bucket_name].put(item.object_key, data)
except Exception as exc:
raise HTTPException(
@@ -529,7 +529,7 @@ async def purge_trash_object(
)
if item.trash_key:
try:
await request.app.state.object_stores[settings.s3_trash_bucket].delete(
await request.app.state.object_stores[actual_bucket_name("trash")].delete(
item.trash_key
)
except Exception as exc: