From c8e19e24408f470269cca1415c28e331514002d5 Mon Sep 17 00:00:00 2001
From: Stefan Sosic <59221826+StefanSosic@users.noreply.github.com>
Date: Thu, 21 Aug 2025 02:27:47 +0200
Subject: [PATCH 01/34] Add integration events for file scenario management and
enhance UI actions
---
.../src/Scenario/FileScenario.Codeunit.al | 66 +++++++++++++++++++
.../src/Scenario/FileScenarioImpl.Codeunit.al | 14 +++-
.../src/Scenario/FileScenarioSetup.Page.al | 20 ++++++
3 files changed, 99 insertions(+), 1 deletion(-)
diff --git a/src/System Application/App/External File Storage/src/Scenario/FileScenario.Codeunit.al b/src/System Application/App/External File Storage/src/Scenario/FileScenario.Codeunit.al
index 82200e7283..73eea7b557 100644
--- a/src/System Application/App/External File Storage/src/Scenario/FileScenario.Codeunit.al
+++ b/src/System Application/App/External File Storage/src/Scenario/FileScenario.Codeunit.al
@@ -60,6 +60,39 @@ codeunit 9452 "File Scenario"
FileScenarioImpl.UnassignScenario(Scenario);
end;
+ ///
+ /// Event for additional setup of a file scenario.
+ ///
+ /// The scenario to set up.
+ /// The connector to use.
+ /// Indicates whether the event was handled.
+ procedure GetAdditionalScenarioSetup(Scenario: Integer; Connector: Enum System.ExternalFileStorage."Ext. File Storage Connector"; var IsHandled: Boolean)
+ begin
+ OnScenarioSetupAction(Scenario, Connector, IsHandled);
+ end;
+
+ ///
+ /// Checks whether a file scenario can be deleted.
+ ///
+ /// The scenario to check.
+ /// The connector to use.
+ /// Indicates whether the event was handled.
+ procedure BeforeDeleteFileScenarioCheck(Scenario: Integer; Connector: Enum System.ExternalFileStorage."Ext. File Storage Connector"; var IsHandled: Boolean)
+ begin
+ OnBeforeFileScenarioDelete(Scenario, Connector, IsHandled);
+ end;
+
+ ///
+ /// Checks whether a file scenario can be added or modified.
+ ///
+ /// The scenario to check.
+ /// The connector to use.
+ /// Indicates whether the event was handled.
+ procedure BeforeAddOrModifyFileScenarioCheck(Scenario: Integer; Connector: Enum System.ExternalFileStorage."Ext. File Storage Connector"; var IsHandled: Boolean)
+ begin
+ OnBeforeAddOrModifyFileScenario(Scenario, Connector, IsHandled);
+ end;
+
///
/// Event for changing whether an file scenario should be added to the list of assignable scenarios.
/// If the scenario has already been assigned or is the default scenario, this event won't be published.
@@ -71,6 +104,39 @@ codeunit 9452 "File Scenario"
begin
end;
+ ///
+ /// Event for additional setup of a file scenario.
+ ///
+ /// The scenario to set up.
+ /// The connector to use.
+ /// Indicates whether the event was handled.
+ [IntegrationEvent(false, false, true)]
+ internal procedure OnScenarioSetupAction(Scenario: Integer; Connector: Enum System.ExternalFileStorage."Ext. File Storage Connector"; var IsHandled: Boolean)
+ begin
+ end;
+
+ ///
+ /// Event that is raised before a file scenario is added or modified.
+ ///
+ /// The scenario to add or modify.
+ /// The connector to use.
+ /// Indicates whether the event was handled.
+ [IntegrationEvent(false, false, true)]
+ internal procedure OnBeforeAddOrModifyFileScenario(Scenario: Integer; Connector: Enum System.ExternalFileStorage."Ext. File Storage Connector"; var IsHandled: Boolean)
+ begin
+ end;
+
+ ///
+ /// Event that is raised before a file scenario is deleted.
+ ///
+ /// The scenario to delete.
+ /// The connector to use.
+ /// Indicates whether the event was handled.
+ [IntegrationEvent(false, false, true)]
+ internal procedure OnBeforeFileScenarioDelete(Scenario: Integer; Connector: Enum System.ExternalFileStorage."Ext. File Storage Connector"; var IsHandled: Boolean)
+ begin
+ end;
+
var
FileScenarioImpl: Codeunit "File Scenario Impl.";
}
\ No newline at end of file
diff --git a/src/System Application/App/External File Storage/src/Scenario/FileScenarioImpl.Codeunit.al b/src/System Application/App/External File Storage/src/Scenario/FileScenarioImpl.Codeunit.al
index a4903091fb..9f8d13f49e 100644
--- a/src/System Application/App/External File Storage/src/Scenario/FileScenarioImpl.Codeunit.al
+++ b/src/System Application/App/External File Storage/src/Scenario/FileScenarioImpl.Codeunit.al
@@ -173,7 +173,9 @@ codeunit 9453 "File Scenario Impl."
var
TempSelectedFileAccScenarios: Record "File Account Scenario" temporary;
FileScenario: Record "File Scenario";
+ FileScenarioMgt: Codeunit "File Scenario";
FileScenariosForAccount: Page "File Scenarios for Account";
+ IsHandled: Boolean;
begin
FileAccountImpl.CheckPermissions();
@@ -193,6 +195,11 @@ codeunit 9453 "File Scenario Impl."
exit;
repeat
+ IsHandled := false;
+ FileScenarioMgt.BeforeAddOrModifyFileScenarioCheck(TempSelectedFileAccScenarios.Scenario, TempSelectedFileAccScenarios.Connector, IsHandled);
+ if IsHandled then
+ exit;
+
if not FileScenario.Get(TempSelectedFileAccScenarios.Scenario) then begin
FileScenario."Account Id" := TempFileAccountScenario."Account Id";
FileScenario.Connector := TempFileAccountScenario.Connector;
@@ -289,6 +296,8 @@ codeunit 9453 "File Scenario Impl."
procedure DeleteScenario(var TempFileAccountScenario: Record "File Account Scenario" temporary): Boolean
var
FileScenario: Record "File Scenario";
+ FileScenarioMgt: Codeunit "File Scenario";
+ IsHandled: Boolean;
begin
FileAccountImpl.CheckPermissions();
@@ -301,7 +310,10 @@ codeunit 9453 "File Scenario Impl."
FileScenario.SetRange("Account Id", TempFileAccountScenario."Account Id");
FileScenario.SetRange(Connector, TempFileAccountScenario.Connector);
- FileScenario.DeleteAll();
+ IsHandled := false;
+ FileScenarioMgt.BeforeDeleteFileScenarioCheck(TempFileAccountScenario.Scenario, TempFileAccountScenario.Connector, IsHandled);
+ if not IsHandled then
+ FileScenario.DeleteAll();
end;
until TempFileAccountScenario.Next() = 0;
diff --git a/src/System Application/App/External File Storage/src/Scenario/FileScenarioSetup.Page.al b/src/System Application/App/External File Storage/src/Scenario/FileScenarioSetup.Page.al
index 6470b1f0d0..b7af2a1050 100644
--- a/src/System Application/App/External File Storage/src/Scenario/FileScenarioSetup.Page.al
+++ b/src/System Application/App/External File Storage/src/Scenario/FileScenarioSetup.Page.al
@@ -80,6 +80,25 @@ page 9452 "File Scenario Setup"
group(Scenario)
{
+ action(ScenarioSetup)
+ {
+ Visible = (TypeOfEntry = TypeOfEntry::Scenario) and CanUserManageFileSetup;
+ Caption = 'Additional Scenario Setup';
+ ToolTip = 'Additional scenario setup for the selected scenario.';
+ Image = Setup;
+ Scope = Repeater;
+ trigger OnAction()
+ var
+ FileScenario: Codeunit "File Scenario";
+ NoSetupAvailableMsg: Label 'No additional setup is available for this scenario.';
+ IsHandled: Boolean;
+ begin
+ IsHandled := false;
+ FileScenario.GetAdditionalScenarioSetup(Rec.Scenario, Rec.Connector, IsHandled);
+ if not IsHandled then
+ Message(NoSetupAvailableMsg);
+ end;
+ }
action(ChangeAccount)
{
Visible = (TypeOfEntry = TypeOfEntry::Scenario) and CanUserManageFileSetup;
@@ -123,6 +142,7 @@ page 9452 "File Scenario Setup"
group(Category_Process)
{
actionref(AddScenario_Promoted; AddScenario) { }
+ actionref(ScenarioSetup_Promoted; ScenarioSetup) { }
actionref(ChangeAccount_Promoted; ChangeAccount) { }
actionref(Unassign_Promoted; Unassign) { }
}
From b81f6eba44ce4c1b0780b5bdb8ac57a079082904 Mon Sep 17 00:00:00 2001
From: Stefan Sosic <59221826+StefanSosic@users.noreply.github.com>
Date: Mon, 25 Aug 2025 14:44:42 +0200
Subject: [PATCH 02/34] External Storage - Document Attachments
---
.../app/ExtensionLogo.png | Bin 0 -> 5446 bytes
.../app/README.md | 88 ++++
.../app/app.json | 32 ++
.../DAExternalStorageSync.Report.al | 166 +++++++
.../app/src/DAExtStorageDeleteAfter.Enum.al | 29 ++
.../DAExternalStorageProcessor.Codeunit.al | 414 ++++++++++++++++++
.../app/src/DAExternalStorageSetup.Page.al | 98 +++++
.../app/src/DAExternalStorageSetup.Table.al | 60 +++
.../DAExtStorageFileScenario.EnumExt.al | 16 +
.../DAExternalStorageSubs.Codeunit.al | 210 +++++++++
.../DocumentAttachmentExtStor.TableExt.al | 78 ++++
.../DocumentAttachmentExternal.Page.al | 209 +++++++++
.../DAExtStorAdmin.PermissionSet.al | 20 +
.../DAExtStorView.PermissionSet.al | 19 +
14 files changed, 1439 insertions(+)
create mode 100644 src/Apps/W1/External Storage - Document Attachments/app/ExtensionLogo.png
create mode 100644 src/Apps/W1/External Storage - Document Attachments/app/README.md
create mode 100644 src/Apps/W1/External Storage - Document Attachments/app/app.json
create mode 100644 src/Apps/W1/External Storage - Document Attachments/app/src/AutomaticSync/DAExternalStorageSync.Report.al
create mode 100644 src/Apps/W1/External Storage - Document Attachments/app/src/DAExtStorageDeleteAfter.Enum.al
create mode 100644 src/Apps/W1/External Storage - Document Attachments/app/src/DAExternalStorageProcessor.Codeunit.al
create mode 100644 src/Apps/W1/External Storage - Document Attachments/app/src/DAExternalStorageSetup.Page.al
create mode 100644 src/Apps/W1/External Storage - Document Attachments/app/src/DAExternalStorageSetup.Table.al
create mode 100644 src/Apps/W1/External Storage - Document Attachments/app/src/DocumentAttachmentIntegration/DAExtStorageFileScenario.EnumExt.al
create mode 100644 src/Apps/W1/External Storage - Document Attachments/app/src/DocumentAttachmentIntegration/DAExternalStorageSubs.Codeunit.al
create mode 100644 src/Apps/W1/External Storage - Document Attachments/app/src/DocumentAttachmentIntegration/DocumentAttachmentExtStor.TableExt.al
create mode 100644 src/Apps/W1/External Storage - Document Attachments/app/src/DocumentAttachmentIntegration/DocumentAttachmentExternal.Page.al
create mode 100644 src/Apps/W1/External Storage - Document Attachments/app/src/Permissions/DAExtStorAdmin.PermissionSet.al
create mode 100644 src/Apps/W1/External Storage - Document Attachments/app/src/Permissions/DAExtStorView.PermissionSet.al
diff --git a/src/Apps/W1/External Storage - Document Attachments/app/ExtensionLogo.png b/src/Apps/W1/External Storage - Document Attachments/app/ExtensionLogo.png
new file mode 100644
index 0000000000000000000000000000000000000000..4d2c9a626cb9617350617c40cd73904129d4c108
GIT binary patch
literal 5446
zcma)=S5VVywD$iAMIcgC2u+$uktV%J6$Dhev_NQrfC^Fsq!|cJ=^|Z`P&U*^N-uuwi#_w5i!*aB*89x5SkKKnv*ua91anhEW+omc005Zp+`e`1
zOsW4C1O3^nWxbYuCX9Z!?E(M*a`E2+jm<_J0|5K}om)4pLZ&wJ&3t(K(|ffcq#?ky
z#^aeQO#|lO9vyUeb0ezqQtpipl3Sj#-xy!eh7lu@5+BnW
zNhL-~3Zpw&1u=bMN*Q(sgYksq4dM>Iw7p&Qk_Su~b*PgEs#LK~^K}aDaTG_6Q?_tM<8wOS}`Z+?~Et8GB>T%(k7$9`DL!d5)f!ZoXco-vj+s_QLEs2cf
zKM&F>#c9w|TmM9MFtl8L*cYQgl9khf5CYMR)DJOUf;M~a9|+ys@RYR
zCusNC(CSlUk|r`qdS&ZKh$O=@#&e0>;W~S#|KjHdfLx!-J9r1JtP4RGIhS|Rm0eZ6
z7eOE~Zfo4Li~K^|&)d^-r?8Rh2Q}#ZjL=?VJZ7~hlp4(!U!0K%679I`OR&x54*0&4
znho|hKu)WR)4PUVA1}N;jXHg}AG+gSKQ6O_fEP^Y51!LwBERH09|t!GNx2KH4co>r
zA%cgSHxh2Sezx-w!S5DTG#0zVCbnLM6BP}2P-G{8
zh**wJHj<652FS05bSQNx-0fS7^(wREYvZwpt;$!!k4H0U*iyhS8(syBDMv>L<)~LI
zPl!Y^-cM{_J@{hY1=XJ#T=Ef(FD!I^r1^lca3c0ftVuvo-(%!Zn)C1bK{}-i*Jc);
zIIc+o&iMgvboj&4`@5sF23MV!*zIVmA0>{1;*H*faMAG6EZ7XydTfaGyABAGx>)yl
z@Y+|)SVxCx@!GWqspay7GBetK*s2@CJ?s{8v!(b|ShLb|O;3T1rAMB?DJ?Z`@013q
zoyIvV84eYiS+?kRJOz`3AFcR~ZQ1Uq7wCnbSJ%-HZwhAnJ^4zDp2W8I)~WI7ush5>
z&f3O)rj~2ZGr!c@=p3!n>jG-O#9`$7&WyF7bB}(rq4ldokUp5TY?E62r+YJbJp8Jf
znDW3fYZ^nBQ9O}3?zH_*mZ9+G#HHnwop1Vfm!Df~{Z%D?5KzMN&RA>q8iCzTfAt
zV#TyMeyyh8=M$8tyA|KeUwo_Q6Si)P)%n(W-*QE~08BG|>J!sQPq?IF;;%1ypP?Z`
zK_0Un>p;9=9d675ELHboC0+fNMY&(;k(|=0TS>ka)BKI3q#)zx!Jp@zv0QfeEAjU<
z=vI5@-d^A^-*#|P+b2QFiGxk4z<8Tp4p6{aOp88x>SQEa0M`VxX%IUb$bya!5EgRf6$fFw
zp}jNTKUXjNe0x(;)Nu)Ij5K?QD0u6~mRHQ-!;6m#VP>)}=irAqy;f$e{W-EWnR75~
zm2b0u@r7ASk4x0oTqs9{f&F|eAmD*Gf^A;te7f}J{dXqLaH_4%D_(mnp0VmWhq>^E
z&7>5*-mh>FX{w5SJf^#th&GrpOQk58U-+4
zq3$q~C4ySH7@lr>W+|c0`UF*ieC+3vC1$4m}F(ic|G7}QDt(t
z7`#>$c4U-4LU_;nWHhdN9Fcv~L8h6M_}nW&EGTjgW(=c}uD9>eU^rDOrkNg_effOV
z^8z_y=vNIt{`wOfgG2o^3ey`R!aP1=t7Mz@&MKK3>_BH_QkgNO@4IoQ-2d8EqsDg)
zTMb-5lqlubRot-7!RD@+udO?O9_Da3XV5bvjW
zXTb2psHUdeiIaI(lknQE_<+YlY31}R!VfoM_BuILQ{>Q89=LB5j;V|-yAW2gY82+~
zYlu~#*R(cHw2NO1h5xaiAD2oiIEQ-aQyA-D^y^z2ZHNfM{o(3M#SbqOP3>k9FOdDO
z(t%c9hk)NCPe_8>=Y^U-_-6IwS-D0cE=pwdyLp!;r-fWiXtbUS$<dl!~WV$TR8
zP$KU?K>m?*O)mSGccn&kn|nj7NXFeo<0D=ue8s^~BK#P?J~gB}v5<0nK9GPipjT#9
zkm6yXFyLlgoUIDEVxw*0Z-WDqp8swCs(bcjAqdDLl1oUqYf#a`NjT6IO3?=P`FvUZ
zlWC&lWb9_dexSz%N~-oscM`oC%b#KS|KS7AptwRX5h&1VDCKWzP{&??TFdF3h53&c
zU(v)WhOr)#!V6Y6d7CzOO-@KF%@67>kh34@Exj7Rh}p5_0?yUeyC7@c7DHf+mW=~wpLeLYDA9#W-Ri*S|M@g
zjPHH@qHrPuzq(+5y$V*UoFEg(g$$mRNUEF!C{IN3Rig{tU54W|OD_`M0G3u)B{WhC
z*D?hTF7J+YdF8-Z-Uuw{3jBx`_!aus`uDDBecwuu&tsVpj2~DZJb2-!a2l??m{}er}lR6Lqu)-2+Vm)jr(g{nfQPx9-<^1d;k-d
zkU{E^g7qwp+D`b+QtU5@+swaVKp9<`>sT~U)O!EEMBo!*)~s_<`6Yl
z7fX2;ki>kVDfdietW1k;TYvaY({>?5X)&(d&_y<-J7Qa@b
z(zwGCI=`P#^b>1>2#Y!9T5|AdtaU|zXxw9^KpIu6CAmQf$GzaeOJmYVsc3eh5%6lb
z)t~(Ak2J`;KW_L6psME-h?xF6ryr4d{q;>-b`Q$L43T{r`{N?U6cqP(Q3f%kA8`c@
z<82KXjte|7u_Lo~MV!d%y$tYi(hzU$6t+*ml~Z&Mg{eK?@}^XEBK+-&j`Uv95x)=_
zZLs=Mpg_IuZenjm(~}b8Aggaaje8NX$A_7^G%-)!xtu)C{N|S<3hVOmU;{|i+q6zn
zfr(1Ua*jF!%-dU3L}O2fvWAe%-4kxtXo_vJHF(AxSx)4AI8-$^uBQO_86Z_y%RZX4
zJpu5`pOAztxv?jXv9yx|r>#9!0|`71C-fli@v${6r+V$hgvcr|W_I`{=7*0s(PKQH
zzn8r2+tSeD15stz|DIJ3%X%8EkyN?bsHhuq4(5D0Oewn_)-o)Nx$eNs{0V*ZTSVt4
z3ifXGGw5fBv+9b6d~Nl+08L4VbbZqf3DL^e?l@!uZVdWkdOpJPaE?{zF!ZI?c(vF3
zvX~OK4vktvm&R$MgNpiKA~&zT!1#H7!q1h7AQiuSNG9<=$64)Zym(UQ``(j#^hDzt}{aur0pS?mmBi&z4I0Jfieqh%Pa_A%N?_1OZHm-S{
zQ*)4(N_J;y7tRh0o>xs25-s9!M-)i;@I68#SGXB2XgS}N
zx_r3%V)z1jLA_M&?)E^DT$kzdHMJF%e2w6BH@iI5tKWM+zcuhCsz@N0a_1RBvrdZx
zjzD>V%;c4*$RkEv{zHuVyaB+ANl(iT8w{pJdziC7YcO2&(ciqGLhs@q-dNh!
zkV_V_(_~$*>ND}j1yozMedYnu-_GKMh?IpP<@D+edeB4M%3@xr3oj{@mdFKoBVpm^)1_}Y^}rOWBSB|Uv)*-pTdiU
ztW9~{qq5@iB+$QpbeJVKH^n^9vV})i>Z@2CHoY2$PC888c;#Yz-pHRK@EVheWhE!>
zZzjPmy?0Ni8#=o_k6_s3DY7nS^&Bm}BW&ZfAuF7bQbDgAGM$dE)RM6RvdobKb&MhsYD4exRm9*jcHPjbz#rI?vj$u
zPLF5Gjv|8}?ta9`&^H}Va3H;llghU-BC7pxo6?-eTP`7CUZHJrw{5
zhkDYeIYlhL%brQJ1X#<#fz#E}Z87Kj=Hde*f{l|A`9E
my8jz0{9hgZgN;Rh%;ug!HJ{lE_@04L;EulOt!iDD=>G@$cU!Ii
literal 0
HcmV?d00001
diff --git a/src/Apps/W1/External Storage - Document Attachments/app/README.md b/src/Apps/W1/External Storage - Document Attachments/app/README.md
new file mode 100644
index 0000000000..299699728b
--- /dev/null
+++ b/src/Apps/W1/External Storage - Document Attachments/app/README.md
@@ -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.**
diff --git a/src/Apps/W1/External Storage - Document Attachments/app/app.json b/src/Apps/W1/External Storage - Document Attachments/app/app.json
new file mode 100644
index 0000000000..d4aad84185
--- /dev/null
+++ b/src/Apps/W1/External Storage - Document Attachments/app/app.json
@@ -0,0 +1,32 @@
+{
+ "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",
+ "platform": "27.0.0.0",
+ "internalsVisibleTo": [
+ ],
+ "dependencies": [],
+ "screenshots": [],
+ "idRanges": [
+ {
+ "from": 8750,
+ "to": 8770
+ }
+ ],
+ "resourceExposurePolicy": {
+ "allowDebugging": true,
+ "allowDownloadingSource": true,
+ "includeSourceInSymbolFile": true
+ },
+ "contextSensitiveHelpUrl": "https://go.microsoft.com/fwlink/?linkid=2134520",
+ "target": "Cloud"
+}
\ No newline at end of file
diff --git a/src/Apps/W1/External Storage - Document Attachments/app/src/AutomaticSync/DAExternalStorageSync.Report.al b/src/Apps/W1/External Storage - Document Attachments/app/src/AutomaticSync/DAExternalStorageSync.Report.al
new file mode 100644
index 0000000000..5820b5578e
--- /dev/null
+++ b/src/Apps/W1/External Storage - Document Attachments/app/src/AutomaticSync/DAExternalStorageSync.Report.al
@@ -0,0 +1,166 @@
+// ------------------------------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+// ------------------------------------------------------------------------------------------------
+
+///
+/// Report for synchronizing document attachments between internal and external storage.
+/// Supports bulk upload, download, and cleanup operations.
+///
+report 8752 "DA External Storage Sync"
+{
+ Caption = 'External Storage Synchronization';
+ ProcessingOnly = true;
+ UseRequestPage = true;
+ Extensible = false;
+
+ 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":
+
+ if not ExternalStorageProcessor.DownloadFromExternalStorage(DocumentAttachment) then
+ FailedCount += 1;
+ end;
+ if DeleteExpiredFiles then
+ if CalcDate('<+' + GetDateFormulaFromExternalStorageSetup() + '>', DT2Date(DocumentAttachment."External Upload Date")) >= Today() then
+ if ExternalStorageProcessor.DeleteFromInternalStorage(DocumentAttachment) then
+ DeleteCount += 1
+ else
+ DeleteFailedCount += 1;
+
+ 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';
+ ToolTip = 'Select whether to sync to external storage, from external storage, or delete expired files.';
+ }
+ field(DeleteExpiredFiles; DeleteExpiredFiles)
+ {
+ ApplicationArea = Basic, Suite;
+ Caption = 'Delete Expired Files';
+ ToolTip = 'Select whether to delete expired files from internal storage.';
+ }
+ field(MaxRecordsToProcessField; MaxRecordsToProcess)
+ {
+ ApplicationArea = Basic, Suite;
+ Caption = 'Maximum Records to Process';
+ ToolTip = 'Specify 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)
+ else
+ DocumentAttachment.Mark(true);
+ until DocumentAttachment.Next() = 0;
+ DocumentAttachment.MarkedOnly(true);
+ end;
+ end;
+ SyncDirection::"From External Storage":
+
+ 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;
+}
diff --git a/src/Apps/W1/External Storage - Document Attachments/app/src/DAExtStorageDeleteAfter.Enum.al b/src/Apps/W1/External Storage - Document Attachments/app/src/DAExtStorageDeleteAfter.Enum.al
new file mode 100644
index 0000000000..6782ad0e30
--- /dev/null
+++ b/src/Apps/W1/External Storage - Document Attachments/app/src/DAExtStorageDeleteAfter.Enum.al
@@ -0,0 +1,29 @@
+// ------------------------------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+// ------------------------------------------------------------------------------------------------
+
+///
+/// Defines when attachments should be deleted from internal storage after upload to external storage.
+///
+enum 8750 "DA Ext. Storage - Delete After"
+{
+ 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';
+ }
+}
diff --git a/src/Apps/W1/External Storage - Document Attachments/app/src/DAExternalStorageProcessor.Codeunit.al b/src/Apps/W1/External Storage - Document Attachments/app/src/DAExternalStorageProcessor.Codeunit.al
new file mode 100644
index 0000000000..8dd825499b
--- /dev/null
+++ b/src/Apps/W1/External Storage - Document Attachments/app/src/DAExternalStorageProcessor.Codeunit.al
@@ -0,0 +1,414 @@
+// ------------------------------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+// ------------------------------------------------------------------------------------------------
+
+///
+/// Provides functionality to manage document attachments in external storage systems.
+/// Handles upload, download, and deletion operations for Business Central attachments.
+///
+codeunit 8750 "DA External Storage Processor"
+{
+ Access = Internal;
+ Permissions = tabledata "Tenant Media" = rimd;
+
+ ///
+ /// Uploads a document attachment to external storage.
+ ///
+ /// The document attachment record to upload.
+ /// True if upload was successful, false otherwise.
+ internal procedure UploadToExternalStorage(var DocumentAttachment: Record "Document Attachment"): Boolean
+ var
+ FileAccount: Record "File Account";
+ TenantMedia: Record "Tenant Media";
+ ExternalFileStorage: Codeunit "External File Storage";
+ FileScenarioCU: Codeunit "File Scenario";
+ TempBlob: Codeunit "Temp Blob";
+ FileScenario: Enum "File Scenario";
+ InStream: InStream;
+ OutStream: OutStream;
+ FileName: Text;
+ begin
+ // Validate input parameters
+ if not DocumentAttachment."Document Reference ID".HasValue() then
+ exit(false);
+
+ // Check if document is already uploaded
+ if DocumentAttachment."External File Path" <> '' then
+ exit(false);
+
+ // Get file content from document attachment
+ TempBlob.CreateOutStream(OutStream);
+ DocumentAttachment.ExportToStream(OutStream);
+ TempBlob.CreateInStream(InStream);
+
+ // Generate unique filename to prevent collisions
+ FileName := DocumentAttachment."File Name" + '-' + Format(CreateGuid()) + '.' + DocumentAttachment."File Extension";
+
+ // Search for External Storage assigned File Scenario
+ FileScenario := FileScenario::"Doc. Attach. - External Storage";
+ if not FileScenarioCU.GetFileAccount(FileScenario, FileAccount) then
+ exit(false);
+
+ // Create the file with connector using the File Account framework
+ ExternalFileStorage.Initialize(FileScenario);
+ if ExternalFileStorage.CreateFile(FileName, InStream) then begin
+ DocumentAttachment.MarkAsUploadedToExternal(FileName);
+ exit(true);
+ end;
+
+ exit(false);
+ end;
+
+ ///
+ /// Downloads a document attachment from external storage and prompts user to save it locally.
+ ///
+ /// The document attachment record to download.
+ /// True if download was successful, false otherwise.
+ internal procedure DownloadFromExternalStorage(var DocumentAttachment: Record "Document Attachment"): Boolean
+ var
+ FileAccount: Record "File Account";
+ ExternalFileStorage: Codeunit "External File Storage";
+ FileScenarioCU: Codeunit "File Scenario";
+ FileScenario: Enum "File Scenario";
+ InStream: InStream;
+ ExternalFilePath, FileName : Text;
+ begin
+ // Validate input parameters
+ if DocumentAttachment."External File Path" = '' then
+ exit(false);
+
+ if not DocumentAttachment."Uploaded Externally" then
+ exit(false);
+
+ // Use the stored external file path
+ ExternalFilePath := DocumentAttachment."External File Path";
+ FileName := DocumentAttachment."File Name" + '.' + DocumentAttachment."File Extension";
+
+ // Search for External Storage assigned File Scenario
+ FileScenario := FileScenario::"Doc. Attach. - External Storage";
+ if not FileScenarioCU.GetFileAccount(FileScenario, FileAccount) then
+ exit(false);
+
+ // Get the file with connector using the File Account framework
+ ExternalFileStorage.Initialize(FileScenario);
+ ExternalFileStorage.GetFile(ExternalFilePath, InStream);
+
+ exit(DownloadFromStream(InStream, '', '', '', FileName));
+ end;
+
+ ///
+ /// Downloads a document attachment from external storage and saves it to internal storage.
+ ///
+ /// The document attachment record to download and restore internally.
+ /// True if download and import was successful, false otherwise.
+ internal procedure DownloadFromExternalStorageToInternal(var DocumentAttachment: Record "Document Attachment"): Boolean
+ var
+ FileAccount: Record "File Account";
+ ExternalFileStorage: Codeunit "External File Storage";
+ FileScenarioCU: Codeunit "File Scenario";
+ TempBlob: Codeunit "Temp Blob";
+ FileScenario: Enum "File Scenario";
+ InStream: InStream;
+ OutStream: OutStream;
+ ExternalFilePath, FileName : Text;
+ begin
+ // Validate input parameters
+ if DocumentAttachment."External File Path" = '' then
+ exit(false);
+
+ if not DocumentAttachment."Uploaded Externally" then
+ exit(false);
+
+ // Use the stored external file path
+ ExternalFilePath := DocumentAttachment."External File Path";
+ FileName := DocumentAttachment."File Name" + '.' + DocumentAttachment."File Extension";
+
+ // Search for External Storage assigned File Scenario
+ FileScenario := FileScenario::"Doc. Attach. - External Storage";
+ if not FileScenarioCU.GetFileAccount(FileScenario, FileAccount) then
+ exit(false);
+
+ // Get the file with connector using the File Account framework
+ ExternalFileStorage.Initialize(FileScenario);
+ ExternalFileStorage.GetFile(ExternalFilePath, InStream);
+
+ // Import the file into the Document Attachment
+ DocumentAttachment.ImportAttachment(InStream, FileName);
+ DocumentAttachment."Deleted Internally" := false;
+ DocumentAttachment.Modify();
+
+ exit(true);
+ end;
+
+ ///
+ /// Downloads and previews a document attachment from external storage.
+ ///
+ /// The document attachment record to preview.
+ /// True if preview was successful, false otherwise.
+ internal procedure DownloadFromExternalStorageAndPreview(var DocumentAttachment: Record "Document Attachment"): Boolean
+ var
+ FileAccount: Record "File Account";
+ ExternalFileStorage: Codeunit "External File Storage";
+ FileScenarioCU: Codeunit "File Scenario";
+ FileScenario: Enum "File Scenario";
+ InStream: InStream;
+ ExternalFilePath, FileName : Text;
+ begin
+ // Validate input parameters
+ if DocumentAttachment."External File Path" = '' then
+ exit(false);
+
+ if not DocumentAttachment."Uploaded Externally" then
+ exit(false);
+
+ // Use the stored external file path
+ ExternalFilePath := DocumentAttachment."External File Path";
+ FileName := DocumentAttachment."File Name" + '.' + DocumentAttachment."File Extension";
+
+ // Search for External Storage assigned File Scenario
+ FileScenario := FileScenario::"Doc. Attach. - External Storage";
+ if not FileScenarioCU.GetFileAccount(FileScenario, FileAccount) then
+ exit(false);
+
+ // Get the file with connector using the File Account framework
+ ExternalFileStorage.Initialize(FileScenario);
+ ExternalFileStorage.GetFile(ExternalFilePath, InStream);
+
+ // Preview the file
+ File.ViewFromStream(InStream, FileName, true);
+ exit(true);
+ end;
+
+ ///
+ /// Downloads a document attachment from external storage to a stream.
+ ///
+ /// The path of the external file to download.
+ /// The output stream to write the attachment to.
+ /// True if the download was successful, false otherwise.
+ internal procedure DownloadFromExternalStorageToStream(ExternalFilePath: Text; var AttachmentOutStream: OutStream): Boolean
+ var
+ FileAccount: Record "File Account";
+ ExternalFileStorage: Codeunit "External File Storage";
+ FileScenarioCU: Codeunit "File Scenario";
+ FileScenario: Enum "File Scenario";
+ InStream: InStream;
+ begin
+ // Search for External Storage assigned File Scenario
+ FileScenario := FileScenario::"Doc. Attach. - External Storage";
+ if not FileScenarioCU.GetFileAccount(FileScenario, FileAccount) then
+ exit(false);
+
+ // Get the file from external storage
+ ExternalFileStorage.Initialize(FileScenario);
+ if not ExternalFileStorage.GetFile(ExternalFilePath, InStream) then
+ exit(false);
+
+ // Copy to output stream
+ CopyStream(AttachmentOutStream, InStream);
+ exit(true);
+ end;
+
+ ///
+ /// Downloads a document attachment from external storage to a temporary blob.
+ ///
+ /// The path of the external file to download.
+ /// The temporary blob to store the downloaded content.
+ internal procedure DownloadFromExternalStorageToTempBlob(ExternalFilePath: Text; var TempBlob: Codeunit "Temp Blob"): Boolean
+ var
+ FileAccount: Record "File Account";
+ ExternalFileStorage: Codeunit "External File Storage";
+ FileScenarioCU: Codeunit "File Scenario";
+ FileScenario: Enum "File Scenario";
+ InStream: InStream;
+ OutStream: OutStream;
+ begin
+ // Search for External Storage assigned File Scenario
+ FileScenario := FileScenario::"Doc. Attach. - External Storage";
+ if not FileScenarioCU.GetFileAccount(FileScenario, FileAccount) then
+ exit(false);
+
+ // Get the file from external storage
+ ExternalFileStorage.Initialize(FileScenario);
+ if not ExternalFileStorage.GetFile(ExternalFilePath, InStream) then
+ exit(false);
+
+ // Copy to TempBlob
+ TempBlob.CreateOutStream(OutStream);
+ CopyStream(OutStream, InStream);
+ exit(true);
+ end;
+
+ ///
+ /// Checks if a file exists in external storage.
+ ///
+ /// The path of the external file to check.
+ /// True if the file exists, false otherwise.
+ internal procedure CheckIfFileExistInExternalStorage(ExternalFilePath: Text): Boolean
+ var
+ FileAccount: Record "File Account";
+ ExternalFileStorage: Codeunit "External File Storage";
+ FileScenarioCU: Codeunit "File Scenario";
+ FileScenario: Enum "File Scenario";
+ InStream: InStream;
+ begin
+ // Search for External Storage assigned File Scenario
+ FileScenario := FileScenario::"Doc. Attach. - External Storage";
+ if not FileScenarioCU.GetFileAccount(FileScenario, FileAccount) then
+ exit(false);
+
+ // Get the file from external storage
+ ExternalFileStorage.Initialize(FileScenario);
+ exit(ExternalFileStorage.FileExists(ExternalFilePath));
+ end;
+
+ ///
+ /// Deletes a document attachment from external storage.
+ ///
+ /// The document attachment record to delete from external storage.
+ /// True if deletion was successful, false otherwise.
+ internal procedure DeleteFromExternalStorage(var DocumentAttachment: Record "Document Attachment"): Boolean
+ var
+ FileAccount: Record "File Account";
+ ExternalFileStorage: Codeunit "External File Storage";
+ FileScenarioCU: Codeunit "File Scenario";
+ FileScenario: Enum "File Scenario";
+ ExternalFilePath: Text;
+ begin
+ // Validate input parameters
+ if DocumentAttachment."External File Path" = '' then
+ exit(false);
+
+ if not DocumentAttachment."Uploaded Externally" then
+ exit(false);
+
+ // Use the stored external file path
+ ExternalFilePath := DocumentAttachment."External File Path";
+
+ // Search for External Storage assigned File Scenario
+ FileScenario := FileScenario::"Doc. Attach. - External Storage";
+ if not FileScenarioCU.GetFileAccount(FileScenario, FileAccount) then
+ exit(false);
+
+ // Delete the file with connector using the File Account framework
+ ExternalFileStorage.Initialize(FileScenario);
+ if ExternalFileStorage.DeleteFile(ExternalFilePath) then begin
+ DocumentAttachment.MarkAsNotUploadedToExternal();
+ exit(true);
+ end;
+
+ exit(false);
+ end;
+
+ ///
+ /// Deletes a document attachment from internal storage.
+ ///
+ /// The document attachment record to delete from internal storage.
+ /// True if deletion was successful, false otherwise.
+ internal procedure DeleteFromInternalStorage(var DocumentAttachment: Record "Document Attachment"): Boolean
+ var
+ TenantMedia: Record "Tenant Media";
+ begin
+ // Validate input parameters
+ if not DocumentAttachment."Document Reference ID".HasValue() then
+ exit(false);
+
+ // Check if file is uploaded externally before deleting internally
+ if not DocumentAttachment."Uploaded Externally" then
+ exit(false);
+
+ // Delete from Tenant Media
+ if TenantMedia.Get(DocumentAttachment."Document Reference ID".MediaId) then begin
+ TenantMedia.Delete();
+
+ // Mark Document Attachment as Deleted Internally
+ DocumentAttachment.MarkAsDeletedInternally();
+ exit(true);
+ end;
+
+ exit(false);
+ end;
+
+ ///
+ /// Determines if files should be deleted immediately based on external storage setup.
+ ///
+ /// True if files should be deleted immediately, false otherwise.
+ internal procedure ShouldBeDeleted(): Boolean
+ var
+ ExternalStorageSetup: Record "DA External Storage Setup";
+ begin
+ if not ExternalStorageSetup.Get() then
+ exit(false);
+
+ exit(ExternalStorageSetup."Delete After" = ExternalStorageSetup."Delete After"::Immediately);
+ end;
+
+ ///
+ /// Maps file extensions to their corresponding MIME types.
+ ///
+ /// The document attachment record.
+ /// The content type to set based on the file extension.
+ internal procedure FileExtensionToContentMimeType(var Rec: Record "Document Attachment"; var ContentType: Text[100])
+ begin
+ // Determine content type based on file extension
+ case LowerCase(Rec."File Extension") of
+ 'pdf':
+ ContentType := 'application/pdf';
+ 'jpg', 'jpeg':
+ ContentType := 'image/jpeg';
+ 'png':
+ ContentType := 'image/png';
+ 'gif':
+ ContentType := 'image/gif';
+ 'bmp':
+ ContentType := 'image/bmp';
+ 'tiff', 'tif':
+ ContentType := 'image/tiff';
+ 'doc':
+ ContentType := 'application/msword';
+ 'docx':
+ ContentType := 'application/vnd.openxmlformats-officedocument.wordprocessingml.document';
+ 'xls':
+ ContentType := 'application/vnd.ms-excel';
+ 'xlsx':
+ ContentType := 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
+ 'ppt':
+ ContentType := 'application/vnd.ms-powerpoint';
+ 'pptx':
+ ContentType := 'application/vnd.openxmlformats-officedocument.presentationml.presentation';
+ 'txt':
+ ContentType := 'text/plain';
+ 'xml':
+ ContentType := 'text/xml';
+ 'html', 'htm':
+ ContentType := 'text/html';
+ 'zip':
+ ContentType := 'application/zip';
+ 'rar':
+ ContentType := 'application/x-rar-compressed';
+ else
+ ContentType := 'application/octet-stream';
+ end;
+ end;
+
+ ///
+ /// Checks if a Document Attachment file is uploaded to external storage and deleted internally.
+ ///
+ /// The Document Attachment record to check.
+ /// True if the file is uploaded and deleted, false otherwise.
+ procedure IsFileUploadedToExternalStorageAndDeletedInternally(var DocumentAttachment: Record "Document Attachment"): Boolean
+ begin
+ if not DocumentAttachment."Deleted Internally" then
+ exit(false);
+
+ if not DocumentAttachment."Uploaded Externally" then
+ exit(false);
+
+ if DocumentAttachment."Document Reference ID".HasValue() then
+ exit(false);
+
+ if DocumentAttachment."External File Path" = '' then
+ exit(false);
+ exit(true);
+ end;
+}
diff --git a/src/Apps/W1/External Storage - Document Attachments/app/src/DAExternalStorageSetup.Page.al b/src/Apps/W1/External Storage - Document Attachments/app/src/DAExternalStorageSetup.Page.al
new file mode 100644
index 0000000000..8ccbdf0ab8
--- /dev/null
+++ b/src/Apps/W1/External Storage - Document Attachments/app/src/DAExternalStorageSetup.Page.al
@@ -0,0 +1,98 @@
+// ------------------------------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+// ------------------------------------------------------------------------------------------------
+
+///
+/// Setup page for External Storage functionality.
+/// Allows configuration of automatic upload and deletion policies.
+///
+page 8750 "DA External Storage Setup"
+{
+ PageType = Card;
+ SourceTable = "DA External Storage Setup";
+ Caption = 'External Storage Setup';
+ UsageCategory = None;
+ ApplicationArea = Basic, Suite;
+ InsertAllowed = false;
+ DeleteAllowed = false;
+
+ layout
+ {
+ area(content)
+ {
+ group(General)
+ {
+ Caption = 'General';
+ field("Delete After"; Rec."Delete After")
+ {
+ ApplicationArea = Basic, Suite;
+ }
+ field("Auto Upload"; Rec."Auto Upload")
+ {
+ ApplicationArea = Basic, Suite;
+ }
+ field("Auto Delete"; Rec."Auto Delete")
+ {
+ ApplicationArea = Basic, Suite;
+ }
+ }
+
+ group(Status)
+ {
+ Caption = 'Status';
+
+ field("Has Uploaded Files"; Rec."Has Uploaded Files")
+ {
+ ApplicationArea = Basic, Suite;
+ }
+ }
+ }
+ }
+ actions
+ {
+ area(Processing)
+ {
+ action(RunExternalStorageSync)
+ {
+ ApplicationArea = Basic, Suite;
+ Caption = 'Run External Storage Sync';
+ Image = Process;
+ ToolTip = 'Run the external storage synchronization with options to sync to or from external storage.';
+
+ trigger OnAction()
+ begin
+ Report.Run(Report::"DA External Storage Sync");
+ end;
+ }
+ }
+ area(Navigation)
+ {
+ action(OpenDocumentAttachmentsExternal)
+ {
+ ApplicationArea = Basic, Suite;
+ Caption = 'Open Document Attachments - External Storage List';
+ Image = Document;
+ ToolTip = 'Open the document attachment list with information about the external storage.';
+ RunObject = page "Document Attachment - External";
+ }
+ }
+ area(Promoted)
+ {
+ actionref(RunExternalStorageSync_Promoted; RunExternalStorageSync)
+ {
+ }
+ actionref(OpenDocumentAttachmentsExternal_Promoted; OpenDocumentAttachmentsExternal)
+ {
+ }
+ }
+ }
+
+ trigger OnOpenPage()
+ begin
+ if not Rec.Get() then begin
+ Rec.Init();
+ Rec.Insert();
+ end;
+ end;
+}
diff --git a/src/Apps/W1/External Storage - Document Attachments/app/src/DAExternalStorageSetup.Table.al b/src/Apps/W1/External Storage - Document Attachments/app/src/DAExternalStorageSetup.Table.al
new file mode 100644
index 0000000000..4891151243
--- /dev/null
+++ b/src/Apps/W1/External Storage - Document Attachments/app/src/DAExternalStorageSetup.Table.al
@@ -0,0 +1,60 @@
+// ------------------------------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+// ------------------------------------------------------------------------------------------------
+
+///
+/// Setup table for External Storage functionality.
+/// Contains configuration settings for automatic upload and deletion policies.
+///
+table 8750 "DA External Storage Setup"
+{
+ Caption = 'External Storage Setup';
+ DataClassification = CustomerContent;
+ Access = Internal;
+
+ fields
+ {
+ field(1; "Primary Key"; Code[10])
+ {
+ Caption = 'Primary Key';
+ DataClassification = SystemMetadata;
+ }
+ field(5; "Delete After"; Enum "DA Ext. Storage - Delete After")
+ {
+ Caption = 'Delete After';
+ DataClassification = CustomerContent;
+ ToolTip = 'Specifies when files should be automatically deleted.';
+ }
+ field(6; "Auto Upload"; Boolean)
+ {
+ Caption = 'Auto Upload';
+ DataClassification = CustomerContent;
+ InitValue = true;
+ ToolTip = 'Specifies if new attachments should be automatically uploaded to external storage.';
+ }
+ field(7; "Auto Delete"; Boolean)
+ {
+ Caption = 'Auto Delete';
+ DataClassification = CustomerContent;
+ InitValue = false;
+ ToolTip = 'Specifies if files should be automatically deleted from external storage.';
+ }
+ field(25; "Has Uploaded Files"; Boolean)
+ {
+ Caption = 'Has Uploaded Files';
+ FieldClass = FlowField;
+ CalcFormula = exist("Document Attachment" where("Uploaded Externally" = const(true)));
+ Editable = false;
+ ToolTip = 'Indicates if files have been uploaded using this configuration.';
+ }
+ }
+
+ keys
+ {
+ key(PK; "Primary Key")
+ {
+ Clustered = true;
+ }
+ }
+}
diff --git a/src/Apps/W1/External Storage - Document Attachments/app/src/DocumentAttachmentIntegration/DAExtStorageFileScenario.EnumExt.al b/src/Apps/W1/External Storage - Document Attachments/app/src/DocumentAttachmentIntegration/DAExtStorageFileScenario.EnumExt.al
new file mode 100644
index 0000000000..d19ba169be
--- /dev/null
+++ b/src/Apps/W1/External Storage - Document Attachments/app/src/DocumentAttachmentIntegration/DAExtStorageFileScenario.EnumExt.al
@@ -0,0 +1,16 @@
+// ------------------------------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+// ------------------------------------------------------------------------------------------------
+
+///
+/// Extends File Scenario enum with External Storage option.
+/// Allows File Account framework to recognize external storage scenarios.
+///
+enumextension 8750 "DA Ext. Storage-File Scenario" extends "File Scenario"
+{
+ value(8750; "Doc. Attach. - External Storage")
+ {
+ Caption = 'Document Attachments - External Storage';
+ }
+}
\ No newline at end of file
diff --git a/src/Apps/W1/External Storage - Document Attachments/app/src/DocumentAttachmentIntegration/DAExternalStorageSubs.Codeunit.al b/src/Apps/W1/External Storage - Document Attachments/app/src/DocumentAttachmentIntegration/DAExternalStorageSubs.Codeunit.al
new file mode 100644
index 0000000000..3cac0ec45d
--- /dev/null
+++ b/src/Apps/W1/External Storage - Document Attachments/app/src/DocumentAttachmentIntegration/DAExternalStorageSubs.Codeunit.al
@@ -0,0 +1,210 @@
+// ------------------------------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+// ------------------------------------------------------------------------------------------------
+
+///
+/// Event subscribers for External Storage functionality.
+/// Handles automatic upload of new attachments and cleanup operations.
+///
+codeunit 8752 "DA External Storage Subs."
+{
+ Access = Internal;
+
+ #region Document Attachment Handling
+ ///
+ /// Handles automatic upload of new document attachments to external storage.
+ /// Triggers on insert of Document Attachment records.
+ ///
+ [EventSubscriber(ObjectType::Table, Database::"Document Attachment", 'OnAfterInsertEvent', '', true, true)]
+ local procedure OnAfterInsertDocumentAttachment(var Rec: Record "Document Attachment"; RunTrigger: Boolean)
+ var
+ ExternalStorageSetup: Record "DA External Storage Setup";
+ ExternalStorageProcessor: Codeunit "DA External Storage Processor";
+ begin
+ // Exit early if trigger is not running
+ if not RunTrigger then
+ exit;
+
+ // Check if auto upload is enabled
+ if not ExternalStorageSetup.Get() then
+ exit;
+
+ if not ExternalStorageSetup."Auto Upload" then
+ exit;
+
+ // Only process files with actual content
+ if not Rec."Document Reference ID".HasValue then
+ exit;
+
+ // Upload to external storage
+ if not ExternalStorageProcessor.UploadToExternalStorage(Rec) then
+ exit;
+
+ // Check if it should be immediately deleted
+ if ExternalStorageProcessor.ShouldBeDeleted() then
+ ExternalStorageProcessor.DeleteFromInternalStorage(Rec);
+ end;
+
+ ///
+ /// Handles cleanup of external storage when document attachments are deleted.
+ /// Triggers on delete of Document Attachment records.
+ ///
+ [EventSubscriber(ObjectType::Table, Database::"Document Attachment", 'OnAfterDeleteEvent', '', true, true)]
+ local procedure OnAfterDeleteDocumentAttachment(var Rec: Record "Document Attachment"; RunTrigger: Boolean)
+ var
+ ExternalStorageSetup: Record "DA External Storage Setup";
+ ExternalStorageProcessor: Codeunit "DA External Storage Processor";
+ begin
+ // Exit early if trigger is not running
+ if not RunTrigger then
+ exit;
+
+ // Check if auto upload is enabled
+ if not ExternalStorageSetup.Get() then
+ exit;
+
+ if not ExternalStorageSetup."Auto Delete" then
+ exit;
+
+ // Only process files that were uploaded to external storage
+ if not Rec."Uploaded Externally" then
+ exit;
+
+ // Delete from external storage
+ ExternalStorageProcessor.DeleteFromExternalStorage(Rec);
+ end;
+
+ ///
+ /// Handles export to stream for externally stored document attachments.
+ /// Downloads from external storage when internal content is not available.
+ ///
+ [EventSubscriber(ObjectType::Table, Database::"Document Attachment", 'OnBeforeExportToStream', '', false, false)]
+ local procedure DocumentAttachment_OnBeforeExportToStream(var DocumentAttachment: Record "Document Attachment"; var AttachmentOutStream: OutStream; var IsHandled: Boolean)
+ var
+ ExternalStorageProcessor: Codeunit "DA External Storage Processor";
+ begin
+ // Only handle if file is uploaded externally and not available internally
+ if not ExternalStorageProcessor.IsFileUploadedToExternalStorageAndDeletedInternally(DocumentAttachment) then
+ exit;
+
+ ExternalStorageProcessor.DownloadFromExternalStorageToStream(DocumentAttachment."External File Path", AttachmentOutStream);
+ IsHandled := true;
+ end;
+
+ ///
+ /// Handles getting content as TempBlob for externally stored document attachments.
+ /// Downloads from external storage when internal content is not available.
+ ///
+ [EventSubscriber(ObjectType::Table, Database::"Document Attachment", 'OnBeforeGetAsTempBlob', '', false, false)]
+ local procedure DocumentAttachment_OnBeforeGetAsTempBlob(var DocumentAttachment: Record "Document Attachment"; var TempBlob: Codeunit "Temp Blob"; var IsHandled: Boolean)
+ var
+ ExternalStorageProcessor: Codeunit "DA External Storage Processor";
+ begin
+ // Only handle if file is uploaded externally and not available internally
+ if not ExternalStorageProcessor.IsFileUploadedToExternalStorageAndDeletedInternally(DocumentAttachment) then
+ exit;
+
+ ExternalStorageProcessor.DownloadFromExternalStorageToTempBlob(DocumentAttachment."External File Path", TempBlob);
+ IsHandled := true;
+ end;
+
+ ///
+ /// Handles content type determination for externally stored document attachments.
+ /// Uses file extension to determine content type when internal content is not available.
+ ///
+ [EventSubscriber(ObjectType::Table, Database::"Document Attachment", 'OnBeforeGetContentType', '', false, false)]
+ local procedure DocumentAttachment_OnBeforeGetContentType(var Rec: Record "Document Attachment"; var ContentType: Text[100]; var IsHandled: Boolean)
+ var
+ ExternalStorageProcessor: Codeunit "DA External Storage Processor";
+ begin
+ // Only handle if file is uploaded externally and not available internally
+ if not ExternalStorageProcessor.IsFileUploadedToExternalStorageAndDeletedInternally(Rec) then
+ exit;
+
+ ExternalStorageProcessor.FileExtensionToContentMimeType(Rec, ContentType);
+ IsHandled := true;
+ end;
+
+ ///
+ /// Handles content availability check for externally stored document attachments.
+ /// Returns true if file is available externally even when not available internally.
+ ///
+ [EventSubscriber(ObjectType::Table, Database::"Document Attachment", 'OnBeforeHasContent', '', false, false)]
+ local procedure DocumentAttachment_OnBeforeHasContent(var DocumentAttachment: Record "Document Attachment"; var AttachmentIsAvailable: Boolean; var IsHandled: Boolean)
+ var
+ ExternalStorageProcessor: Codeunit "DA External Storage Processor";
+ begin
+ // Only handle if file is uploaded externally and not available internally
+ if not ExternalStorageProcessor.IsFileUploadedToExternalStorageAndDeletedInternally(DocumentAttachment) then
+ exit;
+
+ AttachmentIsAvailable := ExternalStorageProcessor.CheckIfFileExistInExternalStorage(DocumentAttachment."External File Path");
+ IsHandled := true;
+ end;
+ #endregion
+
+ #region File Scenario Handling
+ ///
+ /// Handles the scenario setup action for External Storage file scenario.
+ /// Opens the External Storage Setup page when triggered.
+ ///
+ [EventSubscriber(ObjectType::Codeunit, Codeunit::"File Scenario", OnScenarioSetupAction, '', false, false)]
+ local procedure FileScenario_OnScenarioSetupAction(Scenario: Integer; Connector: Enum "Ext. File Storage Connector"; var IsHandled: Boolean)
+ var
+ ExternalStorageSetup: Page "DA External Storage Setup";
+ begin
+ if not (Scenario = Enum::"File Scenario"::"Doc. Attach. - External Storage".AsInteger()) then
+ exit;
+
+ ExternalStorageSetup.RunModal();
+ IsHandled := true;
+ end;
+
+ ///
+ /// Shows a disclaimer before enabling External Storage file scenario.
+ /// Warns users about the risks and gets confirmation.
+ ///
+ [EventSubscriber(ObjectType::Codeunit, Codeunit::"File Scenario", OnBeforeAddOrModifyFileScenario, '', false, false)]
+ local procedure FileScenario_OnBeforeAddOrModifyFileScenario(Scenario: Integer; Connector: Enum "Ext. File Storage Connector"; var IsHandled: Boolean)
+ var
+ DisclaimerPart1: Label 'You are about to enable External Storage!!!';
+ DisclaimerPart2: Label '\\This feature is provided as-is, and you use it at your own risk.';
+ DisclaimerPart3: Label '\Microsoft is not responsible for any issues or data loss that may occur.';
+ DisclaimerPart4: Label '\\Do you wish to continue?';
+ begin
+ if not (Scenario = Enum::"File Scenario"::"Doc. Attach. - External Storage".AsInteger()) then
+ exit;
+
+ IsHandled := not Dialog.Confirm(
+ DisclaimerPart1 +
+ DisclaimerPart2 +
+ DisclaimerPart3 +
+ DisclaimerPart4);
+ end;
+
+ ///
+ /// Prevents deletion of External Storage file scenario when there are uploaded files.
+ /// Shows an error message and blocks the operation.
+ ///
+ [EventSubscriber(ObjectType::Codeunit, Codeunit::"File Scenario", OnBeforeFileScenarioDelete, '', false, false)]
+ local procedure FileScenario_OnBeforeFileScenarioDelete(Scenario: Integer; Connector: Enum "Ext. File Storage Connector"; var IsHandled: Boolean)
+ var
+ ExternalStorageSetup: Record "DA External Storage Setup";
+ NotPossibleToUnassignScenarioMsg: Label 'External Storage scenario can not be unassigned when there are uploaded files.';
+ begin
+ if not (Scenario = Enum::"File Scenario"::"Doc. Attach. - External Storage".AsInteger()) then
+ exit;
+
+ if not ExternalStorageSetup.Get() then
+ exit;
+
+ ExternalStorageSetup.CalcFields("Has Uploaded Files");
+ if not ExternalStorageSetup."Has Uploaded Files" then
+ exit;
+
+ Message(NotPossibleToUnassignScenarioMsg);
+ IsHandled := true;
+ end;
+ #endregion
+}
diff --git a/src/Apps/W1/External Storage - Document Attachments/app/src/DocumentAttachmentIntegration/DocumentAttachmentExtStor.TableExt.al b/src/Apps/W1/External Storage - Document Attachments/app/src/DocumentAttachmentIntegration/DocumentAttachmentExtStor.TableExt.al
new file mode 100644
index 0000000000..23f47ea5c2
--- /dev/null
+++ b/src/Apps/W1/External Storage - Document Attachments/app/src/DocumentAttachmentIntegration/DocumentAttachmentExtStor.TableExt.al
@@ -0,0 +1,78 @@
+// ------------------------------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+// ------------------------------------------------------------------------------------------------
+
+///
+/// Extends the Document Attachment table with external storage functionality.
+/// Adds fields and procedures to track attachments in external storage systems.
+///
+tableextension 8750 "Document Attachment Ext.Stor." extends "Document Attachment"
+{
+ fields
+ {
+ field(8750; "Uploaded Externally"; Boolean)
+ {
+ Caption = 'Uploaded Externally';
+ DataClassification = CustomerContent;
+ Editable = false;
+ ToolTip = 'Specifies if the file has been uploaded to external storage.';
+ }
+ field(8751; "External Upload Date"; DateTime)
+ {
+ Caption = 'External Upload Date';
+ DataClassification = CustomerContent;
+ Editable = false;
+ ToolTip = 'Specifies when the file was uploaded to external storage.';
+ }
+ field(8752; "External File Path"; Text[2048])
+ {
+ Caption = 'External File Path';
+ DataClassification = CustomerContent;
+ Editable = false;
+ ToolTip = 'Specifies the path to the file in external storage.';
+ }
+ field(8753; "Deleted Internally"; Boolean)
+ {
+ Caption = 'Deleted Internally';
+ DataClassification = CustomerContent;
+ Editable = false;
+ ToolTip = 'Specifies the value of the Deleted Internally field.';
+ }
+ }
+
+ ///
+ /// Marks the document attachment as uploaded to external storage.
+ ///
+ /// The path to the file in external storage.
+ internal procedure MarkAsUploadedToExternal(ExternalFilePath: Text[250])
+ begin
+ "Uploaded Externally" := true;
+ "External Upload Date" := CurrentDateTime();
+ "External File Path" := ExternalFilePath;
+ Modify();
+ end;
+
+ ///
+ /// Marks the document attachment as not uploaded to external storage.
+ /// Clears all external storage related fields.
+ ///
+ internal procedure MarkAsNotUploadedToExternal()
+ begin
+ "Uploaded Externally" := false;
+ "External Upload Date" := 0DT;
+ "External File Path" := '';
+ Modify();
+ end;
+
+ ///
+ /// Marks the document attachment as deleted from internal storage.
+ /// Clears the Document Reference ID and sets the deleted internally flag.
+ ///
+ internal procedure MarkAsDeletedInternally()
+ begin
+ Clear("Document Reference ID");
+ "Deleted Internally" := true;
+ Modify();
+ end;
+}
diff --git a/src/Apps/W1/External Storage - Document Attachments/app/src/DocumentAttachmentIntegration/DocumentAttachmentExternal.Page.al b/src/Apps/W1/External Storage - Document Attachments/app/src/DocumentAttachmentIntegration/DocumentAttachmentExternal.Page.al
new file mode 100644
index 0000000000..1aaa86b158
--- /dev/null
+++ b/src/Apps/W1/External Storage - Document Attachments/app/src/DocumentAttachmentIntegration/DocumentAttachmentExternal.Page.al
@@ -0,0 +1,209 @@
+// ------------------------------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+// ------------------------------------------------------------------------------------------------
+
+///
+/// List page for managing document attachments with external storage information.
+/// Provides actions for upload, download, and deletion operations.
+///
+page 8751 "Document Attachment - External"
+{
+ PageType = List;
+ SourceTable = "Document Attachment";
+ Caption = 'Document Attachments - External Storage';
+ UsageCategory = None;
+ ApplicationArea = Basic, Suite;
+ Editable = false;
+
+ layout
+ {
+ area(Content)
+ {
+ repeater(General)
+ {
+ field("Table ID"; Rec."Table ID")
+ {
+ ApplicationArea = Basic, Suite;
+ ToolTip = 'Specifies the table ID the attachment belongs to.';
+ }
+ field("No."; Rec."No.")
+ {
+ ApplicationArea = Basic, Suite;
+ ToolTip = 'Specifies the record number the attachment belongs to.';
+ }
+ field("File Name"; Rec."File Name")
+ {
+ ApplicationArea = Basic, Suite;
+ ToolTip = 'Specifies the name of the attached file.';
+ }
+ field("File Extension"; Rec."File Extension")
+ {
+ ApplicationArea = Basic, Suite;
+ ToolTip = 'Specifies the file extension of the attached file.';
+ }
+ field("Attached Date"; Rec."Attached Date")
+ {
+ ApplicationArea = Basic, Suite;
+ ToolTip = 'Specifies the date the file was attached.';
+ }
+ field("Attached By"; Rec."Attached By")
+ {
+ ApplicationArea = Basic, Suite;
+ ToolTip = 'Specifies the user who attached the file.';
+ }
+ field("Deleted Internally"; Rec."Deleted Internally")
+ {
+ ApplicationArea = Basic, Suite;
+ }
+ field("Uploaded to External"; Rec."Uploaded Externally")
+ {
+ ApplicationArea = Basic, Suite;
+ Caption = 'Uploaded to External';
+ }
+ field("External Upload Date"; Rec."External Upload Date")
+ {
+ ApplicationArea = Basic, Suite;
+ Caption = 'Upload Date';
+ }
+ field("External File Path"; Rec."External File Path")
+ {
+ ApplicationArea = Basic, Suite;
+ Caption = 'External File Path';
+ }
+ }
+ }
+ }
+
+ actions
+ {
+ area(Processing)
+ {
+ action("Upload to External Storage")
+ {
+ ApplicationArea = Basic, Suite;
+ Caption = 'Upload to External Storage';
+ ToolTip = 'Upload the selected file to external storage.';
+ Image = Export;
+
+ trigger OnAction()
+ var
+ DocumentAttachment: Record "Document Attachment";
+ ExternalStorageProcessor: Codeunit "DA External Storage Processor";
+ begin
+ CurrPage.SetSelectionFilter(DocumentAttachment);
+ if DocumentAttachment.FindSet() then
+ repeat
+ if ExternalStorageProcessor.UploadToExternalStorage(DocumentAttachment) then
+ Message(FileUploadedMsg)
+ else
+ Message(FailedFileUploadMsg);
+ until DocumentAttachment.Next() = 0;
+ end;
+ }
+ action("Download from External Storage")
+ {
+ ApplicationArea = Basic, Suite;
+ Caption = 'Download from External Storage';
+ ToolTip = 'Download the file from external storage.';
+ Image = Import;
+
+ trigger OnAction()
+ var
+ ExternalStorageProcessor: Codeunit "DA External Storage Processor";
+ begin
+ if ExternalStorageProcessor.DownloadFromExternalStorage(Rec) then
+ Message(FileDownloadedMsg)
+ else
+ Message(FailedFileDownloadMsg);
+ end;
+ }
+ action("Download from External To Internal Storage")
+ {
+ ApplicationArea = Basic, Suite;
+ Caption = 'Download from External To Internal Storage';
+ ToolTip = 'Download the file from external storage to internal storage.';
+ Image = Import;
+
+ trigger OnAction()
+ var
+ ExternalStorageProcessor: Codeunit "DA External Storage Processor";
+ begin
+ if ExternalStorageProcessor.DownloadFromExternalStorageToInternal(Rec) then
+ Message(FileDownloadedMsg)
+ else
+ Message(FailedFileDownloadMsg);
+ end;
+ }
+ action("Delete from External Storage")
+ {
+ ApplicationArea = Basic, Suite;
+ Enabled = not (Rec."Deleted Internally") and Rec."Uploaded Externally";
+ Caption = 'Delete from External Storage';
+ ToolTip = 'Delete the file from external storage.';
+ Image = Delete;
+
+ trigger OnAction()
+ var
+ ExternalStorageProcessor: Codeunit "DA External Storage Processor";
+ begin
+ if Confirm(DeleteFileFromExternalStorageQst) then
+ if ExternalStorageProcessor.DeleteFromExternalStorage(Rec) then
+ Message(FileDeletedExternalStorageMsg)
+ else
+ Message(FailedFileDeleteExternalStorageMsg);
+ end;
+ }
+ action("Delete from Internal Storage")
+ {
+ ApplicationArea = Basic, Suite;
+ Enabled = Rec."Uploaded Externally" and not Rec."Deleted Internally";
+ Caption = 'Delete from Internal Storage';
+ ToolTip = 'Delete the file from Internal storage.';
+ Image = Delete;
+
+ trigger OnAction()
+ var
+ ExternalStorageProcessor: Codeunit "DA External Storage Processor";
+ begin
+ if Confirm(DeleteFileFromIntStorageQst) then
+ if ExternalStorageProcessor.DeleteFromInternalStorage(Rec) then
+ Message(FileDeletedIntStorageMsg)
+ else
+ Message(FailedFileDeleteIntStorageMsg);
+ end;
+ }
+ }
+ area(Navigation)
+ {
+ action("External Storage Setup")
+ {
+ ApplicationArea = Basic, Suite;
+ Caption = 'External Storage Setup';
+ ToolTip = 'Configure external storage settings.';
+ Image = Setup;
+ RunObject = page "DA External Storage Setup";
+ }
+ }
+ area(Promoted)
+ {
+ actionref(UploadToExternalStoragePromoted; "Upload to External Storage") { }
+ actionref(DownloadFromExternalStoragePromoted; "Download from External Storage") { }
+ actionref(DownloadFromExternalToInternalStoragePromoted; "Download from External to Internal Storage") { }
+ actionref(DeleteFromExternalStoragePromoted; "Delete from External Storage") { }
+ actionref(DeleteFromInternalStoragePromoted; "Delete from Internal Storage") { }
+ }
+ }
+
+ var
+ DeleteFileFromExternalStorageQst: Label 'Are you sure you want to delete this file from external storage?';
+ DeleteFileFromIntStorageQst: Label 'Are you sure you want to delete this file from Internal storage?';
+ FailedFileDeleteExternalStorageMsg: Label 'Failed to delete file from external storage.';
+ FailedFileDeleteIntStorageMsg: Label 'Failed to delete file from Internal storage.';
+ FailedFileDownloadMsg: Label 'Failed to download file.';
+ FailedFileUploadMsg: Label 'Failed to upload file.';
+ FileDeletedExternalStorageMsg: Label 'File deleted successfully from external storage.';
+ FileDeletedIntStorageMsg: Label 'File deleted successfully from Internal storage.';
+ FileDownloadedMsg: Label 'File downloaded successfully.';
+ FileUploadedMsg: Label 'File uploaded successfully.';
+}
diff --git a/src/Apps/W1/External Storage - Document Attachments/app/src/Permissions/DAExtStorAdmin.PermissionSet.al b/src/Apps/W1/External Storage - Document Attachments/app/src/Permissions/DAExtStorAdmin.PermissionSet.al
new file mode 100644
index 0000000000..f3038cb4fa
--- /dev/null
+++ b/src/Apps/W1/External Storage - Document Attachments/app/src/Permissions/DAExtStorAdmin.PermissionSet.al
@@ -0,0 +1,20 @@
+// ------------------------------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+// ------------------------------------------------------------------------------------------------
+///
+/// Permission set for External Storage functionality.
+/// Grants necessary permissions to use external storage features.
+///
+permissionset 8751 "DA Ext. Stor. Admin"
+{
+ Assignable = true;
+ Caption = 'Document Attachments - External Storage Admin';
+ Permissions = tabledata "DA External Storage Setup" = RIMD,
+ table "DA External Storage Setup" = X,
+ page "DA External Storage Setup" = X,
+ page "Document Attachment - External" = X,
+ report "DA External Storage Sync" = X,
+ codeunit "DA External Storage Processor" = X,
+ codeunit "DA External Storage Subs." = X;
+}
\ No newline at end of file
diff --git a/src/Apps/W1/External Storage - Document Attachments/app/src/Permissions/DAExtStorView.PermissionSet.al b/src/Apps/W1/External Storage - Document Attachments/app/src/Permissions/DAExtStorView.PermissionSet.al
new file mode 100644
index 0000000000..8557740b7c
--- /dev/null
+++ b/src/Apps/W1/External Storage - Document Attachments/app/src/Permissions/DAExtStorView.PermissionSet.al
@@ -0,0 +1,19 @@
+// ------------------------------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+// ------------------------------------------------------------------------------------------------
+///
+/// Permission set for External Storage functionality.
+/// Grants necessary permissions to use external storage features.
+///
+permissionset 8750 "DA Ext. Stor. View"
+{
+ Assignable = true;
+ Caption = 'Document Attachments - External Storage View';
+ Permissions = tabledata "DA External Storage Setup" = R,
+ table "DA External Storage Setup" = X,
+ page "DA External Storage Setup" = X,
+ page "Document Attachment - External" = X,
+ codeunit "DA External Storage Processor" = X,
+ codeunit "DA External Storage Subs." = X;
+}
\ No newline at end of file
From 4adc2b0ed65eab4a36edf2ed7cbfcaa2b5433557 Mon Sep 17 00:00:00 2001
From: Stefan Sosic <59221826+StefanSosic@users.noreply.github.com>
Date: Mon, 25 Aug 2025 16:33:46 +0200
Subject: [PATCH 03/34] Interface + Improvements
---
.../DAExternalStorageSync.Report.al | 26 ++--
.../app/src/DAExtStorageDeleteAfter.Enum.al | 2 +
.../DAExternalStorageProcessor.Codeunit.al | 61 +++-------
.../app/src/DAExternalStorageSetup.Page.al | 24 ++--
.../app/src/DAExternalStorageSetup.Table.al | 9 +-
.../DAExtStorageFileScenario.EnumExt.al | 1 +
.../DAExternalStorageImpl.Codeunit.al | 74 ++++++++++++
.../DAExternalStorageSubs.Codeunit.al | 113 +++++-------------
.../DocumentAttachmentExtStor.TableExt.al | 2 +-
.../DocumentAttachmentExternal.Page.al | 12 +-
.../DAExtStorAdmin.PermissionSet.al | 5 +-
.../DAExtStorView.PermissionSet.al | 2 +-
.../DefaultFileScenarioImpl.Codeunit.al | 36 ++++++
.../src/Scenario/FileScenario.Codeunit.al | 66 ----------
.../src/Scenario/FileScenario.Enum.al | 4 +-
.../src/Scenario/FileScenario.Interface.al | 31 +++++
.../src/Scenario/FileScenarioImpl.Codeunit.al | 20 ++--
.../src/Scenario/FileScenarioSetup.Page.al | 10 +-
src/System Application/App/app.json | 2 +-
19 files changed, 242 insertions(+), 258 deletions(-)
create mode 100644 src/Apps/W1/External Storage - Document Attachments/app/src/DocumentAttachmentIntegration/DAExternalStorageImpl.Codeunit.al
create mode 100644 src/System Application/App/External File Storage/src/Scenario/DefaultFileScenarioImpl.Codeunit.al
create mode 100644 src/System Application/App/External File Storage/src/Scenario/FileScenario.Interface.al
diff --git a/src/Apps/W1/External Storage - Document Attachments/app/src/AutomaticSync/DAExternalStorageSync.Report.al b/src/Apps/W1/External Storage - Document Attachments/app/src/AutomaticSync/DAExternalStorageSync.Report.al
index 5820b5578e..bc343aaa8f 100644
--- a/src/Apps/W1/External Storage - Document Attachments/app/src/AutomaticSync/DAExternalStorageSync.Report.al
+++ b/src/Apps/W1/External Storage - Document Attachments/app/src/AutomaticSync/DAExternalStorageSync.Report.al
@@ -1,3 +1,7 @@
+namespace Microsoft.ExternalStorage.DocumentAttachments;
+
+using Microsoft.Foundation.Attachment;
+
// ------------------------------------------------------------------------------------------------
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.
@@ -13,6 +17,9 @@ report 8752 "DA External Storage Sync"
ProcessingOnly = true;
UseRequestPage = true;
Extensible = false;
+ UsageCategory = None;
+ Permissions = tabledata "DA External Storage Setup" = r,
+ tabledata "Document Attachment" = r;
dataset
{
@@ -24,7 +31,7 @@ report 8752 "DA External Storage Sync"
TotalCount := Count();
if TotalCount = 0 then begin
- if GuiAllowed then
+ if GuiAllowed() then
Message(NoRecordsMsg);
CurrReport.Break();
end;
@@ -38,7 +45,7 @@ report 8752 "DA External Storage Sync"
DeleteCount := 0;
DeleteFailedCount := 0;
- if GuiAllowed then
+ if GuiAllowed() then
Dialog.Open(ProcessingMsg, TotalCount);
end;
@@ -46,7 +53,7 @@ report 8752 "DA External Storage Sync"
begin
ProcessedCount += 1;
- if GuiAllowed then
+ if GuiAllowed() then
Dialog.Update(1, ProcessedCount);
case SyncDirection of
@@ -60,7 +67,7 @@ report 8752 "DA External Storage Sync"
FailedCount += 1;
end;
if DeleteExpiredFiles then
- if CalcDate('<+' + GetDateFormulaFromExternalStorageSetup() + '>', DT2Date(DocumentAttachment."External Upload Date")) >= Today() then
+ if CalcDate('<+' + GetDateFormulaFromExternalStorageSetup() + '>', DocumentAttachment."External Upload Date".Date()) >= Today() then
if ExternalStorageProcessor.DeleteFromInternalStorage(DocumentAttachment) then
DeleteCount += 1
else
@@ -72,7 +79,7 @@ report 8752 "DA External Storage Sync"
trigger OnPostDataItem()
begin
- if GuiAllowed then begin
+ if GuiAllowed() then begin
if TotalCount <> 0 then
Dialog.Close();
if DeleteExpiredFiles then
@@ -99,19 +106,20 @@ report 8752 "DA External Storage Sync"
{
ApplicationArea = Basic, Suite;
Caption = 'Sync Direction';
- ToolTip = 'Select whether to sync to external storage, from external storage, or delete expired files.';
+ OptionCaption = 'To External Storage,From External Storage';
+ ToolTip = 'Specifies whether to sync to external storage, from external storage, or delete expired files.';
}
- field(DeleteExpiredFiles; DeleteExpiredFiles)
+ field(DeleteExpiredFilesField; DeleteExpiredFiles)
{
ApplicationArea = Basic, Suite;
Caption = 'Delete Expired Files';
- ToolTip = 'Select whether to delete expired files from internal storage.';
+ ToolTip = 'Specifies whether to delete expired files from internal storage.';
}
field(MaxRecordsToProcessField; MaxRecordsToProcess)
{
ApplicationArea = Basic, Suite;
Caption = 'Maximum Records to Process';
- ToolTip = 'Specify the maximum number of records to process in one run. Leave 0 for unlimited.';
+ ToolTip = 'Specifies the maximum number of records to process in one run. Leave 0 for unlimited.';
MinValue = 0;
}
}
diff --git a/src/Apps/W1/External Storage - Document Attachments/app/src/DAExtStorageDeleteAfter.Enum.al b/src/Apps/W1/External Storage - Document Attachments/app/src/DAExtStorageDeleteAfter.Enum.al
index 6782ad0e30..38e7326e2a 100644
--- a/src/Apps/W1/External Storage - Document Attachments/app/src/DAExtStorageDeleteAfter.Enum.al
+++ b/src/Apps/W1/External Storage - Document Attachments/app/src/DAExtStorageDeleteAfter.Enum.al
@@ -1,3 +1,5 @@
+namespace Microsoft.ExternalStorage.DocumentAttachments;
+
// ------------------------------------------------------------------------------------------------
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.
diff --git a/src/Apps/W1/External Storage - Document Attachments/app/src/DAExternalStorageProcessor.Codeunit.al b/src/Apps/W1/External Storage - Document Attachments/app/src/DAExternalStorageProcessor.Codeunit.al
index 8dd825499b..0a8c66161e 100644
--- a/src/Apps/W1/External Storage - Document Attachments/app/src/DAExternalStorageProcessor.Codeunit.al
+++ b/src/Apps/W1/External Storage - Document Attachments/app/src/DAExternalStorageProcessor.Codeunit.al
@@ -1,3 +1,10 @@
+namespace Microsoft.ExternalStorage.DocumentAttachments;
+
+using Microsoft.Foundation.Attachment;
+using System.Environment;
+using System.ExternalFileStorage;
+using System.Utilities;
+
// ------------------------------------------------------------------------------------------------
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.
@@ -10,7 +17,9 @@
codeunit 8750 "DA External Storage Processor"
{
Access = Internal;
- Permissions = tabledata "Tenant Media" = rimd;
+ Permissions = tabledata "Tenant Media" = rimd,
+ tabledata "Document Attachment" = rimd,
+ tabledata "DA External Storage Setup" = r;
///
/// Uploads a document attachment to external storage.
@@ -20,14 +29,13 @@ codeunit 8750 "DA External Storage Processor"
internal procedure UploadToExternalStorage(var DocumentAttachment: Record "Document Attachment"): Boolean
var
FileAccount: Record "File Account";
- TenantMedia: Record "Tenant Media";
ExternalFileStorage: Codeunit "External File Storage";
FileScenarioCU: Codeunit "File Scenario";
TempBlob: Codeunit "Temp Blob";
FileScenario: Enum "File Scenario";
InStream: InStream;
OutStream: OutStream;
- FileName: Text;
+ FileName: Text[2048];
begin
// Validate input parameters
if not DocumentAttachment."Document Reference ID".HasValue() then
@@ -107,10 +115,8 @@ codeunit 8750 "DA External Storage Processor"
FileAccount: Record "File Account";
ExternalFileStorage: Codeunit "External File Storage";
FileScenarioCU: Codeunit "File Scenario";
- TempBlob: Codeunit "Temp Blob";
FileScenario: Enum "File Scenario";
InStream: InStream;
- OutStream: OutStream;
ExternalFilePath, FileName : Text;
begin
// Validate input parameters
@@ -141,45 +147,6 @@ codeunit 8750 "DA External Storage Processor"
exit(true);
end;
- ///
- /// Downloads and previews a document attachment from external storage.
- ///
- /// The document attachment record to preview.
- /// True if preview was successful, false otherwise.
- internal procedure DownloadFromExternalStorageAndPreview(var DocumentAttachment: Record "Document Attachment"): Boolean
- var
- FileAccount: Record "File Account";
- ExternalFileStorage: Codeunit "External File Storage";
- FileScenarioCU: Codeunit "File Scenario";
- FileScenario: Enum "File Scenario";
- InStream: InStream;
- ExternalFilePath, FileName : Text;
- begin
- // Validate input parameters
- if DocumentAttachment."External File Path" = '' then
- exit(false);
-
- if not DocumentAttachment."Uploaded Externally" then
- exit(false);
-
- // Use the stored external file path
- ExternalFilePath := DocumentAttachment."External File Path";
- FileName := DocumentAttachment."File Name" + '.' + DocumentAttachment."File Extension";
-
- // Search for External Storage assigned File Scenario
- FileScenario := FileScenario::"Doc. Attach. - External Storage";
- if not FileScenarioCU.GetFileAccount(FileScenario, FileAccount) then
- exit(false);
-
- // Get the file with connector using the File Account framework
- ExternalFileStorage.Initialize(FileScenario);
- ExternalFileStorage.GetFile(ExternalFilePath, InStream);
-
- // Preview the file
- File.ViewFromStream(InStream, FileName, true);
- exit(true);
- end;
-
///
/// Downloads a document attachment from external storage to a stream.
///
@@ -210,10 +177,11 @@ codeunit 8750 "DA External Storage Processor"
end;
///
- /// Downloads a document attachment from external storage to a temporary blob.
+ /// Downloads a document attachment from external storage to a Temp Blob.
///
/// The path of the external file to download.
/// The temporary blob to store the downloaded content.
+ /// True if the download was successful, false otherwise.
internal procedure DownloadFromExternalStorageToTempBlob(ExternalFilePath: Text; var TempBlob: Codeunit "Temp Blob"): Boolean
var
FileAccount: Record "File Account";
@@ -250,7 +218,6 @@ codeunit 8750 "DA External Storage Processor"
ExternalFileStorage: Codeunit "External File Storage";
FileScenarioCU: Codeunit "File Scenario";
FileScenario: Enum "File Scenario";
- InStream: InStream;
begin
// Search for External Storage assigned File Scenario
FileScenario := FileScenario::"Doc. Attach. - External Storage";
@@ -318,7 +285,7 @@ codeunit 8750 "DA External Storage Processor"
exit(false);
// Delete from Tenant Media
- if TenantMedia.Get(DocumentAttachment."Document Reference ID".MediaId) then begin
+ if TenantMedia.Get(DocumentAttachment."Document Reference ID".MediaId()) then begin
TenantMedia.Delete();
// Mark Document Attachment as Deleted Internally
diff --git a/src/Apps/W1/External Storage - Document Attachments/app/src/DAExternalStorageSetup.Page.al b/src/Apps/W1/External Storage - Document Attachments/app/src/DAExternalStorageSetup.Page.al
index 8ccbdf0ab8..e58bc3e7a8 100644
--- a/src/Apps/W1/External Storage - Document Attachments/app/src/DAExternalStorageSetup.Page.al
+++ b/src/Apps/W1/External Storage - Document Attachments/app/src/DAExternalStorageSetup.Page.al
@@ -1,3 +1,5 @@
+namespace Microsoft.ExternalStorage.DocumentAttachments;
+
// ------------------------------------------------------------------------------------------------
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.
@@ -19,33 +21,21 @@ page 8750 "DA External Storage Setup"
layout
{
- area(content)
+ area(Content)
{
group(General)
{
Caption = 'General';
- field("Delete After"; Rec."Delete After")
- {
- ApplicationArea = Basic, Suite;
- }
- field("Auto Upload"; Rec."Auto Upload")
- {
- ApplicationArea = Basic, Suite;
- }
- field("Auto Delete"; Rec."Auto Delete")
- {
- ApplicationArea = Basic, Suite;
- }
+ field("Delete After"; Rec."Delete After") { }
+ field("Auto Upload"; Rec."Auto Upload") { }
+ field("Auto Delete"; Rec."Auto Delete") { }
}
group(Status)
{
Caption = 'Status';
- field("Has Uploaded Files"; Rec."Has Uploaded Files")
- {
- ApplicationArea = Basic, Suite;
- }
+ field("Has Uploaded Files"; Rec."Has Uploaded Files") { }
}
}
}
diff --git a/src/Apps/W1/External Storage - Document Attachments/app/src/DAExternalStorageSetup.Table.al b/src/Apps/W1/External Storage - Document Attachments/app/src/DAExternalStorageSetup.Table.al
index 4891151243..dfd0015fd3 100644
--- a/src/Apps/W1/External Storage - Document Attachments/app/src/DAExternalStorageSetup.Table.al
+++ b/src/Apps/W1/External Storage - Document Attachments/app/src/DAExternalStorageSetup.Table.al
@@ -1,3 +1,7 @@
+namespace Microsoft.ExternalStorage.DocumentAttachments;
+
+using Microsoft.Foundation.Attachment;
+
// ------------------------------------------------------------------------------------------------
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.
@@ -23,20 +27,17 @@ table 8750 "DA External Storage Setup"
field(5; "Delete After"; Enum "DA Ext. Storage - Delete After")
{
Caption = 'Delete After';
- DataClassification = CustomerContent;
ToolTip = 'Specifies when files should be automatically deleted.';
}
field(6; "Auto Upload"; Boolean)
{
Caption = 'Auto Upload';
- DataClassification = CustomerContent;
InitValue = true;
ToolTip = 'Specifies if new attachments should be automatically uploaded to external storage.';
}
field(7; "Auto Delete"; Boolean)
{
Caption = 'Auto Delete';
- DataClassification = CustomerContent;
InitValue = false;
ToolTip = 'Specifies if files should be automatically deleted from external storage.';
}
@@ -46,7 +47,7 @@ table 8750 "DA External Storage Setup"
FieldClass = FlowField;
CalcFormula = exist("Document Attachment" where("Uploaded Externally" = const(true)));
Editable = false;
- ToolTip = 'Indicates if files have been uploaded using this configuration.';
+ ToolTip = 'Specifies if files have been uploaded using this configuration.';
}
}
diff --git a/src/Apps/W1/External Storage - Document Attachments/app/src/DocumentAttachmentIntegration/DAExtStorageFileScenario.EnumExt.al b/src/Apps/W1/External Storage - Document Attachments/app/src/DocumentAttachmentIntegration/DAExtStorageFileScenario.EnumExt.al
index d19ba169be..1f20bb73a6 100644
--- a/src/Apps/W1/External Storage - Document Attachments/app/src/DocumentAttachmentIntegration/DAExtStorageFileScenario.EnumExt.al
+++ b/src/Apps/W1/External Storage - Document Attachments/app/src/DocumentAttachmentIntegration/DAExtStorageFileScenario.EnumExt.al
@@ -12,5 +12,6 @@ enumextension 8750 "DA Ext. Storage-File Scenario" extends "File Scenario"
value(8750; "Doc. Attach. - External Storage")
{
Caption = 'Document Attachments - External Storage';
+ Implementation = "File Scenario" = "DA External Storage Impl.";
}
}
\ No newline at end of file
diff --git a/src/Apps/W1/External Storage - Document Attachments/app/src/DocumentAttachmentIntegration/DAExternalStorageImpl.Codeunit.al b/src/Apps/W1/External Storage - Document Attachments/app/src/DocumentAttachmentIntegration/DAExternalStorageImpl.Codeunit.al
new file mode 100644
index 0000000000..04ee1624a9
--- /dev/null
+++ b/src/Apps/W1/External Storage - Document Attachments/app/src/DocumentAttachmentIntegration/DAExternalStorageImpl.Codeunit.al
@@ -0,0 +1,74 @@
+// ------------------------------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+// ------------------------------------------------------------------------------------------------
+
+codeunit 8751 "DA External Storage Impl." implements "File Scenario"
+{
+ Permissions = tabledata "DA External Storage Setup" = r;
+
+ ///
+ /// Called before adding or modifying a file scenario.
+ ///
+ /// The ID of the file scenario.
+ /// The file storage connector.
+ /// True if the operation is allowed, otherwise false.
+ procedure BeforeAddOrModifyFileScenarioCheck(Scenario: Integer; Connector: Enum System.ExternalFileStorage."Ext. File Storage Connector") SkipInsertOrModify: Boolean
+ var
+ ConfirmManagement: Codeunit "Confirm Management";
+ DisclaimerPart1Lbl: Label 'You are about to enable External Storage!!!';
+ DisclaimerPart2Lbl: Label '\\This feature is provided as-is, and you use it at your own risk.';
+ DisclaimerPart3Lbl: Label '\Microsoft is not responsible for any issues or data loss that may occur.';
+ DisclaimerPart4Lbl: Label '\\Do you wish to continue?';
+ begin
+ if not (Scenario = Enum::"File Scenario"::"Doc. Attach. - External Storage".AsInteger()) then
+ exit;
+
+ SkipInsertOrModify := not ConfirmManagement.GetResponseOrDefault(DisclaimerPart1Lbl +
+ DisclaimerPart2Lbl +
+ DisclaimerPart3Lbl +
+ DisclaimerPart4Lbl);
+ end;
+
+ ///
+ /// Called to get additional setup for a file scenario.
+ ///
+ /// The ID of the file scenario.
+ /// The file storage connector.
+ /// True if additional setup is available, otherwise false.
+ procedure GetAdditionalScenarioSetup(Scenario: Integer; Connector: Enum System.ExternalFileStorage."Ext. File Storage Connector") SetupExist: Boolean
+ var
+ ExternalStorageSetup: Page "DA External Storage Setup";
+ begin
+ if not (Scenario = Enum::"File Scenario"::"Doc. Attach. - External Storage".AsInteger()) then
+ exit;
+
+ ExternalStorageSetup.RunModal();
+ SetupExist := true;
+ end;
+
+ ///
+ /// Called before deleting a file scenario.
+ ///
+ /// The ID of the file scenario.
+ /// The file storage connector.
+ /// True if the delete operation is handled and should not proceed, otherwise false.
+ procedure BeforeDeleteFileScenarioCheck(Scenario: Integer; Connector: Enum System.ExternalFileStorage."Ext. File Storage Connector") SkipDelete: Boolean
+ var
+ ExternalStorageSetup: Record "DA External Storage Setup";
+ NotPossibleToUnassignScenarioMsg: Label 'External Storage scenario can not be unassigned when there are uploaded files.';
+ begin
+ if not (Scenario = Enum::"File Scenario"::"Doc. Attach. - External Storage".AsInteger()) then
+ exit;
+
+ if not ExternalStorageSetup.Get() then
+ exit;
+
+ ExternalStorageSetup.CalcFields("Has Uploaded Files");
+ if not ExternalStorageSetup."Has Uploaded Files" then
+ exit;
+
+ SkipDelete := true;
+ Message(NotPossibleToUnassignScenarioMsg);
+ end;
+}
\ No newline at end of file
diff --git a/src/Apps/W1/External Storage - Document Attachments/app/src/DocumentAttachmentIntegration/DAExternalStorageSubs.Codeunit.al b/src/Apps/W1/External Storage - Document Attachments/app/src/DocumentAttachmentIntegration/DAExternalStorageSubs.Codeunit.al
index 3cac0ec45d..a81bd03bbb 100644
--- a/src/Apps/W1/External Storage - Document Attachments/app/src/DocumentAttachmentIntegration/DAExternalStorageSubs.Codeunit.al
+++ b/src/Apps/W1/External Storage - Document Attachments/app/src/DocumentAttachmentIntegration/DAExternalStorageSubs.Codeunit.al
@@ -10,13 +10,15 @@
codeunit 8752 "DA External Storage Subs."
{
Access = Internal;
+ Permissions = tabledata "DA External Storage Setup" = r;
#region Document Attachment Handling
///
- /// Handles automatic upload of new document attachments to external storage.
- /// Triggers on insert of Document Attachment records.
+ /// Handles automatic upload of new document attachments to external storage upon insertion of the attachment record.
///
- [EventSubscriber(ObjectType::Table, Database::"Document Attachment", 'OnAfterInsertEvent', '', true, true)]
+ /// The document attachment record.
+ /// Indicates if the trigger should run.
+ [EventSubscriber(ObjectType::Table, Database::"Document Attachment", OnAfterInsertEvent, '', true, true)]
local procedure OnAfterInsertDocumentAttachment(var Rec: Record "Document Attachment"; RunTrigger: Boolean)
var
ExternalStorageSetup: Record "DA External Storage Setup";
@@ -34,7 +36,7 @@ codeunit 8752 "DA External Storage Subs."
exit;
// Only process files with actual content
- if not Rec."Document Reference ID".HasValue then
+ if not Rec."Document Reference ID".HasValue() then
exit;
// Upload to external storage
@@ -47,10 +49,11 @@ codeunit 8752 "DA External Storage Subs."
end;
///
- /// Handles cleanup of external storage when document attachments are deleted.
- /// Triggers on delete of Document Attachment records.
+ /// Handles automatic deletion of document attachments from external storage upon deletion of the attachment record.
///
- [EventSubscriber(ObjectType::Table, Database::"Document Attachment", 'OnAfterDeleteEvent', '', true, true)]
+ /// The document attachment record.
+ /// Indicates if the trigger should run.
+ [EventSubscriber(ObjectType::Table, Database::"Document Attachment", OnAfterDeleteEvent, '', true, true)]
local procedure OnAfterDeleteDocumentAttachment(var Rec: Record "Document Attachment"; RunTrigger: Boolean)
var
ExternalStorageSetup: Record "DA External Storage Setup";
@@ -76,10 +79,12 @@ codeunit 8752 "DA External Storage Subs."
end;
///
- /// Handles export to stream for externally stored document attachments.
- /// Downloads from external storage when internal content is not available.
+ /// Handles exporting document attachment content to a stream for externally stored attachments.
///
- [EventSubscriber(ObjectType::Table, Database::"Document Attachment", 'OnBeforeExportToStream', '', false, false)]
+ /// The document attachment record.
+ /// The output stream for the attachment content.
+ /// Indicates if the event has been handled.
+ [EventSubscriber(ObjectType::Table, Database::"Document Attachment", OnBeforeExportToStream, '', false, false)]
local procedure DocumentAttachment_OnBeforeExportToStream(var DocumentAttachment: Record "Document Attachment"; var AttachmentOutStream: OutStream; var IsHandled: Boolean)
var
ExternalStorageProcessor: Codeunit "DA External Storage Processor";
@@ -93,10 +98,12 @@ codeunit 8752 "DA External Storage Subs."
end;
///
- /// Handles getting content as TempBlob for externally stored document attachments.
- /// Downloads from external storage when internal content is not available.
+ /// Handles getting the temporary blob for externally stored document attachments.
///
- [EventSubscriber(ObjectType::Table, Database::"Document Attachment", 'OnBeforeGetAsTempBlob', '', false, false)]
+ /// The document attachment record.
+ /// The temporary blob to be filled.
+ /// Indicates if the event has been handled.
+ [EventSubscriber(ObjectType::Table, Database::"Document Attachment", OnBeforeGetAsTempBlob, '', false, false)]
local procedure DocumentAttachment_OnBeforeGetAsTempBlob(var DocumentAttachment: Record "Document Attachment"; var TempBlob: Codeunit "Temp Blob"; var IsHandled: Boolean)
var
ExternalStorageProcessor: Codeunit "DA External Storage Processor";
@@ -110,10 +117,12 @@ codeunit 8752 "DA External Storage Subs."
end;
///
- /// Handles content type determination for externally stored document attachments.
- /// Uses file extension to determine content type when internal content is not available.
+ /// Handles getting content type for externally stored document attachments.
///
- [EventSubscriber(ObjectType::Table, Database::"Document Attachment", 'OnBeforeGetContentType', '', false, false)]
+ /// The document attachment record.
+ /// The content type to be set.
+ /// Indicates if the event has been handled.
+ [EventSubscriber(ObjectType::Table, Database::"Document Attachment", OnBeforeGetContentType, '', false, false)]
local procedure DocumentAttachment_OnBeforeGetContentType(var Rec: Record "Document Attachment"; var ContentType: Text[100]; var IsHandled: Boolean)
var
ExternalStorageProcessor: Codeunit "DA External Storage Processor";
@@ -127,10 +136,12 @@ codeunit 8752 "DA External Storage Subs."
end;
///
- /// Handles content availability check for externally stored document attachments.
- /// Returns true if file is available externally even when not available internally.
+ /// Handles checking if attachment content is available for externally stored document attachments.
///
- [EventSubscriber(ObjectType::Table, Database::"Document Attachment", 'OnBeforeHasContent', '', false, false)]
+ /// The document attachment record.
+ /// Indicates if the attachment is available.
+ /// Indicates if the event has been handled.
+ [EventSubscriber(ObjectType::Table, Database::"Document Attachment", OnBeforeHasContent, '', false, false)]
local procedure DocumentAttachment_OnBeforeHasContent(var DocumentAttachment: Record "Document Attachment"; var AttachmentIsAvailable: Boolean; var IsHandled: Boolean)
var
ExternalStorageProcessor: Codeunit "DA External Storage Processor";
@@ -143,68 +154,4 @@ codeunit 8752 "DA External Storage Subs."
IsHandled := true;
end;
#endregion
-
- #region File Scenario Handling
- ///
- /// Handles the scenario setup action for External Storage file scenario.
- /// Opens the External Storage Setup page when triggered.
- ///
- [EventSubscriber(ObjectType::Codeunit, Codeunit::"File Scenario", OnScenarioSetupAction, '', false, false)]
- local procedure FileScenario_OnScenarioSetupAction(Scenario: Integer; Connector: Enum "Ext. File Storage Connector"; var IsHandled: Boolean)
- var
- ExternalStorageSetup: Page "DA External Storage Setup";
- begin
- if not (Scenario = Enum::"File Scenario"::"Doc. Attach. - External Storage".AsInteger()) then
- exit;
-
- ExternalStorageSetup.RunModal();
- IsHandled := true;
- end;
-
- ///
- /// Shows a disclaimer before enabling External Storage file scenario.
- /// Warns users about the risks and gets confirmation.
- ///
- [EventSubscriber(ObjectType::Codeunit, Codeunit::"File Scenario", OnBeforeAddOrModifyFileScenario, '', false, false)]
- local procedure FileScenario_OnBeforeAddOrModifyFileScenario(Scenario: Integer; Connector: Enum "Ext. File Storage Connector"; var IsHandled: Boolean)
- var
- DisclaimerPart1: Label 'You are about to enable External Storage!!!';
- DisclaimerPart2: Label '\\This feature is provided as-is, and you use it at your own risk.';
- DisclaimerPart3: Label '\Microsoft is not responsible for any issues or data loss that may occur.';
- DisclaimerPart4: Label '\\Do you wish to continue?';
- begin
- if not (Scenario = Enum::"File Scenario"::"Doc. Attach. - External Storage".AsInteger()) then
- exit;
-
- IsHandled := not Dialog.Confirm(
- DisclaimerPart1 +
- DisclaimerPart2 +
- DisclaimerPart3 +
- DisclaimerPart4);
- end;
-
- ///
- /// Prevents deletion of External Storage file scenario when there are uploaded files.
- /// Shows an error message and blocks the operation.
- ///
- [EventSubscriber(ObjectType::Codeunit, Codeunit::"File Scenario", OnBeforeFileScenarioDelete, '', false, false)]
- local procedure FileScenario_OnBeforeFileScenarioDelete(Scenario: Integer; Connector: Enum "Ext. File Storage Connector"; var IsHandled: Boolean)
- var
- ExternalStorageSetup: Record "DA External Storage Setup";
- NotPossibleToUnassignScenarioMsg: Label 'External Storage scenario can not be unassigned when there are uploaded files.';
- begin
- if not (Scenario = Enum::"File Scenario"::"Doc. Attach. - External Storage".AsInteger()) then
- exit;
-
- if not ExternalStorageSetup.Get() then
- exit;
-
- ExternalStorageSetup.CalcFields("Has Uploaded Files");
- if not ExternalStorageSetup."Has Uploaded Files" then
- exit;
-
- Message(NotPossibleToUnassignScenarioMsg);
- IsHandled := true;
- end;
- #endregion
}
diff --git a/src/Apps/W1/External Storage - Document Attachments/app/src/DocumentAttachmentIntegration/DocumentAttachmentExtStor.TableExt.al b/src/Apps/W1/External Storage - Document Attachments/app/src/DocumentAttachmentIntegration/DocumentAttachmentExtStor.TableExt.al
index 23f47ea5c2..fc4268d440 100644
--- a/src/Apps/W1/External Storage - Document Attachments/app/src/DocumentAttachmentIntegration/DocumentAttachmentExtStor.TableExt.al
+++ b/src/Apps/W1/External Storage - Document Attachments/app/src/DocumentAttachmentIntegration/DocumentAttachmentExtStor.TableExt.al
@@ -45,7 +45,7 @@ tableextension 8750 "Document Attachment Ext.Stor." extends "Document Attachment
/// Marks the document attachment as uploaded to external storage.
///
/// The path to the file in external storage.
- internal procedure MarkAsUploadedToExternal(ExternalFilePath: Text[250])
+ internal procedure MarkAsUploadedToExternal(ExternalFilePath: Text[2048])
begin
"Uploaded Externally" := true;
"External Upload Date" := CurrentDateTime();
diff --git a/src/Apps/W1/External Storage - Document Attachments/app/src/DocumentAttachmentIntegration/DocumentAttachmentExternal.Page.al b/src/Apps/W1/External Storage - Document Attachments/app/src/DocumentAttachmentIntegration/DocumentAttachmentExternal.Page.al
index 1aaa86b158..e8933d9ab3 100644
--- a/src/Apps/W1/External Storage - Document Attachments/app/src/DocumentAttachmentIntegration/DocumentAttachmentExternal.Page.al
+++ b/src/Apps/W1/External Storage - Document Attachments/app/src/DocumentAttachmentIntegration/DocumentAttachmentExternal.Page.al
@@ -24,51 +24,41 @@ page 8751 "Document Attachment - External"
{
field("Table ID"; Rec."Table ID")
{
- ApplicationArea = Basic, Suite;
ToolTip = 'Specifies the table ID the attachment belongs to.';
}
field("No."; Rec."No.")
{
- ApplicationArea = Basic, Suite;
ToolTip = 'Specifies the record number the attachment belongs to.';
}
field("File Name"; Rec."File Name")
{
- ApplicationArea = Basic, Suite;
ToolTip = 'Specifies the name of the attached file.';
}
field("File Extension"; Rec."File Extension")
{
- ApplicationArea = Basic, Suite;
ToolTip = 'Specifies the file extension of the attached file.';
}
field("Attached Date"; Rec."Attached Date")
{
- ApplicationArea = Basic, Suite;
ToolTip = 'Specifies the date the file was attached.';
}
field("Attached By"; Rec."Attached By")
{
- ApplicationArea = Basic, Suite;
ToolTip = 'Specifies the user who attached the file.';
}
field("Deleted Internally"; Rec."Deleted Internally")
{
- ApplicationArea = Basic, Suite;
}
field("Uploaded to External"; Rec."Uploaded Externally")
{
- ApplicationArea = Basic, Suite;
Caption = 'Uploaded to External';
}
field("External Upload Date"; Rec."External Upload Date")
{
- ApplicationArea = Basic, Suite;
Caption = 'Upload Date';
}
field("External File Path"; Rec."External File Path")
{
- ApplicationArea = Basic, Suite;
Caption = 'External File Path';
}
}
@@ -189,7 +179,7 @@ page 8751 "Document Attachment - External"
{
actionref(UploadToExternalStoragePromoted; "Upload to External Storage") { }
actionref(DownloadFromExternalStoragePromoted; "Download from External Storage") { }
- actionref(DownloadFromExternalToInternalStoragePromoted; "Download from External to Internal Storage") { }
+ actionref(DownloadFromExternalToInternalStoragePromoted; "Download from External To Internal Storage") { }
actionref(DeleteFromExternalStoragePromoted; "Delete from External Storage") { }
actionref(DeleteFromInternalStoragePromoted; "Delete from Internal Storage") { }
}
diff --git a/src/Apps/W1/External Storage - Document Attachments/app/src/Permissions/DAExtStorAdmin.PermissionSet.al b/src/Apps/W1/External Storage - Document Attachments/app/src/Permissions/DAExtStorAdmin.PermissionSet.al
index f3038cb4fa..c6bb844035 100644
--- a/src/Apps/W1/External Storage - Document Attachments/app/src/Permissions/DAExtStorAdmin.PermissionSet.al
+++ b/src/Apps/W1/External Storage - Document Attachments/app/src/Permissions/DAExtStorAdmin.PermissionSet.al
@@ -9,12 +9,13 @@
permissionset 8751 "DA Ext. Stor. Admin"
{
Assignable = true;
- Caption = 'Document Attachments - External Storage Admin';
+ Caption = 'DA - External Storage Admin';
Permissions = tabledata "DA External Storage Setup" = RIMD,
table "DA External Storage Setup" = X,
page "DA External Storage Setup" = X,
page "Document Attachment - External" = X,
report "DA External Storage Sync" = X,
codeunit "DA External Storage Processor" = X,
- codeunit "DA External Storage Subs." = X;
+ codeunit "DA External Storage Subs." = X,
+ codeunit "DA External Storage Impl." = X;
}
\ No newline at end of file
diff --git a/src/Apps/W1/External Storage - Document Attachments/app/src/Permissions/DAExtStorView.PermissionSet.al b/src/Apps/W1/External Storage - Document Attachments/app/src/Permissions/DAExtStorView.PermissionSet.al
index 8557740b7c..3a183bbcb1 100644
--- a/src/Apps/W1/External Storage - Document Attachments/app/src/Permissions/DAExtStorView.PermissionSet.al
+++ b/src/Apps/W1/External Storage - Document Attachments/app/src/Permissions/DAExtStorView.PermissionSet.al
@@ -9,7 +9,7 @@
permissionset 8750 "DA Ext. Stor. View"
{
Assignable = true;
- Caption = 'Document Attachments - External Storage View';
+ Caption = 'DA - External Storage View';
Permissions = tabledata "DA External Storage Setup" = R,
table "DA External Storage Setup" = X,
page "DA External Storage Setup" = X,
diff --git a/src/System Application/App/External File Storage/src/Scenario/DefaultFileScenarioImpl.Codeunit.al b/src/System Application/App/External File Storage/src/Scenario/DefaultFileScenarioImpl.Codeunit.al
new file mode 100644
index 0000000000..17ab45c0e6
--- /dev/null
+++ b/src/System Application/App/External File Storage/src/Scenario/DefaultFileScenarioImpl.Codeunit.al
@@ -0,0 +1,36 @@
+codeunit 9459 "Default File Scenario Impl." implements "File Scenario"
+{
+
+ ///
+ /// Called before adding or modifying a file scenario.
+ ///
+ /// The ID of the file scenario.
+ /// The file storage connector.
+ /// True if the operation is allowed, otherwise false.
+ procedure BeforeAddOrModifyFileScenarioCheck(Scenario: Integer; Connector: Enum System.ExternalFileStorage."Ext. File Storage Connector") SkipInsertOrModify: Boolean
+ begin
+ SkipInsertOrModify := false;
+ end;
+
+ ///
+ /// Called to get additional setup for a file scenario.
+ ///
+ /// The ID of the file scenario.
+ /// The file storage connector.
+ /// True if additional setup is available, otherwise false.
+ procedure GetAdditionalScenarioSetup(Scenario: Integer; Connector: Enum System.ExternalFileStorage."Ext. File Storage Connector") SetupExist: Boolean
+ begin
+ SetupExist := false;
+ end;
+
+ ///
+ /// Called before deleting a file scenario.
+ ///
+ /// The ID of the file scenario.
+ /// The file storage connector.
+ /// True if the delete operation is handled and should not proceed, otherwise false.
+ procedure BeforeDeleteFileScenarioCheck(Scenario: Integer; Connector: Enum System.ExternalFileStorage."Ext. File Storage Connector") SkipDelete: Boolean
+ begin
+ SkipDelete := false;
+ end;
+}
\ No newline at end of file
diff --git a/src/System Application/App/External File Storage/src/Scenario/FileScenario.Codeunit.al b/src/System Application/App/External File Storage/src/Scenario/FileScenario.Codeunit.al
index 73eea7b557..82200e7283 100644
--- a/src/System Application/App/External File Storage/src/Scenario/FileScenario.Codeunit.al
+++ b/src/System Application/App/External File Storage/src/Scenario/FileScenario.Codeunit.al
@@ -60,39 +60,6 @@ codeunit 9452 "File Scenario"
FileScenarioImpl.UnassignScenario(Scenario);
end;
- ///
- /// Event for additional setup of a file scenario.
- ///
- /// The scenario to set up.
- /// The connector to use.
- /// Indicates whether the event was handled.
- procedure GetAdditionalScenarioSetup(Scenario: Integer; Connector: Enum System.ExternalFileStorage."Ext. File Storage Connector"; var IsHandled: Boolean)
- begin
- OnScenarioSetupAction(Scenario, Connector, IsHandled);
- end;
-
- ///
- /// Checks whether a file scenario can be deleted.
- ///
- /// The scenario to check.
- /// The connector to use.
- /// Indicates whether the event was handled.
- procedure BeforeDeleteFileScenarioCheck(Scenario: Integer; Connector: Enum System.ExternalFileStorage."Ext. File Storage Connector"; var IsHandled: Boolean)
- begin
- OnBeforeFileScenarioDelete(Scenario, Connector, IsHandled);
- end;
-
- ///
- /// Checks whether a file scenario can be added or modified.
- ///
- /// The scenario to check.
- /// The connector to use.
- /// Indicates whether the event was handled.
- procedure BeforeAddOrModifyFileScenarioCheck(Scenario: Integer; Connector: Enum System.ExternalFileStorage."Ext. File Storage Connector"; var IsHandled: Boolean)
- begin
- OnBeforeAddOrModifyFileScenario(Scenario, Connector, IsHandled);
- end;
-
///
/// Event for changing whether an file scenario should be added to the list of assignable scenarios.
/// If the scenario has already been assigned or is the default scenario, this event won't be published.
@@ -104,39 +71,6 @@ codeunit 9452 "File Scenario"
begin
end;
- ///
- /// Event for additional setup of a file scenario.
- ///
- /// The scenario to set up.
- /// The connector to use.
- /// Indicates whether the event was handled.
- [IntegrationEvent(false, false, true)]
- internal procedure OnScenarioSetupAction(Scenario: Integer; Connector: Enum System.ExternalFileStorage."Ext. File Storage Connector"; var IsHandled: Boolean)
- begin
- end;
-
- ///
- /// Event that is raised before a file scenario is added or modified.
- ///
- /// The scenario to add or modify.
- /// The connector to use.
- /// Indicates whether the event was handled.
- [IntegrationEvent(false, false, true)]
- internal procedure OnBeforeAddOrModifyFileScenario(Scenario: Integer; Connector: Enum System.ExternalFileStorage."Ext. File Storage Connector"; var IsHandled: Boolean)
- begin
- end;
-
- ///
- /// Event that is raised before a file scenario is deleted.
- ///
- /// The scenario to delete.
- /// The connector to use.
- /// Indicates whether the event was handled.
- [IntegrationEvent(false, false, true)]
- internal procedure OnBeforeFileScenarioDelete(Scenario: Integer; Connector: Enum System.ExternalFileStorage."Ext. File Storage Connector"; var IsHandled: Boolean)
- begin
- end;
-
var
FileScenarioImpl: Codeunit "File Scenario Impl.";
}
\ No newline at end of file
diff --git a/src/System Application/App/External File Storage/src/Scenario/FileScenario.Enum.al b/src/System Application/App/External File Storage/src/Scenario/FileScenario.Enum.al
index 381e03b4ad..80266851dd 100644
--- a/src/System Application/App/External File Storage/src/Scenario/FileScenario.Enum.al
+++ b/src/System Application/App/External File Storage/src/Scenario/FileScenario.Enum.al
@@ -9,9 +9,10 @@ namespace System.ExternalFileStorage;
/// File scenarios.
/// Used to decouple file accounts from sending files.
///
-enum 9451 "File Scenario"
+enum 9451 "File Scenario" implements "File Scenario"
{
Extensible = true;
+ DefaultImplementation = "File Scenario" = "Default File Scenario Impl.";
///
/// The default file scenario.
@@ -20,5 +21,6 @@ enum 9451 "File Scenario"
value(0; Default)
{
Caption = 'Default';
+ Implementation = "File Scenario" = "Default File Scenario Impl.";
}
}
\ No newline at end of file
diff --git a/src/System Application/App/External File Storage/src/Scenario/FileScenario.Interface.al b/src/System Application/App/External File Storage/src/Scenario/FileScenario.Interface.al
new file mode 100644
index 0000000000..1af8601dcd
--- /dev/null
+++ b/src/System Application/App/External File Storage/src/Scenario/FileScenario.Interface.al
@@ -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.
+// ------------------------------------------------------------------------------------------------
+
+interface "File Scenario"
+{
+ ///
+ /// Called before adding or modifying a file scenario.
+ ///
+ /// The ID of the file scenario.
+ /// The file storage connector.
+ /// True if the operation is allowed; otherwise false.
+ procedure BeforeAddOrModifyFileScenarioCheck(Scenario: Integer; Connector: Enum System.ExternalFileStorage."Ext. File Storage Connector") SkipInsertOrModify: Boolean;
+
+ ///
+ /// Called to get additional setup for a file scenario.
+ ///
+ /// The ID of the file scenario.
+ /// The file storage connector.
+ /// True if additional setup is available, otherwise false.
+ procedure GetAdditionalScenarioSetup(Scenario: Integer; Connector: Enum System.ExternalFileStorage."Ext. File Storage Connector") SetupExist: Boolean;
+
+ ///
+ /// Called before deleting a file scenario.
+ ///
+ /// The ID of the file scenario.
+ /// The file storage connector.
+ /// True if the delete operation is handled and should not proceed; otherwise false.
+ procedure BeforeDeleteFileScenarioCheck(Scenario: Integer; Connector: Enum System.ExternalFileStorage."Ext. File Storage Connector") SkipDelete: Boolean;
+}
diff --git a/src/System Application/App/External File Storage/src/Scenario/FileScenarioImpl.Codeunit.al b/src/System Application/App/External File Storage/src/Scenario/FileScenarioImpl.Codeunit.al
index 9f8d13f49e..8955f647b9 100644
--- a/src/System Application/App/External File Storage/src/Scenario/FileScenarioImpl.Codeunit.al
+++ b/src/System Application/App/External File Storage/src/Scenario/FileScenarioImpl.Codeunit.al
@@ -173,9 +173,9 @@ codeunit 9453 "File Scenario Impl."
var
TempSelectedFileAccScenarios: Record "File Account Scenario" temporary;
FileScenario: Record "File Scenario";
- FileScenarioMgt: Codeunit "File Scenario";
FileScenariosForAccount: Page "File Scenarios for Account";
- IsHandled: Boolean;
+ FileScenarioInterface: Interface "File Scenario";
+ FileScenarioEnum: Enum "File Scenario";
begin
FileAccountImpl.CheckPermissions();
@@ -195,9 +195,9 @@ codeunit 9453 "File Scenario Impl."
exit;
repeat
- IsHandled := false;
- FileScenarioMgt.BeforeAddOrModifyFileScenarioCheck(TempSelectedFileAccScenarios.Scenario, TempSelectedFileAccScenarios.Connector, IsHandled);
- if IsHandled then
+ FileScenarioEnum := Enum::"File Scenario".FromInteger(TempSelectedFileAccScenarios.Scenario);
+ FileScenarioInterface := FileScenarioEnum;
+ if FileScenarioInterface.BeforeAddOrModifyFileScenarioCheck(TempSelectedFileAccScenarios.Scenario, TempSelectedFileAccScenarios.Connector) then
exit;
if not FileScenario.Get(TempSelectedFileAccScenarios.Scenario) then begin
@@ -296,8 +296,8 @@ codeunit 9453 "File Scenario Impl."
procedure DeleteScenario(var TempFileAccountScenario: Record "File Account Scenario" temporary): Boolean
var
FileScenario: Record "File Scenario";
- FileScenarioMgt: Codeunit "File Scenario";
- IsHandled: Boolean;
+ FileScenarioInterface: Interface "File Scenario";
+ FileScenarioEnum: Enum "File Scenario";
begin
FileAccountImpl.CheckPermissions();
@@ -310,9 +310,9 @@ codeunit 9453 "File Scenario Impl."
FileScenario.SetRange("Account Id", TempFileAccountScenario."Account Id");
FileScenario.SetRange(Connector, TempFileAccountScenario.Connector);
- IsHandled := false;
- FileScenarioMgt.BeforeDeleteFileScenarioCheck(TempFileAccountScenario.Scenario, TempFileAccountScenario.Connector, IsHandled);
- if not IsHandled then
+ FileScenarioEnum := Enum::"File Scenario".FromInteger(TempFileAccountScenario.Scenario);
+ FileScenarioInterface := FileScenarioEnum;
+ if not FileScenarioInterface.BeforeDeleteFileScenarioCheck(TempFileAccountScenario.Scenario, TempFileAccountScenario.Connector) then
FileScenario.DeleteAll();
end;
until TempFileAccountScenario.Next() = 0;
diff --git a/src/System Application/App/External File Storage/src/Scenario/FileScenarioSetup.Page.al b/src/System Application/App/External File Storage/src/Scenario/FileScenarioSetup.Page.al
index b7af2a1050..e40def3d62 100644
--- a/src/System Application/App/External File Storage/src/Scenario/FileScenarioSetup.Page.al
+++ b/src/System Application/App/External File Storage/src/Scenario/FileScenarioSetup.Page.al
@@ -89,13 +89,13 @@ page 9452 "File Scenario Setup"
Scope = Repeater;
trigger OnAction()
var
- FileScenario: Codeunit "File Scenario";
+ FileScenarioInterface: Interface "File Scenario";
+ FileScenarioEnum: Enum "File Scenario";
NoSetupAvailableMsg: Label 'No additional setup is available for this scenario.';
- IsHandled: Boolean;
begin
- IsHandled := false;
- FileScenario.GetAdditionalScenarioSetup(Rec.Scenario, Rec.Connector, IsHandled);
- if not IsHandled then
+ FileScenarioEnum := Enum::"File Scenario".FromInteger(Rec.Scenario);
+ FileScenarioInterface := FileScenarioEnum;
+ if not FileScenarioInterface.GetAdditionalScenarioSetup(Rec.Scenario, Rec.Connector) then
Message(NoSetupAvailableMsg);
end;
}
diff --git a/src/System Application/App/app.json b/src/System Application/App/app.json
index f3b2179f88..29dd8e2d6a 100644
--- a/src/System Application/App/app.json
+++ b/src/System Application/App/app.json
@@ -4,7 +4,7 @@
"publisher": "Microsoft",
"brief": "Provides a standard set of capabilities that serve as a foundation for developing business apps.",
"description": "Contains an expansive set of open source modules that make it easier to build, maintain, and easily upgrade on-premises and online apps. These modules let you focus on the business logic, and the needs of your users or customers.",
- "version": "27.0.0.0",
+ "version": "27.0.90003.0",
"privacyStatement": "https://go.microsoft.com/fwlink/?linkid=724009",
"EULA": "https://go.microsoft.com/fwlink/?linkid=2009120",
"help": "https://go.microsoft.com/fwlink/?linkid=2103698",
From d04ef8bcc64cdab87b0696bf6e3f78cc8f91ec7b Mon Sep 17 00:00:00 2001
From: Stefan Sosic <59221826+StefanSosic@users.noreply.github.com>
Date: Mon, 25 Aug 2025 16:50:55 +0200
Subject: [PATCH 04/34] Fixes
---
.../src/AutomaticSync/DAExternalStorageSync.Report.al | 8 ++++----
.../app/src/DAExtStorageDeleteAfter.Enum.al | 4 ++--
.../app/src/DAExternalStorageProcessor.Codeunit.al | 10 +++++-----
.../app/src/DAExternalStorageSetup.Page.al | 4 ++--
.../app/src/DAExternalStorageSetup.Table.al | 8 ++++----
.../DAExtStorageFileScenario.EnumExt.al | 4 ++++
.../DAExternalStorageImpl.Codeunit.al | 5 +++++
.../DAExternalStorageSubs.Codeunit.al | 5 +++++
.../DocumentAttachmentExtStor.TableExt.al | 4 ++++
.../DocumentAttachmentExternal.Page.al | 4 ++++
.../src/Permissions/DAExtStorAdmin.PermissionSet.al | 3 +++
.../app/src/Permissions/DAExtStorView.PermissionSet.al | 3 +++
12 files changed, 45 insertions(+), 17 deletions(-)
diff --git a/src/Apps/W1/External Storage - Document Attachments/app/src/AutomaticSync/DAExternalStorageSync.Report.al b/src/Apps/W1/External Storage - Document Attachments/app/src/AutomaticSync/DAExternalStorageSync.Report.al
index bc343aaa8f..38340c90f9 100644
--- a/src/Apps/W1/External Storage - Document Attachments/app/src/AutomaticSync/DAExternalStorageSync.Report.al
+++ b/src/Apps/W1/External Storage - Document Attachments/app/src/AutomaticSync/DAExternalStorageSync.Report.al
@@ -1,12 +1,12 @@
-namespace Microsoft.ExternalStorage.DocumentAttachments;
-
-using Microsoft.Foundation.Attachment;
-
// ------------------------------------------------------------------------------------------------
// 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;
+
///
/// Report for synchronizing document attachments between internal and external storage.
/// Supports bulk upload, download, and cleanup operations.
diff --git a/src/Apps/W1/External Storage - Document Attachments/app/src/DAExtStorageDeleteAfter.Enum.al b/src/Apps/W1/External Storage - Document Attachments/app/src/DAExtStorageDeleteAfter.Enum.al
index 38e7326e2a..c173c4fc09 100644
--- a/src/Apps/W1/External Storage - Document Attachments/app/src/DAExtStorageDeleteAfter.Enum.al
+++ b/src/Apps/W1/External Storage - Document Attachments/app/src/DAExtStorageDeleteAfter.Enum.al
@@ -1,10 +1,10 @@
-namespace Microsoft.ExternalStorage.DocumentAttachments;
-
// ------------------------------------------------------------------------------------------------
// 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;
+
///
/// Defines when attachments should be deleted from internal storage after upload to external storage.
///
diff --git a/src/Apps/W1/External Storage - Document Attachments/app/src/DAExternalStorageProcessor.Codeunit.al b/src/Apps/W1/External Storage - Document Attachments/app/src/DAExternalStorageProcessor.Codeunit.al
index 0a8c66161e..a99364f386 100644
--- a/src/Apps/W1/External Storage - Document Attachments/app/src/DAExternalStorageProcessor.Codeunit.al
+++ b/src/Apps/W1/External Storage - Document Attachments/app/src/DAExternalStorageProcessor.Codeunit.al
@@ -1,3 +1,8 @@
+// ------------------------------------------------------------------------------------------------
+// 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;
@@ -5,11 +10,6 @@ using System.Environment;
using System.ExternalFileStorage;
using System.Utilities;
-// ------------------------------------------------------------------------------------------------
-// Copyright (c) Microsoft Corporation. All rights reserved.
-// Licensed under the MIT License. See License.txt in the project root for license information.
-// ------------------------------------------------------------------------------------------------
-
///
/// Provides functionality to manage document attachments in external storage systems.
/// Handles upload, download, and deletion operations for Business Central attachments.
diff --git a/src/Apps/W1/External Storage - Document Attachments/app/src/DAExternalStorageSetup.Page.al b/src/Apps/W1/External Storage - Document Attachments/app/src/DAExternalStorageSetup.Page.al
index e58bc3e7a8..e65a7e3eb5 100644
--- a/src/Apps/W1/External Storage - Document Attachments/app/src/DAExternalStorageSetup.Page.al
+++ b/src/Apps/W1/External Storage - Document Attachments/app/src/DAExternalStorageSetup.Page.al
@@ -1,10 +1,10 @@
-namespace Microsoft.ExternalStorage.DocumentAttachments;
-
// ------------------------------------------------------------------------------------------------
// 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;
+
///
/// Setup page for External Storage functionality.
/// Allows configuration of automatic upload and deletion policies.
diff --git a/src/Apps/W1/External Storage - Document Attachments/app/src/DAExternalStorageSetup.Table.al b/src/Apps/W1/External Storage - Document Attachments/app/src/DAExternalStorageSetup.Table.al
index dfd0015fd3..a71c3115fd 100644
--- a/src/Apps/W1/External Storage - Document Attachments/app/src/DAExternalStorageSetup.Table.al
+++ b/src/Apps/W1/External Storage - Document Attachments/app/src/DAExternalStorageSetup.Table.al
@@ -1,12 +1,12 @@
-namespace Microsoft.ExternalStorage.DocumentAttachments;
-
-using Microsoft.Foundation.Attachment;
-
// ------------------------------------------------------------------------------------------------
// 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;
+
///
/// Setup table for External Storage functionality.
/// Contains configuration settings for automatic upload and deletion policies.
diff --git a/src/Apps/W1/External Storage - Document Attachments/app/src/DocumentAttachmentIntegration/DAExtStorageFileScenario.EnumExt.al b/src/Apps/W1/External Storage - Document Attachments/app/src/DocumentAttachmentIntegration/DAExtStorageFileScenario.EnumExt.al
index 1f20bb73a6..837531dbbd 100644
--- a/src/Apps/W1/External Storage - Document Attachments/app/src/DocumentAttachmentIntegration/DAExtStorageFileScenario.EnumExt.al
+++ b/src/Apps/W1/External Storage - Document Attachments/app/src/DocumentAttachmentIntegration/DAExtStorageFileScenario.EnumExt.al
@@ -3,6 +3,10 @@
// Licensed under the MIT License. See License.txt in the project root for license information.
// ------------------------------------------------------------------------------------------------
+namespace Microsoft.ExternalStorage.DocumentAttachments;
+
+using System.ExternalFileStorage;
+
///
/// Extends File Scenario enum with External Storage option.
/// Allows File Account framework to recognize external storage scenarios.
diff --git a/src/Apps/W1/External Storage - Document Attachments/app/src/DocumentAttachmentIntegration/DAExternalStorageImpl.Codeunit.al b/src/Apps/W1/External Storage - Document Attachments/app/src/DocumentAttachmentIntegration/DAExternalStorageImpl.Codeunit.al
index 04ee1624a9..fcb8bd6468 100644
--- a/src/Apps/W1/External Storage - Document Attachments/app/src/DocumentAttachmentIntegration/DAExternalStorageImpl.Codeunit.al
+++ b/src/Apps/W1/External Storage - Document Attachments/app/src/DocumentAttachmentIntegration/DAExternalStorageImpl.Codeunit.al
@@ -3,6 +3,11 @@
// Licensed under the MIT License. See License.txt in the project root for license information.
// ------------------------------------------------------------------------------------------------
+namespace Microsoft.ExternalStorage.DocumentAttachments;
+
+using System.ExternalFileStorage;
+using System.Utilities;
+
codeunit 8751 "DA External Storage Impl." implements "File Scenario"
{
Permissions = tabledata "DA External Storage Setup" = r;
diff --git a/src/Apps/W1/External Storage - Document Attachments/app/src/DocumentAttachmentIntegration/DAExternalStorageSubs.Codeunit.al b/src/Apps/W1/External Storage - Document Attachments/app/src/DocumentAttachmentIntegration/DAExternalStorageSubs.Codeunit.al
index a81bd03bbb..f2b63809df 100644
--- a/src/Apps/W1/External Storage - Document Attachments/app/src/DocumentAttachmentIntegration/DAExternalStorageSubs.Codeunit.al
+++ b/src/Apps/W1/External Storage - Document Attachments/app/src/DocumentAttachmentIntegration/DAExternalStorageSubs.Codeunit.al
@@ -3,6 +3,11 @@
// Licensed under the MIT License. See License.txt in the project root for license information.
// ------------------------------------------------------------------------------------------------
+namespace Microsoft.ExternalStorage.DocumentAttachments;
+
+using Microsoft.Foundation.Attachment;
+using System.Utilities;
+
///
/// Event subscribers for External Storage functionality.
/// Handles automatic upload of new attachments and cleanup operations.
diff --git a/src/Apps/W1/External Storage - Document Attachments/app/src/DocumentAttachmentIntegration/DocumentAttachmentExtStor.TableExt.al b/src/Apps/W1/External Storage - Document Attachments/app/src/DocumentAttachmentIntegration/DocumentAttachmentExtStor.TableExt.al
index fc4268d440..6b30bb2b8c 100644
--- a/src/Apps/W1/External Storage - Document Attachments/app/src/DocumentAttachmentIntegration/DocumentAttachmentExtStor.TableExt.al
+++ b/src/Apps/W1/External Storage - Document Attachments/app/src/DocumentAttachmentIntegration/DocumentAttachmentExtStor.TableExt.al
@@ -3,6 +3,10 @@
// Licensed under the MIT License. See License.txt in the project root for license information.
// ------------------------------------------------------------------------------------------------
+namespace Microsoft.ExternalStorage.DocumentAttachments;
+
+using Microsoft.Foundation.Attachment;
+
///
/// Extends the Document Attachment table with external storage functionality.
/// Adds fields and procedures to track attachments in external storage systems.
diff --git a/src/Apps/W1/External Storage - Document Attachments/app/src/DocumentAttachmentIntegration/DocumentAttachmentExternal.Page.al b/src/Apps/W1/External Storage - Document Attachments/app/src/DocumentAttachmentIntegration/DocumentAttachmentExternal.Page.al
index e8933d9ab3..e979971208 100644
--- a/src/Apps/W1/External Storage - Document Attachments/app/src/DocumentAttachmentIntegration/DocumentAttachmentExternal.Page.al
+++ b/src/Apps/W1/External Storage - Document Attachments/app/src/DocumentAttachmentIntegration/DocumentAttachmentExternal.Page.al
@@ -3,6 +3,10 @@
// Licensed under the MIT License. See License.txt in the project root for license information.
// ------------------------------------------------------------------------------------------------
+namespace Microsoft.ExternalStorage.DocumentAttachments;
+
+using Microsoft.Foundation.Attachment;
+
///
/// List page for managing document attachments with external storage information.
/// Provides actions for upload, download, and deletion operations.
diff --git a/src/Apps/W1/External Storage - Document Attachments/app/src/Permissions/DAExtStorAdmin.PermissionSet.al b/src/Apps/W1/External Storage - Document Attachments/app/src/Permissions/DAExtStorAdmin.PermissionSet.al
index c6bb844035..75c333c8d7 100644
--- a/src/Apps/W1/External Storage - Document Attachments/app/src/Permissions/DAExtStorAdmin.PermissionSet.al
+++ b/src/Apps/W1/External Storage - Document Attachments/app/src/Permissions/DAExtStorAdmin.PermissionSet.al
@@ -2,6 +2,9 @@
// 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;
+
///
/// Permission set for External Storage functionality.
/// Grants necessary permissions to use external storage features.
diff --git a/src/Apps/W1/External Storage - Document Attachments/app/src/Permissions/DAExtStorView.PermissionSet.al b/src/Apps/W1/External Storage - Document Attachments/app/src/Permissions/DAExtStorView.PermissionSet.al
index 3a183bbcb1..799717a427 100644
--- a/src/Apps/W1/External Storage - Document Attachments/app/src/Permissions/DAExtStorView.PermissionSet.al
+++ b/src/Apps/W1/External Storage - Document Attachments/app/src/Permissions/DAExtStorView.PermissionSet.al
@@ -2,6 +2,9 @@
// 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;
+
///
/// Permission set for External Storage functionality.
/// Grants necessary permissions to use external storage features.
From 4978d52b7f975df6f7b9762f978254229238424e Mon Sep 17 00:00:00 2001
From: Stefan Sosic <59221826+StefanSosic@users.noreply.github.com>
Date: Mon, 25 Aug 2025 16:51:38 +0200
Subject: [PATCH 05/34] Trailing space
---
.../DAExternalStorageImpl.Codeunit.al | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/Apps/W1/External Storage - Document Attachments/app/src/DocumentAttachmentIntegration/DAExternalStorageImpl.Codeunit.al b/src/Apps/W1/External Storage - Document Attachments/app/src/DocumentAttachmentIntegration/DAExternalStorageImpl.Codeunit.al
index fcb8bd6468..39b56bca9f 100644
--- a/src/Apps/W1/External Storage - Document Attachments/app/src/DocumentAttachmentIntegration/DAExternalStorageImpl.Codeunit.al
+++ b/src/Apps/W1/External Storage - Document Attachments/app/src/DocumentAttachmentIntegration/DAExternalStorageImpl.Codeunit.al
@@ -56,7 +56,7 @@ codeunit 8751 "DA External Storage Impl." implements "File Scenario"
/// Called before deleting a file scenario.
///
/// The ID of the file scenario.
- /// The file storage connector.
+ /// The file storage connector.
/// True if the delete operation is handled and should not proceed, otherwise false.
procedure BeforeDeleteFileScenarioCheck(Scenario: Integer; Connector: Enum System.ExternalFileStorage."Ext. File Storage Connector") SkipDelete: Boolean
var
From 63577099d2b6963e58cb347b63b3e81ab5849425 Mon Sep 17 00:00:00 2001
From: Stefan Sosic <59221826+StefanSosic@users.noreply.github.com>
Date: Mon, 25 Aug 2025 16:54:17 +0200
Subject: [PATCH 06/34] Internal
---
.../DAExternalStorageImpl.Codeunit.al | 1 +
1 file changed, 1 insertion(+)
diff --git a/src/Apps/W1/External Storage - Document Attachments/app/src/DocumentAttachmentIntegration/DAExternalStorageImpl.Codeunit.al b/src/Apps/W1/External Storage - Document Attachments/app/src/DocumentAttachmentIntegration/DAExternalStorageImpl.Codeunit.al
index 39b56bca9f..0cdb976066 100644
--- a/src/Apps/W1/External Storage - Document Attachments/app/src/DocumentAttachmentIntegration/DAExternalStorageImpl.Codeunit.al
+++ b/src/Apps/W1/External Storage - Document Attachments/app/src/DocumentAttachmentIntegration/DAExternalStorageImpl.Codeunit.al
@@ -10,6 +10,7 @@ using System.Utilities;
codeunit 8751 "DA External Storage Impl." implements "File Scenario"
{
+ Access = Internal;
Permissions = tabledata "DA External Storage Setup" = r;
///
From e5dbc09d4d0a59caecd54c619a25c5032f591cf4 Mon Sep 17 00:00:00 2001
From: Stefan Sosic <59221826+StefanSosic@users.noreply.github.com>
Date: Mon, 25 Aug 2025 16:55:30 +0200
Subject: [PATCH 07/34] Internal
---
.../app/src/DAExternalStorageSetup.Page.al | 1 +
.../DocumentAttachmentExternal.Page.al | 1 +
2 files changed, 2 insertions(+)
diff --git a/src/Apps/W1/External Storage - Document Attachments/app/src/DAExternalStorageSetup.Page.al b/src/Apps/W1/External Storage - Document Attachments/app/src/DAExternalStorageSetup.Page.al
index e65a7e3eb5..38da822634 100644
--- a/src/Apps/W1/External Storage - Document Attachments/app/src/DAExternalStorageSetup.Page.al
+++ b/src/Apps/W1/External Storage - Document Attachments/app/src/DAExternalStorageSetup.Page.al
@@ -18,6 +18,7 @@ page 8750 "DA External Storage Setup"
ApplicationArea = Basic, Suite;
InsertAllowed = false;
DeleteAllowed = false;
+ Extensible = false;
layout
{
diff --git a/src/Apps/W1/External Storage - Document Attachments/app/src/DocumentAttachmentIntegration/DocumentAttachmentExternal.Page.al b/src/Apps/W1/External Storage - Document Attachments/app/src/DocumentAttachmentIntegration/DocumentAttachmentExternal.Page.al
index e979971208..a883f87b79 100644
--- a/src/Apps/W1/External Storage - Document Attachments/app/src/DocumentAttachmentIntegration/DocumentAttachmentExternal.Page.al
+++ b/src/Apps/W1/External Storage - Document Attachments/app/src/DocumentAttachmentIntegration/DocumentAttachmentExternal.Page.al
@@ -19,6 +19,7 @@ page 8751 "Document Attachment - External"
UsageCategory = None;
ApplicationArea = Basic, Suite;
Editable = false;
+ Extensible = false;
layout
{
From 6ecd65ff0c3dc9a5561920d2ba02b04771b1ef12 Mon Sep 17 00:00:00 2001
From: Stefan Sosic <59221826+StefanSosic@users.noreply.github.com>
Date: Mon, 25 Aug 2025 17:02:03 +0200
Subject: [PATCH 08/34] app json fixes
---
.../W1/External Storage - Document Attachments/app/app.json | 3 +++
src/System Application/App/app.json | 2 +-
2 files changed, 4 insertions(+), 1 deletion(-)
diff --git a/src/Apps/W1/External Storage - Document Attachments/app/app.json b/src/Apps/W1/External Storage - Document Attachments/app/app.json
index d4aad84185..2e843022ff 100644
--- a/src/Apps/W1/External Storage - Document Attachments/app/app.json
+++ b/src/Apps/W1/External Storage - Document Attachments/app/app.json
@@ -27,6 +27,9 @@
"allowDownloadingSource": true,
"includeSourceInSymbolFile": true
},
+ "features": [
+ "TranslationFile"
+ ],
"contextSensitiveHelpUrl": "https://go.microsoft.com/fwlink/?linkid=2134520",
"target": "Cloud"
}
\ No newline at end of file
diff --git a/src/System Application/App/app.json b/src/System Application/App/app.json
index 29dd8e2d6a..f3b2179f88 100644
--- a/src/System Application/App/app.json
+++ b/src/System Application/App/app.json
@@ -4,7 +4,7 @@
"publisher": "Microsoft",
"brief": "Provides a standard set of capabilities that serve as a foundation for developing business apps.",
"description": "Contains an expansive set of open source modules that make it easier to build, maintain, and easily upgrade on-premises and online apps. These modules let you focus on the business logic, and the needs of your users or customers.",
- "version": "27.0.90003.0",
+ "version": "27.0.0.0",
"privacyStatement": "https://go.microsoft.com/fwlink/?linkid=724009",
"EULA": "https://go.microsoft.com/fwlink/?linkid=2009120",
"help": "https://go.microsoft.com/fwlink/?linkid=2103698",
From c454a9a691d77116760e35593d991d7377ff91df Mon Sep 17 00:00:00 2001
From: Stefan Sosic <59221826+StefanSosic@users.noreply.github.com>
Date: Mon, 25 Aug 2025 17:03:11 +0200
Subject: [PATCH 09/34] DataClassification
---
.../DocumentAttachmentExtStor.TableExt.al | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/src/Apps/W1/External Storage - Document Attachments/app/src/DocumentAttachmentIntegration/DocumentAttachmentExtStor.TableExt.al b/src/Apps/W1/External Storage - Document Attachments/app/src/DocumentAttachmentIntegration/DocumentAttachmentExtStor.TableExt.al
index 6b30bb2b8c..a126502b30 100644
--- a/src/Apps/W1/External Storage - Document Attachments/app/src/DocumentAttachmentIntegration/DocumentAttachmentExtStor.TableExt.al
+++ b/src/Apps/W1/External Storage - Document Attachments/app/src/DocumentAttachmentIntegration/DocumentAttachmentExtStor.TableExt.al
@@ -18,14 +18,14 @@ tableextension 8750 "Document Attachment Ext.Stor." extends "Document Attachment
field(8750; "Uploaded Externally"; Boolean)
{
Caption = 'Uploaded Externally';
- DataClassification = CustomerContent;
+ DataClassification = SystemMetadata;
Editable = false;
ToolTip = 'Specifies if the file has been uploaded to external storage.';
}
field(8751; "External Upload Date"; DateTime)
{
Caption = 'External Upload Date';
- DataClassification = CustomerContent;
+ DataClassification = SystemMetadata;
Editable = false;
ToolTip = 'Specifies when the file was uploaded to external storage.';
}
@@ -39,7 +39,7 @@ tableextension 8750 "Document Attachment Ext.Stor." extends "Document Attachment
field(8753; "Deleted Internally"; Boolean)
{
Caption = 'Deleted Internally';
- DataClassification = CustomerContent;
+ DataClassification = SystemMetadata;
Editable = false;
ToolTip = 'Specifies the value of the Deleted Internally field.';
}
From b4855798a6eb177969a9694295e3e591fb6f370c Mon Sep 17 00:00:00 2001
From: Stefan Sosic <59221826+StefanSosic@users.noreply.github.com>
Date: Mon, 25 Aug 2025 17:05:20 +0200
Subject: [PATCH 10/34] Namespaces
---
.../src/Scenario/DefaultFileScenarioImpl.Codeunit.al | 7 +++++++
.../src/Scenario/FileScenario.Interface.al | 2 ++
2 files changed, 9 insertions(+)
diff --git a/src/System Application/App/External File Storage/src/Scenario/DefaultFileScenarioImpl.Codeunit.al b/src/System Application/App/External File Storage/src/Scenario/DefaultFileScenarioImpl.Codeunit.al
index 17ab45c0e6..a20fad4c0e 100644
--- a/src/System Application/App/External File Storage/src/Scenario/DefaultFileScenarioImpl.Codeunit.al
+++ b/src/System Application/App/External File Storage/src/Scenario/DefaultFileScenarioImpl.Codeunit.al
@@ -1,3 +1,10 @@
+// ------------------------------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+// ------------------------------------------------------------------------------------------------
+
+namespace System.ExternalFileStorage;
+
codeunit 9459 "Default File Scenario Impl." implements "File Scenario"
{
diff --git a/src/System Application/App/External File Storage/src/Scenario/FileScenario.Interface.al b/src/System Application/App/External File Storage/src/Scenario/FileScenario.Interface.al
index 1af8601dcd..ad50339cab 100644
--- a/src/System Application/App/External File Storage/src/Scenario/FileScenario.Interface.al
+++ b/src/System Application/App/External File Storage/src/Scenario/FileScenario.Interface.al
@@ -3,6 +3,8 @@
// Licensed under the MIT License. See License.txt in the project root for license information.
// ------------------------------------------------------------------------------------------------
+namespace System.ExternalFileStorage;
+
interface "File Scenario"
{
///
From a28fe498d00bd5cacf4dde6e3b192ab3c33b4965 Mon Sep 17 00:00:00 2001
From: Stefan Sosic <59221826+StefanSosic@users.noreply.github.com>
Date: Mon, 25 Aug 2025 23:27:38 +0200
Subject: [PATCH 11/34] InherentPermissions + InherentEntitlements
---
.../DAExternalStorageImpl.Codeunit.al | 2 ++
.../src/Scenario/DefaultFileScenarioImpl.Codeunit.al | 3 +++
2 files changed, 5 insertions(+)
diff --git a/src/Apps/W1/External Storage - Document Attachments/app/src/DocumentAttachmentIntegration/DAExternalStorageImpl.Codeunit.al b/src/Apps/W1/External Storage - Document Attachments/app/src/DocumentAttachmentIntegration/DAExternalStorageImpl.Codeunit.al
index 0cdb976066..9ddca5407e 100644
--- a/src/Apps/W1/External Storage - Document Attachments/app/src/DocumentAttachmentIntegration/DAExternalStorageImpl.Codeunit.al
+++ b/src/Apps/W1/External Storage - Document Attachments/app/src/DocumentAttachmentIntegration/DAExternalStorageImpl.Codeunit.al
@@ -11,6 +11,8 @@ using System.Utilities;
codeunit 8751 "DA External Storage Impl." implements "File Scenario"
{
Access = Internal;
+ InherentPermissions = X;
+ InherentEntitlements = X;
Permissions = tabledata "DA External Storage Setup" = r;
///
diff --git a/src/System Application/App/External File Storage/src/Scenario/DefaultFileScenarioImpl.Codeunit.al b/src/System Application/App/External File Storage/src/Scenario/DefaultFileScenarioImpl.Codeunit.al
index a20fad4c0e..ad9cbc6a0d 100644
--- a/src/System Application/App/External File Storage/src/Scenario/DefaultFileScenarioImpl.Codeunit.al
+++ b/src/System Application/App/External File Storage/src/Scenario/DefaultFileScenarioImpl.Codeunit.al
@@ -7,6 +7,9 @@ namespace System.ExternalFileStorage;
codeunit 9459 "Default File Scenario Impl." implements "File Scenario"
{
+ Access = Internal;
+ InherentPermissions = X;
+ InherentEntitlements = X;
///
/// Called before adding or modifying a file scenario.
From 26e1c3a41789801da9f62e63653f27f74bbf1726 Mon Sep 17 00:00:00 2001
From: Stefan Sosic <59221826+StefanSosic@users.noreply.github.com>
Date: Mon, 25 Aug 2025 23:28:33 +0200
Subject: [PATCH 12/34] Update tooltip for sync direction field in DA External
Storage Sync report
---
.../app/src/AutomaticSync/DAExternalStorageSync.Report.al | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/Apps/W1/External Storage - Document Attachments/app/src/AutomaticSync/DAExternalStorageSync.Report.al b/src/Apps/W1/External Storage - Document Attachments/app/src/AutomaticSync/DAExternalStorageSync.Report.al
index 38340c90f9..7f26f4d7fa 100644
--- a/src/Apps/W1/External Storage - Document Attachments/app/src/AutomaticSync/DAExternalStorageSync.Report.al
+++ b/src/Apps/W1/External Storage - Document Attachments/app/src/AutomaticSync/DAExternalStorageSync.Report.al
@@ -107,7 +107,7 @@ report 8752 "DA External Storage Sync"
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.';
+ ToolTip = 'Specifies whether to sync to external storage or from external storage.';
}
field(DeleteExpiredFilesField; DeleteExpiredFiles)
{
From 68ef801a0be35dc6333b0ec7fddc7aa4d4457f81 Mon Sep 17 00:00:00 2001
From: Stefan Sosic <59221826+StefanSosic@users.noreply.github.com>
Date: Fri, 29 Aug 2025 10:26:51 +0200
Subject: [PATCH 13/34] Deafult Scenario
---
.../src/Scenario/FileScenario.Codeunit.al | 13 ++++++++++++-
.../src/Scenario/FileScenarioImpl.Codeunit.al | 12 ++++++++++++
2 files changed, 24 insertions(+), 1 deletion(-)
diff --git a/src/System Application/App/External File Storage/src/Scenario/FileScenario.Codeunit.al b/src/System Application/App/External File Storage/src/Scenario/FileScenario.Codeunit.al
index 82200e7283..6a0f3313f1 100644
--- a/src/System Application/App/External File Storage/src/Scenario/FileScenario.Codeunit.al
+++ b/src/System Application/App/External File Storage/src/Scenario/FileScenario.Codeunit.al
@@ -17,7 +17,7 @@ codeunit 9452 "File Scenario"
/// True if an account for the default scenario was found; otherwise - false.
procedure GetDefaultFileAccount(var TempFileAccount: Record "File Account" temporary): Boolean
begin
- exit(FileScenarioImpl.GetFileAccount(Enum::"File Scenario"::Default, TempFileAccount));
+ exit(FileScenarioImpl.GetFileAccountOrDefault(Enum::"File Scenario"::Default, TempFileAccount));
end;
///
@@ -32,6 +32,17 @@ codeunit 9452 "File Scenario"
exit(FileScenarioImpl.GetFileAccount(Scenario, TempFileAccount));
end;
+ ///
+ /// Gets the file account used by the given file scenario or the default file account if no specific account is assigned to the scenario.
+ ///
+ /// The scenario to look for.
+ /// Out parameter holding information about the file account.
+ /// True if an account for the specified scenario was found; otherwise - false.
+ procedure GetFileAccountOrDefault(Scenario: Enum "File Scenario"; var TempFileAccount: Record "File Account" temporary): Boolean
+ begin
+ exit(FileScenarioImpl.GetFileAccountOrDefault(Scenario, TempFileAccount));
+ end;
+
///
/// Sets a default file account.
///
diff --git a/src/System Application/App/External File Storage/src/Scenario/FileScenarioImpl.Codeunit.al b/src/System Application/App/External File Storage/src/Scenario/FileScenarioImpl.Codeunit.al
index 8955f647b9..f95118aec8 100644
--- a/src/System Application/App/External File Storage/src/Scenario/FileScenarioImpl.Codeunit.al
+++ b/src/System Application/App/External File Storage/src/Scenario/FileScenarioImpl.Codeunit.al
@@ -28,8 +28,20 @@ codeunit 9453 "File Scenario Impl."
TempFileAccount := TempAllFileAccounts;
exit(true);
end;
+ end;
+
+ procedure GetFileAccountOrDefault(Scenario: Enum "File Scenario"; var TempFileAccount: Record "File Account" temporary): Boolean
+ var
+ TempAllFileAccounts: Record "File Account" temporary;
+ FileScenario: Record "File Scenario";
+ FileAccounts: Codeunit "File Account";
+ begin
+ // Find the account for the provided scenario
+ if GetFileAccount(Scenario, TempFileAccount) then
+ exit(true);
// Fallback to the default account if the scenario isn't mapped or the mapped account doesn't exist
+ FileAccounts.GetAllAccounts(TempAllFileAccounts);
if FileScenario.Get(Enum::"File Scenario"::Default) then
if TempAllFileAccounts.Get(FileScenario."Account Id", FileScenario.Connector) then begin
TempFileAccount := TempAllFileAccounts;
From 8832186b2dc0f803cf3b8ff4cdda0b7eae2f7101 Mon Sep 17 00:00:00 2001
From: Stefan Sosic <59221826+StefanSosic@users.noreply.github.com>
Date: Fri, 29 Aug 2025 10:28:12 +0200
Subject: [PATCH 14/34] Empty Line
---
.../app/src/AutomaticSync/DAExternalStorageSync.Report.al | 1 -
1 file changed, 1 deletion(-)
diff --git a/src/Apps/W1/External Storage - Document Attachments/app/src/AutomaticSync/DAExternalStorageSync.Report.al b/src/Apps/W1/External Storage - Document Attachments/app/src/AutomaticSync/DAExternalStorageSync.Report.al
index 7f26f4d7fa..fcdffad0ad 100644
--- a/src/Apps/W1/External Storage - Document Attachments/app/src/AutomaticSync/DAExternalStorageSync.Report.al
+++ b/src/Apps/W1/External Storage - Document Attachments/app/src/AutomaticSync/DAExternalStorageSync.Report.al
@@ -58,7 +58,6 @@ report 8752 "DA External Storage Sync"
case SyncDirection of
SyncDirection::"To External Storage":
-
if not ExternalStorageProcessor.UploadToExternalStorage(DocumentAttachment) then
FailedCount += 1;
SyncDirection::"From External Storage":
From ecd581aad5731535a3cb029d7835a90b4db3068c Mon Sep 17 00:00:00 2001
From: Stefan Sosic <59221826+StefanSosic@users.noreply.github.com>
Date: Fri, 29 Aug 2025 10:34:56 +0200
Subject: [PATCH 15/34] Permission Refactor & Message
---
.../DAExternalStorageImpl.Codeunit.al | 10 ++-------
.../DAExtStorAdmin.PermissionSet.al | 11 ++--------
.../DAExtStorExec.PermissionSet.al | 22 +++++++++++++++++++
.../DAExtStorView.PermissionSet.al | 8 ++-----
4 files changed, 28 insertions(+), 23 deletions(-)
create mode 100644 src/Apps/W1/External Storage - Document Attachments/app/src/Permissions/DAExtStorExec.PermissionSet.al
diff --git a/src/Apps/W1/External Storage - Document Attachments/app/src/DocumentAttachmentIntegration/DAExternalStorageImpl.Codeunit.al b/src/Apps/W1/External Storage - Document Attachments/app/src/DocumentAttachmentIntegration/DAExternalStorageImpl.Codeunit.al
index 9ddca5407e..85f2a5e689 100644
--- a/src/Apps/W1/External Storage - Document Attachments/app/src/DocumentAttachmentIntegration/DAExternalStorageImpl.Codeunit.al
+++ b/src/Apps/W1/External Storage - Document Attachments/app/src/DocumentAttachmentIntegration/DAExternalStorageImpl.Codeunit.al
@@ -24,18 +24,12 @@ codeunit 8751 "DA External Storage Impl." implements "File Scenario"
procedure BeforeAddOrModifyFileScenarioCheck(Scenario: Integer; Connector: Enum System.ExternalFileStorage."Ext. File Storage Connector") SkipInsertOrModify: Boolean
var
ConfirmManagement: Codeunit "Confirm Management";
- DisclaimerPart1Lbl: Label 'You are about to enable External Storage!!!';
- DisclaimerPart2Lbl: Label '\\This feature is provided as-is, and you use it at your own risk.';
- DisclaimerPart3Lbl: Label '\Microsoft is not responsible for any issues or data loss that may occur.';
- DisclaimerPart4Lbl: Label '\\Do you wish to continue?';
+ DisclaimerMsg: Label 'You are about to enable External Storage!!!\\This feature is provided as-is, and you use it at your own risk.\Microsoft is not responsible for any issues or data loss that may occur.\\Do you wish to continue?';
begin
if not (Scenario = Enum::"File Scenario"::"Doc. Attach. - External Storage".AsInteger()) then
exit;
- SkipInsertOrModify := not ConfirmManagement.GetResponseOrDefault(DisclaimerPart1Lbl +
- DisclaimerPart2Lbl +
- DisclaimerPart3Lbl +
- DisclaimerPart4Lbl);
+ SkipInsertOrModify := not ConfirmManagement.GetResponseOrDefault(DisclaimerMsg);
end;
///
diff --git a/src/Apps/W1/External Storage - Document Attachments/app/src/Permissions/DAExtStorAdmin.PermissionSet.al b/src/Apps/W1/External Storage - Document Attachments/app/src/Permissions/DAExtStorAdmin.PermissionSet.al
index 75c333c8d7..babc6bd859 100644
--- a/src/Apps/W1/External Storage - Document Attachments/app/src/Permissions/DAExtStorAdmin.PermissionSet.al
+++ b/src/Apps/W1/External Storage - Document Attachments/app/src/Permissions/DAExtStorAdmin.PermissionSet.al
@@ -2,7 +2,6 @@
// 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;
///
@@ -13,12 +12,6 @@ permissionset 8751 "DA Ext. Stor. Admin"
{
Assignable = true;
Caption = 'DA - External Storage Admin';
- Permissions = tabledata "DA External Storage Setup" = RIMD,
- table "DA External Storage Setup" = X,
- page "DA External Storage Setup" = X,
- page "Document Attachment - External" = X,
- report "DA External Storage Sync" = X,
- codeunit "DA External Storage Processor" = X,
- codeunit "DA External Storage Subs." = X,
- codeunit "DA External Storage Impl." = X;
+ IncludedPermissionSets = "DA Ext. Stor. View";
+ Permissions = tabledata "DA External Storage Setup" = IMD;
}
\ No newline at end of file
diff --git a/src/Apps/W1/External Storage - Document Attachments/app/src/Permissions/DAExtStorExec.PermissionSet.al b/src/Apps/W1/External Storage - Document Attachments/app/src/Permissions/DAExtStorExec.PermissionSet.al
new file mode 100644
index 0000000000..8576fed8bf
--- /dev/null
+++ b/src/Apps/W1/External Storage - Document Attachments/app/src/Permissions/DAExtStorExec.PermissionSet.al
@@ -0,0 +1,22 @@
+// ------------------------------------------------------------------------------------------------
+// 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;
+
+///
+/// Permission set for External Storage functionality.
+/// Grants necessary permissions to use external storage features.
+///
+permissionset 8752 "DA Ext. Stor. Exec."
+{
+ Assignable = false;
+ Caption = 'DA - External Storage Exec.';
+ Permissions = table "DA External Storage Setup" = X,
+ page "DA External Storage Setup" = X,
+ page "Document Attachment - External" = X,
+ report "DA External Storage Sync" = X,
+ codeunit "DA External Storage Processor" = X,
+ codeunit "DA External Storage Subs." = X,
+ codeunit "DA External Storage Impl." = X;
+}
\ No newline at end of file
diff --git a/src/Apps/W1/External Storage - Document Attachments/app/src/Permissions/DAExtStorView.PermissionSet.al b/src/Apps/W1/External Storage - Document Attachments/app/src/Permissions/DAExtStorView.PermissionSet.al
index 799717a427..dd27bf0e1e 100644
--- a/src/Apps/W1/External Storage - Document Attachments/app/src/Permissions/DAExtStorView.PermissionSet.al
+++ b/src/Apps/W1/External Storage - Document Attachments/app/src/Permissions/DAExtStorView.PermissionSet.al
@@ -13,10 +13,6 @@ permissionset 8750 "DA Ext. Stor. View"
{
Assignable = true;
Caption = 'DA - External Storage View';
- Permissions = tabledata "DA External Storage Setup" = R,
- table "DA External Storage Setup" = X,
- page "DA External Storage Setup" = X,
- page "Document Attachment - External" = X,
- codeunit "DA External Storage Processor" = X,
- codeunit "DA External Storage Subs." = X;
+ IncludedPermissionSets = "DA Ext. Stor. Exec.";
+ Permissions = tabledata "DA External Storage Setup" = R;
}
\ No newline at end of file
From 4b57268edeb46c0a5bc12c54150a20e9f9e62b16 Mon Sep 17 00:00:00 2001
From: Stefan Sosic <59221826+StefanSosic@users.noreply.github.com>
Date: Fri, 29 Aug 2025 10:38:59 +0200
Subject: [PATCH 16/34] Add label for date formula in DA External Storage Sync
report
---
.../app/src/AutomaticSync/DAExternalStorageSync.Report.al | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/src/Apps/W1/External Storage - Document Attachments/app/src/AutomaticSync/DAExternalStorageSync.Report.al b/src/Apps/W1/External Storage - Document Attachments/app/src/AutomaticSync/DAExternalStorageSync.Report.al
index fcdffad0ad..2fc37c22de 100644
--- a/src/Apps/W1/External Storage - Document Attachments/app/src/AutomaticSync/DAExternalStorageSync.Report.al
+++ b/src/Apps/W1/External Storage - Document Attachments/app/src/AutomaticSync/DAExternalStorageSync.Report.al
@@ -50,6 +50,8 @@ report 8752 "DA External Storage Sync"
end;
trigger OnAfterGetRecord()
+ var
+ DateFormulaLbl: Label '<+%1>', Locked = true;
begin
ProcessedCount += 1;
@@ -66,7 +68,7 @@ report 8752 "DA External Storage Sync"
FailedCount += 1;
end;
if DeleteExpiredFiles then
- if CalcDate('<+' + GetDateFormulaFromExternalStorageSetup() + '>', DocumentAttachment."External Upload Date".Date()) >= Today() then
+ if CalcDate(StrSubstNo(DateFormulaLbl, GetDateFormulaFromExternalStorageSetup()), DocumentAttachment."External Upload Date".Date()) >= Today() then
if ExternalStorageProcessor.DeleteFromInternalStorage(DocumentAttachment) then
DeleteCount += 1
else
From 7eb7ebf91e7e6bd2c60394525bce5e330f4c46bc Mon Sep 17 00:00:00 2001
From: Stefan Sosic <59221826+StefanSosic@users.noreply.github.com>
Date: Thu, 9 Oct 2025 14:35:30 +0200
Subject: [PATCH 17/34] FIxes from PR comments
---
.../app/app.json | 2 +-
.../DAExternalStorageSync.Report.al | 27 +-
.../app/src/DAExtStorageDeleteAfter.Enum.al | 31 -
.../DAExternalStorageProcessor.Codeunit.al | 381 -------------
.../app/src/DAExternalStorageSetup.Page.al | 1 +
.../app/src/DAExternalStorageSetup.Table.al | 2 +-
.../DAExternalStorageImpl.Codeunit.al | 539 +++++++++++++++++-
.../DAExternalStorageSubs.Codeunit.al | 162 ------
.../DocumentAttachmentExtStor.TableExt.al | 12 -
.../DocumentAttachmentExternal.Page.al | 20 +-
.../DAExtStorAdmin.PermissionSet.al | 1 -
.../DAExtStorExec.PermissionSet.al | 22 -
.../DAExtStorView.PermissionSet.al | 18 -
13 files changed, 553 insertions(+), 665 deletions(-)
delete mode 100644 src/Apps/W1/External Storage - Document Attachments/app/src/DAExtStorageDeleteAfter.Enum.al
delete mode 100644 src/Apps/W1/External Storage - Document Attachments/app/src/DAExternalStorageProcessor.Codeunit.al
delete mode 100644 src/Apps/W1/External Storage - Document Attachments/app/src/DocumentAttachmentIntegration/DAExternalStorageSubs.Codeunit.al
delete mode 100644 src/Apps/W1/External Storage - Document Attachments/app/src/Permissions/DAExtStorExec.PermissionSet.al
delete mode 100644 src/Apps/W1/External Storage - Document Attachments/app/src/Permissions/DAExtStorView.PermissionSet.al
diff --git a/src/Apps/W1/External Storage - Document Attachments/app/app.json b/src/Apps/W1/External Storage - Document Attachments/app/app.json
index 2e843022ff..d1b91a1e44 100644
--- a/src/Apps/W1/External Storage - Document Attachments/app/app.json
+++ b/src/Apps/W1/External Storage - Document Attachments/app/app.json
@@ -2,7 +2,7 @@
"id": "5f2e93a0-6083-4718-b05a-7ac89be5644d",
"name": "External Storage - Document Attachments",
"publisher": "Microsoft",
- "version": "27.0.0.0",
+ "version": "28.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",
diff --git a/src/Apps/W1/External Storage - Document Attachments/app/src/AutomaticSync/DAExternalStorageSync.Report.al b/src/Apps/W1/External Storage - Document Attachments/app/src/AutomaticSync/DAExternalStorageSync.Report.al
index 2fc37c22de..dbd3b1faea 100644
--- a/src/Apps/W1/External Storage - Document Attachments/app/src/AutomaticSync/DAExternalStorageSync.Report.al
+++ b/src/Apps/W1/External Storage - Document Attachments/app/src/AutomaticSync/DAExternalStorageSync.Report.al
@@ -60,19 +60,20 @@ report 8752 "DA External Storage Sync"
case SyncDirection of
SyncDirection::"To External Storage":
- if not ExternalStorageProcessor.UploadToExternalStorage(DocumentAttachment) then
+ if not ExternalStorageImpl.UploadToExternalStorage(DocumentAttachment) then
FailedCount += 1;
SyncDirection::"From External Storage":
- if not ExternalStorageProcessor.DownloadFromExternalStorage(DocumentAttachment) then
+ if not ExternalStorageImpl.DownloadFromExternalStorage(DocumentAttachment) then
FailedCount += 1;
end;
if DeleteExpiredFiles then
if CalcDate(StrSubstNo(DateFormulaLbl, GetDateFormulaFromExternalStorageSetup()), DocumentAttachment."External Upload Date".Date()) >= Today() then
- if ExternalStorageProcessor.DeleteFromInternalStorage(DocumentAttachment) then
+ if ExternalStorageImpl.DeleteFromInternalStorage(DocumentAttachment) then
DeleteCount += 1
else
DeleteFailedCount += 1;
+ Commit(); // Commit after each record to avoid lost in communication error with external storage service
if (MaxRecordsToProcess > 0) and (ProcessedCount >= MaxRecordsToProcess) then
CurrReport.Break();
@@ -129,7 +130,7 @@ report 8752 "DA External Storage Sync"
}
var
- ExternalStorageProcessor: Codeunit "DA External Storage Processor";
+ ExternalStorageImpl: Codeunit "DA External Storage Impl.";
DeleteExpiredFiles: Boolean;
Dialog: Dialog;
DeleteCount, DeleteFailedCount : Integer;
@@ -147,29 +148,17 @@ report 8752 "DA External Storage Sync"
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)
- else
- DocumentAttachment.Mark(true);
- until DocumentAttachment.Next() = 0;
- DocumentAttachment.MarkedOnly(true);
- end;
- end;
+ DocumentAttachment.SetRange("Uploaded Externally", false);
SyncDirection::"From External Storage":
-
DocumentAttachment.SetRange("Uploaded Externally", true);
end;
end;
- local procedure GetDateFormulaFromExternalStorageSetup(): Text
+ local procedure GetDateFormulaFromExternalStorageSetup(): DateFormula
var
ExternalStorageSetup: Record "DA External Storage Setup";
begin
ExternalStorageSetup.Get();
- exit(Format(ExternalStorageSetup."Delete After".AsInteger()) + 'D');
+ exit(ExternalStorageSetup."Delete After");
end;
}
diff --git a/src/Apps/W1/External Storage - Document Attachments/app/src/DAExtStorageDeleteAfter.Enum.al b/src/Apps/W1/External Storage - Document Attachments/app/src/DAExtStorageDeleteAfter.Enum.al
deleted file mode 100644
index c173c4fc09..0000000000
--- a/src/Apps/W1/External Storage - Document Attachments/app/src/DAExtStorageDeleteAfter.Enum.al
+++ /dev/null
@@ -1,31 +0,0 @@
-// ------------------------------------------------------------------------------------------------
-// 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;
-
-///
-/// Defines when attachments should be deleted from internal storage after upload to external storage.
-///
-enum 8750 "DA Ext. Storage - Delete After"
-{
- 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';
- }
-}
diff --git a/src/Apps/W1/External Storage - Document Attachments/app/src/DAExternalStorageProcessor.Codeunit.al b/src/Apps/W1/External Storage - Document Attachments/app/src/DAExternalStorageProcessor.Codeunit.al
deleted file mode 100644
index a99364f386..0000000000
--- a/src/Apps/W1/External Storage - Document Attachments/app/src/DAExternalStorageProcessor.Codeunit.al
+++ /dev/null
@@ -1,381 +0,0 @@
-// ------------------------------------------------------------------------------------------------
-// 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;
-using System.Environment;
-using System.ExternalFileStorage;
-using System.Utilities;
-
-///
-/// Provides functionality to manage document attachments in external storage systems.
-/// Handles upload, download, and deletion operations for Business Central attachments.
-///
-codeunit 8750 "DA External Storage Processor"
-{
- Access = Internal;
- Permissions = tabledata "Tenant Media" = rimd,
- tabledata "Document Attachment" = rimd,
- tabledata "DA External Storage Setup" = r;
-
- ///
- /// Uploads a document attachment to external storage.
- ///
- /// The document attachment record to upload.
- /// True if upload was successful, false otherwise.
- internal procedure UploadToExternalStorage(var DocumentAttachment: Record "Document Attachment"): Boolean
- var
- FileAccount: Record "File Account";
- ExternalFileStorage: Codeunit "External File Storage";
- FileScenarioCU: Codeunit "File Scenario";
- TempBlob: Codeunit "Temp Blob";
- FileScenario: Enum "File Scenario";
- InStream: InStream;
- OutStream: OutStream;
- FileName: Text[2048];
- begin
- // Validate input parameters
- if not DocumentAttachment."Document Reference ID".HasValue() then
- exit(false);
-
- // Check if document is already uploaded
- if DocumentAttachment."External File Path" <> '' then
- exit(false);
-
- // Get file content from document attachment
- TempBlob.CreateOutStream(OutStream);
- DocumentAttachment.ExportToStream(OutStream);
- TempBlob.CreateInStream(InStream);
-
- // Generate unique filename to prevent collisions
- FileName := DocumentAttachment."File Name" + '-' + Format(CreateGuid()) + '.' + DocumentAttachment."File Extension";
-
- // Search for External Storage assigned File Scenario
- FileScenario := FileScenario::"Doc. Attach. - External Storage";
- if not FileScenarioCU.GetFileAccount(FileScenario, FileAccount) then
- exit(false);
-
- // Create the file with connector using the File Account framework
- ExternalFileStorage.Initialize(FileScenario);
- if ExternalFileStorage.CreateFile(FileName, InStream) then begin
- DocumentAttachment.MarkAsUploadedToExternal(FileName);
- exit(true);
- end;
-
- exit(false);
- end;
-
- ///
- /// Downloads a document attachment from external storage and prompts user to save it locally.
- ///
- /// The document attachment record to download.
- /// True if download was successful, false otherwise.
- internal procedure DownloadFromExternalStorage(var DocumentAttachment: Record "Document Attachment"): Boolean
- var
- FileAccount: Record "File Account";
- ExternalFileStorage: Codeunit "External File Storage";
- FileScenarioCU: Codeunit "File Scenario";
- FileScenario: Enum "File Scenario";
- InStream: InStream;
- ExternalFilePath, FileName : Text;
- begin
- // Validate input parameters
- if DocumentAttachment."External File Path" = '' then
- exit(false);
-
- if not DocumentAttachment."Uploaded Externally" then
- exit(false);
-
- // Use the stored external file path
- ExternalFilePath := DocumentAttachment."External File Path";
- FileName := DocumentAttachment."File Name" + '.' + DocumentAttachment."File Extension";
-
- // Search for External Storage assigned File Scenario
- FileScenario := FileScenario::"Doc. Attach. - External Storage";
- if not FileScenarioCU.GetFileAccount(FileScenario, FileAccount) then
- exit(false);
-
- // Get the file with connector using the File Account framework
- ExternalFileStorage.Initialize(FileScenario);
- ExternalFileStorage.GetFile(ExternalFilePath, InStream);
-
- exit(DownloadFromStream(InStream, '', '', '', FileName));
- end;
-
- ///
- /// Downloads a document attachment from external storage and saves it to internal storage.
- ///
- /// The document attachment record to download and restore internally.
- /// True if download and import was successful, false otherwise.
- internal procedure DownloadFromExternalStorageToInternal(var DocumentAttachment: Record "Document Attachment"): Boolean
- var
- FileAccount: Record "File Account";
- ExternalFileStorage: Codeunit "External File Storage";
- FileScenarioCU: Codeunit "File Scenario";
- FileScenario: Enum "File Scenario";
- InStream: InStream;
- ExternalFilePath, FileName : Text;
- begin
- // Validate input parameters
- if DocumentAttachment."External File Path" = '' then
- exit(false);
-
- if not DocumentAttachment."Uploaded Externally" then
- exit(false);
-
- // Use the stored external file path
- ExternalFilePath := DocumentAttachment."External File Path";
- FileName := DocumentAttachment."File Name" + '.' + DocumentAttachment."File Extension";
-
- // Search for External Storage assigned File Scenario
- FileScenario := FileScenario::"Doc. Attach. - External Storage";
- if not FileScenarioCU.GetFileAccount(FileScenario, FileAccount) then
- exit(false);
-
- // Get the file with connector using the File Account framework
- ExternalFileStorage.Initialize(FileScenario);
- ExternalFileStorage.GetFile(ExternalFilePath, InStream);
-
- // Import the file into the Document Attachment
- DocumentAttachment.ImportAttachment(InStream, FileName);
- DocumentAttachment."Deleted Internally" := false;
- DocumentAttachment.Modify();
-
- exit(true);
- end;
-
- ///
- /// Downloads a document attachment from external storage to a stream.
- ///
- /// The path of the external file to download.
- /// The output stream to write the attachment to.
- /// True if the download was successful, false otherwise.
- internal procedure DownloadFromExternalStorageToStream(ExternalFilePath: Text; var AttachmentOutStream: OutStream): Boolean
- var
- FileAccount: Record "File Account";
- ExternalFileStorage: Codeunit "External File Storage";
- FileScenarioCU: Codeunit "File Scenario";
- FileScenario: Enum "File Scenario";
- InStream: InStream;
- begin
- // Search for External Storage assigned File Scenario
- FileScenario := FileScenario::"Doc. Attach. - External Storage";
- if not FileScenarioCU.GetFileAccount(FileScenario, FileAccount) then
- exit(false);
-
- // Get the file from external storage
- ExternalFileStorage.Initialize(FileScenario);
- if not ExternalFileStorage.GetFile(ExternalFilePath, InStream) then
- exit(false);
-
- // Copy to output stream
- CopyStream(AttachmentOutStream, InStream);
- exit(true);
- end;
-
- ///
- /// Downloads a document attachment from external storage to a Temp Blob.
- ///
- /// The path of the external file to download.
- /// The temporary blob to store the downloaded content.
- /// True if the download was successful, false otherwise.
- internal procedure DownloadFromExternalStorageToTempBlob(ExternalFilePath: Text; var TempBlob: Codeunit "Temp Blob"): Boolean
- var
- FileAccount: Record "File Account";
- ExternalFileStorage: Codeunit "External File Storage";
- FileScenarioCU: Codeunit "File Scenario";
- FileScenario: Enum "File Scenario";
- InStream: InStream;
- OutStream: OutStream;
- begin
- // Search for External Storage assigned File Scenario
- FileScenario := FileScenario::"Doc. Attach. - External Storage";
- if not FileScenarioCU.GetFileAccount(FileScenario, FileAccount) then
- exit(false);
-
- // Get the file from external storage
- ExternalFileStorage.Initialize(FileScenario);
- if not ExternalFileStorage.GetFile(ExternalFilePath, InStream) then
- exit(false);
-
- // Copy to TempBlob
- TempBlob.CreateOutStream(OutStream);
- CopyStream(OutStream, InStream);
- exit(true);
- end;
-
- ///
- /// Checks if a file exists in external storage.
- ///
- /// The path of the external file to check.
- /// True if the file exists, false otherwise.
- internal procedure CheckIfFileExistInExternalStorage(ExternalFilePath: Text): Boolean
- var
- FileAccount: Record "File Account";
- ExternalFileStorage: Codeunit "External File Storage";
- FileScenarioCU: Codeunit "File Scenario";
- FileScenario: Enum "File Scenario";
- begin
- // Search for External Storage assigned File Scenario
- FileScenario := FileScenario::"Doc. Attach. - External Storage";
- if not FileScenarioCU.GetFileAccount(FileScenario, FileAccount) then
- exit(false);
-
- // Get the file from external storage
- ExternalFileStorage.Initialize(FileScenario);
- exit(ExternalFileStorage.FileExists(ExternalFilePath));
- end;
-
- ///
- /// Deletes a document attachment from external storage.
- ///
- /// The document attachment record to delete from external storage.
- /// True if deletion was successful, false otherwise.
- internal procedure DeleteFromExternalStorage(var DocumentAttachment: Record "Document Attachment"): Boolean
- var
- FileAccount: Record "File Account";
- ExternalFileStorage: Codeunit "External File Storage";
- FileScenarioCU: Codeunit "File Scenario";
- FileScenario: Enum "File Scenario";
- ExternalFilePath: Text;
- begin
- // Validate input parameters
- if DocumentAttachment."External File Path" = '' then
- exit(false);
-
- if not DocumentAttachment."Uploaded Externally" then
- exit(false);
-
- // Use the stored external file path
- ExternalFilePath := DocumentAttachment."External File Path";
-
- // Search for External Storage assigned File Scenario
- FileScenario := FileScenario::"Doc. Attach. - External Storage";
- if not FileScenarioCU.GetFileAccount(FileScenario, FileAccount) then
- exit(false);
-
- // Delete the file with connector using the File Account framework
- ExternalFileStorage.Initialize(FileScenario);
- if ExternalFileStorage.DeleteFile(ExternalFilePath) then begin
- DocumentAttachment.MarkAsNotUploadedToExternal();
- exit(true);
- end;
-
- exit(false);
- end;
-
- ///
- /// Deletes a document attachment from internal storage.
- ///
- /// The document attachment record to delete from internal storage.
- /// True if deletion was successful, false otherwise.
- internal procedure DeleteFromInternalStorage(var DocumentAttachment: Record "Document Attachment"): Boolean
- var
- TenantMedia: Record "Tenant Media";
- begin
- // Validate input parameters
- if not DocumentAttachment."Document Reference ID".HasValue() then
- exit(false);
-
- // Check if file is uploaded externally before deleting internally
- if not DocumentAttachment."Uploaded Externally" then
- exit(false);
-
- // Delete from Tenant Media
- if TenantMedia.Get(DocumentAttachment."Document Reference ID".MediaId()) then begin
- TenantMedia.Delete();
-
- // Mark Document Attachment as Deleted Internally
- DocumentAttachment.MarkAsDeletedInternally();
- exit(true);
- end;
-
- exit(false);
- end;
-
- ///
- /// Determines if files should be deleted immediately based on external storage setup.
- ///
- /// True if files should be deleted immediately, false otherwise.
- internal procedure ShouldBeDeleted(): Boolean
- var
- ExternalStorageSetup: Record "DA External Storage Setup";
- begin
- if not ExternalStorageSetup.Get() then
- exit(false);
-
- exit(ExternalStorageSetup."Delete After" = ExternalStorageSetup."Delete After"::Immediately);
- end;
-
- ///
- /// Maps file extensions to their corresponding MIME types.
- ///
- /// The document attachment record.
- /// The content type to set based on the file extension.
- internal procedure FileExtensionToContentMimeType(var Rec: Record "Document Attachment"; var ContentType: Text[100])
- begin
- // Determine content type based on file extension
- case LowerCase(Rec."File Extension") of
- 'pdf':
- ContentType := 'application/pdf';
- 'jpg', 'jpeg':
- ContentType := 'image/jpeg';
- 'png':
- ContentType := 'image/png';
- 'gif':
- ContentType := 'image/gif';
- 'bmp':
- ContentType := 'image/bmp';
- 'tiff', 'tif':
- ContentType := 'image/tiff';
- 'doc':
- ContentType := 'application/msword';
- 'docx':
- ContentType := 'application/vnd.openxmlformats-officedocument.wordprocessingml.document';
- 'xls':
- ContentType := 'application/vnd.ms-excel';
- 'xlsx':
- ContentType := 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
- 'ppt':
- ContentType := 'application/vnd.ms-powerpoint';
- 'pptx':
- ContentType := 'application/vnd.openxmlformats-officedocument.presentationml.presentation';
- 'txt':
- ContentType := 'text/plain';
- 'xml':
- ContentType := 'text/xml';
- 'html', 'htm':
- ContentType := 'text/html';
- 'zip':
- ContentType := 'application/zip';
- 'rar':
- ContentType := 'application/x-rar-compressed';
- else
- ContentType := 'application/octet-stream';
- end;
- end;
-
- ///
- /// Checks if a Document Attachment file is uploaded to external storage and deleted internally.
- ///
- /// The Document Attachment record to check.
- /// True if the file is uploaded and deleted, false otherwise.
- procedure IsFileUploadedToExternalStorageAndDeletedInternally(var DocumentAttachment: Record "Document Attachment"): Boolean
- begin
- if not DocumentAttachment."Deleted Internally" then
- exit(false);
-
- if not DocumentAttachment."Uploaded Externally" then
- exit(false);
-
- if DocumentAttachment."Document Reference ID".HasValue() then
- exit(false);
-
- if DocumentAttachment."External File Path" = '' then
- exit(false);
- exit(true);
- end;
-}
diff --git a/src/Apps/W1/External Storage - Document Attachments/app/src/DAExternalStorageSetup.Page.al b/src/Apps/W1/External Storage - Document Attachments/app/src/DAExternalStorageSetup.Page.al
index 38da822634..15759e967a 100644
--- a/src/Apps/W1/External Storage - Document Attachments/app/src/DAExternalStorageSetup.Page.al
+++ b/src/Apps/W1/External Storage - Document Attachments/app/src/DAExternalStorageSetup.Page.al
@@ -19,6 +19,7 @@ page 8750 "DA External Storage Setup"
InsertAllowed = false;
DeleteAllowed = false;
Extensible = false;
+ Permissions = tabledata "DA External Storage Setup" = rmid;
layout
{
diff --git a/src/Apps/W1/External Storage - Document Attachments/app/src/DAExternalStorageSetup.Table.al b/src/Apps/W1/External Storage - Document Attachments/app/src/DAExternalStorageSetup.Table.al
index a71c3115fd..52169b5f12 100644
--- a/src/Apps/W1/External Storage - Document Attachments/app/src/DAExternalStorageSetup.Table.al
+++ b/src/Apps/W1/External Storage - Document Attachments/app/src/DAExternalStorageSetup.Table.al
@@ -24,7 +24,7 @@ table 8750 "DA External Storage Setup"
Caption = 'Primary Key';
DataClassification = SystemMetadata;
}
- field(5; "Delete After"; Enum "DA Ext. Storage - Delete After")
+ field(5; "Delete After"; DateFormula)
{
Caption = 'Delete After';
ToolTip = 'Specifies when files should be automatically deleted.';
diff --git a/src/Apps/W1/External Storage - Document Attachments/app/src/DocumentAttachmentIntegration/DAExternalStorageImpl.Codeunit.al b/src/Apps/W1/External Storage - Document Attachments/app/src/DocumentAttachmentIntegration/DAExternalStorageImpl.Codeunit.al
index 85f2a5e689..ae302f4ed9 100644
--- a/src/Apps/W1/External Storage - Document Attachments/app/src/DocumentAttachmentIntegration/DAExternalStorageImpl.Codeunit.al
+++ b/src/Apps/W1/External Storage - Document Attachments/app/src/DocumentAttachmentIntegration/DAExternalStorageImpl.Codeunit.al
@@ -7,26 +7,32 @@ namespace Microsoft.ExternalStorage.DocumentAttachments;
using System.ExternalFileStorage;
using System.Utilities;
+using System.Environment;
+using Microsoft.Foundation.Attachment;
codeunit 8751 "DA External Storage Impl." implements "File Scenario"
{
Access = Internal;
InherentPermissions = X;
InherentEntitlements = X;
- Permissions = tabledata "DA External Storage Setup" = r;
+ Permissions = tabledata "Tenant Media" = rimd,
+ tabledata "Document Attachment" = rimd,
+ tabledata "File Account" = r,
+ tabledata "DA External Storage Setup" = r;
+ #region File Scenario Interface Implementation
///
/// Called before adding or modifying a file scenario.
///
/// The ID of the file scenario.
/// The file storage connector.
/// True if the operation is allowed, otherwise false.
- procedure BeforeAddOrModifyFileScenarioCheck(Scenario: Integer; Connector: Enum System.ExternalFileStorage."Ext. File Storage Connector") SkipInsertOrModify: Boolean
+ procedure BeforeAddOrModifyFileScenarioCheck(Scenario: Enum "File Scenario"; Connector: Enum System.ExternalFileStorage."Ext. File Storage Connector") SkipInsertOrModify: Boolean;
var
ConfirmManagement: Codeunit "Confirm Management";
DisclaimerMsg: Label 'You are about to enable External Storage!!!\\This feature is provided as-is, and you use it at your own risk.\Microsoft is not responsible for any issues or data loss that may occur.\\Do you wish to continue?';
begin
- if not (Scenario = Enum::"File Scenario"::"Doc. Attach. - External Storage".AsInteger()) then
+ if not (Scenario = Enum::"File Scenario"::"Doc. Attach. - External Storage") then
exit;
SkipInsertOrModify := not ConfirmManagement.GetResponseOrDefault(DisclaimerMsg);
@@ -38,11 +44,11 @@ codeunit 8751 "DA External Storage Impl." implements "File Scenario"
/// The ID of the file scenario.
/// The file storage connector.
/// True if additional setup is available, otherwise false.
- procedure GetAdditionalScenarioSetup(Scenario: Integer; Connector: Enum System.ExternalFileStorage."Ext. File Storage Connector") SetupExist: Boolean
+ procedure GetAdditionalScenarioSetup(Scenario: Enum "File Scenario"; Connector: Enum System.ExternalFileStorage."Ext. File Storage Connector") SetupExist: Boolean;
var
ExternalStorageSetup: Page "DA External Storage Setup";
begin
- if not (Scenario = Enum::"File Scenario"::"Doc. Attach. - External Storage".AsInteger()) then
+ if not (Scenario = Enum::"File Scenario"::"Doc. Attach. - External Storage") then
exit;
ExternalStorageSetup.RunModal();
@@ -55,12 +61,12 @@ codeunit 8751 "DA External Storage Impl." implements "File Scenario"
/// The ID of the file scenario.
/// The file storage connector.
/// True if the delete operation is handled and should not proceed, otherwise false.
- procedure BeforeDeleteFileScenarioCheck(Scenario: Integer; Connector: Enum System.ExternalFileStorage."Ext. File Storage Connector") SkipDelete: Boolean
+ procedure BeforeDeleteFileScenarioCheck(Scenario: Enum "File Scenario"; Connector: Enum System.ExternalFileStorage."Ext. File Storage Connector") SkipDelete: Boolean;
var
ExternalStorageSetup: Record "DA External Storage Setup";
NotPossibleToUnassignScenarioMsg: Label 'External Storage scenario can not be unassigned when there are uploaded files.';
begin
- if not (Scenario = Enum::"File Scenario"::"Doc. Attach. - External Storage".AsInteger()) then
+ if not (Scenario = Enum::"File Scenario"::"Doc. Attach. - External Storage") then
exit;
if not ExternalStorageSetup.Get() then
@@ -73,4 +79,523 @@ codeunit 8751 "DA External Storage Impl." implements "File Scenario"
SkipDelete := true;
Message(NotPossibleToUnassignScenarioMsg);
end;
+ #endregion
+
+ ///
+ /// Provides functionality to manage document attachments in external storage systems.
+ /// Handles upload, download, and deletion operations for Business Central attachments.
+ ///
+ #region External Storage Operations
+ ///
+ /// Uploads a document attachment to external storage.
+ ///
+ /// The document attachment record to upload.
+ /// True if upload was successful, false otherwise.
+ procedure UploadToExternalStorage(var DocumentAttachment: Record "Document Attachment"): Boolean
+ var
+ FileAccount: Record "File Account";
+ ExternalFileStorage: Codeunit "External File Storage";
+ FileScenarioCU: Codeunit "File Scenario";
+ TempBlob: Codeunit "Temp Blob";
+ FileScenario: Enum "File Scenario";
+ InStream: InStream;
+ OutStream: OutStream;
+ FileName: Text[2048];
+ begin
+ // Validate input parameters
+ if not DocumentAttachment."Document Reference ID".HasValue() then
+ exit(false);
+
+ // Check if document is already uploaded
+ if DocumentAttachment."External File Path" <> '' then
+ exit(false);
+
+ // Get file content from document attachment
+ TempBlob.CreateOutStream(OutStream);
+ DocumentAttachment.ExportToStream(OutStream);
+ TempBlob.CreateInStream(InStream);
+
+ // Generate unique filename to prevent collisions
+ FileName := DocumentAttachment."File Name" + '-' + Format(CreateGuid()) + '.' + DocumentAttachment."File Extension";
+
+ // Search for External Storage assigned File Scenario
+ FileScenario := FileScenario::"Doc. Attach. - External Storage";
+ if not FileScenarioCU.GetFileAccount(FileScenario, FileAccount) then
+ exit(false);
+
+ // Create the file with connector using the File Account framework
+ ExternalFileStorage.Initialize(FileScenario);
+ if ExternalFileStorage.CreateFile(FileName, InStream) then begin
+ DocumentAttachment."Uploaded Externally" := true;
+ DocumentAttachment."External Upload Date" := CurrentDateTime();
+ DocumentAttachment."External File Path" := FileName;
+ DocumentAttachment.Modify();
+ exit(true);
+ end;
+
+ exit(false);
+ end;
+
+ ///
+ /// Downloads a document attachment from external storage and prompts user to save it locally.
+ ///
+ /// The document attachment record to download.
+ /// True if download was successful, false otherwise.
+ procedure DownloadFromExternalStorage(var DocumentAttachment: Record "Document Attachment"): Boolean
+ var
+ FileAccount: Record "File Account";
+ ExternalFileStorage: Codeunit "External File Storage";
+ FileScenarioCU: Codeunit "File Scenario";
+ FileScenario: Enum "File Scenario";
+ InStream: InStream;
+ ExternalFilePath, FileName : Text;
+ begin
+ // Validate input parameters
+ if DocumentAttachment."External File Path" = '' then
+ exit(false);
+
+ if not DocumentAttachment."Uploaded Externally" then
+ exit(false);
+
+ // Use the stored external file path
+ ExternalFilePath := DocumentAttachment."External File Path";
+ FileName := DocumentAttachment."File Name" + '.' + DocumentAttachment."File Extension";
+
+ // Search for External Storage assigned File Scenario
+ FileScenario := FileScenario::"Doc. Attach. - External Storage";
+ if not FileScenarioCU.GetFileAccount(FileScenario, FileAccount) then
+ exit(false);
+
+ // Get the file with connector using the File Account framework
+ ExternalFileStorage.Initialize(FileScenario);
+ ExternalFileStorage.GetFile(ExternalFilePath, InStream);
+
+ exit(DownloadFromStream(InStream, '', '', '', FileName));
+ end;
+
+ ///
+ /// Downloads a document attachment from external storage and saves it to internal storage.
+ ///
+ /// The document attachment record to download and restore internally.
+ /// True if download and import was successful, false otherwise.
+ procedure DownloadFromExternalStorageToInternal(var DocumentAttachment: Record "Document Attachment"): Boolean
+ var
+ FileAccount: Record "File Account";
+ ExternalFileStorage: Codeunit "External File Storage";
+ FileScenarioCU: Codeunit "File Scenario";
+ FileScenario: Enum "File Scenario";
+ InStream: InStream;
+ ExternalFilePath, FileName : Text;
+ begin
+ // Validate input parameters
+ if DocumentAttachment."External File Path" = '' then
+ exit(false);
+
+ if not DocumentAttachment."Uploaded Externally" then
+ exit(false);
+
+ // Use the stored external file path
+ ExternalFilePath := DocumentAttachment."External File Path";
+ FileName := DocumentAttachment."File Name" + '.' + DocumentAttachment."File Extension";
+
+ // Search for External Storage assigned File Scenario
+ FileScenario := FileScenario::"Doc. Attach. - External Storage";
+ if not FileScenarioCU.GetFileAccount(FileScenario, FileAccount) then
+ exit(false);
+
+ // Get the file with connector using the File Account framework
+ ExternalFileStorage.Initialize(FileScenario);
+ ExternalFileStorage.GetFile(ExternalFilePath, InStream);
+
+ // Import the file into the Document Attachment
+ DocumentAttachment.ImportAttachment(InStream, FileName);
+ DocumentAttachment."Deleted Internally" := false;
+ DocumentAttachment.Modify();
+
+ exit(true);
+ end;
+
+ ///
+ /// Downloads a document attachment from external storage to a stream.
+ ///
+ /// The path of the external file to download.
+ /// The output stream to write the attachment to.
+ /// True if the download was successful, false otherwise.
+ procedure DownloadFromExternalStorageToStream(ExternalFilePath: Text; var AttachmentOutStream: OutStream): Boolean
+ var
+ FileAccount: Record "File Account";
+ ExternalFileStorage: Codeunit "External File Storage";
+ FileScenarioCU: Codeunit "File Scenario";
+ FileScenario: Enum "File Scenario";
+ InStream: InStream;
+ begin
+ // Search for External Storage assigned File Scenario
+ FileScenario := FileScenario::"Doc. Attach. - External Storage";
+ if not FileScenarioCU.GetFileAccount(FileScenario, FileAccount) then
+ exit(false);
+
+ // Get the file from external storage
+ ExternalFileStorage.Initialize(FileScenario);
+ if not ExternalFileStorage.GetFile(ExternalFilePath, InStream) then
+ exit(false);
+
+ // Copy to output stream
+ CopyStream(AttachmentOutStream, InStream);
+ exit(true);
+ end;
+
+ ///
+ /// Downloads a document attachment from external storage to a Temp Blob.
+ ///
+ /// The path of the external file to download.
+ /// The temporary blob to store the downloaded content.
+ /// True if the download was successful, false otherwise.
+ procedure DownloadFromExternalStorageToTempBlob(ExternalFilePath: Text; var TempBlob: Codeunit "Temp Blob"): Boolean
+ var
+ FileAccount: Record "File Account";
+ ExternalFileStorage: Codeunit "External File Storage";
+ FileScenarioCU: Codeunit "File Scenario";
+ FileScenario: Enum "File Scenario";
+ InStream: InStream;
+ OutStream: OutStream;
+ begin
+ // Search for External Storage assigned File Scenario
+ FileScenario := FileScenario::"Doc. Attach. - External Storage";
+ if not FileScenarioCU.GetFileAccount(FileScenario, FileAccount) then
+ exit(false);
+
+ // Get the file from external storage
+ ExternalFileStorage.Initialize(FileScenario);
+ if not ExternalFileStorage.GetFile(ExternalFilePath, InStream) then
+ exit(false);
+
+ // Copy to TempBlob
+ TempBlob.CreateOutStream(OutStream);
+ CopyStream(OutStream, InStream);
+ exit(true);
+ end;
+
+ ///
+ /// Checks if a file exists in external storage.
+ ///
+ /// The path of the external file to check.
+ /// True if the file exists, false otherwise.
+ procedure CheckIfFileExistInExternalStorage(ExternalFilePath: Text): Boolean
+ var
+ FileAccount: Record "File Account";
+ ExternalFileStorage: Codeunit "External File Storage";
+ FileScenarioCU: Codeunit "File Scenario";
+ FileScenario: Enum "File Scenario";
+ begin
+ // Search for External Storage assigned File Scenario
+ FileScenario := FileScenario::"Doc. Attach. - External Storage";
+ if not FileScenarioCU.GetFileAccount(FileScenario, FileAccount) then
+ exit(false);
+
+ // Get the file from external storage
+ ExternalFileStorage.Initialize(FileScenario);
+ exit(ExternalFileStorage.FileExists(ExternalFilePath));
+ end;
+
+ ///
+ /// Deletes a document attachment from external storage.
+ ///
+ /// The document attachment record to delete from external storage.
+ /// True if deletion was successful, false otherwise.
+ procedure DeleteFromExternalStorage(var DocumentAttachment: Record "Document Attachment"): Boolean
+ var
+ FileAccount: Record "File Account";
+ ExternalFileStorage: Codeunit "External File Storage";
+ FileScenarioCU: Codeunit "File Scenario";
+ FileScenario: Enum "File Scenario";
+ ExternalFilePath: Text;
+ begin
+ // Validate input parameters
+ if DocumentAttachment."External File Path" = '' then
+ exit(false);
+
+ if not DocumentAttachment."Uploaded Externally" then
+ exit(false);
+
+ // Use the stored external file path
+ ExternalFilePath := DocumentAttachment."External File Path";
+
+ // Search for External Storage assigned File Scenario
+ FileScenario := FileScenario::"Doc. Attach. - External Storage";
+ if not FileScenarioCU.GetFileAccount(FileScenario, FileAccount) then
+ exit(false);
+
+ // Delete the file with connector using the File Account framework
+ ExternalFileStorage.Initialize(FileScenario);
+ if ExternalFileStorage.DeleteFile(ExternalFilePath) then begin
+ DocumentAttachment.MarkAsNotUploadedToExternal();
+ exit(true);
+ end;
+
+ exit(false);
+ end;
+
+ ///
+ /// Deletes a document attachment from internal storage.
+ ///
+ /// The document attachment record to delete from internal storage.
+ /// True if deletion was successful, false otherwise.
+ procedure DeleteFromInternalStorage(var DocumentAttachment: Record "Document Attachment"): Boolean
+ var
+ TenantMedia: Record "Tenant Media";
+ begin
+ // Validate input parameters
+ if not DocumentAttachment."Document Reference ID".HasValue() then
+ exit(false);
+
+ // Check if file is uploaded externally before deleting internally
+ if not DocumentAttachment."Uploaded Externally" then
+ exit(false);
+
+ // Delete from Tenant Media
+ if TenantMedia.Get(DocumentAttachment."Document Reference ID".MediaId()) then begin
+ TenantMedia.Delete();
+
+ // Mark Document Attachment as Deleted Internally
+ DocumentAttachment.MarkAsDeletedInternally();
+ exit(true);
+ end;
+
+ exit(false);
+ end;
+
+ ///
+ /// Determines if files should be deleted immediately based on external storage setup.
+ ///
+ /// True if files should be deleted immediately, false otherwise.
+ procedure ShouldBeDeleted(): Boolean
+ var
+ ExternalStorageSetup: Record "DA External Storage Setup";
+ begin
+ if not ExternalStorageSetup.Get() then
+ exit(false);
+
+ exit(CalcDate(ExternalStorageSetup."Delete After", Today()) <= Today());
+ end;
+
+ ///
+ /// Maps file extensions to their corresponding MIME types.
+ ///
+ /// The document attachment record.
+ /// The content type to set based on the file extension.
+ procedure FileExtensionToContentMimeType(var Rec: Record "Document Attachment"; var ContentType: Text[100])
+ begin
+ // Determine content type based on file extension
+ case LowerCase(Rec."File Extension") of
+ 'pdf':
+ ContentType := 'application/pdf';
+ 'jpg', 'jpeg':
+ ContentType := 'image/jpeg';
+ 'png':
+ ContentType := 'image/png';
+ 'gif':
+ ContentType := 'image/gif';
+ 'bmp':
+ ContentType := 'image/bmp';
+ 'tiff', 'tif':
+ ContentType := 'image/tiff';
+ 'doc':
+ ContentType := 'application/msword';
+ 'docx':
+ ContentType := 'application/vnd.openxmlformats-officedocument.wordprocessingml.document';
+ 'xls':
+ ContentType := 'application/vnd.ms-excel';
+ 'xlsx':
+ ContentType := 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
+ 'ppt':
+ ContentType := 'application/vnd.ms-powerpoint';
+ 'pptx':
+ ContentType := 'application/vnd.openxmlformats-officedocument.presentationml.presentation';
+ 'txt':
+ ContentType := 'text/plain';
+ 'xml':
+ ContentType := 'text/xml';
+ 'html', 'htm':
+ ContentType := 'text/html';
+ 'zip':
+ ContentType := 'application/zip';
+ 'rar':
+ ContentType := 'application/x-rar-compressed';
+ else
+ ContentType := 'application/octet-stream';
+ end;
+ end;
+
+ ///
+ /// Checks if a Document Attachment file is uploaded to external storage and deleted internally.
+ ///
+ /// The Document Attachment record to check.
+ /// True if the file is uploaded and deleted, false otherwise.
+ procedure IsFileUploadedToExternalStorageAndDeletedInternally(var DocumentAttachment: Record "Document Attachment"): Boolean
+ begin
+ if not DocumentAttachment."Deleted Internally" then
+ exit(false);
+
+ if not DocumentAttachment."Uploaded Externally" then
+ exit(false);
+
+ if DocumentAttachment."Document Reference ID".HasValue() then
+ exit(false);
+
+ if DocumentAttachment."External File Path" = '' then
+ exit(false);
+ exit(true);
+ end;
+ #endregion
+
+ #region Document Attachment Handling
+ ///
+ /// Handles automatic upload of new document attachments to external storage upon insertion of the attachment record.
+ ///
+ /// The document attachment record.
+ /// Indicates if the trigger should run.
+ [EventSubscriber(ObjectType::Table, Database::"Document Attachment", OnAfterInsertEvent, '', true, true)]
+ local procedure OnAfterInsertDocumentAttachment(var Rec: Record "Document Attachment"; RunTrigger: Boolean)
+ var
+ ExternalStorageSetup: Record "DA External Storage Setup";
+ ExternalStorageImpl: Codeunit "DA External Storage Impl.";
+ begin
+ // Exit early if trigger is not running
+ if not RunTrigger then
+ exit;
+
+ // Temporary records are not processed
+ if Rec.IsTemporary() then
+ exit;
+
+ // Check if auto upload is enabled
+ if not ExternalStorageSetup.Get() then
+ exit;
+
+ if not ExternalStorageSetup."Auto Upload" then
+ exit;
+
+ // Only process files with actual content
+ if not Rec."Document Reference ID".HasValue() then
+ exit;
+
+ // Upload to external storage
+ if not ExternalStorageImpl.UploadToExternalStorage(Rec) then
+ exit;
+
+ // Check if it should be immediately deleted
+ if ExternalStorageImpl.ShouldBeDeleted() then
+ ExternalStorageImpl.DeleteFromInternalStorage(Rec);
+ end;
+
+ ///
+ /// Handles automatic deletion of document attachments from external storage upon deletion of the attachment record.
+ ///
+ /// The document attachment record.
+ /// Indicates if the trigger should run.
+ [EventSubscriber(ObjectType::Table, Database::"Document Attachment", OnAfterDeleteEvent, '', true, true)]
+ local procedure OnAfterDeleteDocumentAttachment(var Rec: Record "Document Attachment"; RunTrigger: Boolean)
+ var
+ ExternalStorageSetup: Record "DA External Storage Setup";
+ ExternalStorageImpl: Codeunit "DA External Storage Impl.";
+ begin
+ // Exit early if trigger is not running
+ if not RunTrigger then
+ exit;
+
+ // Temporary records are not processed
+ if Rec.IsTemporary() then
+ exit;
+
+ // Check if auto upload is enabled
+ if not ExternalStorageSetup.Get() then
+ exit;
+
+ if not ExternalStorageSetup."Auto Delete" then
+ exit;
+
+ // Only process files that were uploaded to external storage
+ if not Rec."Uploaded Externally" then
+ exit;
+
+ // Delete from external storage
+ ExternalStorageImpl.DeleteFromExternalStorage(Rec);
+ end;
+
+ ///
+ /// Handles exporting document attachment content to a stream for externally stored attachments.
+ ///
+ /// The document attachment record.
+ /// The output stream for the attachment content.
+ /// Indicates if the event has been handled.
+ [EventSubscriber(ObjectType::Table, Database::"Document Attachment", OnBeforeExportToStream, '', false, false)]
+ local procedure DocumentAttachment_OnBeforeExportToStream(var DocumentAttachment: Record "Document Attachment"; var AttachmentOutStream: OutStream; var IsHandled: Boolean)
+ var
+ ExternalStorageImpl: Codeunit "DA External Storage Impl.";
+ begin
+ // Only handle if file is uploaded externally and not available internally
+ if not ExternalStorageImpl.IsFileUploadedToExternalStorageAndDeletedInternally(DocumentAttachment) then
+ exit;
+
+ ExternalStorageImpl.DownloadFromExternalStorageToStream(DocumentAttachment."External File Path", AttachmentOutStream);
+ IsHandled := true;
+ end;
+
+ ///
+ /// Handles getting the temporary blob for externally stored document attachments.
+ ///
+ /// The document attachment record.
+ /// The temporary blob to be filled.
+ /// Indicates if the event has been handled.
+ [EventSubscriber(ObjectType::Table, Database::"Document Attachment", OnBeforeGetAsTempBlob, '', false, false)]
+ local procedure DocumentAttachment_OnBeforeGetAsTempBlob(var DocumentAttachment: Record "Document Attachment"; var TempBlob: Codeunit "Temp Blob"; var IsHandled: Boolean)
+ var
+ ExternalStorageImpl: Codeunit "DA External Storage Impl.";
+ begin
+ // Only handle if file is uploaded externally and not available internally
+ if not ExternalStorageImpl.IsFileUploadedToExternalStorageAndDeletedInternally(DocumentAttachment) then
+ exit;
+
+ ExternalStorageImpl.DownloadFromExternalStorageToTempBlob(DocumentAttachment."External File Path", TempBlob);
+ IsHandled := true;
+ end;
+
+ ///
+ /// Handles getting content type for externally stored document attachments.
+ ///
+ /// The document attachment record.
+ /// The content type to be set.
+ /// Indicates if the event has been handled.
+ [EventSubscriber(ObjectType::Table, Database::"Document Attachment", OnBeforeGetContentType, '', false, false)]
+ local procedure DocumentAttachment_OnBeforeGetContentType(var Rec: Record "Document Attachment"; var ContentType: Text[100]; var IsHandled: Boolean)
+ var
+ ExternalStorageImpl: Codeunit "DA External Storage Impl.";
+ begin
+ // Only handle if file is uploaded externally and not available internally
+ if not ExternalStorageImpl.IsFileUploadedToExternalStorageAndDeletedInternally(Rec) then
+ exit;
+
+ ExternalStorageImpl.FileExtensionToContentMimeType(Rec, ContentType);
+ IsHandled := true;
+ end;
+
+ ///
+ /// Handles checking if attachment content is available for externally stored document attachments.
+ ///
+ /// The document attachment record.
+ /// Indicates if the attachment is available.
+ /// Indicates if the event has been handled.
+ [EventSubscriber(ObjectType::Table, Database::"Document Attachment", OnBeforeHasContent, '', false, false)]
+ local procedure DocumentAttachment_OnBeforeHasContent(var DocumentAttachment: Record "Document Attachment"; var AttachmentIsAvailable: Boolean; var IsHandled: Boolean)
+ var
+ ExternalStorageImpl: Codeunit "DA External Storage Impl.";
+ begin
+ // Only handle if file is uploaded externally and not available internally
+ if not ExternalStorageImpl.IsFileUploadedToExternalStorageAndDeletedInternally(DocumentAttachment) then
+ exit;
+
+ AttachmentIsAvailable := ExternalStorageImpl.CheckIfFileExistInExternalStorage(DocumentAttachment."External File Path");
+ IsHandled := true;
+ end;
+ #endregion
}
\ No newline at end of file
diff --git a/src/Apps/W1/External Storage - Document Attachments/app/src/DocumentAttachmentIntegration/DAExternalStorageSubs.Codeunit.al b/src/Apps/W1/External Storage - Document Attachments/app/src/DocumentAttachmentIntegration/DAExternalStorageSubs.Codeunit.al
deleted file mode 100644
index f2b63809df..0000000000
--- a/src/Apps/W1/External Storage - Document Attachments/app/src/DocumentAttachmentIntegration/DAExternalStorageSubs.Codeunit.al
+++ /dev/null
@@ -1,162 +0,0 @@
-// ------------------------------------------------------------------------------------------------
-// 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;
-using System.Utilities;
-
-///
-/// Event subscribers for External Storage functionality.
-/// Handles automatic upload of new attachments and cleanup operations.
-///
-codeunit 8752 "DA External Storage Subs."
-{
- Access = Internal;
- Permissions = tabledata "DA External Storage Setup" = r;
-
- #region Document Attachment Handling
- ///
- /// Handles automatic upload of new document attachments to external storage upon insertion of the attachment record.
- ///
- /// The document attachment record.
- /// Indicates if the trigger should run.
- [EventSubscriber(ObjectType::Table, Database::"Document Attachment", OnAfterInsertEvent, '', true, true)]
- local procedure OnAfterInsertDocumentAttachment(var Rec: Record "Document Attachment"; RunTrigger: Boolean)
- var
- ExternalStorageSetup: Record "DA External Storage Setup";
- ExternalStorageProcessor: Codeunit "DA External Storage Processor";
- begin
- // Exit early if trigger is not running
- if not RunTrigger then
- exit;
-
- // Check if auto upload is enabled
- if not ExternalStorageSetup.Get() then
- exit;
-
- if not ExternalStorageSetup."Auto Upload" then
- exit;
-
- // Only process files with actual content
- if not Rec."Document Reference ID".HasValue() then
- exit;
-
- // Upload to external storage
- if not ExternalStorageProcessor.UploadToExternalStorage(Rec) then
- exit;
-
- // Check if it should be immediately deleted
- if ExternalStorageProcessor.ShouldBeDeleted() then
- ExternalStorageProcessor.DeleteFromInternalStorage(Rec);
- end;
-
- ///
- /// Handles automatic deletion of document attachments from external storage upon deletion of the attachment record.
- ///
- /// The document attachment record.
- /// Indicates if the trigger should run.
- [EventSubscriber(ObjectType::Table, Database::"Document Attachment", OnAfterDeleteEvent, '', true, true)]
- local procedure OnAfterDeleteDocumentAttachment(var Rec: Record "Document Attachment"; RunTrigger: Boolean)
- var
- ExternalStorageSetup: Record "DA External Storage Setup";
- ExternalStorageProcessor: Codeunit "DA External Storage Processor";
- begin
- // Exit early if trigger is not running
- if not RunTrigger then
- exit;
-
- // Check if auto upload is enabled
- if not ExternalStorageSetup.Get() then
- exit;
-
- if not ExternalStorageSetup."Auto Delete" then
- exit;
-
- // Only process files that were uploaded to external storage
- if not Rec."Uploaded Externally" then
- exit;
-
- // Delete from external storage
- ExternalStorageProcessor.DeleteFromExternalStorage(Rec);
- end;
-
- ///
- /// Handles exporting document attachment content to a stream for externally stored attachments.
- ///
- /// The document attachment record.
- /// The output stream for the attachment content.
- /// Indicates if the event has been handled.
- [EventSubscriber(ObjectType::Table, Database::"Document Attachment", OnBeforeExportToStream, '', false, false)]
- local procedure DocumentAttachment_OnBeforeExportToStream(var DocumentAttachment: Record "Document Attachment"; var AttachmentOutStream: OutStream; var IsHandled: Boolean)
- var
- ExternalStorageProcessor: Codeunit "DA External Storage Processor";
- begin
- // Only handle if file is uploaded externally and not available internally
- if not ExternalStorageProcessor.IsFileUploadedToExternalStorageAndDeletedInternally(DocumentAttachment) then
- exit;
-
- ExternalStorageProcessor.DownloadFromExternalStorageToStream(DocumentAttachment."External File Path", AttachmentOutStream);
- IsHandled := true;
- end;
-
- ///
- /// Handles getting the temporary blob for externally stored document attachments.
- ///
- /// The document attachment record.
- /// The temporary blob to be filled.
- /// Indicates if the event has been handled.
- [EventSubscriber(ObjectType::Table, Database::"Document Attachment", OnBeforeGetAsTempBlob, '', false, false)]
- local procedure DocumentAttachment_OnBeforeGetAsTempBlob(var DocumentAttachment: Record "Document Attachment"; var TempBlob: Codeunit "Temp Blob"; var IsHandled: Boolean)
- var
- ExternalStorageProcessor: Codeunit "DA External Storage Processor";
- begin
- // Only handle if file is uploaded externally and not available internally
- if not ExternalStorageProcessor.IsFileUploadedToExternalStorageAndDeletedInternally(DocumentAttachment) then
- exit;
-
- ExternalStorageProcessor.DownloadFromExternalStorageToTempBlob(DocumentAttachment."External File Path", TempBlob);
- IsHandled := true;
- end;
-
- ///
- /// Handles getting content type for externally stored document attachments.
- ///
- /// The document attachment record.
- /// The content type to be set.
- /// Indicates if the event has been handled.
- [EventSubscriber(ObjectType::Table, Database::"Document Attachment", OnBeforeGetContentType, '', false, false)]
- local procedure DocumentAttachment_OnBeforeGetContentType(var Rec: Record "Document Attachment"; var ContentType: Text[100]; var IsHandled: Boolean)
- var
- ExternalStorageProcessor: Codeunit "DA External Storage Processor";
- begin
- // Only handle if file is uploaded externally and not available internally
- if not ExternalStorageProcessor.IsFileUploadedToExternalStorageAndDeletedInternally(Rec) then
- exit;
-
- ExternalStorageProcessor.FileExtensionToContentMimeType(Rec, ContentType);
- IsHandled := true;
- end;
-
- ///
- /// Handles checking if attachment content is available for externally stored document attachments.
- ///
- /// The document attachment record.
- /// Indicates if the attachment is available.
- /// Indicates if the event has been handled.
- [EventSubscriber(ObjectType::Table, Database::"Document Attachment", OnBeforeHasContent, '', false, false)]
- local procedure DocumentAttachment_OnBeforeHasContent(var DocumentAttachment: Record "Document Attachment"; var AttachmentIsAvailable: Boolean; var IsHandled: Boolean)
- var
- ExternalStorageProcessor: Codeunit "DA External Storage Processor";
- begin
- // Only handle if file is uploaded externally and not available internally
- if not ExternalStorageProcessor.IsFileUploadedToExternalStorageAndDeletedInternally(DocumentAttachment) then
- exit;
-
- AttachmentIsAvailable := ExternalStorageProcessor.CheckIfFileExistInExternalStorage(DocumentAttachment."External File Path");
- IsHandled := true;
- end;
- #endregion
-}
diff --git a/src/Apps/W1/External Storage - Document Attachments/app/src/DocumentAttachmentIntegration/DocumentAttachmentExtStor.TableExt.al b/src/Apps/W1/External Storage - Document Attachments/app/src/DocumentAttachmentIntegration/DocumentAttachmentExtStor.TableExt.al
index a126502b30..a13a3f64e7 100644
--- a/src/Apps/W1/External Storage - Document Attachments/app/src/DocumentAttachmentIntegration/DocumentAttachmentExtStor.TableExt.al
+++ b/src/Apps/W1/External Storage - Document Attachments/app/src/DocumentAttachmentIntegration/DocumentAttachmentExtStor.TableExt.al
@@ -45,18 +45,6 @@ tableextension 8750 "Document Attachment Ext.Stor." extends "Document Attachment
}
}
- ///
- /// Marks the document attachment as uploaded to external storage.
- ///
- /// The path to the file in external storage.
- internal procedure MarkAsUploadedToExternal(ExternalFilePath: Text[2048])
- begin
- "Uploaded Externally" := true;
- "External Upload Date" := CurrentDateTime();
- "External File Path" := ExternalFilePath;
- Modify();
- end;
-
///
/// Marks the document attachment as not uploaded to external storage.
/// Clears all external storage related fields.
diff --git a/src/Apps/W1/External Storage - Document Attachments/app/src/DocumentAttachmentIntegration/DocumentAttachmentExternal.Page.al b/src/Apps/W1/External Storage - Document Attachments/app/src/DocumentAttachmentIntegration/DocumentAttachmentExternal.Page.al
index a883f87b79..cefbe51873 100644
--- a/src/Apps/W1/External Storage - Document Attachments/app/src/DocumentAttachmentIntegration/DocumentAttachmentExternal.Page.al
+++ b/src/Apps/W1/External Storage - Document Attachments/app/src/DocumentAttachmentIntegration/DocumentAttachmentExternal.Page.al
@@ -84,12 +84,12 @@ page 8751 "Document Attachment - External"
trigger OnAction()
var
DocumentAttachment: Record "Document Attachment";
- ExternalStorageProcessor: Codeunit "DA External Storage Processor";
+ ExternalStorageImpl: Codeunit "DA External Storage Impl.";
begin
CurrPage.SetSelectionFilter(DocumentAttachment);
if DocumentAttachment.FindSet() then
repeat
- if ExternalStorageProcessor.UploadToExternalStorage(DocumentAttachment) then
+ if ExternalStorageImpl.UploadToExternalStorage(DocumentAttachment) then
Message(FileUploadedMsg)
else
Message(FailedFileUploadMsg);
@@ -105,9 +105,9 @@ page 8751 "Document Attachment - External"
trigger OnAction()
var
- ExternalStorageProcessor: Codeunit "DA External Storage Processor";
+ ExternalStorageImpl: Codeunit "DA External Storage Impl.";
begin
- if ExternalStorageProcessor.DownloadFromExternalStorage(Rec) then
+ if ExternalStorageImpl.DownloadFromExternalStorage(Rec) then
Message(FileDownloadedMsg)
else
Message(FailedFileDownloadMsg);
@@ -122,9 +122,9 @@ page 8751 "Document Attachment - External"
trigger OnAction()
var
- ExternalStorageProcessor: Codeunit "DA External Storage Processor";
+ ExternalStorageImpl: Codeunit "DA External Storage Impl.";
begin
- if ExternalStorageProcessor.DownloadFromExternalStorageToInternal(Rec) then
+ if ExternalStorageImpl.DownloadFromExternalStorageToInternal(Rec) then
Message(FileDownloadedMsg)
else
Message(FailedFileDownloadMsg);
@@ -140,10 +140,10 @@ page 8751 "Document Attachment - External"
trigger OnAction()
var
- ExternalStorageProcessor: Codeunit "DA External Storage Processor";
+ ExternalStorageImpl: Codeunit "DA External Storage Impl.";
begin
if Confirm(DeleteFileFromExternalStorageQst) then
- if ExternalStorageProcessor.DeleteFromExternalStorage(Rec) then
+ if ExternalStorageImpl.DeleteFromExternalStorage(Rec) then
Message(FileDeletedExternalStorageMsg)
else
Message(FailedFileDeleteExternalStorageMsg);
@@ -159,10 +159,10 @@ page 8751 "Document Attachment - External"
trigger OnAction()
var
- ExternalStorageProcessor: Codeunit "DA External Storage Processor";
+ ExternalStorageImpl: Codeunit "DA External Storage Impl.";
begin
if Confirm(DeleteFileFromIntStorageQst) then
- if ExternalStorageProcessor.DeleteFromInternalStorage(Rec) then
+ if ExternalStorageImpl.DeleteFromInternalStorage(Rec) then
Message(FileDeletedIntStorageMsg)
else
Message(FailedFileDeleteIntStorageMsg);
diff --git a/src/Apps/W1/External Storage - Document Attachments/app/src/Permissions/DAExtStorAdmin.PermissionSet.al b/src/Apps/W1/External Storage - Document Attachments/app/src/Permissions/DAExtStorAdmin.PermissionSet.al
index babc6bd859..72c1d7b2fc 100644
--- a/src/Apps/W1/External Storage - Document Attachments/app/src/Permissions/DAExtStorAdmin.PermissionSet.al
+++ b/src/Apps/W1/External Storage - Document Attachments/app/src/Permissions/DAExtStorAdmin.PermissionSet.al
@@ -12,6 +12,5 @@ permissionset 8751 "DA Ext. Stor. Admin"
{
Assignable = true;
Caption = 'DA - External Storage Admin';
- IncludedPermissionSets = "DA Ext. Stor. View";
Permissions = tabledata "DA External Storage Setup" = IMD;
}
\ No newline at end of file
diff --git a/src/Apps/W1/External Storage - Document Attachments/app/src/Permissions/DAExtStorExec.PermissionSet.al b/src/Apps/W1/External Storage - Document Attachments/app/src/Permissions/DAExtStorExec.PermissionSet.al
deleted file mode 100644
index 8576fed8bf..0000000000
--- a/src/Apps/W1/External Storage - Document Attachments/app/src/Permissions/DAExtStorExec.PermissionSet.al
+++ /dev/null
@@ -1,22 +0,0 @@
-// ------------------------------------------------------------------------------------------------
-// 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;
-
-///
-/// Permission set for External Storage functionality.
-/// Grants necessary permissions to use external storage features.
-///
-permissionset 8752 "DA Ext. Stor. Exec."
-{
- Assignable = false;
- Caption = 'DA - External Storage Exec.';
- Permissions = table "DA External Storage Setup" = X,
- page "DA External Storage Setup" = X,
- page "Document Attachment - External" = X,
- report "DA External Storage Sync" = X,
- codeunit "DA External Storage Processor" = X,
- codeunit "DA External Storage Subs." = X,
- codeunit "DA External Storage Impl." = X;
-}
\ No newline at end of file
diff --git a/src/Apps/W1/External Storage - Document Attachments/app/src/Permissions/DAExtStorView.PermissionSet.al b/src/Apps/W1/External Storage - Document Attachments/app/src/Permissions/DAExtStorView.PermissionSet.al
deleted file mode 100644
index dd27bf0e1e..0000000000
--- a/src/Apps/W1/External Storage - Document Attachments/app/src/Permissions/DAExtStorView.PermissionSet.al
+++ /dev/null
@@ -1,18 +0,0 @@
-// ------------------------------------------------------------------------------------------------
-// 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;
-
-///
-/// Permission set for External Storage functionality.
-/// Grants necessary permissions to use external storage features.
-///
-permissionset 8750 "DA Ext. Stor. View"
-{
- Assignable = true;
- Caption = 'DA - External Storage View';
- IncludedPermissionSets = "DA Ext. Stor. Exec.";
- Permissions = tabledata "DA External Storage Setup" = R;
-}
\ No newline at end of file
From bf2f5b68db372b7a116e57dcb63d686981b813c7 Mon Sep 17 00:00:00 2001
From: Stefan Sosic <59221826+StefanSosic@users.noreply.github.com>
Date: Tue, 21 Oct 2025 14:31:54 +0200
Subject: [PATCH 18/34] Fixes and Features
---
.../DAExternalStorageSync.Report.al | 16 +
.../app/src/DAExternalStorageSetup.Page.al | 90 -----
.../app/src/DAExternalStorageSetup.Table.al | 61 ----
.../DAExternalStorageImpl.Codeunit.al | 331 +++++++++++++++++-
.../DocumentAttachmentExtStor.TableExt.al | 9 +-
.../DocumentAttachmentExternal.Page.al | 53 ++-
.../src/Setup/DAExternalStorageSetup.Page.al | 246 +++++++++++++
.../src/Setup/DAExternalStorageSetup.Table.al | 184 ++++++++++
.../Telemetry/DAFeatureTelemetry.Codeunit.al | 70 ++++
.../Telemetry/DATelemetryLogger.Codeunit.al | 30 ++
.../DefaultFileScenarioImpl.Codeunit.al | 6 +-
.../src/Scenario/FileScenario.Interface.al | 6 +-
.../src/Scenario/FileScenarioImpl.Codeunit.al | 4 +-
.../src/Scenario/FileScenarioSetup.Page.al | 2 +-
.../src/Scenario/FileScenariosFactBox.Page.al | 24 ++
15 files changed, 939 insertions(+), 193 deletions(-)
delete mode 100644 src/Apps/W1/External Storage - Document Attachments/app/src/DAExternalStorageSetup.Page.al
delete mode 100644 src/Apps/W1/External Storage - Document Attachments/app/src/DAExternalStorageSetup.Table.al
create mode 100644 src/Apps/W1/External Storage - Document Attachments/app/src/Setup/DAExternalStorageSetup.Page.al
create mode 100644 src/Apps/W1/External Storage - Document Attachments/app/src/Setup/DAExternalStorageSetup.Table.al
create mode 100644 src/Apps/W1/External Storage - Document Attachments/app/src/Telemetry/DAFeatureTelemetry.Codeunit.al
create mode 100644 src/Apps/W1/External Storage - Document Attachments/app/src/Telemetry/DATelemetryLogger.Codeunit.al
diff --git a/src/Apps/W1/External Storage - Document Attachments/app/src/AutomaticSync/DAExternalStorageSync.Report.al b/src/Apps/W1/External Storage - Document Attachments/app/src/AutomaticSync/DAExternalStorageSync.Report.al
index dbd3b1faea..75ab9a9986 100644
--- a/src/Apps/W1/External Storage - Document Attachments/app/src/AutomaticSync/DAExternalStorageSync.Report.al
+++ b/src/Apps/W1/External Storage - Document Attachments/app/src/AutomaticSync/DAExternalStorageSync.Report.al
@@ -17,6 +17,7 @@ report 8752 "DA External Storage Sync"
ProcessingOnly = true;
UseRequestPage = true;
Extensible = false;
+ ApplicationArea = Basic, Suite;
UsageCategory = None;
Permissions = tabledata "DA External Storage Setup" = r,
tabledata "Document Attachment" = r;
@@ -81,6 +82,8 @@ report 8752 "DA External Storage Sync"
trigger OnPostDataItem()
begin
+ LogSyncTelemetry();
+
if GuiAllowed() then begin
if TotalCount <> 0 then
Dialog.Close();
@@ -114,12 +117,14 @@ report 8752 "DA External Storage Sync"
field(DeleteExpiredFilesField; DeleteExpiredFiles)
{
ApplicationArea = Basic, Suite;
+ Enabled = SyncDirection = SyncDirection::"To External Storage";
Caption = 'Delete Expired Files';
ToolTip = 'Specifies whether to delete expired files from internal storage.';
}
field(MaxRecordsToProcessField; MaxRecordsToProcess)
{
ApplicationArea = Basic, Suite;
+ Enabled = SyncDirection = SyncDirection::"To External Storage";
Caption = 'Maximum Records to Process';
ToolTip = 'Specifies the maximum number of records to process in one run. Leave 0 for unlimited.';
MinValue = 0;
@@ -161,4 +166,15 @@ report 8752 "DA External Storage Sync"
ExternalStorageSetup.Get();
exit(ExternalStorageSetup."Delete After");
end;
+
+ local procedure LogSyncTelemetry()
+ var
+ DAFeatureTelemetry: Codeunit "DA Feature Telemetry";
+ begin
+ // Log manual sync when run from UI (GuiAllowed), auto sync when run from job queue
+ if GuiAllowed() then
+ DAFeatureTelemetry.LogManualSync()
+ else
+ DAFeatureTelemetry.LogAutoSync();
+ end;
}
diff --git a/src/Apps/W1/External Storage - Document Attachments/app/src/DAExternalStorageSetup.Page.al b/src/Apps/W1/External Storage - Document Attachments/app/src/DAExternalStorageSetup.Page.al
deleted file mode 100644
index 15759e967a..0000000000
--- a/src/Apps/W1/External Storage - Document Attachments/app/src/DAExternalStorageSetup.Page.al
+++ /dev/null
@@ -1,90 +0,0 @@
-// ------------------------------------------------------------------------------------------------
-// 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;
-
-///
-/// Setup page for External Storage functionality.
-/// Allows configuration of automatic upload and deletion policies.
-///
-page 8750 "DA External Storage Setup"
-{
- PageType = Card;
- SourceTable = "DA External Storage Setup";
- Caption = 'External Storage Setup';
- UsageCategory = None;
- ApplicationArea = Basic, Suite;
- InsertAllowed = false;
- DeleteAllowed = false;
- Extensible = false;
- Permissions = tabledata "DA External Storage Setup" = rmid;
-
- layout
- {
- area(Content)
- {
- group(General)
- {
- Caption = 'General';
- field("Delete After"; Rec."Delete After") { }
- field("Auto Upload"; Rec."Auto Upload") { }
- field("Auto Delete"; Rec."Auto Delete") { }
- }
-
- group(Status)
- {
- Caption = 'Status';
-
- field("Has Uploaded Files"; Rec."Has Uploaded Files") { }
- }
- }
- }
- actions
- {
- area(Processing)
- {
- action(RunExternalStorageSync)
- {
- ApplicationArea = Basic, Suite;
- Caption = 'Run External Storage Sync';
- Image = Process;
- ToolTip = 'Run the external storage synchronization with options to sync to or from external storage.';
-
- trigger OnAction()
- begin
- Report.Run(Report::"DA External Storage Sync");
- end;
- }
- }
- area(Navigation)
- {
- action(OpenDocumentAttachmentsExternal)
- {
- ApplicationArea = Basic, Suite;
- Caption = 'Open Document Attachments - External Storage List';
- Image = Document;
- ToolTip = 'Open the document attachment list with information about the external storage.';
- RunObject = page "Document Attachment - External";
- }
- }
- area(Promoted)
- {
- actionref(RunExternalStorageSync_Promoted; RunExternalStorageSync)
- {
- }
- actionref(OpenDocumentAttachmentsExternal_Promoted; OpenDocumentAttachmentsExternal)
- {
- }
- }
- }
-
- trigger OnOpenPage()
- begin
- if not Rec.Get() then begin
- Rec.Init();
- Rec.Insert();
- end;
- end;
-}
diff --git a/src/Apps/W1/External Storage - Document Attachments/app/src/DAExternalStorageSetup.Table.al b/src/Apps/W1/External Storage - Document Attachments/app/src/DAExternalStorageSetup.Table.al
deleted file mode 100644
index 52169b5f12..0000000000
--- a/src/Apps/W1/External Storage - Document Attachments/app/src/DAExternalStorageSetup.Table.al
+++ /dev/null
@@ -1,61 +0,0 @@
-// ------------------------------------------------------------------------------------------------
-// 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;
-
-///
-/// Setup table for External Storage functionality.
-/// Contains configuration settings for automatic upload and deletion policies.
-///
-table 8750 "DA External Storage Setup"
-{
- Caption = 'External Storage Setup';
- DataClassification = CustomerContent;
- Access = Internal;
-
- fields
- {
- field(1; "Primary Key"; Code[10])
- {
- Caption = 'Primary Key';
- DataClassification = SystemMetadata;
- }
- field(5; "Delete After"; DateFormula)
- {
- Caption = 'Delete After';
- ToolTip = 'Specifies when files should be automatically deleted.';
- }
- field(6; "Auto Upload"; Boolean)
- {
- Caption = 'Auto Upload';
- InitValue = true;
- ToolTip = 'Specifies if new attachments should be automatically uploaded to external storage.';
- }
- field(7; "Auto Delete"; Boolean)
- {
- Caption = 'Auto Delete';
- InitValue = false;
- ToolTip = 'Specifies if files should be automatically deleted from external storage.';
- }
- field(25; "Has Uploaded Files"; Boolean)
- {
- Caption = 'Has Uploaded Files';
- FieldClass = FlowField;
- CalcFormula = exist("Document Attachment" where("Uploaded Externally" = const(true)));
- Editable = false;
- ToolTip = 'Specifies if files have been uploaded using this configuration.';
- }
- }
-
- keys
- {
- key(PK; "Primary Key")
- {
- Clustered = true;
- }
- }
-}
diff --git a/src/Apps/W1/External Storage - Document Attachments/app/src/DocumentAttachmentIntegration/DAExternalStorageImpl.Codeunit.al b/src/Apps/W1/External Storage - Document Attachments/app/src/DocumentAttachmentIntegration/DAExternalStorageImpl.Codeunit.al
index ae302f4ed9..aa5e911cdf 100644
--- a/src/Apps/W1/External Storage - Document Attachments/app/src/DocumentAttachmentIntegration/DAExternalStorageImpl.Codeunit.al
+++ b/src/Apps/W1/External Storage - Document Attachments/app/src/DocumentAttachmentIntegration/DAExternalStorageImpl.Codeunit.al
@@ -8,9 +8,10 @@ namespace Microsoft.ExternalStorage.DocumentAttachments;
using System.ExternalFileStorage;
using System.Utilities;
using System.Environment;
+using System.Security.Encryption;
using Microsoft.Foundation.Attachment;
-codeunit 8751 "DA External Storage Impl." implements "File Scenario"
+codeunit 8751 "BCY DA External Storage Impl." implements "File Scenario"
{
Access = Internal;
InherentPermissions = X;
@@ -29,12 +30,20 @@ codeunit 8751 "DA External Storage Impl." implements "File Scenario"
/// True if the operation is allowed, otherwise false.
procedure BeforeAddOrModifyFileScenarioCheck(Scenario: Enum "File Scenario"; Connector: Enum System.ExternalFileStorage."Ext. File Storage Connector") SkipInsertOrModify: Boolean;
var
+ FileAccount: Record "File Account";
ConfirmManagement: Codeunit "Confirm Management";
+ FileScenarioCU: Codeunit "File Scenario";
DisclaimerMsg: Label 'You are about to enable External Storage!!!\\This feature is provided as-is, and you use it at your own risk.\Microsoft is not responsible for any issues or data loss that may occur.\\Do you wish to continue?';
begin
if not (Scenario = Enum::"File Scenario"::"Doc. Attach. - External Storage") then
exit;
+ // Search for External Storage assigned File Scenario
+ if FileScenarioCU.GetFileAccount(Scenario, FileAccount) then begin
+ SkipInsertOrModify := true;
+ exit;
+ end;
+
SkipInsertOrModify := not ConfirmManagement.GetResponseOrDefault(DisclaimerMsg);
end;
@@ -102,6 +111,10 @@ codeunit 8751 "DA External Storage Impl." implements "File Scenario"
OutStream: OutStream;
FileName: Text[2048];
begin
+ // Check if feature is enabled
+ if not IsFeatureEnabled() then
+ exit(false);
+
// Validate input parameters
if not DocumentAttachment."Document Reference ID".HasValue() then
exit(false);
@@ -110,13 +123,16 @@ codeunit 8751 "DA External Storage Impl." implements "File Scenario"
if DocumentAttachment."External File Path" <> '' then
exit(false);
+ // Telemetry logging for feature usage
+ LogFeatureUsedTelemetry();
+
// Get file content from document attachment
TempBlob.CreateOutStream(OutStream);
DocumentAttachment.ExportToStream(OutStream);
TempBlob.CreateInStream(InStream);
// Generate unique filename to prevent collisions
- FileName := DocumentAttachment."File Name" + '-' + Format(CreateGuid()) + '.' + DocumentAttachment."File Extension";
+ FileName := GetFilePathWithRootFolder(DocumentAttachment);
// Search for External Storage assigned File Scenario
FileScenario := FileScenario::"Doc. Attach. - External Storage";
@@ -129,7 +145,9 @@ codeunit 8751 "DA External Storage Impl." implements "File Scenario"
DocumentAttachment."Uploaded Externally" := true;
DocumentAttachment."External Upload Date" := CurrentDateTime();
DocumentAttachment."External File Path" := FileName;
+ DocumentAttachment."Source Environment Hash" := GetCurrentEnvironmentHash();
DocumentAttachment.Modify();
+ LogFileUploadedTelemetry();
exit(true);
end;
@@ -150,6 +168,10 @@ codeunit 8751 "DA External Storage Impl." implements "File Scenario"
InStream: InStream;
ExternalFilePath, FileName : Text;
begin
+ // Check if feature is enabled
+ if not IsFeatureEnabled() then
+ exit(false);
+
// Validate input parameters
if DocumentAttachment."External File Path" = '' then
exit(false);
@@ -170,7 +192,12 @@ codeunit 8751 "DA External Storage Impl." implements "File Scenario"
ExternalFileStorage.Initialize(FileScenario);
ExternalFileStorage.GetFile(ExternalFilePath, InStream);
- exit(DownloadFromStream(InStream, '', '', '', FileName));
+ if DownloadFromStream(InStream, '', '', '', FileName) then begin
+ LogFileDownloadedTelemetry();
+ exit(true);
+ end;
+
+ exit(false);
end;
///
@@ -310,6 +337,15 @@ codeunit 8751 "DA External Storage Impl." implements "File Scenario"
FileScenario: Enum "File Scenario";
ExternalFilePath: Text;
begin
+ // Check if feature is enabled
+ if not IsFeatureEnabled() then
+ exit(false);
+
+ // Check if file belongs to another company and needs migration
+ if IsFileFromAnotherEnvironmentOrCompany(DocumentAttachment) then
+ if not MigrateFileToCurrentEnvironment(DocumentAttachment) then
+ exit(false);
+
// Validate input parameters
if DocumentAttachment."External File Path" = '' then
exit(false);
@@ -329,6 +365,7 @@ codeunit 8751 "DA External Storage Impl." implements "File Scenario"
ExternalFileStorage.Initialize(FileScenario);
if ExternalFileStorage.DeleteFile(ExternalFilePath) then begin
DocumentAttachment.MarkAsNotUploadedToExternal();
+ LogFileDeletedTelemetry();
exit(true);
end;
@@ -344,6 +381,11 @@ codeunit 8751 "DA External Storage Impl." implements "File Scenario"
var
TenantMedia: Record "Tenant Media";
begin
+ // Check if file belongs to another company and needs migration
+ if IsFileFromAnotherEnvironmentOrCompany(DocumentAttachment) then
+ if not MigrateFileToCurrentEnvironment(DocumentAttachment) then
+ exit(false);
+
// Validate input parameters
if not DocumentAttachment."Document Reference ID".HasValue() then
exit(false);
@@ -472,7 +514,7 @@ codeunit 8751 "DA External Storage Impl." implements "File Scenario"
if not ExternalStorageSetup.Get() then
exit;
- if not ExternalStorageSetup."Auto Upload" then
+ if not ExternalStorageSetup."Scheduled Upload" then
exit;
// Only process files with actual content
@@ -511,7 +553,7 @@ codeunit 8751 "DA External Storage Impl." implements "File Scenario"
if not ExternalStorageSetup.Get() then
exit;
- if not ExternalStorageSetup."Auto Delete" then
+ if not ExternalStorageSetup."Delete from External Storage" then
exit;
// Only process files that were uploaded to external storage
@@ -598,4 +640,283 @@ codeunit 8751 "DA External Storage Impl." implements "File Scenario"
IsHandled := true;
end;
#endregion
+
+ ///
+ /// Opens a folder selection dialog for choosing the root folder.
+ ///
+ /// The selected folder path, or empty string if cancelled.
+ procedure SelectRootFolder(): Text
+ var
+ FileAccount: Record "File Account";
+ ExternalFileStorage: Codeunit "External File Storage";
+ FileScenarioCU: Codeunit "File Scenario";
+ SelectFolderPathLbl: Label 'Select Root Folder for Attachments';
+ FileScenario: Enum "File Scenario";
+ begin
+ // Initialize external file storage with the scenario
+ FileScenario := FileScenario::"Doc. Attach. - External Storage";
+ if not FileScenarioCU.GetFileAccount(FileScenario, FileAccount) then
+ exit('');
+
+ ExternalFileStorage.Initialize(FileScenario);
+ exit(ExternalFileStorage.SelectAndGetFolderPath('', SelectFolderPathLbl));
+ end;
+
+ 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[16];
+ FileNamePart: Text;
+ begin
+ // Generate unique filename to prevent collisions
+ FileNamePart := 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
+ // Ensure root folder ends with a separator
+ if not RootFolder.EndsWith('/') and not RootFolder.EndsWith('\') then
+ RootFolder := RootFolder + '/';
+
+ // Ensure environment hash folder exists
+ EnsureFolderExists(RootFolder + EnvironmentHashFolder);
+
+ // Ensure environment hash folder exists within table folder
+ EnsureFolderExists(RootFolder + EnvironmentHashFolder + '/' + TableNameFolder);
+
+ FileName := RootFolder + EnvironmentHashFolder + '/' + TableNameFolder + '/' + FileNamePart;
+ end else begin
+ // No root folder, add environment folder at root level
+ EnsureFolderExists(EnvironmentHashFolder);
+
+ // Ensure environment hash folder exists within table folder
+ EnsureFolderExists(EnvironmentHashFolder + '/' + TableNameFolder);
+
+ FileName := EnvironmentHashFolder + '/' + TableNameFolder + '/' + FileNamePart;
+ end;
+
+ exit(CopyStr(FileName, 1, 2048));
+ end;
+
+ local procedure GetTableNameFolder(TableID: Integer): Text[100]
+ var
+ RecRef: RecordRef;
+ TableName: Text;
+ begin
+ // Open the RecordRef to get table metadata
+ RecRef.Open(TableID, false);
+ TableName := RecRef.Name;
+ RecRef.Close();
+
+ // Replace invalid characters for folder names
+ TableName := DelChr(TableName, '=', '<>:"/\|?*');
+ TableName := ConvertStr(TableName, ' ', '_');
+
+ exit(CopyStr(TableName, 1, 100));
+ end;
+
+ local procedure EnsureFolderExists(CompanyFolderPath: Text)
+ var
+ FileAccount: Record "File Account";
+ ExternalFileStorage: Codeunit "External File Storage";
+ FileScenarioCU: Codeunit "File Scenario";
+ FileScenario: Enum "File Scenario";
+ begin
+ // Initialize external file storage with the scenario
+ FileScenario := FileScenario::"Doc. Attach. - External Storage";
+ if not FileScenarioCU.GetFileAccount(FileScenario, FileAccount) then
+ exit;
+
+ ExternalFileStorage.Initialize(FileScenario);
+
+ // Check if directory exists, if not create it
+ if not ExternalFileStorage.DirectoryExists(CompanyFolderPath) then
+ ExternalFileStorage.CreateDirectory(CompanyFolderPath);
+ end;
+
+ local procedure GetCurrentEnvironmentHash(): Text[16]
+ var
+ Company: Record Company;
+ EnvironmentInformation: Codeunit "Environment Information";
+ CryptographyManagement: Codeunit "Cryptography Management";
+ HashAlgorithmType: Option MD5,SHA1,SHA256,SHA384,SHA512;
+ IdentityString: Text;
+ begin
+ Company.Get(CompanyName());
+
+ // Combine Tenant ID + Environment Name + Company System ID
+ IdentityString := TenantId() + '|' + EnvironmentInformation.GetEnvironmentName() + '|' + Format(Company.SystemId);
+
+ // Generate SHA256 hash and take first 16 characters
+ exit(CopyStr(CryptographyManagement.GenerateHash(IdentityString, HashAlgorithmType::SHA256), 1, 16));
+ end;
+
+ local procedure IsFileFromAnotherEnvironmentOrCompany(DocumentAttachment: Record "Document Attachment"): Boolean
+ var
+ CurrentEnvironmentHash: Text[16];
+ begin
+ // If no source environment hash is set, assume it belongs to current environment
+ if DocumentAttachment."Source Environment Hash" = '' then
+ exit(false);
+
+ CurrentEnvironmentHash := GetCurrentEnvironmentHash();
+ exit(DocumentAttachment."Source Environment Hash" <> CurrentEnvironmentHash);
+ end;
+
+ local procedure MigrateFileToCurrentEnvironment(var DocumentAttachment: Record "Document Attachment"): Boolean
+ var
+ FileAccount: Record "File Account";
+ ExternalFileStorage: Codeunit "External File Storage";
+ FileScenarioCU: Codeunit "File Scenario";
+ TempBlob: Codeunit "Temp Blob";
+ FileScenario: Enum "File Scenario";
+ InStream: InStream;
+ OutStream: OutStream;
+ OldFilePath: Text;
+ NewFilePath: Text[2048];
+ MigrationMsg: Label 'File is being migrated from another environment folder to the current environment folder.';
+ begin
+ if not DocumentAttachment."Uploaded Externally" then
+ exit(false);
+
+ if DocumentAttachment."External File Path" = '' then
+ exit(false);
+
+ Message(MigrationMsg);
+
+ // Initialize external file storage
+ FileScenario := FileScenario::"Doc. Attach. - External Storage";
+ if not FileScenarioCU.GetFileAccount(FileScenario, FileAccount) then
+ exit(false);
+
+ ExternalFileStorage.Initialize(FileScenario);
+
+ // Download file from old location
+ OldFilePath := DocumentAttachment."External File Path";
+ if not ExternalFileStorage.GetFile(OldFilePath, InStream) then
+ exit(false);
+
+ // Copy to TempBlob
+ TempBlob.CreateOutStream(OutStream);
+ CopyStream(OutStream, InStream);
+ TempBlob.CreateInStream(InStream);
+
+ // Generate new file path in current company folder
+ NewFilePath := GetFilePathWithRootFolder(DocumentAttachment);
+
+ // Upload to new location
+ if not ExternalFileStorage.CreateFile(NewFilePath, InStream) then
+ exit(false);
+
+ // Update document attachment record
+ DocumentAttachment."External File Path" := NewFilePath;
+ DocumentAttachment."Source Environment Hash" := GetCurrentEnvironmentHash();
+ DocumentAttachment."External Upload Date" := CurrentDateTime();
+ DocumentAttachment.Modify();
+
+ exit(true);
+ end;
+
+ ///
+ /// Runs migration for all document attachments from a previous company.
+ ///
+ /// Number of files migrated.
+ procedure RunCompanyMigration(): Integer
+ var
+ DocumentAttachment: Record "Document Attachment";
+ MigratedCount: Integer;
+ StartMigrationQst: Label 'This will migrate all document attachments from the previous company folder to the current company folder.\\Do you want to continue?';
+ MigrationCompletedMsg: Label '%1 file(s) have been successfully migrated to the current company folder.', Comment = '%1 = Number of files';
+ begin
+ if not Confirm(StartMigrationQst, false) then
+ exit(0);
+
+ MigratedCount := 0;
+ DocumentAttachment.SetRange("Uploaded Externally", true);
+ DocumentAttachment.SetFilter("External File Path", '<>%1', '');
+ if DocumentAttachment.FindSet(true) then
+ repeat
+ if IsFileFromAnotherEnvironmentOrCompany(DocumentAttachment) then
+ if MigrateFileToCurrentEnvironment(DocumentAttachment) then
+ MigratedCount += 1;
+ until DocumentAttachment.Next() = 0;
+
+ if MigratedCount > 0 then begin
+ LogCompanyMigrationTelemetry();
+ Message(MigrationCompletedMsg, MigratedCount);
+ end;
+
+ exit(MigratedCount);
+ end;
+
+ ///
+ /// Shows the current environment hash for use in another environment.
+ ///
+ procedure ShowCurrentEnvironmentHash()
+ var
+ CurrentHash: Text[16];
+ HashCopiedMsg: Label 'Current environment hash: %1', Comment = '%1 = Hash value';
+ begin
+ CurrentHash := GetCurrentEnvironmentHash();
+ Message(HashCopiedMsg, CurrentHash);
+ end;
+
+ #region Telemetry Logging
+ local procedure LogFeatureUsedTelemetry()
+ var
+ DAFeatureTelemetry: Codeunit "DA Feature Telemetry";
+ begin
+ DAFeatureTelemetry.LogFeatureUsed();
+ end;
+
+ local procedure LogFileUploadedTelemetry()
+ var
+ DAFeatureTelemetry: Codeunit "DA Feature Telemetry";
+ begin
+ DAFeatureTelemetry.LogFileUploaded();
+ end;
+
+ local procedure LogFileDownloadedTelemetry()
+ var
+ DAFeatureTelemetry: Codeunit "DA Feature Telemetry";
+ begin
+ DAFeatureTelemetry.LogFileDownloaded();
+ end;
+
+ local procedure LogFileDeletedTelemetry()
+ var
+ DAFeatureTelemetry: Codeunit "DA Feature Telemetry";
+ begin
+ DAFeatureTelemetry.LogFileDeleted();
+ end;
+
+ local procedure LogCompanyMigrationTelemetry()
+ var
+ DAFeatureTelemetry: Codeunit "DA Feature Telemetry";
+ begin
+ DAFeatureTelemetry.LogCompanyMigration();
+ end;
+
+ local procedure IsFeatureEnabled(): Boolean
+ var
+ ExternalStorageSetup: Record "DA External Storage Setup";
+ begin
+ if not ExternalStorageSetup.Get() then
+ exit(false);
+
+ exit(ExternalStorageSetup.Enabled);
+ end;
+ #endregion
}
\ No newline at end of file
diff --git a/src/Apps/W1/External Storage - Document Attachments/app/src/DocumentAttachmentIntegration/DocumentAttachmentExtStor.TableExt.al b/src/Apps/W1/External Storage - Document Attachments/app/src/DocumentAttachmentIntegration/DocumentAttachmentExtStor.TableExt.al
index a13a3f64e7..588eabc07d 100644
--- a/src/Apps/W1/External Storage - Document Attachments/app/src/DocumentAttachmentIntegration/DocumentAttachmentExtStor.TableExt.al
+++ b/src/Apps/W1/External Storage - Document Attachments/app/src/DocumentAttachmentIntegration/DocumentAttachmentExtStor.TableExt.al
@@ -41,7 +41,14 @@ tableextension 8750 "Document Attachment Ext.Stor." extends "Document Attachment
Caption = 'Deleted Internally';
DataClassification = SystemMetadata;
Editable = false;
- ToolTip = 'Specifies the value of the Deleted Internally field.';
+ ToolTip = 'Specifies if the file has been deleted from internal storage.';
+ }
+ field(8754; "Source Environment Hash"; Text[16])
+ {
+ Caption = 'Source Environment Hash';
+ DataClassification = SystemMetadata;
+ Editable = false;
+ ToolTip = 'Specifies a hash identifying the tenant, environment, and company that originally uploaded this file to external storage.';
}
}
diff --git a/src/Apps/W1/External Storage - Document Attachments/app/src/DocumentAttachmentIntegration/DocumentAttachmentExternal.Page.al b/src/Apps/W1/External Storage - Document Attachments/app/src/DocumentAttachmentIntegration/DocumentAttachmentExternal.Page.al
index cefbe51873..d2d149d1e2 100644
--- a/src/Apps/W1/External Storage - Document Attachments/app/src/DocumentAttachmentIntegration/DocumentAttachmentExternal.Page.al
+++ b/src/Apps/W1/External Storage - Document Attachments/app/src/DocumentAttachmentIntegration/DocumentAttachmentExternal.Page.al
@@ -74,10 +74,10 @@ page 8751 "Document Attachment - External"
{
area(Processing)
{
- action("Upload to External Storage")
+ action("Upload to External")
{
- ApplicationArea = Basic, Suite;
- Caption = 'Upload to External Storage';
+ Enabled = not Rec."Uploaded Externally";
+ Caption = 'Upload to External';
ToolTip = 'Upload the selected file to external storage.';
Image = Export;
@@ -96,28 +96,30 @@ page 8751 "Document Attachment - External"
until DocumentAttachment.Next() = 0;
end;
}
- action("Download from External Storage")
+ action(Download)
{
- ApplicationArea = Basic, Suite;
- Caption = 'Download from External Storage';
- ToolTip = 'Download the file from external storage.';
+ Caption = 'Download';
+ ToolTip = 'Download the selected file from external storage. If the file is not stored externally, it will be exported from internal storage.';
Image = Import;
trigger OnAction()
var
ExternalStorageImpl: Codeunit "DA External Storage Impl.";
begin
- if ExternalStorageImpl.DownloadFromExternalStorage(Rec) then
- Message(FileDownloadedMsg)
- else
- Message(FailedFileDownloadMsg);
+ if Rec."Uploaded Externally" then begin
+ if ExternalStorageImpl.DownloadFromExternalStorage(Rec) then
+ Message(FileDownloadedMsg)
+ else
+ Message(FailedFileDownloadMsg);
+ end else
+ Rec.Export(true);
end;
}
- action("Download from External To Internal Storage")
+ action("Copy from External To Internal")
{
- ApplicationArea = Basic, Suite;
- Caption = 'Download from External To Internal Storage';
- ToolTip = 'Download the file from external storage to internal storage.';
+ Enabled = Rec."Deleted Internally" and Rec."Uploaded Externally";
+ Caption = 'Copy from External To Internal';
+ ToolTip = 'Copy the file from external storage to internal storage.';
Image = Import;
trigger OnAction()
@@ -130,11 +132,10 @@ page 8751 "Document Attachment - External"
Message(FailedFileDownloadMsg);
end;
}
- action("Delete from External Storage")
+ action("Delete from External")
{
- ApplicationArea = Basic, Suite;
Enabled = not (Rec."Deleted Internally") and Rec."Uploaded Externally";
- Caption = 'Delete from External Storage';
+ Caption = 'Delete from External';
ToolTip = 'Delete the file from external storage.';
Image = Delete;
@@ -149,11 +150,10 @@ page 8751 "Document Attachment - External"
Message(FailedFileDeleteExternalStorageMsg);
end;
}
- action("Delete from Internal Storage")
+ action("Delete from Internal")
{
- ApplicationArea = Basic, Suite;
Enabled = Rec."Uploaded Externally" and not Rec."Deleted Internally";
- Caption = 'Delete from Internal Storage';
+ Caption = 'Delete from Internal';
ToolTip = 'Delete the file from Internal storage.';
Image = Delete;
@@ -173,7 +173,6 @@ page 8751 "Document Attachment - External"
{
action("External Storage Setup")
{
- ApplicationArea = Basic, Suite;
Caption = 'External Storage Setup';
ToolTip = 'Configure external storage settings.';
Image = Setup;
@@ -182,11 +181,11 @@ page 8751 "Document Attachment - External"
}
area(Promoted)
{
- actionref(UploadToExternalStoragePromoted; "Upload to External Storage") { }
- actionref(DownloadFromExternalStoragePromoted; "Download from External Storage") { }
- actionref(DownloadFromExternalToInternalStoragePromoted; "Download from External To Internal Storage") { }
- actionref(DeleteFromExternalStoragePromoted; "Delete from External Storage") { }
- actionref(DeleteFromInternalStoragePromoted; "Delete from Internal Storage") { }
+ actionref(UploadToExternal_Promoted; "Upload to External") { }
+ actionref(DownloadFromExternal_Promoted; Download) { }
+ actionref(CopyFromExternalToInternal_Promoted; "Copy from External To Internal") { }
+ actionref(DeleteFromExternal_Promoted; "Delete from External") { }
+ actionref(DeleteFromInternal_Promoted; "Delete from Internal") { }
}
}
diff --git a/src/Apps/W1/External Storage - Document Attachments/app/src/Setup/DAExternalStorageSetup.Page.al b/src/Apps/W1/External Storage - Document Attachments/app/src/Setup/DAExternalStorageSetup.Page.al
new file mode 100644
index 0000000000..2fdf9c12ef
--- /dev/null
+++ b/src/Apps/W1/External Storage - Document Attachments/app/src/Setup/DAExternalStorageSetup.Page.al
@@ -0,0 +1,246 @@
+// ------------------------------------------------------------------------------------------------
+// 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 System.Utilities;
+using System.Threading;
+
+///
+/// Setup page for External Storage functionality.
+/// Allows configuration of automatic upload and deletion policies.
+///
+page 8750 "DA External Storage Setup"
+{
+ PageType = Card;
+ SourceTable = "DA External Storage Setup";
+ Caption = 'External Storage Setup';
+ UsageCategory = None;
+ ApplicationArea = Basic, Suite;
+ InsertAllowed = false;
+ DeleteAllowed = false;
+ Extensible = false;
+ Permissions = tabledata "DA External Storage Setup" = rmid,
+ tabledata "Job Queue Entry" = r;
+
+ layout
+ {
+ area(Content)
+ {
+ group(General)
+ {
+ Caption = 'General';
+ field(Enabled; Rec.Enabled)
+ {
+ ToolTip = 'Specifies if the External Storage feature is enabled. Enable this to start using external storage for document attachments.';
+ Importance = Promoted;
+ }
+ field("Root Folder"; Rec."Root Folder")
+ {
+ ShowMandatory = true;
+ Editable = false;
+ trigger OnAssistEdit()
+ begin
+ SelectRootFolder();
+ end;
+ }
+ }
+ group(UploadAndDeletePolicy)
+ {
+ Caption = 'Upload and Delete Policy';
+ field("Delete from BC after Upload"; Rec."Delete from BC after Upload")
+ {
+ trigger OnValidate()
+ begin
+ UpdateDeleteAfterVisibility();
+ CurrPage.Update(false);
+ end;
+ }
+ field("Delete After"; Rec."Delete After")
+ {
+ ShowMandatory = true;
+ Enabled = ShowDeleteAfter;
+ }
+ field("Scheduled Upload"; Rec."Scheduled Upload") { }
+ field("Delete from External Storage"; Rec."Delete from External Storage") { }
+ }
+
+ group(JobQueueInformation)
+ {
+ Caption = 'Job Queue Information';
+ field("Job Queue Entry ID"; Rec."Job Queue Entry ID")
+ {
+ ToolTip = 'Specifies the ID of the job queue entry for automatic synchronization.';
+
+ trigger OnDrillDown()
+ begin
+ ShowJobQueueEntry();
+ end;
+ }
+ field(JobQueueStatus; GetJobQueueStatus())
+ {
+ Caption = 'Job Queue Status';
+ Editable = false;
+ ToolTip = 'Specifies the current status of the job queue entry.';
+ }
+ }
+ }
+ }
+ actions
+ {
+ area(Processing)
+ {
+ action(StorageSync)
+ {
+ Caption = 'Storage Sync';
+ Image = Process;
+ ToolTip = 'Run the synchronization job to move document attachments to or from external storage.';
+
+ trigger OnAction()
+ begin
+ Report.Run(Report::"DA External Storage Sync");
+ end;
+ }
+ action(MigrateFiles)
+ {
+ Caption = 'Migrate Files';
+ Image = MoveToNextPeriod;
+ ToolTip = 'Migrate all document attachments from the previous environment/company folder to the current environment/company folder.';
+
+ trigger OnAction()
+ var
+ DAExternalStorageImpl: Codeunit "DA External Storage Impl.";
+ begin
+ DAExternalStorageImpl.RunCompanyMigration();
+ end;
+ }
+ action(ShowCurrentHash)
+ {
+ Caption = 'Show Current Environment Hash';
+ Image = Copy;
+ ToolTip = 'Show the current environment hash used in the folder structure in external storage.';
+
+ trigger OnAction()
+ begin
+ ShowCurrentEnvironmentHash();
+ end;
+ }
+ }
+ area(Navigation)
+ {
+ action(ShowJobQueue)
+ {
+ Caption = 'Show Job Queue Entry';
+ Image = JobListSetup;
+ ToolTip = 'View the job queue entry for automatic synchronization.';
+
+ trigger OnAction()
+ begin
+ ShowJobQueueEntry();
+ end;
+ }
+ action(DocumentAttachments)
+ {
+ Caption = 'Document Attachments';
+ Image = Document;
+ ToolTip = 'Open the document attachment list with information about the external storage.';
+ RunObject = page "Document Attachment - External";
+ }
+ }
+ area(Promoted)
+ {
+ actionref(StorageSync_Promoted; StorageSync)
+ {
+ }
+ actionref(MigrateFiles_Promoted; MigrateFiles)
+ {
+ }
+ actionref(DocumentAttachments_Promoted; DocumentAttachments)
+ {
+ }
+ group(InfoGroup)
+ {
+ Caption = 'Info';
+ actionref(ShowCurrentHash_Promoted; ShowCurrentHash)
+ {
+ }
+ actionref(ShowJobQueue_Promoted; ShowJobQueue)
+ {
+ }
+ }
+ }
+ }
+
+ trigger OnOpenPage()
+ begin
+ if not Rec.Get() then begin
+ Rec.Init();
+ Rec.Insert();
+ end;
+ UpdateDeleteAfterVisibility();
+ end;
+
+ trigger OnAfterGetCurrRecord()
+ begin
+ UpdateDeleteAfterVisibility();
+ end;
+
+ var
+ ShowDeleteAfter: Boolean;
+
+ local procedure UpdateDeleteAfterVisibility()
+ begin
+ ShowDeleteAfter := not Rec."Delete from BC after Upload";
+ end;
+
+ local procedure SelectRootFolder()
+ var
+ DAExternalStorageImpl: Codeunit "DA External Storage Impl.";
+ ConfirmManagement: Codeunit "Confirm Management";
+ NoFolderSelectedLbl: Label 'No folder selected. Do you want to clear the current root folder?';
+ FolderPath: Text;
+ begin
+ FolderPath := DAExternalStorageImpl.SelectRootFolder();
+ if FolderPath <> '' then begin
+ Rec."Root Folder" := CopyStr(FolderPath, 1, MaxStrLen(Rec."Root Folder"));
+ CurrPage.Update();
+ end else begin
+ if ConfirmManagement.GetResponseOrDefault(NoFolderSelectedLbl, false) then
+ Rec."Root Folder" := '';
+ CurrPage.Update();
+ end;
+ end;
+
+ local procedure ShowCurrentEnvironmentHash()
+ var
+ DAExternalStorageImpl: Codeunit "DA External Storage Impl.";
+ begin
+ DAExternalStorageImpl.ShowCurrentEnvironmentHash();
+ end;
+
+ local procedure ShowJobQueueEntry()
+ var
+ JobQueueEntry: Record "Job Queue Entry";
+ begin
+ if not IsNullGuid(Rec."Job Queue Entry ID") then
+ if JobQueueEntry.Get(Rec."Job Queue Entry ID") then
+ Page.Run(Page::"Job Queue Entry Card", JobQueueEntry);
+ end;
+
+ local procedure GetJobQueueStatus(): Text
+ var
+ JobQueueEntry: Record "Job Queue Entry";
+ NotCreatedLbl: Label 'Not Created';
+ DeletedLbl: Label 'Deleted';
+ begin
+ if IsNullGuid(Rec."Job Queue Entry ID") then
+ exit(NotCreatedLbl);
+
+ if not JobQueueEntry.Get(Rec."Job Queue Entry ID") then
+ exit(DeletedLbl);
+
+ exit(Format(JobQueueEntry.Status));
+ end;
+}
diff --git a/src/Apps/W1/External Storage - Document Attachments/app/src/Setup/DAExternalStorageSetup.Table.al b/src/Apps/W1/External Storage - Document Attachments/app/src/Setup/DAExternalStorageSetup.Table.al
new file mode 100644
index 0000000000..f148662397
--- /dev/null
+++ b/src/Apps/W1/External Storage - Document Attachments/app/src/Setup/DAExternalStorageSetup.Table.al
@@ -0,0 +1,184 @@
+// ------------------------------------------------------------------------------------------------
+// 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;
+using System.Threading;
+
+///
+/// Setup table for External Storage functionality.
+/// Contains configuration settings for automatic upload and deletion policies.
+///
+table 8750 "DA External Storage Setup"
+{
+ Caption = 'External Storage Setup';
+ DataClassification = CustomerContent;
+ Access = Internal;
+ Permissions = tabledata "Job Queue Entry" = rimd;
+
+ fields
+ {
+ field(1; "Primary Key"; Code[10])
+ {
+ Caption = 'Primary Key';
+ DataClassification = SystemMetadata;
+ }
+ field(2; Enabled; Boolean)
+ {
+ Caption = 'Enabled';
+ ToolTip = 'Specifies if the External Storage feature is enabled.';
+
+ trigger OnValidate()
+ var
+ DAFeatureTelemetry: Codeunit "DA Feature Telemetry";
+ begin
+ if Enabled then
+ DAFeatureTelemetry.LogFeatureEnabled()
+ else
+ DAFeatureTelemetry.LogFeatureDisabled();
+ end;
+ }
+ field(5; "Delete After"; DateFormula)
+ {
+ Caption = 'Delete After';
+ ToolTip = 'Specifies the duration after which files should be deleted from external storage. Use <0D> for immediate deletion after upload, <30D> for 30 days, etc.';
+ InitValue = 0D;
+ }
+ field(8; "Delete from BC after Upload"; Boolean)
+ {
+ Caption = 'Delete from BC after Upload';
+ ToolTip = 'Specifies if files should be deleted immediately after upload. When enabled, Delete After is set to 0D.';
+
+ trigger OnValidate()
+ begin
+ if "Delete from BC after Upload" then
+ Evaluate("Delete After", '<0D>');
+ end;
+ }
+ field(6; "Scheduled Upload"; Boolean)
+ {
+ Caption = 'Scheduled Upload';
+ ToolTip = 'Specifies if files should be uploaded automatically with using the Job Queue. When enabled, a Job Queue entry is created to run the upload process in the background.';
+
+ trigger OnValidate()
+ begin
+ ManageJobQueue();
+ end;
+ }
+ field(7; "Delete from External Storage"; Boolean)
+ {
+ Caption = 'Delete External File on Attachment Delete';
+ ToolTip = 'Specifies if files should be deleted from external storage when the attachment is deleted from Business Central.';
+ InitValue = true;
+ }
+ field(10; "Root Folder"; Text[250])
+ {
+ Caption = 'Root Folder';
+ ToolTip = 'Specifies the root folder path where attachments will be stored in external storage.';
+
+ trigger OnValidate()
+ var
+ DAFeatureTelemetry: Codeunit "DA Feature Telemetry";
+ begin
+ if "Root Folder" <> '' then
+ DAFeatureTelemetry.LogRootFolderConfigured();
+ end;
+ }
+ field(12; "Job Queue Entry ID"; Guid)
+ {
+ Caption = 'Job Queue Entry ID';
+ DataClassification = SystemMetadata;
+ Editable = false;
+ ToolTip = 'Specifies the ID of the job queue entry for automatic synchronization.';
+ }
+ field(25; "Has Uploaded Files"; Boolean)
+ {
+ Caption = 'Has Uploaded Files';
+ FieldClass = FlowField;
+ CalcFormula = exist("Document Attachment" where("Uploaded Externally" = const(true)));
+ Editable = false;
+ ToolTip = 'Specifies if files have been uploaded using this configuration.';
+ }
+ }
+
+ keys
+ {
+ key(PK; "Primary Key")
+ {
+ Clustered = true;
+ }
+ }
+
+ local procedure ManageJobQueue()
+ var
+ JobQueueEntry: Record "Job Queue Entry";
+ begin
+ if "Scheduled Upload" then begin
+ // Create job queue if it doesn't exist
+ if IsNullGuid("Job Queue Entry ID") or not JobQueueEntry.Get("Job Queue Entry ID") then
+ CreateJobQueue()
+ else
+ // Reactivate if it exists but is not ready
+ if JobQueueEntry.Status <> JobQueueEntry.Status::Ready then begin
+ JobQueueEntry.Status := JobQueueEntry.Status::Ready;
+ JobQueueEntry.Modify(true);
+ end;
+ end else
+ // Delete or set to on hold when Auto Upload is disabled
+ if not IsNullGuid("Job Queue Entry ID") then
+ if JobQueueEntry.Get("Job Queue Entry ID") then begin
+ JobQueueEntry.Status := JobQueueEntry.Status::"On Hold";
+ JobQueueEntry.Modify(true);
+ end;
+ end;
+
+ local procedure CreateJobQueue()
+ var
+ JobQueueEntry: Record "Job Queue Entry";
+ JobQueueCategoryLbl: Label 'EXTATTACH', Locked = true;
+ JobQueueDescriptionLbl: Label 'External Storage - Automatic Upload';
+ OneAmTime: Time;
+ begin
+ OneAmTime := 010000T; // 1:00 AM
+
+ JobQueueEntry.Init();
+ JobQueueEntry."Object Type to Run" := JobQueueEntry."Object Type to Run"::Report;
+ JobQueueEntry."Object ID to Run" := Report::"DA External Storage Sync";
+ JobQueueEntry.Description := JobQueueDescriptionLbl;
+ JobQueueEntry."Job Queue Category Code" := JobQueueCategoryLbl;
+ JobQueueEntry."Run in User Session" := false;
+ JobQueueEntry."Maximum No. of Attempts to Run" := 3;
+
+ // Schedule for 1 AM daily
+ JobQueueEntry."Earliest Start Date/Time" := CreateDateTime(Today() + 1, OneAmTime);
+ if Time() < OneAmTime then
+ JobQueueEntry."Earliest Start Date/Time" := CreateDateTime(Today(), OneAmTime);
+
+ JobQueueEntry."Recurring Job" := true;
+ JobQueueEntry."No. of Minutes between Runs" := 1440; // 24 hours
+
+ // Set report parameters to upload to external storage
+ JobQueueEntry."Report Request Page Options" := true;
+
+ JobQueueEntry.Status := JobQueueEntry.Status::Ready;
+ JobQueueEntry.Insert(true);
+
+ "Job Queue Entry ID" := JobQueueEntry.ID;
+ Modify();
+ end;
+
+ internal procedure DeleteJobQueue()
+ var
+ JobQueueEntry: Record "Job Queue Entry";
+ begin
+ if not IsNullGuid("Job Queue Entry ID") then
+ if JobQueueEntry.Get("Job Queue Entry ID") then
+ JobQueueEntry.Delete(true);
+
+ Clear("Job Queue Entry ID");
+ Modify();
+ end;
+}
diff --git a/src/Apps/W1/External Storage - Document Attachments/app/src/Telemetry/DAFeatureTelemetry.Codeunit.al b/src/Apps/W1/External Storage - Document Attachments/app/src/Telemetry/DAFeatureTelemetry.Codeunit.al
new file mode 100644
index 0000000000..a3c6b2c4aa
--- /dev/null
+++ b/src/Apps/W1/External Storage - Document Attachments/app/src/Telemetry/DAFeatureTelemetry.Codeunit.al
@@ -0,0 +1,70 @@
+// ------------------------------------------------------------------------------------------------
+// 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 System.Telemetry;
+
+///
+/// Provides telemetry logging for External Storage - Document Attachments feature.
+///
+codeunit 8754 "DA Feature Telemetry"
+{
+ Access = Internal;
+
+ var
+ FeatureTelemetry: Codeunit "Feature Telemetry";
+ ExternalStorageTok: Label 'External Storage - Document Attachments', Locked = true;
+
+ internal procedure LogFeatureEnabled()
+ begin
+ FeatureTelemetry.LogUptake('', ExternalStorageTok, Enum::"Feature Uptake Status"::"Set up");
+ end;
+
+ internal procedure LogFeatureDisabled()
+ begin
+ FeatureTelemetry.LogUptake('', ExternalStorageTok, Enum::"Feature Uptake Status"::"Undiscovered");
+ end;
+
+ internal procedure LogFeatureUsed()
+ begin
+ FeatureTelemetry.LogUptake('', ExternalStorageTok, Enum::"Feature Uptake Status"::Used);
+ end;
+
+ internal procedure LogFileUploaded()
+ begin
+ FeatureTelemetry.LogUsage('', ExternalStorageTok, 'File Uploaded');
+ end;
+
+ internal procedure LogFileDownloaded()
+ begin
+ FeatureTelemetry.LogUsage('', ExternalStorageTok, 'File Downloaded');
+ end;
+
+ internal procedure LogFileDeleted()
+ begin
+ FeatureTelemetry.LogUsage('', ExternalStorageTok, 'File Deleted');
+ end;
+
+ internal procedure LogCompanyMigration()
+ begin
+ FeatureTelemetry.LogUsage('', ExternalStorageTok, 'Company Migration');
+ end;
+
+ internal procedure LogManualSync()
+ begin
+ FeatureTelemetry.LogUsage('', ExternalStorageTok, 'Manual Sync');
+ end;
+
+ internal procedure LogAutoSync()
+ begin
+ FeatureTelemetry.LogUsage('', ExternalStorageTok, 'Auto Sync');
+ end;
+
+ internal procedure LogRootFolderConfigured()
+ begin
+ FeatureTelemetry.LogUsage('', ExternalStorageTok, 'Root Folder Configured');
+ end;
+}
diff --git a/src/Apps/W1/External Storage - Document Attachments/app/src/Telemetry/DATelemetryLogger.Codeunit.al b/src/Apps/W1/External Storage - Document Attachments/app/src/Telemetry/DATelemetryLogger.Codeunit.al
new file mode 100644
index 0000000000..5c101c0b54
--- /dev/null
+++ b/src/Apps/W1/External Storage - Document Attachments/app/src/Telemetry/DATelemetryLogger.Codeunit.al
@@ -0,0 +1,30 @@
+// ------------------------------------------------------------------------------------------------
+// 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 System.Telemetry;
+
+///
+/// Adds support for the extension to use the "Telemetry" and "Feature Telemetry" codeunits.
+///
+codeunit 8753 "DA Telemetry Logger" implements "Telemetry Logger"
+{
+ Access = Internal;
+
+ procedure LogMessage(EventId: Text; Message: Text; Verbosity: Verbosity; DataClassification: DataClassification; TelemetryScope: TelemetryScope; CustomDimensions: Dictionary of [Text, Text])
+ begin
+ Session.LogMessage(EventId, Message, Verbosity, DataClassification, TelemetryScope, CustomDimensions);
+ end;
+
+ // For the functionality to behave as expected, there must be exactly one implementation of the "Telemetry Logger" interface registered per app publisher
+ [EventSubscriber(ObjectType::Codeunit, Codeunit::"Telemetry Loggers", 'OnRegisterTelemetryLogger', '', true, true)]
+ local procedure OnRegisterTelemetryLogger(var Sender: Codeunit "Telemetry Loggers")
+ var
+ DATelemetryLogger: Codeunit "DA Telemetry Logger";
+ begin
+ Sender.Register(DATelemetryLogger);
+ end;
+}
diff --git a/src/System Application/App/External File Storage/src/Scenario/DefaultFileScenarioImpl.Codeunit.al b/src/System Application/App/External File Storage/src/Scenario/DefaultFileScenarioImpl.Codeunit.al
index ad9cbc6a0d..fb6033ae74 100644
--- a/src/System Application/App/External File Storage/src/Scenario/DefaultFileScenarioImpl.Codeunit.al
+++ b/src/System Application/App/External File Storage/src/Scenario/DefaultFileScenarioImpl.Codeunit.al
@@ -17,7 +17,7 @@ codeunit 9459 "Default File Scenario Impl." implements "File Scenario"
/// The ID of the file scenario.
/// The file storage connector.
/// True if the operation is allowed, otherwise false.
- procedure BeforeAddOrModifyFileScenarioCheck(Scenario: Integer; Connector: Enum System.ExternalFileStorage."Ext. File Storage Connector") SkipInsertOrModify: Boolean
+ procedure BeforeAddOrModifyFileScenarioCheck(Scenario: Enum "File Scenario"; Connector: Enum System.ExternalFileStorage."Ext. File Storage Connector") SkipInsertOrModify: Boolean
begin
SkipInsertOrModify := false;
end;
@@ -28,7 +28,7 @@ codeunit 9459 "Default File Scenario Impl." implements "File Scenario"
/// The ID of the file scenario.
/// The file storage connector.
/// True if additional setup is available, otherwise false.
- procedure GetAdditionalScenarioSetup(Scenario: Integer; Connector: Enum System.ExternalFileStorage."Ext. File Storage Connector") SetupExist: Boolean
+ procedure GetAdditionalScenarioSetup(Scenario: Enum "File Scenario"; Connector: Enum System.ExternalFileStorage."Ext. File Storage Connector") SetupExist: Boolean
begin
SetupExist := false;
end;
@@ -39,7 +39,7 @@ codeunit 9459 "Default File Scenario Impl." implements "File Scenario"
/// The ID of the file scenario.
/// The file storage connector.
/// True if the delete operation is handled and should not proceed, otherwise false.
- procedure BeforeDeleteFileScenarioCheck(Scenario: Integer; Connector: Enum System.ExternalFileStorage."Ext. File Storage Connector") SkipDelete: Boolean
+ procedure BeforeDeleteFileScenarioCheck(Scenario: Enum "File Scenario"; Connector: Enum System.ExternalFileStorage."Ext. File Storage Connector") SkipDelete: Boolean
begin
SkipDelete := false;
end;
diff --git a/src/System Application/App/External File Storage/src/Scenario/FileScenario.Interface.al b/src/System Application/App/External File Storage/src/Scenario/FileScenario.Interface.al
index ad50339cab..b41b0fa56b 100644
--- a/src/System Application/App/External File Storage/src/Scenario/FileScenario.Interface.al
+++ b/src/System Application/App/External File Storage/src/Scenario/FileScenario.Interface.al
@@ -13,7 +13,7 @@ interface "File Scenario"
/// The ID of the file scenario.
/// The file storage connector.
/// True if the operation is allowed; otherwise false.
- procedure BeforeAddOrModifyFileScenarioCheck(Scenario: Integer; Connector: Enum System.ExternalFileStorage."Ext. File Storage Connector") SkipInsertOrModify: Boolean;
+ procedure BeforeAddOrModifyFileScenarioCheck(Scenario: Enum "File Scenario"; Connector: Enum System.ExternalFileStorage."Ext. File Storage Connector") SkipInsertOrModify: Boolean;
///
/// Called to get additional setup for a file scenario.
@@ -21,7 +21,7 @@ interface "File Scenario"
/// The ID of the file scenario.
/// The file storage connector.
/// True if additional setup is available, otherwise false.
- procedure GetAdditionalScenarioSetup(Scenario: Integer; Connector: Enum System.ExternalFileStorage."Ext. File Storage Connector") SetupExist: Boolean;
+ procedure GetAdditionalScenarioSetup(Scenario: Enum "File Scenario"; Connector: Enum System.ExternalFileStorage."Ext. File Storage Connector") SetupExist: Boolean;
///
/// Called before deleting a file scenario.
@@ -29,5 +29,5 @@ interface "File Scenario"
/// The ID of the file scenario.
/// The file storage connector.
/// True if the delete operation is handled and should not proceed; otherwise false.
- procedure BeforeDeleteFileScenarioCheck(Scenario: Integer; Connector: Enum System.ExternalFileStorage."Ext. File Storage Connector") SkipDelete: Boolean;
+ procedure BeforeDeleteFileScenarioCheck(Scenario: Enum "File Scenario"; Connector: Enum System.ExternalFileStorage."Ext. File Storage Connector") SkipDelete: Boolean;
}
diff --git a/src/System Application/App/External File Storage/src/Scenario/FileScenarioImpl.Codeunit.al b/src/System Application/App/External File Storage/src/Scenario/FileScenarioImpl.Codeunit.al
index f95118aec8..2a03512b32 100644
--- a/src/System Application/App/External File Storage/src/Scenario/FileScenarioImpl.Codeunit.al
+++ b/src/System Application/App/External File Storage/src/Scenario/FileScenarioImpl.Codeunit.al
@@ -209,7 +209,7 @@ codeunit 9453 "File Scenario Impl."
repeat
FileScenarioEnum := Enum::"File Scenario".FromInteger(TempSelectedFileAccScenarios.Scenario);
FileScenarioInterface := FileScenarioEnum;
- if FileScenarioInterface.BeforeAddOrModifyFileScenarioCheck(TempSelectedFileAccScenarios.Scenario, TempSelectedFileAccScenarios.Connector) then
+ if FileScenarioInterface.BeforeAddOrModifyFileScenarioCheck(FileScenarioEnum, TempSelectedFileAccScenarios.Connector) then
exit;
if not FileScenario.Get(TempSelectedFileAccScenarios.Scenario) then begin
@@ -324,7 +324,7 @@ codeunit 9453 "File Scenario Impl."
FileScenarioEnum := Enum::"File Scenario".FromInteger(TempFileAccountScenario.Scenario);
FileScenarioInterface := FileScenarioEnum;
- if not FileScenarioInterface.BeforeDeleteFileScenarioCheck(TempFileAccountScenario.Scenario, TempFileAccountScenario.Connector) then
+ if not FileScenarioInterface.BeforeDeleteFileScenarioCheck(FileScenarioEnum, TempFileAccountScenario.Connector) then
FileScenario.DeleteAll();
end;
until TempFileAccountScenario.Next() = 0;
diff --git a/src/System Application/App/External File Storage/src/Scenario/FileScenarioSetup.Page.al b/src/System Application/App/External File Storage/src/Scenario/FileScenarioSetup.Page.al
index e40def3d62..8a3fc7b11c 100644
--- a/src/System Application/App/External File Storage/src/Scenario/FileScenarioSetup.Page.al
+++ b/src/System Application/App/External File Storage/src/Scenario/FileScenarioSetup.Page.al
@@ -95,7 +95,7 @@ page 9452 "File Scenario Setup"
begin
FileScenarioEnum := Enum::"File Scenario".FromInteger(Rec.Scenario);
FileScenarioInterface := FileScenarioEnum;
- if not FileScenarioInterface.GetAdditionalScenarioSetup(Rec.Scenario, Rec.Connector) then
+ if not FileScenarioInterface.GetAdditionalScenarioSetup(FileScenarioEnum, Rec.Connector) then
Message(NoSetupAvailableMsg);
end;
}
diff --git a/src/System Application/App/External File Storage/src/Scenario/FileScenariosFactBox.Page.al b/src/System Application/App/External File Storage/src/Scenario/FileScenariosFactBox.Page.al
index 533492a3a7..c73e46617b 100644
--- a/src/System Application/App/External File Storage/src/Scenario/FileScenariosFactBox.Page.al
+++ b/src/System Application/App/External File Storage/src/Scenario/FileScenariosFactBox.Page.al
@@ -39,4 +39,28 @@ page 9453 "File Scenarios FactBox"
}
}
}
+ actions
+ {
+ area(Processing)
+ {
+ action(ScenarioSetup)
+ {
+ Caption = 'Additional Scenario Setup';
+ ToolTip = 'Additional scenario setup for the selected scenario.';
+ Image = Setup;
+ Scope = Repeater;
+ trigger OnAction()
+ var
+ FileScenarioInterface: Interface "File Scenario";
+ FileScenarioEnum: Enum "File Scenario";
+ NoSetupAvailableMsg: Label 'No additional setup is available for this scenario.';
+ begin
+ FileScenarioEnum := Rec.Scenario;
+ FileScenarioInterface := FileScenarioEnum;
+ if not FileScenarioInterface.GetAdditionalScenarioSetup(Rec.Scenario, Rec.Connector) then
+ Message(NoSetupAvailableMsg);
+ end;
+ }
+ }
+ }
}
\ No newline at end of file
From 509618d2c630e80553351c1112d1f32d01eb0aa3 Mon Sep 17 00:00:00 2001
From: Stefan Sosic <59221826+StefanSosic@users.noreply.github.com>
Date: Tue, 21 Oct 2025 14:36:47 +0200
Subject: [PATCH 19/34] Add disable check
---
.../app/src/Setup/DAExternalStorageSetup.Table.al | 7 +++++++
1 file changed, 7 insertions(+)
diff --git a/src/Apps/W1/External Storage - Document Attachments/app/src/Setup/DAExternalStorageSetup.Table.al b/src/Apps/W1/External Storage - Document Attachments/app/src/Setup/DAExternalStorageSetup.Table.al
index f148662397..d28097ef94 100644
--- a/src/Apps/W1/External Storage - Document Attachments/app/src/Setup/DAExternalStorageSetup.Table.al
+++ b/src/Apps/W1/External Storage - Document Attachments/app/src/Setup/DAExternalStorageSetup.Table.al
@@ -34,7 +34,14 @@ table 8750 "DA External Storage Setup"
trigger OnValidate()
var
DAFeatureTelemetry: Codeunit "DA Feature Telemetry";
+ DisableSetupErr: Label 'Cannot disable External Storage setup because there are files uploaded using this configuration. Please delete the uploaded files before disabling the setup.';
begin
+ if xRec.Enabled and not Rec.Enabled then begin
+ CalcFields("Has Uploaded Files");
+ if "Has Uploaded Files" then
+ Error(DisableSetupErr);
+ end;
+
if Enabled then
DAFeatureTelemetry.LogFeatureEnabled()
else
From 244043f6c27b7aec247134760ba46495dc62e9f3 Mon Sep 17 00:00:00 2001
From: Stefan Sosic <59221826+StefanSosic@users.noreply.github.com>
Date: Tue, 21 Oct 2025 14:37:29 +0200
Subject: [PATCH 20/34] Remove prefix
---
.../DAExternalStorageImpl.Codeunit.al | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/Apps/W1/External Storage - Document Attachments/app/src/DocumentAttachmentIntegration/DAExternalStorageImpl.Codeunit.al b/src/Apps/W1/External Storage - Document Attachments/app/src/DocumentAttachmentIntegration/DAExternalStorageImpl.Codeunit.al
index aa5e911cdf..95b540af46 100644
--- a/src/Apps/W1/External Storage - Document Attachments/app/src/DocumentAttachmentIntegration/DAExternalStorageImpl.Codeunit.al
+++ b/src/Apps/W1/External Storage - Document Attachments/app/src/DocumentAttachmentIntegration/DAExternalStorageImpl.Codeunit.al
@@ -11,7 +11,7 @@ using System.Environment;
using System.Security.Encryption;
using Microsoft.Foundation.Attachment;
-codeunit 8751 "BCY DA External Storage Impl." implements "File Scenario"
+codeunit 8751 "DA External Storage Impl." implements "File Scenario"
{
Access = Internal;
InherentPermissions = X;
From 0db76107acf57906003b58f22d4f697247111a2c Mon Sep 17 00:00:00 2001
From: Stefan Sosic <59221826+StefanSosic@users.noreply.github.com>
Date: Tue, 21 Oct 2025 14:47:28 +0200
Subject: [PATCH 21/34] Adjustments to deletion
---
.../DAExternalStorageImpl.Codeunit.al | 19 ++++++-------------
1 file changed, 6 insertions(+), 13 deletions(-)
diff --git a/src/Apps/W1/External Storage - Document Attachments/app/src/DocumentAttachmentIntegration/DAExternalStorageImpl.Codeunit.al b/src/Apps/W1/External Storage - Document Attachments/app/src/DocumentAttachmentIntegration/DAExternalStorageImpl.Codeunit.al
index 95b540af46..07d527d494 100644
--- a/src/Apps/W1/External Storage - Document Attachments/app/src/DocumentAttachmentIntegration/DAExternalStorageImpl.Codeunit.al
+++ b/src/Apps/W1/External Storage - Document Attachments/app/src/DocumentAttachmentIntegration/DAExternalStorageImpl.Codeunit.al
@@ -341,11 +341,6 @@ codeunit 8751 "DA External Storage Impl." implements "File Scenario"
if not IsFeatureEnabled() then
exit(false);
- // Check if file belongs to another company and needs migration
- if IsFileFromAnotherEnvironmentOrCompany(DocumentAttachment) then
- if not MigrateFileToCurrentEnvironment(DocumentAttachment) then
- exit(false);
-
// Validate input parameters
if DocumentAttachment."External File Path" = '' then
exit(false);
@@ -353,6 +348,12 @@ codeunit 8751 "DA External Storage Impl." implements "File Scenario"
if not DocumentAttachment."Uploaded Externally" then
exit(false);
+ // Check if file belongs to another environment - if so, just clear the reference
+ if IsFileFromAnotherEnvironmentOrCompany(DocumentAttachment) then begin
+ DocumentAttachment.MarkAsNotUploadedToExternal();
+ exit(true);
+ end;
+
// Use the stored external file path
ExternalFilePath := DocumentAttachment."External File Path";
@@ -381,11 +382,6 @@ codeunit 8751 "DA External Storage Impl." implements "File Scenario"
var
TenantMedia: Record "Tenant Media";
begin
- // Check if file belongs to another company and needs migration
- if IsFileFromAnotherEnvironmentOrCompany(DocumentAttachment) then
- if not MigrateFileToCurrentEnvironment(DocumentAttachment) then
- exit(false);
-
// Validate input parameters
if not DocumentAttachment."Document Reference ID".HasValue() then
exit(false);
@@ -786,7 +782,6 @@ codeunit 8751 "DA External Storage Impl." implements "File Scenario"
OutStream: OutStream;
OldFilePath: Text;
NewFilePath: Text[2048];
- MigrationMsg: Label 'File is being migrated from another environment folder to the current environment folder.';
begin
if not DocumentAttachment."Uploaded Externally" then
exit(false);
@@ -794,8 +789,6 @@ codeunit 8751 "DA External Storage Impl." implements "File Scenario"
if DocumentAttachment."External File Path" = '' then
exit(false);
- Message(MigrationMsg);
-
// Initialize external file storage
FileScenario := FileScenario::"Doc. Attach. - External Storage";
if not FileScenarioCU.GetFileAccount(FileScenario, FileAccount) then
From ad8638e5437fc875e6908b9511dc37465627c662 Mon Sep 17 00:00:00 2001
From: Stefan Sosic <59221826+StefanSosic@users.noreply.github.com>
Date: Tue, 21 Oct 2025 15:02:44 +0200
Subject: [PATCH 22/34] Remove logger
---
.../Telemetry/DATelemetryLogger.Codeunit.al | 30 -------------------
1 file changed, 30 deletions(-)
delete mode 100644 src/Apps/W1/External Storage - Document Attachments/app/src/Telemetry/DATelemetryLogger.Codeunit.al
diff --git a/src/Apps/W1/External Storage - Document Attachments/app/src/Telemetry/DATelemetryLogger.Codeunit.al b/src/Apps/W1/External Storage - Document Attachments/app/src/Telemetry/DATelemetryLogger.Codeunit.al
deleted file mode 100644
index 5c101c0b54..0000000000
--- a/src/Apps/W1/External Storage - Document Attachments/app/src/Telemetry/DATelemetryLogger.Codeunit.al
+++ /dev/null
@@ -1,30 +0,0 @@
-// ------------------------------------------------------------------------------------------------
-// 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 System.Telemetry;
-
-///
-/// Adds support for the extension to use the "Telemetry" and "Feature Telemetry" codeunits.
-///
-codeunit 8753 "DA Telemetry Logger" implements "Telemetry Logger"
-{
- Access = Internal;
-
- procedure LogMessage(EventId: Text; Message: Text; Verbosity: Verbosity; DataClassification: DataClassification; TelemetryScope: TelemetryScope; CustomDimensions: Dictionary of [Text, Text])
- begin
- Session.LogMessage(EventId, Message, Verbosity, DataClassification, TelemetryScope, CustomDimensions);
- end;
-
- // For the functionality to behave as expected, there must be exactly one implementation of the "Telemetry Logger" interface registered per app publisher
- [EventSubscriber(ObjectType::Codeunit, Codeunit::"Telemetry Loggers", 'OnRegisterTelemetryLogger', '', true, true)]
- local procedure OnRegisterTelemetryLogger(var Sender: Codeunit "Telemetry Loggers")
- var
- DATelemetryLogger: Codeunit "DA Telemetry Logger";
- begin
- Sender.Register(DATelemetryLogger);
- end;
-}
From f358ddb2a741fe252f83ae3c59c849bc9d6381ea Mon Sep 17 00:00:00 2001
From: Stefan Sosic <59221826+StefanSosic@users.noreply.github.com>
Date: Tue, 28 Oct 2025 02:35:39 +0100
Subject: [PATCH 23/34] Update readme.md
---
.../app/README.md | 143 +++++++++++++++---
1 file changed, 126 insertions(+), 17 deletions(-)
diff --git a/src/Apps/W1/External Storage - Document Attachments/app/README.md b/src/Apps/W1/External Storage - Document Attachments/app/README.md
index 299699728b..dc3f543671 100644
--- a/src/Apps/W1/External Storage - Document Attachments/app/README.md
+++ b/src/Apps/W1/External Storage - Document Attachments/app/README.md
@@ -6,17 +6,30 @@ The External Storage extension provides seamless integration between Microsoft D
## Key Features
-### **Automatic Upload**
-- Automatically uploads new document attachments to configured external storage
+### **Automatic Scheduled Upload**
+- Automatically uploads new document attachments to configured external storage via scheduled job queue
- Supports multiple storage connectors via the File Account framework
- Generates unique file names to prevent collisions
- Maintains original file metadata and associations
+- Configurable job queue runs daily at 1:00 AM with automatic retry capability
+
+### **Multi-Tenant, Multi-Environment, and Multi-Company Support**
+- **Environment Hash**: Unique hash based on Tenant ID + Environment Name + Company System ID
+- **Organized Folder Structure**: Files are stored in hierarchical folders: `RootFolder/EnvironmentHash/TableName/FileName`
+- **Cross-Environment Compatibility**: Files from different tenants, environments, or companies are properly isolated
+- **Migration Support**: Built-in migration tool to move files between company folders when needed
+- **Environment Hash Display**: View current environment hash for reference and troubleshooting
### **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
+- **Delete from BC after Upload**: Immediately delete from internal storage right after external upload
+- **Delete After**: Configurable retention period using date formulas (e.g., `<7D>` for 7 days, `<30D>` for 30 days)
+- **Delete from External Storage**: Optionally delete files from external storage when attachments are removed from BC
+- **Automatic Cleanup**: Scheduled job queue can automatically delete expired files based on retention policy
+
+### **Customizable Root Folder**
+- Configure a custom root folder path for all attachments
+- Interactive folder browser for easy selection
+- Automatic folder creation and hierarchy management
### **Bulk Operations**
- Synchronize multiple files between internal and external storage
@@ -27,9 +40,10 @@ The External Storage extension provides seamless integration between Microsoft D
## Installation & Setup
### Prerequisites
-- Microsoft Dynamics 365 Business Central version 27.0 or later
+- Microsoft Dynamics 365 Business Central version 28.0 or later
- File Account module configured with external storage connector
- Appropriate permissions for file operations
+- Job Queue functionality enabled for scheduled uploads
### Installation Steps
@@ -46,33 +60,100 @@ The External Storage extension provides seamless integration between Microsoft D
- 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
+ - **Enabled**: Enable the External Storage feature
+ - **Root Folder**: Select the root folder path for attachments (use AssistEdit to browse)
+ - **Delete from BC after Upload**: Enable to immediately delete files from BC after upload
+ - **Delete After**: Set retention period using date formula (e.g., `<7D>` for 7 days)
+ - **Scheduled Upload**: Enable automatic background upload via job queue
+ - **Delete from External Storage**: Enable to delete external files when attachments are removed from BC
### Configuration Options
-#### Auto Upload Settings
-- **Enabled**: New document attachments are automatically uploaded to external storage
-- **Disabled**: Manual upload required via actions
+#### General Settings
+- **Enabled**: Master switch to activate/deactivate the External Storage feature
+- **Root Folder**: Base folder path in external storage where all attachments will be organized
+ - Files are stored in: `RootFolder/EnvironmentHash/TableName/FileName`
+ - Use AssistEdit button to browse and select folder interactively
+
+#### Upload and Delete Policy
+- **Delete from BC after Upload**: When enabled, files are immediately removed from internal storage after successful upload to external storage
+- **Delete After**: Date formula specifying retention period before internal deletion (e.g., `<7D>`, `<30D>`)
+ - Only active when "Delete from BC after Upload" is disabled
+- **Scheduled Upload**: Enable automatic background upload using job queue
+ - Job runs daily at 1:00 AM
+ - Maximum 3 retry attempts on failure
+- **Delete from External Storage**: When enabled, files are deleted from external storage when the attachment is removed from Business Central
+
+#### Job Queue Information
+- **Job Queue Entry ID**: System-generated ID for the scheduled upload job
+- **Job Queue Status**: Current status (Not Created, Ready, On Hold, Deleted)
+- Click on Job Queue Entry ID to open detailed job queue card
## Usage
### Automatic Mode
-When Auto Upload is enabled:
+When Scheduled Upload is enabled:
1. User attaches a document to any Business Central record
-2. System automatically uploads to external storage
+2. System automatically uploads to external storage via scheduled job queue (runs daily at 1:00 AM)
3. File remains accessible through standard attachment functionality
4. Internal file is deleted based on configured retention policy
+### Multi-Company and Multi-Environment Support
+
+#### Environment Hash
+Every file uploaded to external storage includes an environment hash that uniquely identifies:
+- **Tenant ID**: Your Business Central tenant
+- **Environment Name**: Current environment (e.g., Production, Sandbox)
+- **Company System ID**: Unique identifier for the company
+
+This ensures files from different tenants, environments, or companies are properly isolated in external storage.
+
+#### Folder Structure
+Files are organized hierarchically:
+```
+RootFolder/
+ ├── [EnvironmentHash-1]/
+ │ ├── Sales_Header/
+ │ │ └── invoice-{guid}.pdf
+ │ └── Purchase_Header/
+ │ └── order-{guid}.pdf
+ └── [EnvironmentHash-2]/
+ └── Sales_Header/
+ └── quote-{guid}.pdf
+```
+
+#### File Migration
+When moving data between environments or companies:
+1. Open **External Storage Setup** page
+2. Click **Migrate Files** action
+3. System automatically:
+ - Identifies files from previous environment/company
+ - Copies files to current environment/company folder structure
+ - Updates file paths and environment hash
+ - Maintains all file metadata and associations
+
+#### Environment Hash Display
+- Click **Show Current Environment Hash** to view your current hash
+- Use this hash to identify your files in external storage
+- Helpful for troubleshooting and cross-environment scenarios
+
### Manual Operations
+#### Setup Page Actions
+From **External Storage Setup** page:
+- **Storage Sync**: Run synchronization manually to upload/download files
+- **Migrate Files**: Migrate all files from previous environment/company to current folder structure
+- **Show Current Environment Hash**: Display the current environment hash for reference
+- **Show Job Queue Entry**: View and manage the scheduled upload job
+- **Document Attachments**: Open the list of all document attachments with external storage information
+
#### Individual File Operations
From **Document Attachment - External** page:
-- **Upload to External Storage**: Upload selected file
+- **Upload to External Storage**: Upload selected file manually
- **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
+- **Delete from Internal Storage**: Remove file from internal storage only
#### Bulk Operations
From **External Storage Synchronize** report:
@@ -80,9 +161,37 @@ From **External Storage Synchronize** report:
- **From External Storage**: Download multiple files from external storage
- **Delete Expired Files**: Clean up files based on retention policy
-### File Access
+### File Access and Compatibility
- Files uploaded to external storage remain fully accessible through standard Business Central functionality
- Document preview, download, and management work seamlessly
+- Files deleted internally are automatically retrieved from external storage when accessed
- No change to end-user experience
+- Cross-environment and cross-company access is handled automatically
+
+### Job Queue Management
+- Scheduled upload job runs daily at 1:00 AM
+- Automatic retry on failure (up to 3 attempts)
+- Job status visible in setup page
+- Can be manually triggered via **Storage Sync** action
+- Job can be paused by disabling **Scheduled Upload**
+
+## Important Notes
+
+### Data Safety
+- **This extension is provided as-is**
+- Always maintain proper backups of your external storage
+- Test thoroughly in a sandbox environment before production use
+- Verify file accessibility after migration
+
+### Environment Changes
+- When moving between environments, use the **Migrate Files** action
+- Environment hash changes with tenant, environment, or company changes
+- Files from previous environments are not automatically deleted
+- Manual cleanup of old environment folders may be required
+
+### Feature Disable Protection
+- Cannot disable External Storage setup if files are uploaded
+- Must delete all uploaded files before disabling the feature
+- Cannot unassign External Storage scenario if files exist in external storage
**© 2025 Microsoft Corporation. All rights reserved.**
From b6c89f6b5a03fc6bdb1b4eb5b3629d7589b8cd53 Mon Sep 17 00:00:00 2001
From: Jesper Schulz-Wedde
Date: Fri, 19 Dec 2025 12:46:36 +0100
Subject: [PATCH 24/34] Improve warning message
---
.../DAExternalStorageImpl.Codeunit.al | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/Apps/W1/External Storage - Document Attachments/app/src/DocumentAttachmentIntegration/DAExternalStorageImpl.Codeunit.al b/src/Apps/W1/External Storage - Document Attachments/app/src/DocumentAttachmentIntegration/DAExternalStorageImpl.Codeunit.al
index 07d527d494..6a4a5ed43a 100644
--- a/src/Apps/W1/External Storage - Document Attachments/app/src/DocumentAttachmentIntegration/DAExternalStorageImpl.Codeunit.al
+++ b/src/Apps/W1/External Storage - Document Attachments/app/src/DocumentAttachmentIntegration/DAExternalStorageImpl.Codeunit.al
@@ -33,7 +33,7 @@ codeunit 8751 "DA External Storage Impl." implements "File Scenario"
FileAccount: Record "File Account";
ConfirmManagement: Codeunit "Confirm Management";
FileScenarioCU: Codeunit "File Scenario";
- DisclaimerMsg: Label 'You are about to enable External Storage!!!\\This feature is provided as-is, and you use it at your own risk.\Microsoft is not responsible for any issues or data loss that may occur.\\Do you wish to continue?';
+ DisclaimerMsg: Label 'You are about to enable External Storage.\\When this feature is enabled, files will be stored outside the Business Central service boundary.\Microsoft does not manage, back up, or restore data stored in external storage.\\You are responsible for the configuration, security, compliance, backup, and recovery of all externally stored files.\This feature is provided as-is, and you enable it at your own risk.\\Do you want to continue?';
begin
if not (Scenario = Enum::"File Scenario"::"Doc. Attach. - External Storage") then
exit;
From 4f4c2077da9d68607baf60e47a3684083d229774 Mon Sep 17 00:00:00 2001
From: Stefan Sosic <59221826+StefanSosic@users.noreply.github.com>
Date: Fri, 19 Dec 2025 13:49:45 +0100
Subject: [PATCH 25/34] Fallback for table name if 3rd party App Uninstalled
---
.../DAExternalStorageImpl.Codeunit.al | 20 ++++++++++++++-----
1 file changed, 15 insertions(+), 5 deletions(-)
diff --git a/src/Apps/W1/External Storage - Document Attachments/app/src/DocumentAttachmentIntegration/DAExternalStorageImpl.Codeunit.al b/src/Apps/W1/External Storage - Document Attachments/app/src/DocumentAttachmentIntegration/DAExternalStorageImpl.Codeunit.al
index 07d527d494..2d4d2560a6 100644
--- a/src/Apps/W1/External Storage - Document Attachments/app/src/DocumentAttachmentIntegration/DAExternalStorageImpl.Codeunit.al
+++ b/src/Apps/W1/External Storage - Document Attachments/app/src/DocumentAttachmentIntegration/DAExternalStorageImpl.Codeunit.al
@@ -708,13 +708,11 @@ codeunit 8751 "DA External Storage Impl." implements "File Scenario"
local procedure GetTableNameFolder(TableID: Integer): Text[100]
var
- RecRef: RecordRef;
TableName: Text;
begin
- // Open the RecordRef to get table metadata
- RecRef.Open(TableID, false);
- TableName := RecRef.Name;
- RecRef.Close();
+ // Try to get table name from metadata, fallback to table ID if not available
+ if not TryGetTableName(TableID, TableName) then
+ TableName := 'Table_' + Format(TableID);
// Replace invalid characters for folder names
TableName := DelChr(TableName, '=', '<>:"/\|?*');
@@ -723,6 +721,18 @@ codeunit 8751 "DA External Storage Impl." implements "File Scenario"
exit(CopyStr(TableName, 1, 100));
end;
+ [TryFunction]
+ local procedure TryGetTableName(TableID: Integer; var TableName: Text)
+ var
+ RecRef: RecordRef;
+ begin
+ // Open the RecordRef to get table metadata
+ // This will fail if the table no longer exists (e.g., after uninstalling a 3rd party app)
+ RecRef.Open(TableID, false);
+ TableName := RecRef.Name;
+ RecRef.Close();
+ end;
+
local procedure EnsureFolderExists(CompanyFolderPath: Text)
var
FileAccount: Record "File Account";
From f12957b72cd9db0d02fc316459e712d79764ccce Mon Sep 17 00:00:00 2001
From: Stefan Sosic <59221826+StefanSosic@users.noreply.github.com>
Date: Fri, 19 Dec 2025 13:55:54 +0100
Subject: [PATCH 26/34] Add migration report
---
.../DAExternalStorageMigration.Report.al | 124 ++++++++++++++++++
.../DAExternalStorageImpl.Codeunit.al | 14 +-
.../src/Setup/DAExternalStorageSetup.Page.al | 4 +-
3 files changed, 137 insertions(+), 5 deletions(-)
create mode 100644 src/Apps/W1/External Storage - Document Attachments/app/src/AutomaticSync/DAExternalStorageMigration.Report.al
diff --git a/src/Apps/W1/External Storage - Document Attachments/app/src/AutomaticSync/DAExternalStorageMigration.Report.al b/src/Apps/W1/External Storage - Document Attachments/app/src/AutomaticSync/DAExternalStorageMigration.Report.al
new file mode 100644
index 0000000000..29fd8e6f2f
--- /dev/null
+++ b/src/Apps/W1/External Storage - Document Attachments/app/src/AutomaticSync/DAExternalStorageMigration.Report.al
@@ -0,0 +1,124 @@
+// ------------------------------------------------------------------------------------------------
+// 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;
+
+///
+/// Report for migrating document attachments from previous environment/company folder to current environment/company folder.
+/// Can be scheduled via job queue for background processing.
+///
+report 8753 "DA External Storage Migration"
+{
+ Caption = 'External Storage Migration';
+ ProcessingOnly = true;
+ UseRequestPage = true;
+ Extensible = false;
+ ApplicationArea = Basic, Suite;
+ UsageCategory = None;
+ Permissions = tabledata "DA External Storage Setup" = r,
+ tabledata "Document Attachment" = rimd;
+
+ dataset
+ {
+ dataitem(DocumentAttachment; "Document Attachment")
+ {
+ trigger OnPreDataItem()
+ begin
+ // Filter for files that are uploaded externally and need migration
+ SetRange("Uploaded Externally", true);
+ SetFilter("External File Path", '<>%1', '');
+
+ TotalCount := Count();
+
+ if TotalCount = 0 then begin
+ if GuiAllowed() then
+ Message(NoFilesToMigrateLbl);
+ CurrReport.Break();
+ end;
+
+ ProcessedCount := 0;
+ MigratedCount := 0;
+ FailedCount := 0;
+
+ if GuiAllowed() then
+ Dialog.Open(ProcessingMsg, TotalCount);
+ end;
+
+ trigger OnAfterGetRecord()
+ begin
+ ProcessedCount += 1;
+
+ if GuiAllowed() then
+ Dialog.Update(1, ProcessedCount);
+
+ // Check if file needs migration
+ if ExternalStorageImpl.IsFileFromAnotherEnvironmentOrCompany(DocumentAttachment) then
+ if ExternalStorageImpl.MigrateFileToCurrentEnvironment(DocumentAttachment) then
+ MigratedCount += 1
+ else
+ FailedCount += 1;
+
+ Commit(); // Commit after each record to avoid lost in communication error with external storage service
+ end;
+
+ trigger OnPostDataItem()
+ begin
+ if MigratedCount > 0 then
+ LogMigrationTelemetry();
+
+ if GuiAllowed() then begin
+ if TotalCount <> 0 then
+ Dialog.Close();
+
+ if MigratedCount > 0 then
+ Message(MigrationCompletedMsg, MigratedCount, FailedCount)
+ else
+ Message(NoFilesToMigrateLbl);
+ end;
+ end;
+ }
+ }
+
+ requestpage
+ {
+ SaveValues = true;
+
+ layout
+ {
+ area(Content)
+ {
+ group(General)
+ {
+ Caption = 'General';
+ label(InfoLabel)
+ {
+ ApplicationArea = Basic, Suite;
+ Caption = 'This report will migrate all document attachments from a previous environment or company folder to the current environment/company folder in external storage.';
+ }
+ }
+ }
+ }
+ }
+
+ var
+ ExternalStorageImpl: Codeunit "DA External Storage Impl.";
+ Dialog: Dialog;
+ FailedCount: Integer;
+ MigratedCount: Integer;
+ ProcessedCount: Integer;
+ TotalCount: Integer;
+ MigrationCompletedMsg: Label '%1 file(s) have been successfully migrated to the current company folder. %2 failed.', Comment = '%1 = Number of migrated files, %2 = Number of failed migrations';
+ NoFilesToMigrateLbl: Label 'All Files are already in the current company folder. No migration needed.';
+ ProcessingMsg: Label 'Processing #1###### attachments...', Comment = '%1 - Total Number of Attachments';
+
+ local procedure LogMigrationTelemetry()
+ var
+ DAFeatureTelemetry: Codeunit "DA Feature Telemetry";
+ begin
+ DAFeatureTelemetry.LogCompanyMigration();
+ end;
+}
diff --git a/src/Apps/W1/External Storage - Document Attachments/app/src/DocumentAttachmentIntegration/DAExternalStorageImpl.Codeunit.al b/src/Apps/W1/External Storage - Document Attachments/app/src/DocumentAttachmentIntegration/DAExternalStorageImpl.Codeunit.al
index 65c1dfc737..0c294b4f0d 100644
--- a/src/Apps/W1/External Storage - Document Attachments/app/src/DocumentAttachmentIntegration/DAExternalStorageImpl.Codeunit.al
+++ b/src/Apps/W1/External Storage - Document Attachments/app/src/DocumentAttachmentIntegration/DAExternalStorageImpl.Codeunit.al
@@ -769,7 +769,12 @@ codeunit 8751 "DA External Storage Impl." implements "File Scenario"
exit(CopyStr(CryptographyManagement.GenerateHash(IdentityString, HashAlgorithmType::SHA256), 1, 16));
end;
- local procedure IsFileFromAnotherEnvironmentOrCompany(DocumentAttachment: Record "Document Attachment"): Boolean
+ ///
+ /// Checks if a file belongs to another environment or company.
+ ///
+ /// The document attachment record to check.
+ /// True if the file is from another environment or company, false otherwise.
+ procedure IsFileFromAnotherEnvironmentOrCompany(DocumentAttachment: Record "Document Attachment"): Boolean
var
CurrentEnvironmentHash: Text[16];
begin
@@ -781,7 +786,12 @@ codeunit 8751 "DA External Storage Impl." implements "File Scenario"
exit(DocumentAttachment."Source Environment Hash" <> CurrentEnvironmentHash);
end;
- local procedure MigrateFileToCurrentEnvironment(var DocumentAttachment: Record "Document Attachment"): Boolean
+ ///
+ /// Migrates a file from a previous environment or company folder to the current one.
+ ///
+ /// The document attachment record to migrate.
+ /// True if migration was successful, false otherwise.
+ procedure MigrateFileToCurrentEnvironment(var DocumentAttachment: Record "Document Attachment"): Boolean
var
FileAccount: Record "File Account";
ExternalFileStorage: Codeunit "External File Storage";
diff --git a/src/Apps/W1/External Storage - Document Attachments/app/src/Setup/DAExternalStorageSetup.Page.al b/src/Apps/W1/External Storage - Document Attachments/app/src/Setup/DAExternalStorageSetup.Page.al
index 2fdf9c12ef..88f34dee05 100644
--- a/src/Apps/W1/External Storage - Document Attachments/app/src/Setup/DAExternalStorageSetup.Page.al
+++ b/src/Apps/W1/External Storage - Document Attachments/app/src/Setup/DAExternalStorageSetup.Page.al
@@ -110,10 +110,8 @@ page 8750 "DA External Storage Setup"
ToolTip = 'Migrate all document attachments from the previous environment/company folder to the current environment/company folder.';
trigger OnAction()
- var
- DAExternalStorageImpl: Codeunit "DA External Storage Impl.";
begin
- DAExternalStorageImpl.RunCompanyMigration();
+ Report.Run(Report::"DA External Storage Migration");
end;
}
action(ShowCurrentHash)
From 14164b20be0cd8cf7d84a94462bd87a0b5999b12 Mon Sep 17 00:00:00 2001
From: Stefan Sosic <59221826+StefanSosic@users.noreply.github.com>
Date: Mon, 19 Jan 2026 12:30:15 +0100
Subject: [PATCH 27/34] File Scenario fix breaking change
---
.../DAExternalStorageMigration.Report.al | 2 +-
.../DAExternalStorageImpl.Codeunit.al | 28 +++++++++----------
.../src/Setup/DAExternalStorageSetup.Page.al | 2 +-
.../src/Scenario/FileScenario.Codeunit.al | 13 ++++++++-
.../src/Scenario/FileScenarioImpl.Codeunit.al | 20 ++++++-------
5 files changed, 38 insertions(+), 27 deletions(-)
diff --git a/src/Apps/W1/External Storage - Document Attachments/app/src/AutomaticSync/DAExternalStorageMigration.Report.al b/src/Apps/W1/External Storage - Document Attachments/app/src/AutomaticSync/DAExternalStorageMigration.Report.al
index 29fd8e6f2f..b2de62ab2d 100644
--- a/src/Apps/W1/External Storage - Document Attachments/app/src/AutomaticSync/DAExternalStorageMigration.Report.al
+++ b/src/Apps/W1/External Storage - Document Attachments/app/src/AutomaticSync/DAExternalStorageMigration.Report.al
@@ -112,7 +112,7 @@ report 8753 "DA External Storage Migration"
ProcessedCount: Integer;
TotalCount: Integer;
MigrationCompletedMsg: Label '%1 file(s) have been successfully migrated to the current company folder. %2 failed.', Comment = '%1 = Number of migrated files, %2 = Number of failed migrations';
- NoFilesToMigrateLbl: Label 'All Files are already in the current company folder. No migration needed.';
+ NoFilesToMigrateLbl: Label 'No files need to be migrated.';
ProcessingMsg: Label 'Processing #1###### attachments...', Comment = '%1 - Total Number of Attachments';
local procedure LogMigrationTelemetry()
diff --git a/src/Apps/W1/External Storage - Document Attachments/app/src/DocumentAttachmentIntegration/DAExternalStorageImpl.Codeunit.al b/src/Apps/W1/External Storage - Document Attachments/app/src/DocumentAttachmentIntegration/DAExternalStorageImpl.Codeunit.al
index 0c294b4f0d..4971745840 100644
--- a/src/Apps/W1/External Storage - Document Attachments/app/src/DocumentAttachmentIntegration/DAExternalStorageImpl.Codeunit.al
+++ b/src/Apps/W1/External Storage - Document Attachments/app/src/DocumentAttachmentIntegration/DAExternalStorageImpl.Codeunit.al
@@ -5,11 +5,11 @@
namespace Microsoft.ExternalStorage.DocumentAttachments;
-using System.ExternalFileStorage;
-using System.Utilities;
+using Microsoft.Foundation.Attachment;
using System.Environment;
+using System.ExternalFileStorage;
using System.Security.Encryption;
-using Microsoft.Foundation.Attachment;
+using System.Utilities;
codeunit 8751 "DA External Storage Impl." implements "File Scenario"
{
@@ -39,7 +39,7 @@ codeunit 8751 "DA External Storage Impl." implements "File Scenario"
exit;
// Search for External Storage assigned File Scenario
- if FileScenarioCU.GetFileAccount(Scenario, FileAccount) then begin
+ if FileScenarioCU.GetSpecificFileAccount(Scenario, FileAccount) then begin
SkipInsertOrModify := true;
exit;
end;
@@ -136,7 +136,7 @@ codeunit 8751 "DA External Storage Impl." implements "File Scenario"
// Search for External Storage assigned File Scenario
FileScenario := FileScenario::"Doc. Attach. - External Storage";
- if not FileScenarioCU.GetFileAccount(FileScenario, FileAccount) then
+ if not FileScenarioCU.GetSpecificFileAccount(FileScenario, FileAccount) then
exit(false);
// Create the file with connector using the File Account framework
@@ -185,7 +185,7 @@ codeunit 8751 "DA External Storage Impl." implements "File Scenario"
// Search for External Storage assigned File Scenario
FileScenario := FileScenario::"Doc. Attach. - External Storage";
- if not FileScenarioCU.GetFileAccount(FileScenario, FileAccount) then
+ if not FileScenarioCU.GetSpecificFileAccount(FileScenario, FileAccount) then
exit(false);
// Get the file with connector using the File Account framework
@@ -227,7 +227,7 @@ codeunit 8751 "DA External Storage Impl." implements "File Scenario"
// Search for External Storage assigned File Scenario
FileScenario := FileScenario::"Doc. Attach. - External Storage";
- if not FileScenarioCU.GetFileAccount(FileScenario, FileAccount) then
+ if not FileScenarioCU.GetSpecificFileAccount(FileScenario, FileAccount) then
exit(false);
// Get the file with connector using the File Account framework
@@ -258,7 +258,7 @@ codeunit 8751 "DA External Storage Impl." implements "File Scenario"
begin
// Search for External Storage assigned File Scenario
FileScenario := FileScenario::"Doc. Attach. - External Storage";
- if not FileScenarioCU.GetFileAccount(FileScenario, FileAccount) then
+ if not FileScenarioCU.GetSpecificFileAccount(FileScenario, FileAccount) then
exit(false);
// Get the file from external storage
@@ -288,7 +288,7 @@ codeunit 8751 "DA External Storage Impl." implements "File Scenario"
begin
// Search for External Storage assigned File Scenario
FileScenario := FileScenario::"Doc. Attach. - External Storage";
- if not FileScenarioCU.GetFileAccount(FileScenario, FileAccount) then
+ if not FileScenarioCU.GetSpecificFileAccount(FileScenario, FileAccount) then
exit(false);
// Get the file from external storage
@@ -316,7 +316,7 @@ codeunit 8751 "DA External Storage Impl." implements "File Scenario"
begin
// Search for External Storage assigned File Scenario
FileScenario := FileScenario::"Doc. Attach. - External Storage";
- if not FileScenarioCU.GetFileAccount(FileScenario, FileAccount) then
+ if not FileScenarioCU.GetSpecificFileAccount(FileScenario, FileAccount) then
exit(false);
// Get the file from external storage
@@ -359,7 +359,7 @@ codeunit 8751 "DA External Storage Impl." implements "File Scenario"
// Search for External Storage assigned File Scenario
FileScenario := FileScenario::"Doc. Attach. - External Storage";
- if not FileScenarioCU.GetFileAccount(FileScenario, FileAccount) then
+ if not FileScenarioCU.GetSpecificFileAccount(FileScenario, FileAccount) then
exit(false);
// Delete the file with connector using the File Account framework
@@ -651,7 +651,7 @@ codeunit 8751 "DA External Storage Impl." implements "File Scenario"
begin
// Initialize external file storage with the scenario
FileScenario := FileScenario::"Doc. Attach. - External Storage";
- if not FileScenarioCU.GetFileAccount(FileScenario, FileAccount) then
+ if not FileScenarioCU.GetSpecificFileAccount(FileScenario, FileAccount) then
exit('');
ExternalFileStorage.Initialize(FileScenario);
@@ -742,7 +742,7 @@ codeunit 8751 "DA External Storage Impl." implements "File Scenario"
begin
// Initialize external file storage with the scenario
FileScenario := FileScenario::"Doc. Attach. - External Storage";
- if not FileScenarioCU.GetFileAccount(FileScenario, FileAccount) then
+ if not FileScenarioCU.GetSpecificFileAccount(FileScenario, FileAccount) then
exit;
ExternalFileStorage.Initialize(FileScenario);
@@ -811,7 +811,7 @@ codeunit 8751 "DA External Storage Impl." implements "File Scenario"
// Initialize external file storage
FileScenario := FileScenario::"Doc. Attach. - External Storage";
- if not FileScenarioCU.GetFileAccount(FileScenario, FileAccount) then
+ if not FileScenarioCU.GetSpecificFileAccount(FileScenario, FileAccount) then
exit(false);
ExternalFileStorage.Initialize(FileScenario);
diff --git a/src/Apps/W1/External Storage - Document Attachments/app/src/Setup/DAExternalStorageSetup.Page.al b/src/Apps/W1/External Storage - Document Attachments/app/src/Setup/DAExternalStorageSetup.Page.al
index 88f34dee05..9f80306efa 100644
--- a/src/Apps/W1/External Storage - Document Attachments/app/src/Setup/DAExternalStorageSetup.Page.al
+++ b/src/Apps/W1/External Storage - Document Attachments/app/src/Setup/DAExternalStorageSetup.Page.al
@@ -5,8 +5,8 @@
namespace Microsoft.ExternalStorage.DocumentAttachments;
-using System.Utilities;
using System.Threading;
+using System.Utilities;
///
/// Setup page for External Storage functionality.
diff --git a/src/System Application/App/External File Storage/src/Scenario/FileScenario.Codeunit.al b/src/System Application/App/External File Storage/src/Scenario/FileScenario.Codeunit.al
index 6a0f3313f1..a3ae1ede6e 100644
--- a/src/System Application/App/External File Storage/src/Scenario/FileScenario.Codeunit.al
+++ b/src/System Application/App/External File Storage/src/Scenario/FileScenario.Codeunit.al
@@ -17,7 +17,7 @@ codeunit 9452 "File Scenario"
/// True if an account for the default scenario was found; otherwise - false.
procedure GetDefaultFileAccount(var TempFileAccount: Record "File Account" temporary): Boolean
begin
- exit(FileScenarioImpl.GetFileAccountOrDefault(Enum::"File Scenario"::Default, TempFileAccount));
+ exit(FileScenarioImpl.GetFileAccount(Enum::"File Scenario"::Default, TempFileAccount));
end;
///
@@ -32,6 +32,17 @@ codeunit 9452 "File Scenario"
exit(FileScenarioImpl.GetFileAccount(Scenario, TempFileAccount));
end;
+ ///
+ /// Gets the file account used by the given file scenario.
+ ///
+ /// The scenario to look for.
+ /// Out parameter holding information about the file account.
+ /// True if an account for the specified scenario was found; otherwise - false.
+ procedure GetSpecificFileAccount(Scenario: Enum "File Scenario"; var TempFileAccount: Record "File Account" temporary): Boolean
+ begin
+ exit(FileScenarioImpl.GetSpecificFileAccount(Scenario, TempFileAccount));
+ end;
+
///
/// Gets the file account used by the given file scenario or the default file account if no specific account is assigned to the scenario.
///
diff --git a/src/System Application/App/External File Storage/src/Scenario/FileScenarioImpl.Codeunit.al b/src/System Application/App/External File Storage/src/Scenario/FileScenarioImpl.Codeunit.al
index 2a03512b32..f236a1dd9b 100644
--- a/src/System Application/App/External File Storage/src/Scenario/FileScenarioImpl.Codeunit.al
+++ b/src/System Application/App/External File Storage/src/Scenario/FileScenarioImpl.Codeunit.al
@@ -20,29 +20,29 @@ codeunit 9453 "File Scenario Impl."
FileScenario: Record "File Scenario";
FileAccounts: Codeunit "File Account";
begin
- FileAccounts.GetAllAccounts(TempAllFileAccounts);
-
// Find the account for the provided scenario
- if FileScenario.Get(Scenario) then
+ if GetSpecificFileAccount(Scenario, TempFileAccount) then
+ exit(true);
+
+ // Fallback to the default account if the scenario isn't mapped or the mapped account doesn't exist
+ FileAccounts.GetAllAccounts(TempAllFileAccounts);
+ if FileScenario.Get(Enum::"File Scenario"::Default) then
if TempAllFileAccounts.Get(FileScenario."Account Id", FileScenario.Connector) then begin
TempFileAccount := TempAllFileAccounts;
exit(true);
end;
end;
- procedure GetFileAccountOrDefault(Scenario: Enum "File Scenario"; var TempFileAccount: Record "File Account" temporary): Boolean
+ procedure GetSpecificFileAccount(Scenario: Enum "File Scenario"; var TempFileAccount: Record "File Account" temporary): Boolean
var
TempAllFileAccounts: Record "File Account" temporary;
FileScenario: Record "File Scenario";
FileAccounts: Codeunit "File Account";
begin
- // Find the account for the provided scenario
- if GetFileAccount(Scenario, TempFileAccount) then
- exit(true);
-
- // Fallback to the default account if the scenario isn't mapped or the mapped account doesn't exist
FileAccounts.GetAllAccounts(TempAllFileAccounts);
- if FileScenario.Get(Enum::"File Scenario"::Default) then
+
+ // Find the account for the provided scenario
+ if FileScenario.Get(Scenario) then
if TempAllFileAccounts.Get(FileScenario."Account Id", FileScenario.Connector) then begin
TempFileAccount := TempAllFileAccounts;
exit(true);
From 53648cf1ff118694cc1c993c4f8f798635647708 Mon Sep 17 00:00:00 2001
From: Stefan Sosic <59221826+StefanSosic@users.noreply.github.com>
Date: Mon, 19 Jan 2026 13:57:13 +0100
Subject: [PATCH 28/34] Remove
---
.../src/Scenario/FileScenario.Codeunit.al | 11 -----------
1 file changed, 11 deletions(-)
diff --git a/src/System Application/App/External File Storage/src/Scenario/FileScenario.Codeunit.al b/src/System Application/App/External File Storage/src/Scenario/FileScenario.Codeunit.al
index a3ae1ede6e..b7662d6a8a 100644
--- a/src/System Application/App/External File Storage/src/Scenario/FileScenario.Codeunit.al
+++ b/src/System Application/App/External File Storage/src/Scenario/FileScenario.Codeunit.al
@@ -43,17 +43,6 @@ codeunit 9452 "File Scenario"
exit(FileScenarioImpl.GetSpecificFileAccount(Scenario, TempFileAccount));
end;
- ///
- /// Gets the file account used by the given file scenario or the default file account if no specific account is assigned to the scenario.
- ///
- /// The scenario to look for.
- /// Out parameter holding information about the file account.
- /// True if an account for the specified scenario was found; otherwise - false.
- procedure GetFileAccountOrDefault(Scenario: Enum "File Scenario"; var TempFileAccount: Record "File Account" temporary): Boolean
- begin
- exit(FileScenarioImpl.GetFileAccountOrDefault(Scenario, TempFileAccount));
- end;
-
///
/// Sets a default file account.
///
From 852773ad4ccd3c594af02376b6e5c5e896bd2515 Mon Sep 17 00:00:00 2001
From: Stefan Sosic <59221826+StefanSosic@users.noreply.github.com>
Date: Mon, 19 Jan 2026 14:05:11 +0100
Subject: [PATCH 29/34] Fix
---
.../W1/External Storage - Document Attachments/app/app.json | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/Apps/W1/External Storage - Document Attachments/app/app.json b/src/Apps/W1/External Storage - Document Attachments/app/app.json
index d1b91a1e44..ad78fde90a 100644
--- a/src/Apps/W1/External Storage - Document Attachments/app/app.json
+++ b/src/Apps/W1/External Storage - Document Attachments/app/app.json
@@ -10,8 +10,8 @@
"help": "https://go.microsoft.com/fwlink/?linkid=2134520",
"url": "https://go.microsoft.com/fwlink/?linkid=724011",
"logo": "ExtensionLogo.png",
- "application": "27.0.0.0",
- "platform": "27.0.0.0",
+ "application": "28.0.0.0",
+ "platform": "28.0.0.0",
"internalsVisibleTo": [
],
"dependencies": [],
From 3e45ce4f48abd398b90690801857536edf92ccaf Mon Sep 17 00:00:00 2001
From: Stefan Sosic <59221826+StefanSosic@users.noreply.github.com>
Date: Mon, 19 Jan 2026 14:23:53 +0100
Subject: [PATCH 30/34] Remove upload
---
.../app/README.md | 7 ------
.../DAExternalStorageSync.Report.al | 19 ++++-----------
.../DAExternalStorageImpl.Codeunit.al | 18 +-------------
.../src/Setup/DAExternalStorageSetup.Page.al | 24 -------------------
.../src/Setup/DAExternalStorageSetup.Table.al | 17 -------------
5 files changed, 6 insertions(+), 79 deletions(-)
diff --git a/src/Apps/W1/External Storage - Document Attachments/app/README.md b/src/Apps/W1/External Storage - Document Attachments/app/README.md
index dc3f543671..a03b9a2456 100644
--- a/src/Apps/W1/External Storage - Document Attachments/app/README.md
+++ b/src/Apps/W1/External Storage - Document Attachments/app/README.md
@@ -21,8 +21,6 @@ The External Storage extension provides seamless integration between Microsoft D
- **Environment Hash Display**: View current environment hash for reference and troubleshooting
### **Flexible Deletion Policies**
-- **Delete from BC after Upload**: Immediately delete from internal storage right after external upload
-- **Delete After**: Configurable retention period using date formulas (e.g., `<7D>` for 7 days, `<30D>` for 30 days)
- **Delete from External Storage**: Optionally delete files from external storage when attachments are removed from BC
- **Automatic Cleanup**: Scheduled job queue can automatically delete expired files based on retention policy
@@ -62,8 +60,6 @@ The External Storage extension provides seamless integration between Microsoft D
- Configure settings:
- **Enabled**: Enable the External Storage feature
- **Root Folder**: Select the root folder path for attachments (use AssistEdit to browse)
- - **Delete from BC after Upload**: Enable to immediately delete files from BC after upload
- - **Delete After**: Set retention period using date formula (e.g., `<7D>` for 7 days)
- **Scheduled Upload**: Enable automatic background upload via job queue
- **Delete from External Storage**: Enable to delete external files when attachments are removed from BC
@@ -76,9 +72,6 @@ The External Storage extension provides seamless integration between Microsoft D
- Use AssistEdit button to browse and select folder interactively
#### Upload and Delete Policy
-- **Delete from BC after Upload**: When enabled, files are immediately removed from internal storage after successful upload to external storage
-- **Delete After**: Date formula specifying retention period before internal deletion (e.g., `<7D>`, `<30D>`)
- - Only active when "Delete from BC after Upload" is disabled
- **Scheduled Upload**: Enable automatic background upload using job queue
- Job runs daily at 1:00 AM
- Maximum 3 retry attempts on failure
diff --git a/src/Apps/W1/External Storage - Document Attachments/app/src/AutomaticSync/DAExternalStorageSync.Report.al b/src/Apps/W1/External Storage - Document Attachments/app/src/AutomaticSync/DAExternalStorageSync.Report.al
index 75ab9a9986..a814a57e3a 100644
--- a/src/Apps/W1/External Storage - Document Attachments/app/src/AutomaticSync/DAExternalStorageSync.Report.al
+++ b/src/Apps/W1/External Storage - Document Attachments/app/src/AutomaticSync/DAExternalStorageSync.Report.al
@@ -68,12 +68,11 @@ report 8752 "DA External Storage Sync"
if not ExternalStorageImpl.DownloadFromExternalStorage(DocumentAttachment) then
FailedCount += 1;
end;
- if DeleteExpiredFiles then
- if CalcDate(StrSubstNo(DateFormulaLbl, GetDateFormulaFromExternalStorageSetup()), DocumentAttachment."External Upload Date".Date()) >= Today() then
- if ExternalStorageImpl.DeleteFromInternalStorage(DocumentAttachment) then
- DeleteCount += 1
- else
- DeleteFailedCount += 1;
+ if (DocumentAttachment."Uploaded Externally") and (DocumentAttachment."Deleted Internally" = false) then
+ if ExternalStorageImpl.DeleteFromInternalStorage(DocumentAttachment) then
+ DeleteCount += 1
+ else
+ DeleteFailedCount += 1;
Commit(); // Commit after each record to avoid lost in communication error with external storage service
if (MaxRecordsToProcess > 0) and (ProcessedCount >= MaxRecordsToProcess) then
@@ -159,14 +158,6 @@ report 8752 "DA External Storage Sync"
end;
end;
- local procedure GetDateFormulaFromExternalStorageSetup(): DateFormula
- var
- ExternalStorageSetup: Record "DA External Storage Setup";
- begin
- ExternalStorageSetup.Get();
- exit(ExternalStorageSetup."Delete After");
- end;
-
local procedure LogSyncTelemetry()
var
DAFeatureTelemetry: Codeunit "DA Feature Telemetry";
diff --git a/src/Apps/W1/External Storage - Document Attachments/app/src/DocumentAttachmentIntegration/DAExternalStorageImpl.Codeunit.al b/src/Apps/W1/External Storage - Document Attachments/app/src/DocumentAttachmentIntegration/DAExternalStorageImpl.Codeunit.al
index 4971745840..2d06e7b90f 100644
--- a/src/Apps/W1/External Storage - Document Attachments/app/src/DocumentAttachmentIntegration/DAExternalStorageImpl.Codeunit.al
+++ b/src/Apps/W1/External Storage - Document Attachments/app/src/DocumentAttachmentIntegration/DAExternalStorageImpl.Codeunit.al
@@ -402,20 +402,6 @@ codeunit 8751 "DA External Storage Impl." implements "File Scenario"
exit(false);
end;
- ///
- /// Determines if files should be deleted immediately based on external storage setup.
- ///
- /// True if files should be deleted immediately, false otherwise.
- procedure ShouldBeDeleted(): Boolean
- var
- ExternalStorageSetup: Record "DA External Storage Setup";
- begin
- if not ExternalStorageSetup.Get() then
- exit(false);
-
- exit(CalcDate(ExternalStorageSetup."Delete After", Today()) <= Today());
- end;
-
///
/// Maps file extensions to their corresponding MIME types.
///
@@ -521,9 +507,7 @@ codeunit 8751 "DA External Storage Impl." implements "File Scenario"
if not ExternalStorageImpl.UploadToExternalStorage(Rec) then
exit;
- // Check if it should be immediately deleted
- if ExternalStorageImpl.ShouldBeDeleted() then
- ExternalStorageImpl.DeleteFromInternalStorage(Rec);
+ ExternalStorageImpl.DeleteFromInternalStorage(Rec);
end;
///
diff --git a/src/Apps/W1/External Storage - Document Attachments/app/src/Setup/DAExternalStorageSetup.Page.al b/src/Apps/W1/External Storage - Document Attachments/app/src/Setup/DAExternalStorageSetup.Page.al
index 9f80306efa..02f0300c6d 100644
--- a/src/Apps/W1/External Storage - Document Attachments/app/src/Setup/DAExternalStorageSetup.Page.al
+++ b/src/Apps/W1/External Storage - Document Attachments/app/src/Setup/DAExternalStorageSetup.Page.al
@@ -50,19 +50,6 @@ page 8750 "DA External Storage Setup"
group(UploadAndDeletePolicy)
{
Caption = 'Upload and Delete Policy';
- field("Delete from BC after Upload"; Rec."Delete from BC after Upload")
- {
- trigger OnValidate()
- begin
- UpdateDeleteAfterVisibility();
- CurrPage.Update(false);
- end;
- }
- field("Delete After"; Rec."Delete After")
- {
- ShowMandatory = true;
- Enabled = ShowDeleteAfter;
- }
field("Scheduled Upload"; Rec."Scheduled Upload") { }
field("Delete from External Storage"; Rec."Delete from External Storage") { }
}
@@ -177,22 +164,11 @@ page 8750 "DA External Storage Setup"
Rec.Init();
Rec.Insert();
end;
- UpdateDeleteAfterVisibility();
- end;
-
- trigger OnAfterGetCurrRecord()
- begin
- UpdateDeleteAfterVisibility();
end;
var
ShowDeleteAfter: Boolean;
- local procedure UpdateDeleteAfterVisibility()
- begin
- ShowDeleteAfter := not Rec."Delete from BC after Upload";
- end;
-
local procedure SelectRootFolder()
var
DAExternalStorageImpl: Codeunit "DA External Storage Impl.";
diff --git a/src/Apps/W1/External Storage - Document Attachments/app/src/Setup/DAExternalStorageSetup.Table.al b/src/Apps/W1/External Storage - Document Attachments/app/src/Setup/DAExternalStorageSetup.Table.al
index d28097ef94..d359f3aebe 100644
--- a/src/Apps/W1/External Storage - Document Attachments/app/src/Setup/DAExternalStorageSetup.Table.al
+++ b/src/Apps/W1/External Storage - Document Attachments/app/src/Setup/DAExternalStorageSetup.Table.al
@@ -48,23 +48,6 @@ table 8750 "DA External Storage Setup"
DAFeatureTelemetry.LogFeatureDisabled();
end;
}
- field(5; "Delete After"; DateFormula)
- {
- Caption = 'Delete After';
- ToolTip = 'Specifies the duration after which files should be deleted from external storage. Use <0D> for immediate deletion after upload, <30D> for 30 days, etc.';
- InitValue = 0D;
- }
- field(8; "Delete from BC after Upload"; Boolean)
- {
- Caption = 'Delete from BC after Upload';
- ToolTip = 'Specifies if files should be deleted immediately after upload. When enabled, Delete After is set to 0D.';
-
- trigger OnValidate()
- begin
- if "Delete from BC after Upload" then
- Evaluate("Delete After", '<0D>');
- end;
- }
field(6; "Scheduled Upload"; Boolean)
{
Caption = 'Scheduled Upload';
From 3e9f8a5d92e6d9d9be30b428ba684527de005f05 Mon Sep 17 00:00:00 2001
From: Stefan Sosic <59221826+StefanSosic@users.noreply.github.com>
Date: Mon, 19 Jan 2026 16:20:09 +0100
Subject: [PATCH 31/34] Fix Copy
---
.../app/README.md | 31 ------
.../DAExternalStorageSync.Report.al | 26 ++---
.../DAExternalStorageImpl.Codeunit.al | 24 ++++-
.../src/Setup/DAExternalStorageSetup.Page.al | 95 ++-----------------
.../src/Setup/DAExternalStorageSetup.Table.al | 80 ----------------
5 files changed, 36 insertions(+), 220 deletions(-)
diff --git a/src/Apps/W1/External Storage - Document Attachments/app/README.md b/src/Apps/W1/External Storage - Document Attachments/app/README.md
index a03b9a2456..ea283d21b0 100644
--- a/src/Apps/W1/External Storage - Document Attachments/app/README.md
+++ b/src/Apps/W1/External Storage - Document Attachments/app/README.md
@@ -6,13 +6,6 @@ The External Storage extension provides seamless integration between Microsoft D
## Key Features
-### **Automatic Scheduled Upload**
-- Automatically uploads new document attachments to configured external storage via scheduled job queue
-- Supports multiple storage connectors via the File Account framework
-- Generates unique file names to prevent collisions
-- Maintains original file metadata and associations
-- Configurable job queue runs daily at 1:00 AM with automatic retry capability
-
### **Multi-Tenant, Multi-Environment, and Multi-Company Support**
- **Environment Hash**: Unique hash based on Tenant ID + Environment Name + Company System ID
- **Organized Folder Structure**: Files are stored in hierarchical folders: `RootFolder/EnvironmentHash/TableName/FileName`
@@ -41,7 +34,6 @@ The External Storage extension provides seamless integration between Microsoft D
- Microsoft Dynamics 365 Business Central version 28.0 or later
- File Account module configured with external storage connector
- Appropriate permissions for file operations
-- Job Queue functionality enabled for scheduled uploads
### Installation Steps
@@ -60,7 +52,6 @@ The External Storage extension provides seamless integration between Microsoft D
- Configure settings:
- **Enabled**: Enable the External Storage feature
- **Root Folder**: Select the root folder path for attachments (use AssistEdit to browse)
- - **Scheduled Upload**: Enable automatic background upload via job queue
- **Delete from External Storage**: Enable to delete external files when attachments are removed from BC
### Configuration Options
@@ -72,25 +63,11 @@ The External Storage extension provides seamless integration between Microsoft D
- Use AssistEdit button to browse and select folder interactively
#### Upload and Delete Policy
-- **Scheduled Upload**: Enable automatic background upload using job queue
- - Job runs daily at 1:00 AM
- - Maximum 3 retry attempts on failure
- **Delete from External Storage**: When enabled, files are deleted from external storage when the attachment is removed from Business Central
-#### Job Queue Information
-- **Job Queue Entry ID**: System-generated ID for the scheduled upload job
-- **Job Queue Status**: Current status (Not Created, Ready, On Hold, Deleted)
-- Click on Job Queue Entry ID to open detailed job queue card
## Usage
-### Automatic Mode
-When Scheduled Upload is enabled:
-1. User attaches a document to any Business Central record
-2. System automatically uploads to external storage via scheduled job queue (runs daily at 1:00 AM)
-3. File remains accessible through standard attachment functionality
-4. Internal file is deleted based on configured retention policy
-
### Multi-Company and Multi-Environment Support
#### Environment Hash
@@ -137,7 +114,6 @@ From **External Storage Setup** page:
- **Storage Sync**: Run synchronization manually to upload/download files
- **Migrate Files**: Migrate all files from previous environment/company to current folder structure
- **Show Current Environment Hash**: Display the current environment hash for reference
-- **Show Job Queue Entry**: View and manage the scheduled upload job
- **Document Attachments**: Open the list of all document attachments with external storage information
#### Individual File Operations
@@ -161,13 +137,6 @@ From **External Storage Synchronize** report:
- No change to end-user experience
- Cross-environment and cross-company access is handled automatically
-### Job Queue Management
-- Scheduled upload job runs daily at 1:00 AM
-- Automatic retry on failure (up to 3 attempts)
-- Job status visible in setup page
-- Can be manually triggered via **Storage Sync** action
-- Job can be paused by disabling **Scheduled Upload**
-
## Important Notes
### Data Safety
diff --git a/src/Apps/W1/External Storage - Document Attachments/app/src/AutomaticSync/DAExternalStorageSync.Report.al b/src/Apps/W1/External Storage - Document Attachments/app/src/AutomaticSync/DAExternalStorageSync.Report.al
index a814a57e3a..ff8822495e 100644
--- a/src/Apps/W1/External Storage - Document Attachments/app/src/AutomaticSync/DAExternalStorageSync.Report.al
+++ b/src/Apps/W1/External Storage - Document Attachments/app/src/AutomaticSync/DAExternalStorageSync.Report.al
@@ -43,16 +43,14 @@ report 8752 "DA External Storage Sync"
ProcessedCount := 0;
FailedCount := 0;
- DeleteCount := 0;
- DeleteFailedCount := 0;
+ DeleteAfterUploadCount := 0;
+ DeleteAfterUploadFailedCount := 0;
if GuiAllowed() then
Dialog.Open(ProcessingMsg, TotalCount);
end;
trigger OnAfterGetRecord()
- var
- DateFormulaLbl: Label '<+%1>', Locked = true;
begin
ProcessedCount += 1;
@@ -70,9 +68,9 @@ report 8752 "DA External Storage Sync"
end;
if (DocumentAttachment."Uploaded Externally") and (DocumentAttachment."Deleted Internally" = false) then
if ExternalStorageImpl.DeleteFromInternalStorage(DocumentAttachment) then
- DeleteCount += 1
+ DeleteAfterUploadCount += 1
else
- DeleteFailedCount += 1;
+ DeleteAfterUploadFailedCount += 1;
Commit(); // Commit after each record to avoid lost in communication error with external storage service
if (MaxRecordsToProcess > 0) and (ProcessedCount >= MaxRecordsToProcess) then
@@ -86,10 +84,7 @@ report 8752 "DA External Storage Sync"
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);
+ Message(ProcessedMsg, ProcessedCount - FailedCount, FailedCount);
end;
end;
}
@@ -113,13 +108,6 @@ report 8752 "DA External Storage Sync"
OptionCaption = 'To External Storage,From External Storage';
ToolTip = 'Specifies whether to sync to external storage or from external storage.';
}
- field(DeleteExpiredFilesField; DeleteExpiredFiles)
- {
- ApplicationArea = Basic, Suite;
- Enabled = SyncDirection = SyncDirection::"To External Storage";
- Caption = 'Delete Expired Files';
- ToolTip = 'Specifies whether to delete expired files from internal storage.';
- }
field(MaxRecordsToProcessField; MaxRecordsToProcess)
{
ApplicationArea = Basic, Suite;
@@ -135,14 +123,12 @@ report 8752 "DA External Storage Sync"
var
ExternalStorageImpl: Codeunit "DA External Storage Impl.";
- DeleteExpiredFiles: Boolean;
Dialog: Dialog;
- DeleteCount, DeleteFailedCount : Integer;
+ DeleteAfterUploadCount, DeleteAfterUploadFailedCount : 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';
diff --git a/src/Apps/W1/External Storage - Document Attachments/app/src/DocumentAttachmentIntegration/DAExternalStorageImpl.Codeunit.al b/src/Apps/W1/External Storage - Document Attachments/app/src/DocumentAttachmentIntegration/DAExternalStorageImpl.Codeunit.al
index 2d06e7b90f..9e18a00e47 100644
--- a/src/Apps/W1/External Storage - Document Attachments/app/src/DocumentAttachmentIntegration/DAExternalStorageImpl.Codeunit.al
+++ b/src/Apps/W1/External Storage - Document Attachments/app/src/DocumentAttachmentIntegration/DAExternalStorageImpl.Codeunit.al
@@ -341,6 +341,9 @@ codeunit 8751 "DA External Storage Impl." implements "File Scenario"
if not IsFeatureEnabled() then
exit(false);
+ if not DocumentAttachment.Find() then
+ exit(false);
+
// Validate input parameters
if DocumentAttachment."External File Path" = '' then
exit(false);
@@ -496,9 +499,6 @@ codeunit 8751 "DA External Storage Impl." implements "File Scenario"
if not ExternalStorageSetup.Get() then
exit;
- if not ExternalStorageSetup."Scheduled Upload" then
- exit;
-
// Only process files with actual content
if not Rec."Document Reference ID".HasValue() then
exit;
@@ -619,6 +619,18 @@ codeunit 8751 "DA External Storage Impl." implements "File Scenario"
AttachmentIsAvailable := ExternalStorageImpl.CheckIfFileExistInExternalStorage(DocumentAttachment."External File Path");
IsHandled := true;
end;
+
+ ///
+ /// Event handler for before checking Document Reference ID on insert.
+ ///
+ /// The document attachment record.
+ /// Indicates if the event has been handled.
+ [EventSubscriber(ObjectType::Table, Database::"Document Attachment", OnInsertOnBeforeCheckDocRefID, '', false, false)]
+ local procedure "Document Attachment_OnInsertOnBeforeCheckDocRefID"(var DocumentAttachment: Record "Document Attachment"; var IsHandled: Boolean)
+ begin
+ if DocumentAttachment."Uploaded Externally" then
+ IsHandled := true;
+ end;
#endregion
///
@@ -736,7 +748,11 @@ codeunit 8751 "DA External Storage Impl." implements "File Scenario"
ExternalFileStorage.CreateDirectory(CompanyFolderPath);
end;
- local procedure GetCurrentEnvironmentHash(): Text[16]
+ ///
+ /// Gets the current environment hash for use in folder structure.
+ ///
+ /// The hash value (first 16 characters of SHA256).
+ procedure GetCurrentEnvironmentHash(): Text[16]
var
Company: Record Company;
EnvironmentInformation: Codeunit "Environment Information";
diff --git a/src/Apps/W1/External Storage - Document Attachments/app/src/Setup/DAExternalStorageSetup.Page.al b/src/Apps/W1/External Storage - Document Attachments/app/src/Setup/DAExternalStorageSetup.Page.al
index 02f0300c6d..060f92e7fa 100644
--- a/src/Apps/W1/External Storage - Document Attachments/app/src/Setup/DAExternalStorageSetup.Page.al
+++ b/src/Apps/W1/External Storage - Document Attachments/app/src/Setup/DAExternalStorageSetup.Page.al
@@ -46,33 +46,18 @@ page 8750 "DA External Storage Setup"
SelectRootFolder();
end;
}
+ field(CurrentEnvironmentHash; CurrentEnvironmentHash)
+ {
+ Caption = 'Current Environment Hash';
+ ToolTip = 'Specifies the current environment hash used in the folder structure in external storage.';
+ Editable = false;
+ }
}
group(UploadAndDeletePolicy)
{
Caption = 'Upload and Delete Policy';
- field("Scheduled Upload"; Rec."Scheduled Upload") { }
field("Delete from External Storage"; Rec."Delete from External Storage") { }
}
-
- group(JobQueueInformation)
- {
- Caption = 'Job Queue Information';
- field("Job Queue Entry ID"; Rec."Job Queue Entry ID")
- {
- ToolTip = 'Specifies the ID of the job queue entry for automatic synchronization.';
-
- trigger OnDrillDown()
- begin
- ShowJobQueueEntry();
- end;
- }
- field(JobQueueStatus; GetJobQueueStatus())
- {
- Caption = 'Job Queue Status';
- Editable = false;
- ToolTip = 'Specifies the current status of the job queue entry.';
- }
- }
}
}
actions
@@ -101,31 +86,9 @@ page 8750 "DA External Storage Setup"
Report.Run(Report::"DA External Storage Migration");
end;
}
- action(ShowCurrentHash)
- {
- Caption = 'Show Current Environment Hash';
- Image = Copy;
- ToolTip = 'Show the current environment hash used in the folder structure in external storage.';
-
- trigger OnAction()
- begin
- ShowCurrentEnvironmentHash();
- end;
- }
}
area(Navigation)
{
- action(ShowJobQueue)
- {
- Caption = 'Show Job Queue Entry';
- Image = JobListSetup;
- ToolTip = 'View the job queue entry for automatic synchronization.';
-
- trigger OnAction()
- begin
- ShowJobQueueEntry();
- end;
- }
action(DocumentAttachments)
{
Caption = 'Document Attachments';
@@ -145,29 +108,22 @@ page 8750 "DA External Storage Setup"
actionref(DocumentAttachments_Promoted; DocumentAttachments)
{
}
- group(InfoGroup)
- {
- Caption = 'Info';
- actionref(ShowCurrentHash_Promoted; ShowCurrentHash)
- {
- }
- actionref(ShowJobQueue_Promoted; ShowJobQueue)
- {
- }
- }
}
}
trigger OnOpenPage()
+ var
+ DAExternalStorageImpl: Codeunit "DA External Storage Impl.";
begin
if not Rec.Get() then begin
Rec.Init();
Rec.Insert();
end;
+ CurrentEnvironmentHash := DAExternalStorageImpl.GetCurrentEnvironmentHash();
end;
var
- ShowDeleteAfter: Boolean;
+ CurrentEnvironmentHash: Text[16];
local procedure SelectRootFolder()
var
@@ -186,35 +142,4 @@ page 8750 "DA External Storage Setup"
CurrPage.Update();
end;
end;
-
- local procedure ShowCurrentEnvironmentHash()
- var
- DAExternalStorageImpl: Codeunit "DA External Storage Impl.";
- begin
- DAExternalStorageImpl.ShowCurrentEnvironmentHash();
- end;
-
- local procedure ShowJobQueueEntry()
- var
- JobQueueEntry: Record "Job Queue Entry";
- begin
- if not IsNullGuid(Rec."Job Queue Entry ID") then
- if JobQueueEntry.Get(Rec."Job Queue Entry ID") then
- Page.Run(Page::"Job Queue Entry Card", JobQueueEntry);
- end;
-
- local procedure GetJobQueueStatus(): Text
- var
- JobQueueEntry: Record "Job Queue Entry";
- NotCreatedLbl: Label 'Not Created';
- DeletedLbl: Label 'Deleted';
- begin
- if IsNullGuid(Rec."Job Queue Entry ID") then
- exit(NotCreatedLbl);
-
- if not JobQueueEntry.Get(Rec."Job Queue Entry ID") then
- exit(DeletedLbl);
-
- exit(Format(JobQueueEntry.Status));
- end;
}
diff --git a/src/Apps/W1/External Storage - Document Attachments/app/src/Setup/DAExternalStorageSetup.Table.al b/src/Apps/W1/External Storage - Document Attachments/app/src/Setup/DAExternalStorageSetup.Table.al
index d359f3aebe..1d30cb2fff 100644
--- a/src/Apps/W1/External Storage - Document Attachments/app/src/Setup/DAExternalStorageSetup.Table.al
+++ b/src/Apps/W1/External Storage - Document Attachments/app/src/Setup/DAExternalStorageSetup.Table.al
@@ -48,16 +48,6 @@ table 8750 "DA External Storage Setup"
DAFeatureTelemetry.LogFeatureDisabled();
end;
}
- field(6; "Scheduled Upload"; Boolean)
- {
- Caption = 'Scheduled Upload';
- ToolTip = 'Specifies if files should be uploaded automatically with using the Job Queue. When enabled, a Job Queue entry is created to run the upload process in the background.';
-
- trigger OnValidate()
- begin
- ManageJobQueue();
- end;
- }
field(7; "Delete from External Storage"; Boolean)
{
Caption = 'Delete External File on Attachment Delete';
@@ -101,74 +91,4 @@ table 8750 "DA External Storage Setup"
Clustered = true;
}
}
-
- local procedure ManageJobQueue()
- var
- JobQueueEntry: Record "Job Queue Entry";
- begin
- if "Scheduled Upload" then begin
- // Create job queue if it doesn't exist
- if IsNullGuid("Job Queue Entry ID") or not JobQueueEntry.Get("Job Queue Entry ID") then
- CreateJobQueue()
- else
- // Reactivate if it exists but is not ready
- if JobQueueEntry.Status <> JobQueueEntry.Status::Ready then begin
- JobQueueEntry.Status := JobQueueEntry.Status::Ready;
- JobQueueEntry.Modify(true);
- end;
- end else
- // Delete or set to on hold when Auto Upload is disabled
- if not IsNullGuid("Job Queue Entry ID") then
- if JobQueueEntry.Get("Job Queue Entry ID") then begin
- JobQueueEntry.Status := JobQueueEntry.Status::"On Hold";
- JobQueueEntry.Modify(true);
- end;
- end;
-
- local procedure CreateJobQueue()
- var
- JobQueueEntry: Record "Job Queue Entry";
- JobQueueCategoryLbl: Label 'EXTATTACH', Locked = true;
- JobQueueDescriptionLbl: Label 'External Storage - Automatic Upload';
- OneAmTime: Time;
- begin
- OneAmTime := 010000T; // 1:00 AM
-
- JobQueueEntry.Init();
- JobQueueEntry."Object Type to Run" := JobQueueEntry."Object Type to Run"::Report;
- JobQueueEntry."Object ID to Run" := Report::"DA External Storage Sync";
- JobQueueEntry.Description := JobQueueDescriptionLbl;
- JobQueueEntry."Job Queue Category Code" := JobQueueCategoryLbl;
- JobQueueEntry."Run in User Session" := false;
- JobQueueEntry."Maximum No. of Attempts to Run" := 3;
-
- // Schedule for 1 AM daily
- JobQueueEntry."Earliest Start Date/Time" := CreateDateTime(Today() + 1, OneAmTime);
- if Time() < OneAmTime then
- JobQueueEntry."Earliest Start Date/Time" := CreateDateTime(Today(), OneAmTime);
-
- JobQueueEntry."Recurring Job" := true;
- JobQueueEntry."No. of Minutes between Runs" := 1440; // 24 hours
-
- // Set report parameters to upload to external storage
- JobQueueEntry."Report Request Page Options" := true;
-
- JobQueueEntry.Status := JobQueueEntry.Status::Ready;
- JobQueueEntry.Insert(true);
-
- "Job Queue Entry ID" := JobQueueEntry.ID;
- Modify();
- end;
-
- internal procedure DeleteJobQueue()
- var
- JobQueueEntry: Record "Job Queue Entry";
- begin
- if not IsNullGuid("Job Queue Entry ID") then
- if JobQueueEntry.Get("Job Queue Entry ID") then
- JobQueueEntry.Delete(true);
-
- Clear("Job Queue Entry ID");
- Modify();
- end;
}
From fca4ef1e04a15cd93b92bdc35bad428dbb4754a5 Mon Sep 17 00:00:00 2001
From: Stefan Sosic <59221826+StefanSosic@users.noreply.github.com>
Date: Mon, 19 Jan 2026 16:22:25 +0100
Subject: [PATCH 32/34] Edge case
---
.../DAExternalStorageImpl.Codeunit.al | 14 ++++++++++++++
.../DocumentAttachmentExtStor.TableExt.al | 7 +++++++
2 files changed, 21 insertions(+)
diff --git a/src/Apps/W1/External Storage - Document Attachments/app/src/DocumentAttachmentIntegration/DAExternalStorageImpl.Codeunit.al b/src/Apps/W1/External Storage - Document Attachments/app/src/DocumentAttachmentIntegration/DAExternalStorageImpl.Codeunit.al
index 9e18a00e47..7ea46cd5f7 100644
--- a/src/Apps/W1/External Storage - Document Attachments/app/src/DocumentAttachmentIntegration/DAExternalStorageImpl.Codeunit.al
+++ b/src/Apps/W1/External Storage - Document Attachments/app/src/DocumentAttachmentIntegration/DAExternalStorageImpl.Codeunit.al
@@ -351,6 +351,9 @@ codeunit 8751 "DA External Storage Impl." implements "File Scenario"
if not DocumentAttachment."Uploaded Externally" then
exit(false);
+ if DocumentAttachment."Skip Delete On Copy" then
+ exit(false);
+
// Check if file belongs to another environment - if so, just clear the reference
if IsFileFromAnotherEnvironmentOrCompany(DocumentAttachment) then begin
DocumentAttachment.MarkAsNotUploadedToExternal();
@@ -542,6 +545,11 @@ codeunit 8751 "DA External Storage Impl." implements "File Scenario"
// Delete from external storage
ExternalStorageImpl.DeleteFromExternalStorage(Rec);
+
+ if Rec."Skip Delete On Copy" then begin
+ Rec."Skip Delete On Copy" := false;
+ Rec.Modify();
+ end;
end;
///
@@ -631,6 +639,12 @@ codeunit 8751 "DA External Storage Impl." implements "File Scenario"
if DocumentAttachment."Uploaded Externally" then
IsHandled := true;
end;
+
+ [EventSubscriber(ObjectType::Codeunit, Codeunit::"Document Attachment Mgmt", OnCopyAttachmentsOnAfterSetToDocumentFilters, '', false, false)]
+ local procedure "Document Attachment Mgmt_OnCopyAttachmentsOnAfterSetToDocumentFilters"(var ToDocumentAttachment: Record "Document Attachment"; ToRecRef: RecordRef; ToAttachmentDocumentType: Enum "Attachment Document Type"; ToNo: Code[20]; ToLineNo: Integer)
+ begin
+ ToDocumentAttachment."Skip Delete On Copy" := ToDocumentAttachment."Uploaded Externally";
+ end;
#endregion
///
diff --git a/src/Apps/W1/External Storage - Document Attachments/app/src/DocumentAttachmentIntegration/DocumentAttachmentExtStor.TableExt.al b/src/Apps/W1/External Storage - Document Attachments/app/src/DocumentAttachmentIntegration/DocumentAttachmentExtStor.TableExt.al
index 588eabc07d..5778adbcec 100644
--- a/src/Apps/W1/External Storage - Document Attachments/app/src/DocumentAttachmentIntegration/DocumentAttachmentExtStor.TableExt.al
+++ b/src/Apps/W1/External Storage - Document Attachments/app/src/DocumentAttachmentIntegration/DocumentAttachmentExtStor.TableExt.al
@@ -50,6 +50,13 @@ tableextension 8750 "Document Attachment Ext.Stor." extends "Document Attachment
Editable = false;
ToolTip = 'Specifies a hash identifying the tenant, environment, and company that originally uploaded this file to external storage.';
}
+ field(8755; "Skip Delete On Copy"; Boolean)
+ {
+ Caption = 'Skip Delete On Copy';
+ DataClassification = SystemMetadata;
+ Editable = false;
+ ToolTip = 'Specifies whether to skip deletion of this attachment from external storage.';
+ }
}
///
From 4edda71bd6755d7a4c3ec3223c4e2b7ceba45229 Mon Sep 17 00:00:00 2001
From: Stefan Sosic <59221826+StefanSosic@users.noreply.github.com>
Date: Mon, 19 Jan 2026 16:24:14 +0100
Subject: [PATCH 33/34] One Drive
---
.../DAExternalStorageImpl.Codeunit.al | 11 +++++++++++
1 file changed, 11 insertions(+)
diff --git a/src/Apps/W1/External Storage - Document Attachments/app/src/DocumentAttachmentIntegration/DAExternalStorageImpl.Codeunit.al b/src/Apps/W1/External Storage - Document Attachments/app/src/DocumentAttachmentIntegration/DAExternalStorageImpl.Codeunit.al
index 7ea46cd5f7..94dbf73b25 100644
--- a/src/Apps/W1/External Storage - Document Attachments/app/src/DocumentAttachmentIntegration/DAExternalStorageImpl.Codeunit.al
+++ b/src/Apps/W1/External Storage - Document Attachments/app/src/DocumentAttachmentIntegration/DAExternalStorageImpl.Codeunit.al
@@ -645,6 +645,17 @@ codeunit 8751 "DA External Storage Impl." implements "File Scenario"
begin
ToDocumentAttachment."Skip Delete On Copy" := ToDocumentAttachment."Uploaded Externally";
end;
+
+ [EventSubscriber(ObjectType::Table, Database::"Document Attachment", OnBeforeOpenInOneDrive, '', false, false)]
+ local procedure "Document Attachment_OnBeforeOpenInOneDrive"(var Rec: Record "Document Attachment"; var IsHandled: Boolean)
+ var
+ NotSupportedMsg: Label 'Opening Document Attachments stored in External Storage via OneDrive is not supported.';
+ begin
+ if Rec."Uploaded Externally" then begin
+ IsHandled := true;
+ Message(NotSupportedMsg);
+ end;
+ end;
#endregion
///
From 90f2a8a9b1aa79d7305703b6800c707ce3e878cf Mon Sep 17 00:00:00 2001
From: Stefan Sosic <59221826+StefanSosic@users.noreply.github.com>
Date: Mon, 19 Jan 2026 16:32:42 +0100
Subject: [PATCH 34/34] Polishing
---
.../AutomaticSync/DAExternalStorageSync.Report.al | 8 --------
.../DAExternalStorageImpl.Codeunit.al | 12 ------------
.../src/Permissions/DAExtStorAdmin.PermissionSet.al | 2 +-
.../app/src/Setup/DAExternalStorageSetup.Page.al | 1 -
.../app/src/Setup/DAExternalStorageSetup.Table.al | 2 +-
5 files changed, 2 insertions(+), 23 deletions(-)
diff --git a/src/Apps/W1/External Storage - Document Attachments/app/src/AutomaticSync/DAExternalStorageSync.Report.al b/src/Apps/W1/External Storage - Document Attachments/app/src/AutomaticSync/DAExternalStorageSync.Report.al
index ff8822495e..5deeb11ce2 100644
--- a/src/Apps/W1/External Storage - Document Attachments/app/src/AutomaticSync/DAExternalStorageSync.Report.al
+++ b/src/Apps/W1/External Storage - Document Attachments/app/src/AutomaticSync/DAExternalStorageSync.Report.al
@@ -43,8 +43,6 @@ report 8752 "DA External Storage Sync"
ProcessedCount := 0;
FailedCount := 0;
- DeleteAfterUploadCount := 0;
- DeleteAfterUploadFailedCount := 0;
if GuiAllowed() then
Dialog.Open(ProcessingMsg, TotalCount);
@@ -66,11 +64,6 @@ report 8752 "DA External Storage Sync"
if not ExternalStorageImpl.DownloadFromExternalStorage(DocumentAttachment) then
FailedCount += 1;
end;
- if (DocumentAttachment."Uploaded Externally") and (DocumentAttachment."Deleted Internally" = false) then
- if ExternalStorageImpl.DeleteFromInternalStorage(DocumentAttachment) then
- DeleteAfterUploadCount += 1
- else
- DeleteAfterUploadFailedCount += 1;
Commit(); // Commit after each record to avoid lost in communication error with external storage service
if (MaxRecordsToProcess > 0) and (ProcessedCount >= MaxRecordsToProcess) then
@@ -124,7 +117,6 @@ report 8752 "DA External Storage Sync"
var
ExternalStorageImpl: Codeunit "DA External Storage Impl.";
Dialog: Dialog;
- DeleteAfterUploadCount, DeleteAfterUploadFailedCount : Integer;
FailedCount: Integer;
MaxRecordsToProcess: Integer;
ProcessedCount: Integer;
diff --git a/src/Apps/W1/External Storage - Document Attachments/app/src/DocumentAttachmentIntegration/DAExternalStorageImpl.Codeunit.al b/src/Apps/W1/External Storage - Document Attachments/app/src/DocumentAttachmentIntegration/DAExternalStorageImpl.Codeunit.al
index 94dbf73b25..20f25867d9 100644
--- a/src/Apps/W1/External Storage - Document Attachments/app/src/DocumentAttachmentIntegration/DAExternalStorageImpl.Codeunit.al
+++ b/src/Apps/W1/External Storage - Document Attachments/app/src/DocumentAttachmentIntegration/DAExternalStorageImpl.Codeunit.al
@@ -899,18 +899,6 @@ codeunit 8751 "DA External Storage Impl." implements "File Scenario"
exit(MigratedCount);
end;
- ///
- /// Shows the current environment hash for use in another environment.
- ///
- procedure ShowCurrentEnvironmentHash()
- var
- CurrentHash: Text[16];
- HashCopiedMsg: Label 'Current environment hash: %1', Comment = '%1 = Hash value';
- begin
- CurrentHash := GetCurrentEnvironmentHash();
- Message(HashCopiedMsg, CurrentHash);
- end;
-
#region Telemetry Logging
local procedure LogFeatureUsedTelemetry()
var
diff --git a/src/Apps/W1/External Storage - Document Attachments/app/src/Permissions/DAExtStorAdmin.PermissionSet.al b/src/Apps/W1/External Storage - Document Attachments/app/src/Permissions/DAExtStorAdmin.PermissionSet.al
index 72c1d7b2fc..1956990135 100644
--- a/src/Apps/W1/External Storage - Document Attachments/app/src/Permissions/DAExtStorAdmin.PermissionSet.al
+++ b/src/Apps/W1/External Storage - Document Attachments/app/src/Permissions/DAExtStorAdmin.PermissionSet.al
@@ -12,5 +12,5 @@ permissionset 8751 "DA Ext. Stor. Admin"
{
Assignable = true;
Caption = 'DA - External Storage Admin';
- Permissions = tabledata "DA External Storage Setup" = IMD;
+ Permissions = tabledata "DA External Storage Setup" = RIMD;
}
\ No newline at end of file
diff --git a/src/Apps/W1/External Storage - Document Attachments/app/src/Setup/DAExternalStorageSetup.Page.al b/src/Apps/W1/External Storage - Document Attachments/app/src/Setup/DAExternalStorageSetup.Page.al
index 060f92e7fa..a60dbcd0aa 100644
--- a/src/Apps/W1/External Storage - Document Attachments/app/src/Setup/DAExternalStorageSetup.Page.al
+++ b/src/Apps/W1/External Storage - Document Attachments/app/src/Setup/DAExternalStorageSetup.Page.al
@@ -34,7 +34,6 @@ page 8750 "DA External Storage Setup"
Caption = 'General';
field(Enabled; Rec.Enabled)
{
- ToolTip = 'Specifies if the External Storage feature is enabled. Enable this to start using external storage for document attachments.';
Importance = Promoted;
}
field("Root Folder"; Rec."Root Folder")
diff --git a/src/Apps/W1/External Storage - Document Attachments/app/src/Setup/DAExternalStorageSetup.Table.al b/src/Apps/W1/External Storage - Document Attachments/app/src/Setup/DAExternalStorageSetup.Table.al
index 1d30cb2fff..13ee7f91b7 100644
--- a/src/Apps/W1/External Storage - Document Attachments/app/src/Setup/DAExternalStorageSetup.Table.al
+++ b/src/Apps/W1/External Storage - Document Attachments/app/src/Setup/DAExternalStorageSetup.Table.al
@@ -29,7 +29,7 @@ table 8750 "DA External Storage Setup"
field(2; Enabled; Boolean)
{
Caption = 'Enabled';
- ToolTip = 'Specifies if the External Storage feature is enabled.';
+ ToolTip = 'Specifies if the External Storage feature is enabled. Enable this to start using external storage for document attachments.';
trigger OnValidate()
var