fix: path error
This commit is contained in:
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user