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