fix: delete error

This commit is contained in:
tao.chen
2026-08-14 19:19:41 +08:00
parent 2e070fae78
commit 139f2c02c1
5 changed files with 44 additions and 24 deletions
+3 -3
View File
@@ -61,9 +61,9 @@ S3_PORT=9000
S3_ENDPOINT=http://127.0.0.1:9000
S3_ACCESS_KEY=change-me
S3_SECRET_KEY=change-me
S3_WORKSPACE_BUCKET=workspaces
S3_VERSION_BUCKET=versions
S3_RUN_LOG_BUCKET=run-logs
S3_WORKSPACE_BUCKET=workspace
S3_VERSION_BUCKET=version
S3_RUN_LOG_BUCKET=run-log
S3_TRASH_BUCKET=trash
S3_TRASH_RETENTION_DAYS=30
+8 -3
View File
@@ -48,7 +48,7 @@ from common.storage.schemas import (
DownloadUrlRequest,
ServerObjectRequest,
)
from common.storage import actual_bucket_name, build_storage_uri
from common.storage import USAGE_TYPE_TO_PURPOSE, actual_bucket_name, build_storage_uri
# ── shared low-level helpers (module-private) ────────────────────────────
@@ -594,11 +594,13 @@ async def soft_delete_object(
}
}
if item.storage_backend == settings.storage_backend and item.bucket_name and item.object_key:
trash_key = f"{item.bucket_name}/{item.object_key}"
source_purpose = USAGE_TYPE_TO_PURPOSE[item.usage_type]
trash_key = f"{source_purpose}/{item.object_key}"
trash_bucket = actual_bucket_name("trash")
try:
object_stores = request.app.state.object_stores
data = await object_stores[item.bucket_name].get(item.object_key)
await object_stores[actual_bucket_name("trash")].put(trash_key, data)
await object_stores[trash_bucket].put(trash_key, data)
await object_stores[item.bucket_name].delete(item.object_key)
except Exception as exc:
raise HTTPException(
@@ -606,6 +608,9 @@ async def soft_delete_object(
f"failed to move object to trash: {exc}",
) from exc
item.trash_key = trash_key
item.bucket_name = trash_bucket
item.object_key = trash_key
item.storage_uri = build_storage_uri(trash_bucket, trash_key)
item.object_status = "deleted"
item.deleted_at = _utcnow_naive()
item.is_deleted = 1
+16 -18
View File
@@ -19,7 +19,7 @@ from common.db.models import (
Workspaces,
)
from common.ids import new_ulid
from common.storage import actual_bucket_name, build_storage_uri
from common.storage import USAGE_TYPE_TO_PURPOSE, actual_bucket_name, build_storage_uri
from common.storage.schemas import (
CreateUploadRequest,
DownloadUrlRequest,
@@ -56,20 +56,6 @@ def safe_file_name(value: str) -> str:
return name
# Map an upload's usage_type to its purpose (which then resolves to the
# actual bucket / directory via ``actual_bucket_name``). Keeping the
# purpose as the intermediate value means s3 mode and local mode share
# the same routing logic — only the final ``actual_bucket_name`` differs.
USAGE_TYPE_TO_PURPOSE: dict[str, str] = {
"working_copy": "workspace",
"public_script": "workspace",
"data_resource": "workspace",
"snapshot": "workspace",
"version_artifact": "version",
"run_log": "run_log",
"run_result": "run_log",
}
# Pre-resolved bucket map (for read-only callers like services/storage.py).
# Re-resolved at module load; re-resolve via resolve_bucket() if the
# workspace.artifact_bucket override matters.
@@ -479,8 +465,12 @@ async def restore_object(
try:
# Cross-backend copy: get from trash, put back to source bucket.
object_stores = request.app.state.object_stores
data = await object_stores[actual_bucket_name("trash")].get(item.trash_key)
await object_stores[item.bucket_name].put(item.object_key, data)
data = await object_stores[actual_bucket_name("trash")].get(item.object_key)
source_purpose, _, source_key = item.object_key.partition("/")
if not source_purpose:
source_purpose = "workspace"
target_bucket = actual_bucket_name(source_purpose)
await object_stores[target_bucket].put(source_key, data)
except Exception as exc:
raise HTTPException(
status.HTTP_502_BAD_GATEWAY,
@@ -488,6 +478,9 @@ async def restore_object(
) from exc
item.object_status = "available"
item.deleted_at = None
item.bucket_name = target_bucket
item.object_key = source_key
item.storage_uri = build_storage_uri(target_bucket, source_key)
# Keep trash_key so the reaper can clean up the duplicate on its
# next pass; we don't try to delete it here because a partial
# failure would leave the user with no data.
@@ -527,10 +520,15 @@ async def purge_trash_object(
status.HTTP_409_CONFLICT,
"object is not in trash; refuse to hard-delete live data",
)
if item.bucket_name != actual_bucket_name("trash"):
raise HTTPException(
status.HTTP_409_CONFLICT,
"object is not in trash bucket; refuse to hard-delete",
)
if item.trash_key:
try:
await request.app.state.object_stores[actual_bucket_name("trash")].delete(
item.trash_key
item.object_key
)
except Exception as exc:
raise HTTPException(
+2
View File
@@ -27,6 +27,7 @@ from .factory import (
build_storage_config,
create_storage,
rclone_remote_spec,
USAGE_TYPE_TO_PURPOSE,
workspaces_root,
)
from .registry import register_backend, registered_backends
@@ -36,6 +37,7 @@ __all__ = [
"build_storage_config",
"actual_bucket_name",
"build_storage_uri",
"USAGE_TYPE_TO_PURPOSE",
"workspaces_root",
"rclone_remote_spec",
"RCLONE_REMOTE_NAME",
+15
View File
@@ -62,6 +62,21 @@ def create_storage(config: Dict[str, Any]) -> AnyStorageBackend:
PURPOSE_BUCKETS: tuple[str, ...] = ("workspace", "version", "run_log", "trash")
# Map an upload's usage_type to its purpose (which then resolves to the
# actual bucket / directory via ``actual_bucket_name``). Keeping the
# purpose as the intermediate value means s3 mode and local mode share
# the same routing logic — only the final ``actual_bucket_name`` differs.
USAGE_TYPE_TO_PURPOSE: dict[str, str] = {
"working_copy": "workspace",
"public_script": "workspace",
"data_resource": "workspace",
"snapshot": "workspace",
"version_artifact": "version",
"run_log": "run_log",
"run_result": "run_log",
}
def actual_bucket_name(purpose: str) -> str:
"""把 purpose 名称解析成实际桶路径 / 名(runtime 数据会存在这个字符串里)。