Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
79 commits
Select commit Hold shift + click to select a range
c8e19e2
Add integration events for file scenario management and enhance UI ac…
StefanSosic Aug 21, 2025
33071a9
Merge branch 'main' of https://github.com/StefanSosic/BCApps into dev…
StefanSosic Aug 21, 2025
b81f6eb
External Storage - Document Attachments
StefanSosic Aug 25, 2025
4adc2b0
Interface + Improvements
StefanSosic Aug 25, 2025
d04ef8b
Fixes
StefanSosic Aug 25, 2025
4978d52
Trailing space
StefanSosic Aug 25, 2025
6357709
Internal
StefanSosic Aug 25, 2025
e5dbc09
Internal
StefanSosic Aug 25, 2025
6ecd65f
app json fixes
StefanSosic Aug 25, 2025
c454a9a
DataClassification
StefanSosic Aug 25, 2025
b485579
Namespaces
StefanSosic Aug 25, 2025
a28fe49
InherentPermissions + InherentEntitlements
StefanSosic Aug 25, 2025
26e1c3a
Update tooltip for sync direction field in DA External Storage Sync r…
StefanSosic Aug 25, 2025
68ef801
Deafult Scenario
StefanSosic Aug 29, 2025
8832186
Empty Line
StefanSosic Aug 29, 2025
ecd581a
Permission Refactor & Message
StefanSosic Aug 29, 2025
4b57268
Add label for date formula in DA External Storage Sync report
StefanSosic Aug 29, 2025
7eb7ebf
FIxes from PR comments
StefanSosic Oct 9, 2025
bf2f5b6
Fixes and Features
StefanSosic Oct 21, 2025
509618d
Add disable check
StefanSosic Oct 21, 2025
244043f
Remove prefix
StefanSosic Oct 21, 2025
0db7610
Adjustments to deletion
StefanSosic Oct 21, 2025
ad8638e
Remove logger
StefanSosic Oct 21, 2025
e87efe6
Merge branch 'main' of https://github.com/StefanSosic/BCApps into dev…
StefanSosic Oct 27, 2025
f358ddb
Update readme.md
StefanSosic Oct 28, 2025
b6c89f6
Improve warning message
JesperSchulz Dec 19, 2025
4f4c207
Fallback for table name if 3rd party App Uninstalled
StefanSosic Dec 19, 2025
372d316
Merge branch 'dev/sso/externalStorageApp' of https://github.com/Stefa…
StefanSosic Dec 19, 2025
f12957b
Add migration report
StefanSosic Dec 19, 2025
14164b2
File Scenario fix breaking change
StefanSosic Jan 19, 2026
3e86854
Merge branch 'main' of https://github.com/StefanSosic/BCApps into dev…
StefanSosic Jan 19, 2026
53648cf
Remove
StefanSosic Jan 19, 2026
852773a
Fix
StefanSosic Jan 19, 2026
3e45ce4
Remove upload
StefanSosic Jan 19, 2026
3e9f8a5
Fix Copy
StefanSosic Jan 19, 2026
fca4ef1
Edge case
StefanSosic Jan 19, 2026
4edda71
One Drive
StefanSosic Jan 19, 2026
90f2a8a
Polishing
StefanSosic Jan 19, 2026
36318ea
Merge branch 'main' of https://github.com/microsoft/BCApps into dev/s…
JesperSchulz Jan 27, 2026
3c06e87
Fixes & Polishing
StefanSosic Jan 29, 2026
c3fe884
Merge branch 'main' of https://github.com/microsoft/BCApps into dev/s…
JesperSchulz Jan 30, 2026
82a537f
Add setup page invocation automatically on insert scenario
StefanSosic Jan 30, 2026
2687d6c
Improvements
StefanSosic Jan 30, 2026
e625564
Merge branch 'dev/sso/externalStorageApp' of https://github.com/stefa…
JesperSchulz Jan 30, 2026
913abe5
Report optimize
StefanSosic Jan 30, 2026
cdd9fa2
Merge branch 'dev/sso/externalStorageApp' of https://github.com/stefa…
JesperSchulz Jan 30, 2026
fb1d1dd
Drill down
StefanSosic Jan 30, 2026
b34c455
Init true
StefanSosic Jan 30, 2026
c55ef6f
Merge branch 'dev/sso/externalStorageApp' of https://github.com/stefa…
JesperSchulz Jan 30, 2026
94e326b
Fix
StefanSosic Jan 30, 2026
07ed610
Merge branch 'dev/sso/externalStorageApp' of https://github.com/stefa…
JesperSchulz Jan 30, 2026
1fd2540
Merge branch 'main' of https://github.com/StefanSosic/BCApps into dev…
StefanSosic Feb 4, 2026
b7ffcc4
Add tests for External Storage - Document Attachments functionality
StefanSosic Feb 4, 2026
585f7f8
New Guid
StefanSosic Feb 4, 2026
5eeff8d
Merge branch 'dev/sso/externalStorageApp' of https://github.com/stefa…
JesperSchulz Feb 5, 2026
bd39320
Merge branch 'main' of https://github.com/microsoft/BCApps into dev/s…
JesperSchulz Feb 5, 2026
711568d
Merge branch 'main' of https://github.com/microsoft/BCApps into dev/s…
JesperSchulz Feb 9, 2026
8c2d4f0
Updates
StefanSosic Feb 10, 2026
8d65717
Update to 32
StefanSosic Feb 10, 2026
acd40e0
Merge branch 'dev/sso/externalStorageApp' of https://github.com/stefa…
JesperSchulz Feb 10, 2026
d9af01e
Merge branch 'main' of https://github.com/microsoft/BCApps into dev/s…
JesperSchulz Feb 10, 2026
ebffc34
Merge branch 'main' of https://github.com/microsoft/BCApps into dev/s…
JesperSchulz Feb 11, 2026
2168b02
Merge branch 'dev/sso/externalStorageApp' of https://github.com/stefa…
JesperSchulz Feb 11, 2026
a0ce75c
Merge branch 'main' of https://github.com/StefanSosic/BCApps into dev…
StefanSosic Feb 11, 2026
791c1f4
Merge branch 'dev/sso/externalStorageApp' of https://github.com/Stefa…
StefanSosic Feb 11, 2026
ebc2b5d
Merge branch 'dev/sso/externalStorageApp' of https://github.com/stefa…
JesperSchulz Feb 11, 2026
11c33b1
Add telemetry tags.
JesperSchulz Feb 11, 2026
32202ff
Update src/Apps/W1/External Storage - Document Attachments/app/src/Se…
StefanSosic Feb 11, 2026
ae20d03
Update src/System Application/App/External File Storage/src/Scenario/…
StefanSosic Feb 11, 2026
929df45
Update src/System Application/App/External File Storage/src/Scenario/…
StefanSosic Feb 11, 2026
bf03c5c
Fixes to PR Comments
StefanSosic Feb 11, 2026
87a03cc
Merge branch 'dev/sso/externalStorageApp' of https://github.com/Stefa…
StefanSosic Feb 11, 2026
0ca6800
Update src/System Application/App/External File Storage/src/Scenario/…
StefanSosic Feb 11, 2026
af4a2cb
Updates
StefanSosic Feb 11, 2026
717fe2c
All ApplicationArea
StefanSosic Feb 11, 2026
5f375f8
Permissions
StefanSosic Feb 11, 2026
f477e66
Entitlements
StefanSosic Feb 11, 2026
220dc0d
Merge branch 'main' of https://github.com/microsoft/BCApps into dev/s…
JesperSchulz Feb 11, 2026
4f111fe
Merge branch 'dev/sso/externalStorageApp' of https://github.com/stefa…
JesperSchulz Feb 11, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
# Document Attachments External Storage for Microsoft Dynamics 365 Business Central

## Overview

The External Storage extension provides seamless integration between Microsoft Dynamics 365 Business Central and external storage systems such as Azure Blob Storage, SharePoint, and File Shares. This extension automatically manages document attachments by storing them in external storage systems while maintaining full functionality within Business Central.

## Key Features

### **Automatic Upload**
- Automatically uploads new document attachments to configured external storage
- Supports multiple storage connectors via the File Account framework
- Generates unique file names to prevent collisions
- Maintains original file metadata and associations

### **Flexible Deletion Policies**
- **Immediately**: Delete from internal storage right after external upload
- **1 Day**: Keep internally for 1 day before deletion
- **7 Days**: Keep internally for 7 days before deletion (default)
- **14 Days**: Keep internally for 14 days before deletion

### **Bulk Operations**
- Synchronize multiple files between internal and external storage
- Bulk upload to external storage
- Bulk download from external storage
- Progress tracking with detailed reporting

## Installation & Setup

### Prerequisites
- Microsoft Dynamics 365 Business Central version 27.0 or later
- File Account module configured with external storage connector
- Appropriate permissions for file operations

### Installation Steps

1. **Configure File Account**
- Open **File Accounts** page
- Create a new File Account with your preferred connector:
- Azure Blob Storage
- SharePoint
- File Share
- Assign the account to **External Storage** scenario

2. **Configure External Storage**
- Open **File Accounts** page
- Select assigned **External Storage** scenario
- Open **Additional Scenario Setup**
- Configure settings:
- **Auto Upload**: Enable automatic upload of new attachments
- **Delete After**: Set retention policy for internal storage

### Configuration Options

#### Auto Upload Settings
- **Enabled**: New document attachments are automatically uploaded to external storage
- **Disabled**: Manual upload required via actions

## Usage

### Automatic Mode
When Auto Upload is enabled:
1. User attaches a document to any Business Central record
2. System automatically uploads to external storage
3. File remains accessible through standard attachment functionality
4. Internal file is deleted based on configured retention policy

### Manual Operations

#### Individual File Operations
From **Document Attachment - External** page:
- **Upload to External Storage**: Upload selected file
- **Download from External Storage**: Download file for viewing
- **Download to Internal Storage**: Restore file to internal storage
- **Delete from External Storage**: Remove file from external storage
- **Delete from Internal Storage**: Remove file from internal storage

#### Bulk Operations
From **External Storage Synchronize** report:
- **To External Storage**: Upload multiple files to external storage
- **From External Storage**: Download multiple files from external storage
- **Delete Expired Files**: Clean up files based on retention policy

### File Access
- Files uploaded to external storage remain fully accessible through standard Business Central functionality
- Document preview, download, and management work seamlessly
- No change to end-user experience

**© 2025 Microsoft Corporation. All rights reserved.**
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
{
"id": "5f2e93a0-6083-4718-b05a-7ac89be5644d",
"name": "External Storage - Document Attachments",
"publisher": "Microsoft",
"version": "27.0.0.0",
"brief": "External Storage processor for Business Central document attachments",
"description": "Processes document attachments from Business Central and stores them in configured External Storage connectors (Azure Blob, File Share, SharePoint)",
"privacyStatement": "https://go.microsoft.com/fwlink/?linkid=724009",
"EULA": "https://go.microsoft.com/fwlink/?linkid=2009120",
"help": "https://go.microsoft.com/fwlink/?linkid=2134520",
"url": "https://go.microsoft.com/fwlink/?linkid=724011",
"logo": "ExtensionLogo.png",
"application": "27.0.0.0",
Comment thread
StefanSosic marked this conversation as resolved.
Outdated
"platform": "27.0.0.0",
"internalsVisibleTo": [
],
"dependencies": [],
"screenshots": [],
"idRanges": [
{
"from": 8750,
"to": 8770
}
],
"resourceExposurePolicy": {
"allowDebugging": true,
"allowDownloadingSource": true,
"includeSourceInSymbolFile": true
},
"features": [
"TranslationFile"
],
"contextSensitiveHelpUrl": "https://go.microsoft.com/fwlink/?linkid=2134520",
"target": "Cloud"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
// ------------------------------------------------------------------------------------------------
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.
// ------------------------------------------------------------------------------------------------

namespace Microsoft.ExternalStorage.DocumentAttachments;

using Microsoft.Foundation.Attachment;

/// <summary>
/// Report for synchronizing document attachments between internal and external storage.
/// Supports bulk upload, download, and cleanup operations.
/// </summary>
report 8752 "DA External Storage Sync"
{
Caption = 'External Storage Synchronization';
ProcessingOnly = true;
UseRequestPage = true;
Extensible = false;
UsageCategory = None;
Permissions = tabledata "DA External Storage Setup" = r,
tabledata "Document Attachment" = r;

dataset
{
dataitem(DocumentAttachment; "Document Attachment")
{
trigger OnPreDataItem()
begin
SetFilters();
TotalCount := Count();

if TotalCount = 0 then begin
if GuiAllowed() then
Message(NoRecordsMsg);
CurrReport.Break();
end;

if MaxRecordsToProcess > 0 then
if TotalCount > MaxRecordsToProcess then
TotalCount := MaxRecordsToProcess;

ProcessedCount := 0;
FailedCount := 0;
DeleteCount := 0;
DeleteFailedCount := 0;

if GuiAllowed() then
Dialog.Open(ProcessingMsg, TotalCount);
end;

trigger OnAfterGetRecord()
begin
ProcessedCount += 1;

if GuiAllowed() then
Dialog.Update(1, ProcessedCount);

case SyncDirection of
SyncDirection::"To External Storage":

if not ExternalStorageProcessor.UploadToExternalStorage(DocumentAttachment) then
FailedCount += 1;
SyncDirection::"From External Storage":

Comment thread
StefanSosic marked this conversation as resolved.
Outdated
if not ExternalStorageProcessor.DownloadFromExternalStorage(DocumentAttachment) then
FailedCount += 1;
end;
if DeleteExpiredFiles then
if CalcDate('<+' + GetDateFormulaFromExternalStorageSetup() + '>', DocumentAttachment."External Upload Date".Date()) >= Today() then
Comment thread
StefanSosic marked this conversation as resolved.
Outdated
if ExternalStorageProcessor.DeleteFromInternalStorage(DocumentAttachment) then
DeleteCount += 1
else
DeleteFailedCount += 1;
Comment thread
StefanSosic marked this conversation as resolved.
Outdated

if (MaxRecordsToProcess > 0) and (ProcessedCount >= MaxRecordsToProcess) then
CurrReport.Break();
end;

trigger OnPostDataItem()
begin
if GuiAllowed() then begin
if TotalCount <> 0 then
Dialog.Close();
if DeleteExpiredFiles then
Message(DeletedExpiredFilesMsg, ProcessedCount - FailedCount, FailedCount, DeleteCount)
else
Message(ProcessedMsg, ProcessedCount - FailedCount, FailedCount);
end;
end;
}
}

requestpage
{
SaveValues = true;

layout
{
area(Content)
{
group(General)
{
Caption = 'General';
field(SyncDirectionField; SyncDirection)
{
ApplicationArea = Basic, Suite;
Caption = 'Sync Direction';
OptionCaption = 'To External Storage,From External Storage';
ToolTip = 'Specifies whether to sync to external storage, from external storage, or delete expired files.';
Comment thread
StefanSosic marked this conversation as resolved.
Outdated
}
field(DeleteExpiredFilesField; DeleteExpiredFiles)
{
ApplicationArea = Basic, Suite;
Caption = 'Delete Expired Files';
ToolTip = 'Specifies whether to delete expired files from internal storage.';
}
field(MaxRecordsToProcessField; MaxRecordsToProcess)
{
ApplicationArea = Basic, Suite;
Caption = 'Maximum Records to Process';
ToolTip = 'Specifies the maximum number of records to process in one run. Leave 0 for unlimited.';
MinValue = 0;
}
}
}
}
}

var
ExternalStorageProcessor: Codeunit "DA External Storage Processor";
DeleteExpiredFiles: Boolean;
Dialog: Dialog;
DeleteCount, DeleteFailedCount : Integer;
FailedCount: Integer;
MaxRecordsToProcess: Integer;
ProcessedCount: Integer;
TotalCount: Integer;
DeletedExpiredFilesMsg: Label 'Processed %1 attachments successfully. %2 failed.//Deleted %3 expired files.', Comment = '%1 - Number of Processed Attachments, %2 - Number of Failed Attachments, %3 - Number of Deleted Expired Files';
NoRecordsMsg: Label 'No records found to process.';
ProcessedMsg: Label 'Processed %1 attachments successfully. %2 failed.', Comment = '%1 - Number of Processed Attachments, %2 - Number of Failed Attachments';
ProcessingMsg: Label 'Processing #1###### attachments...', Comment = '%1 - Total Number of Attachments';
SyncDirection: Option "To External Storage","From External Storage";

local procedure SetFilters()
begin
case SyncDirection of
SyncDirection::"To External Storage":
begin
DocumentAttachment.SetRange("Uploaded Externally", false);
if DocumentAttachment.FindSet() then begin
repeat
if not DocumentAttachment."Document Reference ID".HasValue() then
DocumentAttachment.Mark(false)
Comment thread
StefanSosic marked this conversation as resolved.
Outdated
else
DocumentAttachment.Mark(true);
until DocumentAttachment.Next() = 0;
DocumentAttachment.MarkedOnly(true);
end;
end;
SyncDirection::"From External Storage":

Comment thread
StefanSosic marked this conversation as resolved.
Outdated
DocumentAttachment.SetRange("Uploaded Externally", true);
end;
end;

local procedure GetDateFormulaFromExternalStorageSetup(): Text
var
ExternalStorageSetup: Record "DA External Storage Setup";
begin
ExternalStorageSetup.Get();
exit(Format(ExternalStorageSetup."Delete After".AsInteger()) + 'D');
end;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// ------------------------------------------------------------------------------------------------
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.
// ------------------------------------------------------------------------------------------------

namespace Microsoft.ExternalStorage.DocumentAttachments;

/// <summary>
/// Defines when attachments should be deleted from internal storage after upload to external storage.
/// </summary>
enum 8750 "DA Ext. Storage - Delete After"
Comment thread
StefanSosic marked this conversation as resolved.
Outdated
{
Extensible = false;

value(0; "Immediately")
{
Caption = 'Immediately';
}
value(1; "1 Day")
{
Caption = '1 Day';
}
value(7; "7 Days")
{
Caption = '7 Days';
}
value(14; "14 Days")
{
Caption = '14 Days';
}
}
Loading