microsoft / microsoft/BCApps

[Event Request] Allow Custom File Path Structure for External Storage Attachments

Open
#10,966 3 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

missing-info Team: Integrations
Dominant language
AL
Stars
683
Forks
459
Avg merge
3d 26m
Merged PRs (30d)
633

Description

Why do you need this change?

Multiple customers using the External Storage - Document Attachments app need the ability to customize how files are organized in external storage.

Currently, the file path structure is fixed: RootFolder/EnvironmentHash/TableName/FileName-GUID.Extension. This structure cannot be modified or extended without modifying the base app.

Common customer requirements include:

  • Grouping document attachments into subfolders by Vendor No. or Customer No.
  • Organizing files by Posting Date (e.g. year/month subfolders)
  • Applying custom naming conventions based on document metadata (e.g. document number, description)

These customizations are not possible today because the procedure GetFilePathWithRootFolder in codeunit 8751 "DA External Storage Impl." is local and has no integration event to allow partners to override or extend the path logic.

Describe the request

This is the current standard code — codeunit 8751 "DA External Storage Impl.", local procedure GetFilePathWithRootFolder:

local procedure GetFilePathWithRootFolder(DocumentAttachment: Record "Document Attachment"): Text[2048]
var
    ExternalStorageSetup: Record "DA External Storage Setup";
    FileName: Text;
    RootFolder: Text;
    TableNameFolder: Text[100];
    EnvironmentHashFolder: Text[32];
    FileNamePart: Text;
    IsHandled: Boolean;
    FileNameFormatLbl: Label '%1-%2.%3', Comment = '%1 = File Name, %2 = GUID, %3 = File Extension', Locked = true;
begin
    // Generate unique filename to prevent collisions
    FileNamePart := StrSubstNo(FileNameFormatLbl, DocumentAttachment."File Name", DelChr(Format(CreateGuid()), '=', '{}'), DocumentAttachment."File Extension");

    // Get table name folder (based on the source table of the attachment)
    TableNameFolder := GetTableNameFolder(DocumentAttachment."Table ID");

    // Get environment hash folder (based on tenant + environment + company)
    EnvironmentHashFolder := GetCurrentEnvironmentHash();

    // Get root folder from setup if configured
    if not ExternalStorageSetup.Get() then
        exit;

    RootFolder := ExternalStorageSetup."Root Folder";
    if RootFolder <> '' then begin
        if not RootFolder.EndsWith('/') and not RootFolder.EndsWith('\') then
            RootFolder := RootFolder + '/';

        EnsureFolderExists(RootFolder + EnvironmentHashFolder);
        EnsureFolderExists(RootFolder + EnvironmentHashFolder + '/' + TableNameFolder);

        FileName := RootFolder + EnvironmentHashFolder + '/' + TableNameFolder + '/' + FileNamePart;
    end else begin
        EnsureFolderExists(EnvironmentHashFolder);
        EnsureFolderExists(EnvironmentHashFolder + '/' + TableNameFolder);

        FileName := EnvironmentHashFolder + '/' + TableNameFolder + '/' + FileNamePart;
    end;

    exit(CopyStr(FileName, 1, 2048));
end;

The procedure builds the file path with a fixed structure (RootFolder + EnvironmentHash + TableName + FileName). There is no way for partners to intervene and customize this path.

Could you please add an integration event similar to the following:

[IntegrationEvent(false, false)]
local procedure OnBeforeGetFilePathWithRootFolder(DocumentAttachment: Record "Document Attachment"; var FileName: Text; var IsHandled: Boolean)
begin
end;

The modified procedure would look like:

local procedure GetFilePathWithRootFolder(DocumentAttachment: Record "Document Attachment"): Text[2048]
var
    ExternalStorageSetup: Record "DA External Storage Setup";
    FileName: Text;
    RootFolder: Text;
    TableNameFolder: Text[100];
    EnvironmentHashFolder: Text[32];
    FileNamePart: Text;
    IsHandled: Boolean;
    FileNameFormatLbl: Label '%1-%2.%3', Comment = '%1 = File Name, %2 = GUID, %3 = File Extension', Locked = true;
begin
    //:: --- ::: BEGIN CHANGES :: --- :::
    OnBeforeGetFilePathWithRootFolder(DocumentAttachment, FileName, IsHandled);
    if IsHandled then
        exit(CopyStr(FileName, 1, 2048));
    //:: --- ::: END CHANGES :: --- :::

    // Generate unique filename to prevent collisions
    FileNamePart := StrSubstNo(FileNameFormatLbl, DocumentAttachment."File Name", DelChr(Format(CreateGuid()), '=', '{}'), DocumentAttachment."File Extension");

    // Get table name folder (based on the source table of the attachment)
    TableNameFolder := GetTableNameFolder(DocumentAttachment."Table ID");

    // Get environment hash folder (based on tenant + environment + company)
    EnvironmentHashFolder := GetCurrentEnvironmentHash();

    // Get root folder from setup if configured
    if not ExternalStorageSetup.Get() then
        exit;

    RootFolder := ExternalStorageSetup."Root Folder";
    if RootFolder <> '' then begin
        if not RootFolder.EndsWith('/') and not RootFolder.EndsWith('\') then
            RootFolder := RootFolder + '/';

        EnsureFolderExists(RootFolder + EnvironmentHashFolder);
        EnsureFolderExists(RootFolder + EnvironmentHashFolder + '/' + TableNameFolder);

        FileName := RootFolder + EnvironmentHashFolder + '/' + TableNameFolder + '/' + FileNamePart;
    end else begin
        EnsureFolderExists(EnvironmentHashFolder);
        EnsureFolderExists(EnvironmentHashFolder + '/' + TableNameFolder);

        FileName := EnvironmentHashFolder + '/' + TableNameFolder + '/' + FileNamePart;
    end;

    exit(CopyStr(FileName, 1, 2048));
end;

This would allow partners to subscribe to the event and provide a fully customized file path based on the document attachment metadata — for example, organizing by vendor, customer, posting date, or any other business-relevant criteria — while leaving the default behavior untouched when IsHandled is false.

Provide an implementation (optional)
  • I will provide the implementation for this extensibility request
Updated missing information
Existing alternatives considered
  • The "DA External Storage Setup" record allows a root folder to be configured, but the remaining EnvironmentHash/TableName/FileName-GUID.Extension structure is fixed.
  • The available setup and existing extension points do not allow an extension to replace the generated path or participate in GetFilePathWithRootFolder.
  • Renaming the attachment before upload is insufficient because it affects only the file-name portion and cannot add business-specific folders or replace the environment and table folders.
  • Moving or renaming the file after upload would require an additional storage operation, leave standard-created folders behind, and introduce a period in which the attachment points to the original location.
  • Replacing the complete external-storage implementation would duplicate authentication, upload, download, deletion, and attachment-handling behavior solely to customize path generation.
Justification for IsHandled

The requested customization replaces the complete path-generation operation, not only one segment of the resulting string. The standard procedure also creates the standard environment and table folders and generates a new GUID-based file name. A regular notification event cannot prevent those side effects. An event raised after the standard logic would create folders that the custom path does not use and would not let the subscriber control folder provisioning as one operation.

When IsHandled is true, the subscriber assumes responsibility for returning the complete path, creating any required folders, preserving file-name uniqueness, and complying with the external storage provider's path rules. When IsHandled is false, the current standard path and folder behavior remains unchanged.

Performance considerations

The procedure is called once when the destination path is generated for an attachment upload. The proposed event adds one synchronous publisher invocation per generated path. A typical subscriber would read fields already available on DocumentAttachment, perform limited lookups for related document metadata, build a string, and create only the required folders.

Subscribers should avoid repeated remote queries, cache or minimize metadata lookups where appropriate, and avoid checking or creating the same folder more often than required. The event itself adds negligible overhead when there are no subscribers.

Data sensitivity review

The event passes the DocumentAttachment record, which contains attachment metadata and can identify the source table and file. It does not pass storage credentials. Installed AL extensions already run inside the tenant trust boundary, but a subscriber can use business data when constructing externally visible folder and file names.

Custom implementations must avoid placing confidential, personal, credential, or otherwise sensitive values in the path unless the customer's data-governance policy explicitly permits it. They must also sanitize invalid characters, prevent .. or equivalent path traversal, respect provider length limits, preserve uniqueness, and avoid exposing data through storage logs or URLs. The custom extension assumes responsibility for these controls when it sets IsHandled := true.

Multi-extension interaction

Multiple subscribers should follow a cooperative first-handler-wins convention:

  • If IsHandled is already true, a subscriber must exit without changing FileName or IsHandled.
  • A subscriber should set IsHandled := true only after it has produced a complete valid path and handled any required folder creation.
  • A subscriber that does not apply to the current attachment must leave the parameters unchanged.

AL event subscriber execution order is not guaranteed. If multiple extensions attempt to own path generation for the same attachment, their behavior is inherently conflicting and should be resolved through extension setup or by disabling the overlapping customization. The first subscriber that handles the request establishes the path, and later subscribers must respect IsHandled.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start in codeunit 8751 "DA External Storage Impl." and read the local procedure GetFilePathWithRootFolder, including its folder-creation and default path behavior. Add the requested integration event and verify that handled requests can supply a complete path while unhandled requests preserve the existing behavior; no test file is named in the issue.

Written by the indexing model from the issue text.

Assessment

Domain
backend
Issue type
Feature
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
65/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.