From 49c8a2784193bd997a4aa92ed8892febd3cc5a52 Mon Sep 17 00:00:00 2001 From: "tao.chen" <93983997+taochen-ct@users.noreply.github.com> Date: Thu, 13 Aug 2026 11:01:37 +0800 Subject: [PATCH] update: update storage struct --- backend/src/backend/scripts.py | 64 ++++++++++++++----------- backend/src/backend/services/storage.py | 2 +- backend/src/backend/storage_api.py | 2 +- 3 files changed, 38 insertions(+), 30 deletions(-) diff --git a/backend/src/backend/scripts.py b/backend/src/backend/scripts.py index 1f51065..da047f0 100644 --- a/backend/src/backend/scripts.py +++ b/backend/src/backend/scripts.py @@ -395,9 +395,14 @@ async def create_script_record( # 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. + user_id = context.user.user_id script_id = new_ulid() - jupyter_name = _jupyter_path(script_type, script_id) - nested_jupyter_name = f"{parent_ulid}/{jupyter_name}" if parent_ulid else jupyter_name + jupyter_basename = _jupyter_path(script_type, script_id) + # 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( select(Scripts.script_id).where( Scripts.workspace_id == context.workspace.workspace_id, @@ -421,7 +426,7 @@ async def create_script_record( size_bytes = len(content) if parent_ulid: 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: raise HTTPException( status_code=exc.status_code, @@ -434,15 +439,15 @@ async def create_script_record( logger.debug(notebook) jupyter_resp = await runtime_client.create_notebook( workspace_id, - name=nested_jupyter_name, + name=jupyter_path, cells=notebook.get("cells"), ) else: jupyter_resp = await runtime_client.upload_file( workspace_id, - name=nested_jupyter_name, + name=jupyter_path, 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) 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 # storage_uri points at where the replicated bytes will land. 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 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_id=object_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(), storage_uri=build_storage_uri(bucket_name, object_key), 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, size_bytes=size_bytes, content_hash=content_hash, @@ -507,13 +512,13 @@ async def create_script_record( # Best-effort compensating cleanup: remove the Jupyter file we # just created so a failed flush does not leave an orphan on disk. 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: if cleanup_exc.status_code == 404: pass else: 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}" ) raise @@ -813,13 +818,17 @@ async def create_workspace_directory( # the on-disk directory name match. Create the directory in Jupyter # before persisting the DB row; if persistence fails we clean up. 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 workspace_id = context.workspace.workspace_id if parent_ulid: 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: raise HTTPException( status_code=exc.status_code, @@ -962,7 +971,7 @@ async def delete_workspace_directory( else: try: 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: if exc.status_code != 404: @@ -976,7 +985,7 @@ async def delete_workspace_directory( descendant.deleted_at = datetime.now(UTC).replace(tzinfo=None) 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: if exc.status_code != 404: logger.warning( @@ -1044,26 +1053,25 @@ async def get_script_content( 适用于被锁定的脚本,非所有者只能查看内容,不能编辑。 该接口不检查锁状态,调用方需自行判断权限。 """ - script = await session.scalar( - select(Scripts) - .where( - Scripts.script_id == script_id, - Scripts.workspace_id == context.workspace.workspace_id, - Scripts.status == "active", - ) + script, storage_object = await get_script_row( + script_id, + context, + session, + allow_missing_storage_object=True, ) - if script is None: - raise HTTPException(status.HTTP_404_NOT_FOUND, "script not found") # 获取 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 读取内容 runtime_client = request.app.state.runtime_client try: content_data = await runtime_client.get_file( - context.workspace.workspace_id, - name=jupyter_name, + workspace_id, + name=jupyter_path, ) except RuntimeClientError as exc: raise HTTPException( diff --git a/backend/src/backend/services/storage.py b/backend/src/backend/services/storage.py index 4690832..0ccd62e 100644 --- a/backend/src/backend/services/storage.py +++ b/backend/src/backend/services/storage.py @@ -171,7 +171,7 @@ async def create_upload_record( # Jupyter selects its editor from this suffix, so an extensionless # object would make notebooks look like generic JSON/text files. 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_id=new_ulid(), workspace_id=payload.workspace_id, diff --git a/backend/src/backend/storage_api.py b/backend/src/backend/storage_api.py index 2e35434..40f7085 100644 --- a/backend/src/backend/storage_api.py +++ b/backend/src/backend/storage_api.py @@ -190,7 +190,7 @@ async def create_upload_record( # Jupyter selects its editor from this suffix, so an extensionless # object would make notebooks look like generic JSON/text files. 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_id=upload_id, workspace_id=payload.workspace_id,