This commit is contained in:
tao.chen
2026-07-31 13:34:33 +08:00
parent 28069f516b
commit 5cbbe8213a
4 changed files with 252 additions and 60 deletions
+11 -9
View File
@@ -165,17 +165,15 @@ async def create_upload_record(
upload = existing
else:
upload_id = new_ulid()
file_name = safe_file_name(payload.file_name)
bucket_name = (
workspace.artifact_bucket or request.app.state.default_bucket
)
# Default layout: one top-level folder per workspace inside the
# ``workspaces`` bucket. ``bucket_name`` already encodes the workspace
# namespace, so the key starts with the workspace id directly.
object_key = (
f"{payload.workspace_id}/"
f"{payload.usage_type}/{upload_id}/{file_name}"
)
# Object key is a flat two-level path: workspace id + upload id. The
# original file name and content type live in the StorageObjects row
# (file_name / mime_type / object_key) — they are not part of the
# key itself, so the bucket can be reorganised without rewriting
# the database.
object_key = f"{payload.workspace_id}/{upload_id}"
upload = UploadSessions(
upload_id=upload_id,
workspace_id=payload.workspace_id,
@@ -320,7 +318,11 @@ async def complete_upload_record(
status.HTTP_409_CONFLICT,
"uploaded object hash does not match expected_hash")
file_name = upload.object_key.rsplit("/", 1)[-1]
# The object key is just ``{workspace_id}/{ulid}`` — it does not encode
# the file name. Use the original file name from the upload session
# (carried via payload.file_name) so the StorageObjects row still
# records the user-visible name + extension.
file_name = safe_file_name(payload.file_name)
item = StorageObjects(
storage_object_id=new_ulid(),
workspace_id=upload.workspace_id,
+64 -3
View File
@@ -1,5 +1,66 @@
"""Backward-compatible re-export of the shared storage client."""
"""Backend-bound storage client.
from common.storage.client import StorageClient
Re-exports :class:`StorageClient` under the same name used by callers in
``backend/``. The default client raises :class:`StorageClientError` from
``common.storage.client`` so it stays usable from non-FastAPI contexts.
Inside FastAPI route handlers we want HTTP-shaped errors, so this module
also exposes :class:`BackendStorageClient`, a thin wrapper that translates
the framework-agnostic errors into ``HTTPException``.
"""
__all__ = ["StorageClient"]
from __future__ import annotations
from typing import Any
from fastapi import HTTPException, status
from common.storage.client import (
StorageClient,
StorageClientError,
StorageRequestFailed,
StorageUnavailable,
)
__all__ = ["BackendStorageClient", "StorageClient", "StorageClientError"]
def _to_http_exception(exc: StorageClientError) -> HTTPException:
if isinstance(exc, StorageUnavailable):
return HTTPException(
status.HTTP_503_SERVICE_UNAVAILABLE,
{
"code": "STORAGE_UNAVAILABLE",
"message": "Storage service temporarily unavailable",
"retryable": True,
"details": {},
},
)
if isinstance(exc, StorageRequestFailed):
return HTTPException(exc.status_code, exc.detail)
return HTTPException(
status.HTTP_500_INTERNAL_SERVER_ERROR,
"Storage client error",
)
class BackendStorageClient(StorageClient):
"""Storage client that raises ``HTTPException`` for web callers."""
async def _request(
self,
method: str,
path: str,
*,
payload: dict[str, Any] | None = None,
) -> dict[str, Any]:
try:
return await super()._request(method, path, payload=payload)
except StorageClientError as exc:
raise _to_http_exception(exc) from exc
# Re-bind the imported symbol so existing backend call sites that import
# ``StorageClient`` from this module transparently get the FastAPI-bound
# variant without changing every import statement.
StorageClient = BackendStorageClient