From 245f4d346d06e7891cbbadb0884ef3c3916efc92 Mon Sep 17 00:00:00 2001 From: "tao.chen" <93983997+taochen-ct@users.noreply.github.com> Date: Fri, 31 Jul 2026 15:07:35 +0800 Subject: [PATCH] fix: repair storage hot-path bugs and missing imports - scripts.update_script: repoint script.current_object_id to the newly uploaded StorageObject and best-effort delete the old working copy. Previously the old row was mutated with the new content_hash while the script still pointed at it, silently losing user edits on the next publish_version. - scripts.publish_version: read object bytes via the new RustFSObjectStore.get_bytes() instead of the non-existent .get_object(); publish 500'd on every call. - scripts.upload_script / storage_api.complete_upload_record: Path(file_name) raised NameError (only PurePosixPath imported), crashing every upload and every upload finalization. Use PurePosixPath. - RustFSObjectStore: add get_bytes() helper (sync, body.close in finally) for in-process callers that need raw bytes. Co-Authored-By: Claude --- backend/src/backend/scripts.py | 30 ++++++++++++++--------------- backend/src/backend/storage_api.py | 2 +- common/src/common/storage/rustfs.py | 8 ++++++++ 3 files changed, 23 insertions(+), 17 deletions(-) diff --git a/backend/src/backend/scripts.py b/backend/src/backend/scripts.py index db3791e..990bd8b 100644 --- a/backend/src/backend/scripts.py +++ b/backend/src/backend/scripts.py @@ -371,7 +371,7 @@ async def upload_script( context: RequestContext = Depends(request_context), session: AsyncSession = Depends(database_session), ) -> dict[str, Any]: - suffix = Path(file_name).suffix.lower() + suffix = PurePosixPath(file_name).suffix.lower() if suffix not in {".py", ".ipynb"}: raise HTTPException( status.HTTP_422_UNPROCESSABLE_ENTITY, @@ -637,6 +637,7 @@ async def update_script( status.HTTP_409_CONFLICT, "script has no workspace path", ) + old_object_id = script.current_object_id storage_data = await request.app.state.storage_client.create_server_object( workspace_id=context.workspace.workspace_id, user_id=script.owner_user_id, @@ -653,12 +654,16 @@ async def update_script( f"{hashlib.sha256(content).hexdigest()}" ), ) - storage_object.content_hash = storage_data["content_hash"] - storage_object.size_bytes = storage_data["size_bytes"] + script.current_object_id = storage_data["storage_object_id"] script.updated_at = datetime.now(UTC).replace(tzinfo=None) + if old_object_id != script.current_object_id: + try: + await request.app.state.storage_client.delete_object(old_object_id) + except Exception: + pass return { "request_id": context.request_id, - "data": script_payload(script, storage_object), + "data": script_payload(script, storage_data), "meta": {}, } @@ -738,18 +743,11 @@ async def publish_version( "script working copy is not stored in object storage", ) - def read_object_bytes() -> bytes: - response = request.app.state.object_store.get_object( - Bucket=source_object.bucket_name, - Key=source_object.object_key, - ) - body = response["Body"] - try: - return body.read() - finally: - body.close() - - content = await asyncio.to_thread(read_object_bytes) + content = await asyncio.to_thread( + request.app.state.object_store.get_bytes, + bucket_name=source_object.bucket_name, + object_key=source_object.object_key, + ) content_hash = hashlib.sha256(content).hexdigest() existing = await session.scalar( select(Versions).where( diff --git a/backend/src/backend/storage_api.py b/backend/src/backend/storage_api.py index 2447b79..ca1ead5 100644 --- a/backend/src/backend/storage_api.py +++ b/backend/src/backend/storage_api.py @@ -366,7 +366,7 @@ async def complete_upload_record( object_key_hash=upload.object_key_hash, storage_uri=f"s3://{upload.bucket_name}/{upload.object_key}", file_name=file_name, - file_extension=Path(file_name).suffix.lower() or None, + file_extension=PurePosixPath(file_name).suffix.lower() or None, mime_type=actual_content_type, size_bytes=actual_size, content_hash=actual_hash, diff --git a/common/src/common/storage/rustfs.py b/common/src/common/storage/rustfs.py index 3930d99..4d4314d 100644 --- a/common/src/common/storage/rustfs.py +++ b/common/src/common/storage/rustfs.py @@ -137,6 +137,14 @@ class RustFSObjectStore: def head(self, *, bucket_name: str, object_key: str) -> dict[str, Any]: return self.internal.head_object(Bucket=bucket_name, Key=object_key) + def get_bytes(self, *, bucket_name: str, object_key: str) -> bytes: + response = self.internal.get_object(Bucket=bucket_name, Key=object_key) + body: BinaryIO = response["Body"] + try: + return body.read() + finally: + body.close() + def sha256(self, *, bucket_name: str, object_key: str) -> str: response = self.internal.get_object(Bucket=bucket_name, Key=object_key) body: BinaryIO = response["Body"]