From 405c7a66102175663e65e213ed586a74efc7f841 Mon Sep 17 00:00:00 2001 From: curo1305 Date: Tue, 23 Jun 2026 00:28:34 +0200 Subject: [PATCH] fix(13): CR-03 wrap post-upload DB block in try/except to handle provider-success/DB-fail divergence --- backend/api/cloud/operations.py | 101 ++++++++++++++++++++------------ 1 file changed, 63 insertions(+), 38 deletions(-) diff --git a/backend/api/cloud/operations.py b/backend/api/cloud/operations.py index 7048cb9..f4680ef 100644 --- a/backend/api/cloud/operations.py +++ b/backend/api/cloud/operations.py @@ -1043,47 +1043,72 @@ async def upload_cloud_file( content_type=content_type, size=resolved_size, ) - # Upsert item — establishes stable row identity for navigation - await upsert_cloud_item(session, user_id=str(current_user.id), resource=resource) - # Invalidate parent folder so next browse triggers a provider re-list. - # We do NOT call apply_listing_and_finalize — that requires a full provider - # listing which we don't have. Instead we mark the folder stale with a - # controlled reason so the browse endpoint knows to re-fetch. - invalidate_parent = resolved_parent_ref if resolved_parent_ref is not None else "" - await update_folder_state( - session, - user_id=str(current_user.id), - connection_id=str(connection_id), - parent_ref=invalidate_parent, - refresh_state="warning", - error_code="upload_mutated", - error_message="Folder contents changed by upload — re-listing required.", - ) + # DB mutation block: upsert + folder-state invalidation + audit + commit. + # CR-03: Wrapped in try/except so that a DB failure after a successful + # provider upload is reported distinctly rather than propagating as an + # unhandled 500. The file IS on the provider; the metadata recording + # failed. The frontend receives a typed error so it can surface the + # discrepancy rather than silently losing the upload record. + try: + # Upsert item — establishes stable row identity for navigation + await upsert_cloud_item(session, user_id=str(current_user.id), resource=resource) - # Write metadata-only audit row in the same transaction (Plan 06, Task 2: T-13-20). - # Only authoritative upload success reaches this path. Non-success outcomes - # (conflict, offline, reauth_required) are handled in separate branches below - # and never call write_audit_log — preserving T-13-21 (no false success events). - # The audit payload is metadata-only: no provider URLs, tokens, bytes, or - # document text are included (T-13-02). - await write_audit_log( - session, - event_type="cloud.file_uploaded", - user_id=current_user.id, - actor_id=current_user.id, - resource_id=None, - ip_address=get_client_ip(request), - metadata_={ - "connection_id": str(connection_id), - "provider_item_id": provider_item_id, - "filename": resolved_name, - "size_bytes": resolved_size, - "parent_ref": resolved_parent_ref, - }, - ) + # Invalidate parent folder so next browse triggers a provider re-list. + # We do NOT call apply_listing_and_finalize — that requires a full provider + # listing which we don't have. Instead we mark the folder stale with a + # controlled reason so the browse endpoint knows to re-fetch. + invalidate_parent = resolved_parent_ref if resolved_parent_ref is not None else "" + await update_folder_state( + session, + user_id=str(current_user.id), + connection_id=str(connection_id), + parent_ref=invalidate_parent, + refresh_state="warning", + error_code="upload_mutated", + error_message="Folder contents changed by upload — re-listing required.", + ) - await session.commit() + # Write metadata-only audit row in the same transaction (Plan 06, Task 2: T-13-20). + # Only authoritative upload success reaches this path. Non-success outcomes + # (conflict, offline, reauth_required) are handled in separate branches below + # and never call write_audit_log — preserving T-13-21 (no false success events). + # The audit payload is metadata-only: no provider URLs, tokens, bytes, or + # document text are included (T-13-02). + await write_audit_log( + session, + event_type="cloud.file_uploaded", + user_id=current_user.id, + actor_id=current_user.id, + resource_id=None, + ip_address=get_client_ip(request), + metadata_={ + "connection_id": str(connection_id), + "provider_item_id": provider_item_id, + "filename": resolved_name, + "size_bytes": resolved_size, + "parent_ref": resolved_parent_ref, + }, + ) + + await session.commit() + except Exception: + # Provider upload succeeded but DB recording failed. + # Roll back any partial writes and return a typed error so the frontend + # knows the file is on the provider but DocuVault has no metadata record. + await session.rollback() + return JSONResponse( + status_code=status.HTTP_207_MULTI_STATUS, + content={ + "kind": "provider_success_db_error", + "reason": "metadata_record_failed", + "provider_item_id": provider_item_id, + "message": ( + "File uploaded to provider but DocuVault metadata recording failed. " + "Refresh the folder to re-sync." + ), + }, + ) return { "kind": "uploaded",