fix: path error

This commit is contained in:
tao.chen
2026-08-04 12:57:40 +08:00
parent 4919fe0909
commit 2a23030d6f
+55 -53
View File
@@ -100,6 +100,18 @@ def safe_script_name(value: str, script_type: str) -> str:
return name
def _jupyter_path(script_type: str, script_id: str) -> str:
"""Return the in-Jupyter path used for a script.
The path is ``notebooks/{script_id}.{ext}`` — deterministic and
collision-free because ``script_id`` is a fresh ULID. The user's
``script_name`` is kept on the Scripts row as a display label only;
the on-disk filename is owned by the database.
"""
ext = ".ipynb" if script_type == "notebook" else ".py"
return f"notebooks/{script_id}{ext}"
def validate_script_content(content: str, script_type: str) -> bytes:
encoded = content.encode("utf-8")
if len(encoded) > 10 * 1024 * 1024:
@@ -281,23 +293,25 @@ async def create_script_record(
child_path = f"{folder}/{name}" if folder else name
relative_path = user_relative_path(context, child_path)
logger.debug(relative_path)
existing_script = await session.scalar(
select(Scripts)
.join(
StorageObjects,
StorageObjects.storage_object_id == Scripts.current_object_id,
)
.where(
# The Jupyter-side filename is owned by the database: a fresh ULID
# guarantees uniqueness, so the StorageObject-based 409 check from
# the legacy flow no longer applies. 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.
script_id = new_ulid()
jupyter_name = _jupyter_path(script_type, script_id)
name_clash = await session.scalar(
select(Scripts.script_id).where(
Scripts.workspace_id == context.workspace.workspace_id,
StorageObjects.relative_path == relative_path,
Scripts.script_name == name,
Scripts.status == "active",
Scripts.is_deleted == 0,
)
)
if existing_script is not None:
if name_clash is not None:
raise HTTPException(
status.HTTP_409_CONFLICT,
"a file with the same path already exists",
"a script with the same name already exists in this workspace",
)
# Push the file directly to the workspace's live Jupyter instance.
@@ -314,16 +328,16 @@ async def create_script_record(
logger.debug(notebook)
jupyter_resp = await runtime_client.create_notebook(
workspace_id,
name=name,
name=jupyter_name,
cells=notebook.get("cells"),
)
else:
jupyter_resp = await runtime_client.upload_file(
workspace_id,
name=name,
name=jupyter_name,
content=content.decode("utf-8"),
content_type=(
mimetypes.guess_type(name)[0] or "text/plain"
mimetypes.guess_type(jupyter_name)[0] or "text/plain"
),
)
logger.debug(jupyter_resp)
@@ -338,38 +352,25 @@ async def create_script_record(
# The storage_object_id is a fresh ULID — there is no real
# StorageObject row for this file; downstream list/get operations
# that JOIN StorageObjects will skip jupyter-only scripts.
object_id = new_ulid()
storage_data = {
"storage_object_id": new_ulid(),
"relative_path": relative_path,
"object_key": f"{workspace_id}/{relative_path}",
"storage_object_id": object_id,
"relative_path": jupyter_name,
"object_key": f"{workspace_id}/{jupyter_name}",
"content_hash": content_hash,
"size_bytes": size_bytes,
}
object_id = storage_data["storage_object_id"]
script = await session.scalar(
select(Scripts).where(Scripts.current_object_id == object_id)
script = Scripts(
script_id=script_id,
workspace_id=context.workspace.workspace_id,
current_object_id=object_id,
owner_user_id=context.user.user_id,
script_name=name,
script_type=script_type,
visibility=visibility,
status="active",
)
now = datetime.now(UTC).replace(tzinfo=None)
if script is None:
script = Scripts(
script_id=new_ulid(),
workspace_id=context.workspace.workspace_id,
current_object_id=object_id,
owner_user_id=context.user.user_id,
script_name=name,
script_type=script_type,
visibility=visibility,
status="active",
)
session.add(script)
else:
script.owner_user_id = context.user.user_id
script.script_name = name
script.script_type = script_type
script.visibility = visibility
script.status = "active"
script.deleted_at = None
script.updated_at = now
session.add(script)
await session.flush()
await session.refresh(script)
return script, storage_data
@@ -608,16 +609,15 @@ async def delete_workspace_directory(
runtime_client = request.app.state.runtime_client
workspace_id = context.workspace.workspace_id
for script in rows:
if not script.script_name:
continue
# Best-effort: try to delete from jupyter, but do not let one
# failure abort the rest. The jupyter call will 404 if the
# file is not actually in the workspace root (e.g. legacy
# scripts that were never mirrored to jupyter); we treat that
# as a no-op and still mark the row deleted.
# file is not actually in the workspace (e.g. legacy scripts
# whose on-disk filename we do not know); we treat that as a
# no-op and still mark the row deleted.
try:
await runtime_client.delete_file(
workspace_id, name=script.script_name
workspace_id,
name=_jupyter_path(script.script_type, script.script_id),
)
except RuntimeClientError as exc:
if exc.status_code != 404:
@@ -720,21 +720,22 @@ async def update_script(
content = validate_script_content(payload.content, script.script_type)
runtime_client = request.app.state.runtime_client
workspace_id = context.workspace.workspace_id
jupyter_name = _jupyter_path(script.script_type, script.script_id)
try:
if script.script_type == "notebook":
notebook = json.loads(content.decode("utf-8"))
jupyter_resp = await runtime_client.create_notebook(
workspace_id,
name=script.script_name,
name=jupyter_name,
cells=notebook.get("cells"),
)
else:
jupyter_resp = await runtime_client.upload_file(
workspace_id,
name=script.script_name,
name=jupyter_name,
content=content.decode("utf-8"),
content_type=(
mimetypes.guess_type(script.script_name)[0]
mimetypes.guess_type(jupyter_name)[0]
or "text/plain"
),
)
@@ -747,8 +748,8 @@ async def update_script(
script.updated_at = datetime.now(UTC).replace(tzinfo=None)
storage_data = {
"storage_object_id": script.current_object_id,
"relative_path": script.script_name,
"object_key": f"{workspace_id}/{script.script_name}",
"relative_path": jupyter_name,
"object_key": f"{workspace_id}/{jupyter_name}",
"content_hash": hashlib.sha256(content).hexdigest(),
"size_bytes": len(content),
}
@@ -792,7 +793,8 @@ async def delete_script(
runtime_client = request.app.state.runtime_client
try:
await runtime_client.delete_file(
context.workspace.workspace_id, name=script.script_name
context.workspace.workspace_id,
name=_jupyter_path(script.script_type, script.script_id),
)
except RuntimeClientError as exc:
raise HTTPException(