Repository navigation
Fix external storage migration for unsafe attachment filenames - #11733
Conversation
Preserve attachment display names while sanitizing storage and media-import filenames. Surface per-attachment sync failures and emit sanitized diagnostics for interactive and background runs. Add regression coverage for round-trips, collisions, and failure reporting. AB#647832 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Good Sense Reviewer - Round 1Recommendation: Request ChangesWhat this PR doesThis PR sanitizes generated external-storage path components for document attachments, keeps the visible attachment name unchanged, surfaces per-attachment sync failures, and adds regression coverage. The main filename path is aimed at the right root cause, and the tests cover invalid characters, collisions, and round-trip behavior. Two blocking issues remain: the new telemetry path can log raw customer file data, and the changed test-library app does not compile with its current dependencies. Problem-solution fitFit: Partial The reported problem is clear, and the core path-sanitization design matches it. The solution is incomplete until telemetry uses sanitized error text and the test-library dependency/build issue is fixed. SuggestionsS1 (🔴 High): Raw last error text can leak telemetry S2 (🔴 High): Missing dependency breaks System Application build Risk assessment and necessityRisk: The functional path is important because failed migration can leave many attachments unsynchronized. The current change also adds telemetry and test-library changes; the telemetry path can expose customer file data, and the test-library change currently breaks the System Application module build. Necessity: The fix is necessary and the scope is appropriate: unsafe generated external paths should be sanitized while preserving the visible attachment name, and sync failures should be diagnosable per attachment. The implementation needs the blocking issues fixed before it is safe to merge.
|
The test library now uses Temp Blob and Temp Blob List for in-memory connector content, so it must declare the BLOB Storage module dependency for System Application module builds. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Good Sense Reviewer - Round 2Recommendation: Request ChangesWhat this PR doesThe new commit adds the missing BLOB Storage dependency for the test-library app. That fixes the compile blocker from round 1, but the telemetry failure path is still unchanged: Status of previous suggestions
New observations (commits since round 1)None - the only new change fixes the test-library dependency. Risk assessment and necessityRisk: The attachment migration fix is still needed, and the manifest change reduces build risk. The remaining risk is telemetry exposure: detailed storage errors can include customer-specific filenames or external paths. Necessity: The path sanitization and per-attachment diagnostics remain necessary for failed migrations. Before merge, telemetry should use a fixed safe error template while the detailed error stays only in the user-facing error page.
|
|
Address review feedback on the External Storage Sync report: - Background runs complete without failing the job queue entry and record every failed attachment in the Error Message Register and telemetry. Interactive runs also show the failures. - Upload, download and cleanup now return a specific failure reason, which the report includes for each attachment. Tests assert the exact reasons and run the report with the UI suppressed. AB#647832 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Good Sense Reviewer - Round 3Recommendation: Request ChangesWhat this PR doesThe new commit adds specific per-attachment failure reasons and stores failures in the Error Message Register. This improves diagnostics, but detailed storage errors are still sent to telemetry, and background runs now finish successfully even when attachments fail. Status of previous suggestions
New observations (commits since round 2)S3 (🔴 High): Background failures now report successful completion Risk assessment and necessityRisk: The filename sanitization is targeted and well tested. The remaining risks are customer file data entering telemetry and background synchronization reporting success despite failed attachments. Necessity: Filename sanitization and per-attachment diagnostics are necessary. Persisting failures is useful, but it must not replace the background failure signal, and telemetry must use safe fixed text.
|
- Run each sync step through a new internal worker codeunit with Codeunit.Run so a runtime error on one attachment is registered as that attachment's failure and the batch continues. - Commit before the first step and between copy and source cleanup so a failed cleanup never rolls back a completed copy. - Telemetry now only receives scrubbed platform error text, fixed precondition labels, or a fixed locked fallback; never raw GetLastErrorText(). - Log feature usage only after a successful upload. - Test hygiene: locked label and Assert.ExpectedError in SyncContinuesWithoutReusingPreviousError; new background test for a hard failure. AB#647832 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
|
Replies to the Good Sense Reviewer rounds (5785108869, 5803989380, 5946352127) and the stale status check note (5851486762):
|
|
The FeatureTelemetry custom dimensions include the current User ID, attachment File Name, and Document No.; these values can contain employee identifiers, customer content, or pseudonymous identifiers and are sent to telemetry verbatim. Knowledge: Line mapping was unavailable, so this was posted as an issue comment. 👍 useful · ❤️ especially valuable · 👎 wrong - reply with why · AL review agent v1.48.6 |
- Stop emitting LogUsage when local reference retirement is blocked - Remove User ID, File Name and Document No. from telemetry dimensions - Grant execute permissions and indirect Error Message access in DA Ext. Stor. Admin - Fix ProcessingMsg translator comment placeholder Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
|
Batch update in 103e034 for the top-level review comments: Accepted
Declined for this PR (pre-existing on main, follow-up candidates)
Good Sense Reviewer round 5, S5 (deletion behavior): I am keeping Jesper's retention-only design: guarded local-reference retirement and no remote deletion until #12432/#12438 land. Choosing between that design and restoring the configured external deletion is an owner decision. The related thread on CI: main reverted #12325 in #12514, which fixed the Expense Agent API IntegrationTests failures. That revert is merged into the branch (8c139c3), and CI is rerunning on 103e034. |
Document that #12438 is optional internal-release work, not a prerequisite or remote-deletion foundation. Preserve existing standalone behavior and call out the local-retirement API result. AB#647832 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
|
The Upload to External action iterates with FindSet() but passes each iterator record to UploadToExternalStorage, which modifies that same Document Attachment. Use FindSet(true) so the read takes the update lock required by the per-row write instead of relying on a later lock upgrade. Suggested fix (apply manually — could not be anchored as a one-click suggestion): if DocumentAttachment.FindSet(true) thenKnowledge: Line mapping was unavailable, so this was posted as an issue comment. 👍 useful · ❤️ especially valuable · 👎 wrong - reply with why · AL review agent v1.49.6 |
|
The Copy from External To Internal action iterates with FindSet() but passes each iterator record to DownloadFromExternalStorageToInternal, which imports content and modifies that Document Attachment. Use FindSet(true) so the read takes the update lock required by the per-row write instead of relying on a later lock upgrade. Suggested fix (apply manually — could not be anchored as a one-click suggestion): if DocumentAttachment.FindSet(true) thenKnowledge: Line mapping was unavailable, so this was posted as an issue comment. 👍 useful · ❤️ especially valuable · 👎 wrong - reply with why · AL review agent v1.49.6 |
…s AB#647832 Add AddLoadFields for the fields read during synchronization so the report no longer JIT-loads them per record, and pass the iterated Document Attachment directly to the follow-up worker step instead of cloning it first. The worker receives the record by var, so the upload/download results are already reflected in the iterated record. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
|
The background synchronization uploads the file to external storage using a newly generated GUID path, then updates the attachment row. If the local Modify fails after CreateFile succeeds, a retry can upload the same attachment again under a different path and leave another remote file. Derive the external request/path from a stable attachment identifier, or use an idempotency key that the connector enforces, before allowing the job-queue synchronization to retry. Knowledge: Line mapping was unavailable, so this was posted as an issue comment. 👍 useful · ❤️ especially valuable · 👎 wrong - reply with why · AL review agent v1.49.6 |
|
RunCompanyMigration iterates with FindSet(true), taking update locks on attachment rows, and then calls MigrateFileToCurrentEnvironment, which performs external GetFile/CreateFile operations before Modify. A slow storage request therefore holds database update locks for the duration of the remote I/O across the migration batch. Read the candidates without update locks, then reacquire an update lock only for the validated row update after the external copy, or otherwise move the external transfer outside the locked iteration. Line mapping was unavailable, so this was posted as an issue comment. 👍 useful · ❤️ especially valuable · 👎 wrong - reply with why · AL review agent v1.49.6 |
Good Sense Reviewer - Round 6Recommendation: AcceptWhat this PR doesThis change sanitizes generated attachment filenames, preserves display names and saved external paths, and records individual synchronization failures. It also retains remote files and clears local external references only after checking that valid internal content exists. The new commits remove content-bearing usage telemetry dimensions, clarify standalone retention, and load synchronization fields before iteration. Passing the iterated attachment to later worker steps preserves the refreshed upload state and restored media reference. The locked retirement checks reject stale, temporary and external-only rows. Regression coverage exercises unsafe names, round-trip content, hard-error rollback, failure continuation and guarded retirement; no new defect was found in the author-only changes. Status of previous suggestions
New observations (commits since round 5)None — no new actionable issue was found. The disputed findings above no longer establish a blocker. Risk assessment and necessityRisk: Synchronization now has per-step transaction boundaries, and Move and deletion workflows intentionally retain remote bytes. Remote files can remain after their local attachment is removed, so storage retention and cleanup must account for this contract. Guarded local retirement does not establish a global ownership guarantee for internal media. Necessity: Generated filenames must satisfy storage validation without changing attachment display names, and users need specific failure reasons. Without this change, otherwise valid attachments can still fail migration without a useful diagnosis. The retention behavior is a separate, explicit part of this standalone change and does not require an optional future cleanup implementation.
|
What & why
Attachment filenames such as
Invoice Nov'25.pdffail migration because generated external paths violate storage validation. Storage Sync previously hid individual reasons behind an aggregate failure count.5e1de1c93383b0bdc9df5b905ba47040b5cc53c0: fresh permanent row and media under update locks, unchanged requested identity/path/upload/source/media snapshot, Stored Internally, and actual nonempty Tenant Media Content. True means LOCAL reference retired only; remote bytes remain. Temporary, missing, stale and external-only rows are blocked.0000VL5); retention uses canonical0000W5Q/0000W5Rwith non-content dimensions. Add restored/external-only/stale worker and compatibility-overload regressions.This PR is standalone against main and does not require #12438. That proposal concerns internal release, not remote ownership or deletion authority. Remote retention and guarded LOCAL external-reference retirement are intentional independently of whether #12438 ships; no future physical remote deletion is promised.
Linked work
Fixes AB#647832
GitHub issue: N/A; this fix is tracked by the approved Azure DevOps bug.
How I validated this
What I tested and the outcome (required - be specific: scenarios, commands, screenshots for UI changes)
Current head:
234464d4f3990fd1a7b22463bd80dd40ae62f3ce(documentation-only follow-up, 2026-10-08). Code head inspected:103e034068185e61930c946fbbadd16dc48ac6f2. The author merged main in8c139c3a29to include the upstream Expense API revert #12514, then corrected telemetry dimensions, permissions, and a translator comment. This follow-up changes documentation only; no runtime redesign, PR-base change, force push, or sibling source import.git diff --check: passed for the documentation-only follow-up. No new AL code or tests are introduced by this follow-up.gh run view 37742245234 --repo microsoft/BCApps --json headSha,status,conclusion,jobs: one-shot inspection showed the code-head run still in progress, 49 jobs completed, 113 in progress, and no failed completed jobs. This is not a passing run or current-head test evidence; no monitoring loop or automatic retry was started.ea863079ededfb678e4d4359aeb883b8d9cc29f4,gh run download 37517120439 --repo microsoft/BCApps --pattern '*-TestResults-*' --dir <session-artifact-directory>succeeded. Parsing the actual XML proved all 56 declared attachment tests executed and passed in each of 23 country/default artifacts (1,288 passes, no skips or missing attachment cases), on platform30.0.55814.0. W1/System Default and Clean builds passed. The run still failed in 20 Expense API integration jobs; matched server event logs show a SQL-connection/metadata OData null reference. This is historical evidence, not proof for103e034068or this documentation commit. Detailed evidence.python verify_combined_retention.py .source assertions passed at063c259a0a; that is source-only historical evidence, not current-head compilation or runtime proof.Run-TestsInBcContainerexecution for this documentation follow-up: no approved local BC30 compiler/runtime is available and the local helper previously reported missing Docker permissions. Repository setup is documented inLOCAL_DEV_ENV.md; no container was created or deployed. Current code-head runtime results remain unverified.Historical evidence only: preceding head
da749476compiled the app/tests in W1 Default/Clean with compiler30.0.43.2701, with no diagnostics in their compiler blocks. Earlier author-reported validation reproduced the apostrophe failure, compiled in leased BC artifact30.0.54812.0, ranRun-TestsInBcContainerfor136820with 48/48 passing tests, and captured six interactive telemetry events plus a non-GUI invocation. That earlier invocation raised the first failure and used a temporary event tag. Those older results also do not validate the current head.Risk & compatibility
No schema changes or existing external-file renames. Display names stay unchanged. The user-approved retention policy intentionally changes deletion semantics: both DeleteFromExternalStorage overloads return true only for verified LOCAL metadata retirement, never remote deletion. Remote files may remain after record deletion, successful restoration or failed uploads; a logged logical path is diagnostic, not ownership/deletion authority.
Remaining standalone review and validation gates; no merge-ready claim:
DeleteFromExternalStoragecompatibility/deprecation decision remains open in two review threads. Both aliases retain their signatures but return true only for guarded LOCAL retirement. Restoring physical remote deletion would contradict the user-approved retention policy; preserving signatures does not erase the semantic change. No deprecation decision has been made on the owner's behalf.AddLoadFieldsperformance review remains open and needs its own scoped disposition; it is unrelated to [AB#651782] Block unsafe internal attachment cleanup #12438.