Skip to content

[28.x] Bug 636342: External Storage - Document Attachments orphans blobs on delete#8434

Merged
Groenbech96 merged 2 commits into
releases/28.xfrom
magnushar/backport-636342-28.x
Jun 4, 2026
Merged

[28.x] Bug 636342: External Storage - Document Attachments orphans blobs on delete#8434
Groenbech96 merged 2 commits into
releases/28.xfrom
magnushar/backport-636342-28.x

Conversation

@Groenbech96
Copy link
Copy Markdown
Contributor

@Groenbech96 Groenbech96 commented Jun 3, 2026

Backport of #8392 to releases/28.x. Tracks AB#637539 (forward-port of AB#636342).

What

The OnAfterDelete subscriber on Table 1173 "Document Attachment" silently failed to delete the corresponding Azure blob, because DeleteFromExternalStorage started with Rec.Find() on a row that the OnAfterDelete trigger had already removed. The opt-in setting "Delete from External Storage" = true (default) was non-functional on the automatic deletion path, and every deleted attachment orphaned its blob on the customer's Azure storage account.

How

Extract the blob-delete + telemetry into a private path-based helper DeleteExternalFile(ExternalFilePath; DocumentAttachmentForTelemetry). The two live-row callers (DocumentAttachmentExternal.Page.al, DAExternalStorageSync.Report.al) keep going through DeleteFromExternalStorage, which now delegates to the helper and calls MarkAsNotUploadedToExternal() on success — observable behavior preserved. The subscriber now gates on Rec's still-populated field values (Stored Externally, External File Path, Skip Delete On Copy, IsFileFromAnotherEnvironmentOrCompany) and calls the helper directly, with no Find(), no Modify(). The dead post-call Modify() block that tried to clear Skip Delete On Copy on the already-deleted row is removed.

Tests

New regression tests in DAExtStorageImplTests.Codeunit.al:

  • RecordDeleteRemovesBlobFromExternalStorage — the direct regression.
  • RecordDeleteKeepsBlobWhenSkipDeleteOnCopyIsSet — copied-attachment guard.
  • RecordDeleteKeepsBlobWhenDeleteFromExternalStorageDisabled — opt-out guard.
  • Cross-environment guard from the original PR's follow-up commit is included.

Tests assert DeleteFile was (or was not) called via a SingleInstance spy on Test File Storage Connector (GetLastDeletedPath()), because the mock's FileExists is a no-op and cannot be used to validate the contract. The connector spy cannot Modify() a table from inside DeleteFile (TryFunction), hence the SingleInstance text field.

Risk

Low. Same scope, same code paths as #8392 — backport-only. The two live-row entry points keep identical observable behavior. The subscriber's behavior goes from "silently does nothing" to "actually deletes the blob," which is the documented and intended behavior. Bounded to one app + one test library.

AB#637539

…obs on delete

Backport of #8392 (AB#636342) to releases/28.x.

The OnAfterDeleteEvent subscriber called DeleteFromExternalStorage(Rec),
which started with Rec.Find() and exited as soon as that returned false
because the row had already been deleted. The blob in Azure was never
removed, telemetry tag 0000RNT never fired, and customers' Azure storage
bills grew silently for every deleted attachment.

Extract the blob-delete + telemetry logic into a path-based helper
(DeleteExternalFile), keep DeleteFromExternalStorage for callers that
still hold a live row (page action, sync report), and rewrite the
OnAfterDelete subscriber to gate on Rec's field values (Stored
Externally, External File Path, Skip Delete On Copy, Source Environment
Hash) and call the helper directly, without Find() or Modify(). Also
drop the dead post-call Modify() block that attempted to clear Skip
Delete On Copy on the already-deleted row.

Add regression tests covering the subscriber path, using a SingleInstance
spy on Test File Storage Connector to record DeleteFile invocations
(the mock's FileExists is a no-op and cannot be used to assert the
contract). Tests cover: delete with the feature enabled removes the
blob; Skip Delete On Copy preserves the shared blob from a copied
attachment; "Delete from External Storage = false" preserves the blob;
cross-environment files are not deleted by the local subscriber.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@Groenbech96 Groenbech96 requested a review from a team as a code owner June 3, 2026 07:28
@Groenbech96 Groenbech96 enabled auto-merge (squash) June 3, 2026 07:30
@github-actions github-actions Bot added this to the Version 28.2 milestone Jun 3, 2026
nikolakukrika
nikolakukrika previously approved these changes Jun 3, 2026
gggdttt
gggdttt previously approved these changes Jun 3, 2026
The variable is not a temporary record (no .Init/.Insert); it only holds
the GetSpecificFileAccount out-parameter result, so the Temp prefix is
prohibited by AA0237.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@Groenbech96 Groenbech96 dismissed stale reviews from gggdttt and nikolakukrika via 81ad9ab June 3, 2026 08:47
@Groenbech96 Groenbech96 merged commit 6e3cb8e into releases/28.x Jun 4, 2026
78 of 81 checks passed
@Groenbech96 Groenbech96 deleted the magnushar/backport-636342-28.x branch June 4, 2026 12:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants