[Event Request] Allow Custom File Path Structure for External Storage Attachments
Nobody has claimed this yet.
- 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.Extensionstructure 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
IsHandledis alreadytrue, a subscriber must exit without changingFileNameorIsHandled. - A subscriber should set
IsHandled := trueonly 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
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- 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