Develop #16

Merged
tao.chen merged 273 commits from develop into main 2026-08-21 10:42:09 +08:00
3 changed files with 38 additions and 30 deletions
Showing only changes of commit 49c8a27841 - Show all commits
+36 -28
View File
@@ -395,9 +395,14 @@ async def create_script_record(
# the legacy flow no longer applies. We still do a Scripts-only # the legacy flow no longer applies. We still do a Scripts-only
# conflict check on the user-supplied name so two scripts cannot # conflict check on the user-supplied name so two scripts cannot
# claim the same display name within the same workspace. # claim the same display name within the same workspace.
user_id = context.user.user_id
script_id = new_ulid() script_id = new_ulid()
jupyter_name = _jupyter_path(script_type, script_id) jupyter_basename = _jupyter_path(script_type, script_id)
nested_jupyter_name = f"{parent_ulid}/{jupyter_name}" if parent_ulid else jupyter_name # Jupyter path inside workspace root: {user_id}/{parent_or_self}/{basename}
if parent_ulid:
jupyter_path = f"{user_id}/{parent_ulid}/{jupyter_basename}"
else:
jupyter_path = f"{user_id}/{jupyter_basename}"
name_clash = await session.scalar( name_clash = await session.scalar(
select(Scripts.script_id).where( select(Scripts.script_id).where(
Scripts.workspace_id == context.workspace.workspace_id, Scripts.workspace_id == context.workspace.workspace_id,
@@ -421,7 +426,7 @@ async def create_script_record(
size_bytes = len(content) size_bytes = len(content)
if parent_ulid: if parent_ulid:
try: try:
await runtime_client.ensure_directory(workspace_id, parent_ulid) await runtime_client.ensure_directory(workspace_id, f"{user_id}/{parent_ulid}")
except RuntimeClientError as exc: except RuntimeClientError as exc:
raise HTTPException( raise HTTPException(
status_code=exc.status_code, status_code=exc.status_code,
@@ -434,15 +439,15 @@ 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=nested_jupyter_name, name=jupyter_path,
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=nested_jupyter_name, name=jupyter_path,
content=content.decode("utf-8"), content=content.decode("utf-8"),
content_type=(mimetypes.guess_type(jupyter_name)[0] or "text/plain"), content_type=(mimetypes.guess_type(jupyter_basename)[0] or "text/plain"),
) )
logger.debug(jupyter_resp) logger.debug(jupyter_resp)
except RuntimeClientError as exc: except RuntimeClientError as exc:
@@ -458,12 +463,12 @@ async def create_script_record(
# file is queryable as a workspace file from the user's POV; the # file is queryable as a workspace file from the user's POV; the
# storage_uri points at where the replicated bytes will land. # storage_uri points at where the replicated bytes will land.
object_id = new_ulid() object_id = new_ulid()
object_key = f"{workspace_id}/{nested_jupyter_name}" object_key = f"{workspace_id}/{jupyter_path}"
bucket_name = settings.s3_workspace_bucket bucket_name = settings.s3_workspace_bucket
relative_path = user_relative_path( relative_path = user_relative_path(
context, f"{parent}/{jupyter_name}" if parent else jupyter_name context, f"{parent}/{jupyter_basename}" if parent else jupyter_basename
) )
mime_type = mimetypes.guess_type(jupyter_name)[0] mime_type = mimetypes.guess_type(jupyter_basename)[0]
storage_object = StorageObjects( storage_object = StorageObjects(
storage_object_id=object_id, storage_object_id=object_id,
workspace_id=context.workspace.workspace_id, workspace_id=context.workspace.workspace_id,
@@ -476,7 +481,7 @@ async def create_script_record(
object_key_hash=hashlib.sha256(object_key.encode("utf-8")).digest(), object_key_hash=hashlib.sha256(object_key.encode("utf-8")).digest(),
storage_uri=build_storage_uri(bucket_name, object_key), storage_uri=build_storage_uri(bucket_name, object_key),
file_name=name, file_name=name,
file_extension=PurePosixPath(jupyter_name).suffix.lower() or None, file_extension=PurePosixPath(jupyter_basename).suffix.lower() or None,
mime_type=mime_type, mime_type=mime_type,
size_bytes=size_bytes, size_bytes=size_bytes,
content_hash=content_hash, content_hash=content_hash,
@@ -507,13 +512,13 @@ async def create_script_record(
# Best-effort compensating cleanup: remove the Jupyter file we # Best-effort compensating cleanup: remove the Jupyter file we
# just created so a failed flush does not leave an orphan on disk. # just created so a failed flush does not leave an orphan on disk.
try: try:
await runtime_client.delete_file(workspace_id, name=nested_jupyter_name) await runtime_client.delete_file(workspace_id, name=jupyter_path)
except RuntimeClientError as cleanup_exc: except RuntimeClientError as cleanup_exc:
if cleanup_exc.status_code == 404: if cleanup_exc.status_code == 404:
pass pass
else: else:
logger.warning( logger.warning(
f"failed to clean up Jupyter file {nested_jupyter_name} " f"failed to clean up Jupyter file {jupyter_path} "
f"after DB flush error: {cleanup_exc.status_code} {cleanup_exc.detail}" f"after DB flush error: {cleanup_exc.status_code} {cleanup_exc.detail}"
) )
raise raise
@@ -813,13 +818,17 @@ async def create_workspace_directory(
# the on-disk directory name match. Create the directory in Jupyter # the on-disk directory name match. Create the directory in Jupyter
# before persisting the DB row; if persistence fails we clean up. # before persisting the DB row; if persistence fails we clean up.
dir_id = new_ulid() dir_id = new_ulid()
jupyter_path = f"{parent_ulid}/{dir_id}" if parent_ulid else dir_id user_id = context.user.user_id
if parent_ulid:
jupyter_path = f"{user_id}/{parent_ulid}/{dir_id}"
else:
jupyter_path = f"{user_id}/{dir_id}"
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
if parent_ulid: if parent_ulid:
try: try:
await runtime_client.ensure_directory(workspace_id, parent_ulid) await runtime_client.ensure_directory(workspace_id, f"{user_id}/{parent_ulid}")
except RuntimeClientError as exc: except RuntimeClientError as exc:
raise HTTPException( raise HTTPException(
status_code=exc.status_code, status_code=exc.status_code,
@@ -962,7 +971,7 @@ async def delete_workspace_directory(
else: else:
try: try:
await runtime_client.delete_directory( await runtime_client.delete_directory(
workspace_id, name=descendant.storage_object_id workspace_id, name=f"{descendant.owner_user_id}/{descendant.storage_object_id}"
) )
except RuntimeClientError as exc: except RuntimeClientError as exc:
if exc.status_code != 404: if exc.status_code != 404:
@@ -976,7 +985,7 @@ async def delete_workspace_directory(
descendant.deleted_at = datetime.now(UTC).replace(tzinfo=None) descendant.deleted_at = datetime.now(UTC).replace(tzinfo=None)
try: try:
await runtime_client.delete_directory(workspace_id, name=target_ulid) await runtime_client.delete_directory(workspace_id, name=f"{context.user.user_id}/{target_ulid}")
except RuntimeClientError as exc: except RuntimeClientError as exc:
if exc.status_code != 404: if exc.status_code != 404:
logger.warning( logger.warning(
@@ -1044,26 +1053,25 @@ async def get_script_content(
适用于被锁定的脚本,非所有者只能查看内容,不能编辑。 适用于被锁定的脚本,非所有者只能查看内容,不能编辑。
该接口不检查锁状态,调用方需自行判断权限。 该接口不检查锁状态,调用方需自行判断权限。
""" """
script = await session.scalar( script, storage_object = await get_script_row(
select(Scripts) script_id,
.where( context,
Scripts.script_id == script_id, session,
Scripts.workspace_id == context.workspace.workspace_id, allow_missing_storage_object=True,
Scripts.status == "active",
)
) )
if script is None:
raise HTTPException(status.HTTP_404_NOT_FOUND, "script not found")
# 获取 Jupyter 路径 # 获取 Jupyter 路径
jupyter_name = _jupyter_path(script.script_type, script.script_id) workspace_id = context.workspace.workspace_id
jupyter_path = _derive_jupyter_path(
storage_object, workspace_id, script.script_type, script.script_id
)
# 从 Jupyter 读取内容 # 从 Jupyter 读取内容
runtime_client = request.app.state.runtime_client runtime_client = request.app.state.runtime_client
try: try:
content_data = await runtime_client.get_file( content_data = await runtime_client.get_file(
context.workspace.workspace_id, workspace_id,
name=jupyter_name, name=jupyter_path,
) )
except RuntimeClientError as exc: except RuntimeClientError as exc:
raise HTTPException( raise HTTPException(
+1 -1
View File
@@ -171,7 +171,7 @@ async def create_upload_record(
# Jupyter selects its editor from this suffix, so an extensionless # Jupyter selects its editor from this suffix, so an extensionless
# object would make notebooks look like generic JSON/text files. # object would make notebooks look like generic JSON/text files.
file_extension = PurePosixPath(_safe_file_name(payload.file_name)).suffix.lower() file_extension = PurePosixPath(_safe_file_name(payload.file_name)).suffix.lower()
object_key = f"{payload.workspace_id}/{new_ulid()}{file_extension}" object_key = f"{payload.workspace_id}/{payload.user_id}/{new_ulid()}{file_extension}"
upload = UploadSessions( upload = UploadSessions(
upload_id=new_ulid(), upload_id=new_ulid(),
workspace_id=payload.workspace_id, workspace_id=payload.workspace_id,
+1 -1
View File
@@ -190,7 +190,7 @@ async def create_upload_record(
# Jupyter selects its editor from this suffix, so an extensionless # Jupyter selects its editor from this suffix, so an extensionless
# object would make notebooks look like generic JSON/text files. # object would make notebooks look like generic JSON/text files.
file_extension = PurePosixPath(safe_file_name(payload.file_name)).suffix.lower() file_extension = PurePosixPath(safe_file_name(payload.file_name)).suffix.lower()
object_key = f"{payload.workspace_id}/{upload_id}{file_extension}" object_key = f"{payload.workspace_id}/{payload.user_id}/{upload_id}{file_extension}"
upload = UploadSessions( upload = UploadSessions(
upload_id=upload_id, upload_id=upload_id,
workspace_id=payload.workspace_id, workspace_id=payload.workspace_id,