feat(06.2-03): backend — cloud-aware delete routing + skip_quota + remove_only param
- storage.delete_document gains skip_quota=False param; quota decrement gated on it
- DELETE /api/documents/{id} gains remove_only=bool query param
- Cloud docs (storage_backend != minio): attempt cloud backend delete_object first
- On failure: return HTTP 200 {success: false, cloud_delete_failed: true} (not 4xx)
- On success or remove_only: delete DB row with skip_quota=True
- Cloud creds/exception message never included in response body (T-06.2-03-02)
- Promote 3 xfail stubs to real tests (propagates, failure, remove_only)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+17
-13
@@ -140,12 +140,15 @@ async def list_metadata(
|
||||
return rows
|
||||
|
||||
|
||||
async def delete_document(session: AsyncSession, doc_id: str) -> bool:
|
||||
async def delete_document(session: AsyncSession, doc_id: str, skip_quota: bool = False) -> bool:
|
||||
"""Delete a document's MinIO object and its PostgreSQL row.
|
||||
|
||||
Returns False if the document is not found; True on success.
|
||||
MinIO deletion failures are logged to stderr but do not prevent the DB row
|
||||
deletion (the bytes may already be gone).
|
||||
|
||||
skip_quota=True skips the quota decrement — used for cloud-stored documents
|
||||
that were never charged against the user's MinIO quota (T-06.2-03-01).
|
||||
"""
|
||||
try:
|
||||
uid = uuid.UUID(doc_id)
|
||||
@@ -161,18 +164,19 @@ async def delete_document(session: AsyncSession, doc_id: str) -> bool:
|
||||
except Exception as exc:
|
||||
print(f"[storage] WARNING: MinIO delete_object failed for {doc.object_key!r}: {exc}", file=sys.stderr)
|
||||
|
||||
# Atomic quota decrement (STORE-06, D-07).
|
||||
# user_id is always set post-migration (Plan 03-03+) — guard removed.
|
||||
# Use CASE WHEN instead of GREATEST() for SQLite compatibility
|
||||
# (PostgreSQL supports both; SQLite lacks the GREATEST scalar function).
|
||||
await session.execute(
|
||||
text(
|
||||
"UPDATE quotas "
|
||||
"SET used_bytes = CASE WHEN used_bytes > :delta THEN used_bytes - :delta ELSE 0 END "
|
||||
"WHERE user_id = :uid"
|
||||
),
|
||||
{"delta": doc.size_bytes, "uid": str(doc.user_id)},
|
||||
)
|
||||
if not skip_quota:
|
||||
# Atomic quota decrement (STORE-06, D-07).
|
||||
# user_id is always set post-migration (Plan 03-03+) — guard removed.
|
||||
# Use CASE WHEN instead of GREATEST() for SQLite compatibility
|
||||
# (PostgreSQL supports both; SQLite lacks the GREATEST scalar function).
|
||||
await session.execute(
|
||||
text(
|
||||
"UPDATE quotas "
|
||||
"SET used_bytes = CASE WHEN used_bytes > :delta THEN used_bytes - :delta ELSE 0 END "
|
||||
"WHERE user_id = :uid"
|
||||
),
|
||||
{"delta": doc.size_bytes, "uid": str(doc.user_id)},
|
||||
)
|
||||
|
||||
await session.delete(doc)
|
||||
await session.commit()
|
||||
|
||||
Reference in New Issue
Block a user