From 77cfb3966b47c7c53f39278b8f70f8bc0f0a38dc Mon Sep 17 00:00:00 2001
From: IceOnly
Date: Wed, 29 Jan 2025 14:35:28 +0100
Subject: [PATCH] Azure Blob Storage, Azure File Share and SharePoint Online
Connector Apps (#23225)
This PR contains three new connector apps, to connect Azure Blob
Storage, Azure File Share and SharePoint Online with the New File System
Module in the System App.
File System Module PR:
https://github.com/microsoft/BCApps/pull/663
Here are some Screenshots:
![image](https://user-images.githubusercontent.com/3911556/236433843-11ee0b26-ee9c-4da2-8bc6-efb32675090c.png)
![image](https://user-images.githubusercontent.com/3911556/236433887-bf2bf2f2-f68e-4efa-9db5-074a6f68fad0.png)
![image](https://user-images.githubusercontent.com/3911556/236433950-e8117496-16f6-4b25-a575-42ba729ca6c3.png)
![image](https://user-images.githubusercontent.com/3911556/236434182-97fd304a-95ac-4180-9d52-9ead78899822.png)
![image](https://user-images.githubusercontent.com/3911556/236435173-a01a77bc-04de-4063-b0bb-bf18e28b2817.png)
![image](https://user-images.githubusercontent.com/3911556/236435258-56d68d50-7581-4527-bcdd-5b6e3ed53ac8.png)
Fixes #22691
Fixes
[AB#559148](https://dynamicssmb2.visualstudio.com/1fcb79e7-ab07-432a-a3c6-6cf5a88ba4a5/_workitems/edit/559148)
---
.../ExtBlobStorageConnector.Entitlement.al | 13 +
.../app/ExtensionLogo.png | Bin 0 -> 4681 bytes
.../app/README.md | 2 +
.../app/app.json | 37 ++
.../app/data/connector-logo.png | Bin 0 -> 2140 bytes
.../ExtBlobStorEdit.PermissionSet.al | 18 +
.../ExtBlobStorObjects.PermissionSet.al | 19 +
.../ExtBlobStorRead.PermissionSet.al | 18 +
...ageAdminExtBlobStorage.PermissionSetExt.al | 11 +
...rageEditExtBlobStorage.PermissionSetExt.al | 11 +
.../src/ExtBlobStoConnectorImpl.Codeunit.al | 508 ++++++++++++++++++
.../app/src/ExtBlobStoContainerLookup.Page.al | 33 ++
.../app/src/ExtBlobStorAccountWizard.Page.al | 166 ++++++
.../app/src/ExtBlobStorageAccount.Page.al | 80 +++
.../app/src/ExtBlobStorageAccount.Table.al | 91 ++++
.../app/src/ExtBlobStorageAuthType.Enum.al | 20 +
.../src/ExtBlobStorageConnector.EnumExt.al | 21 +
.../test/ExtensionLogo.png | Bin 0 -> 4681 bytes
.../test/README.md | 0
.../test/app.json | 55 ++
.../src/ExtAzureBlobServiceTest.Codeunit.al | 150 ++++++
.../src/mocks/ExtBlobAccountMock.Codeunit.al | 67 +++
.../ExtFileShareConnector.Entitlement.al | 13 +
.../app/ExtensionLogo.png | Bin 0 -> 4681 bytes
.../app/README.md | 2 +
.../app/app.json | 37 ++
.../app/data/connector-logo.png | Bin 0 -> 4430 bytes
.../ExtFileShareEdit.PermissionSet.al | 18 +
.../ExtFileShareObjects.PermissionSet.al | 18 +
.../ExtFileShareRead.PermissionSet.al | 18 +
...orageAdminExtFileShare.PermissionSetExt.al | 11 +
...torageEditExtFileShare.PermissionSetExt.al | 11 +
.../app/src/ExtFileShareAccount.Page.al | 67 +++
.../app/src/ExtFileShareAccount.Table.al | 89 +++
.../app/src/ExtFileShareAccountWizard.Page.al | 154 ++++++
.../app/src/ExtFileShareAuthType.Enum.al | 20 +
.../app/src/ExtFileShareConnector.EnumExt.al | 21 +
.../src/ExtFileShareConnectorImpl.Codeunit.al | 472 ++++++++++++++++
.../test/ExtensionLogo.png | Bin 0 -> 4681 bytes
.../test/README.md | 0
.../test/app.json | 55 ++
.../src/ExtAzureFileServiceTest.Codeunit.al | 150 ++++++
.../src/mocks/ExtFileAccountMock.Codeunit.al | 68 +++
.../ExtSharePointConnector.Entitlement.al | 13 +
.../app/ExtensionLogo.png | Bin 0 -> 4681 bytes
.../app/README.md | 3 +
.../app/app.json | 37 ++
.../app/data/connector-logo.png | Bin 0 -> 13403 bytes
.../ExtSharePointEdit.PermissionSet.al | 18 +
.../ExtSharePointObjects.PermissionSet.al | 18 +
.../ExtSharePointRead.PermissionSet.al | 18 +
...rageAdminExtSharePoint.PermissionSetExt.al | 11 +
...orageEditExtSharePoint.PermissionSetExt.al | 11 +
.../app/src/ExtSharePointAccount.Page.al | 68 +++
.../app/src/ExtSharePointAccount.Table.al | 95 ++++
.../src/ExtSharePointAccountWizard.Page.al | 169 ++++++
.../app/src/ExtSharePointConnector.EnumExt.al | 21 +
.../ExtSharePointConnectorImpl.Codeunit.al | 458 ++++++++++++++++
.../test/ExtensionLogo.png | Bin 0 -> 4681 bytes
.../test/README.md | 0
.../test/app.json | 55 ++
.../ExtSharePointConnectorTest.Codeunit.al | 153 ++++++
.../ExtSharePointAccountMock.Codeunit.al | 79 +++
.../.AL-Go/settings.json | 5 +-
64 files changed, 3775 insertions(+), 1 deletion(-)
create mode 100644 Apps/W1/External File Storage - Azure Blob Service Connector/app/Entitlements/ExtBlobStorageConnector.Entitlement.al
create mode 100644 Apps/W1/External File Storage - Azure Blob Service Connector/app/ExtensionLogo.png
create mode 100644 Apps/W1/External File Storage - Azure Blob Service Connector/app/README.md
create mode 100644 Apps/W1/External File Storage - Azure Blob Service Connector/app/app.json
create mode 100644 Apps/W1/External File Storage - Azure Blob Service Connector/app/data/connector-logo.png
create mode 100644 Apps/W1/External File Storage - Azure Blob Service Connector/app/permissions/ExtBlobStorEdit.PermissionSet.al
create mode 100644 Apps/W1/External File Storage - Azure Blob Service Connector/app/permissions/ExtBlobStorObjects.PermissionSet.al
create mode 100644 Apps/W1/External File Storage - Azure Blob Service Connector/app/permissions/ExtBlobStorRead.PermissionSet.al
create mode 100644 Apps/W1/External File Storage - Azure Blob Service Connector/app/permissions/FileStorageAdminExtBlobStorage.PermissionSetExt.al
create mode 100644 Apps/W1/External File Storage - Azure Blob Service Connector/app/permissions/FileStorageEditExtBlobStorage.PermissionSetExt.al
create mode 100644 Apps/W1/External File Storage - Azure Blob Service Connector/app/src/ExtBlobStoConnectorImpl.Codeunit.al
create mode 100644 Apps/W1/External File Storage - Azure Blob Service Connector/app/src/ExtBlobStoContainerLookup.Page.al
create mode 100644 Apps/W1/External File Storage - Azure Blob Service Connector/app/src/ExtBlobStorAccountWizard.Page.al
create mode 100644 Apps/W1/External File Storage - Azure Blob Service Connector/app/src/ExtBlobStorageAccount.Page.al
create mode 100644 Apps/W1/External File Storage - Azure Blob Service Connector/app/src/ExtBlobStorageAccount.Table.al
create mode 100644 Apps/W1/External File Storage - Azure Blob Service Connector/app/src/ExtBlobStorageAuthType.Enum.al
create mode 100644 Apps/W1/External File Storage - Azure Blob Service Connector/app/src/ExtBlobStorageConnector.EnumExt.al
create mode 100644 Apps/W1/External File Storage - Azure Blob Service Connector/test/ExtensionLogo.png
create mode 100644 Apps/W1/External File Storage - Azure Blob Service Connector/test/README.md
create mode 100644 Apps/W1/External File Storage - Azure Blob Service Connector/test/app.json
create mode 100644 Apps/W1/External File Storage - Azure Blob Service Connector/test/src/ExtAzureBlobServiceTest.Codeunit.al
create mode 100644 Apps/W1/External File Storage - Azure Blob Service Connector/test/src/mocks/ExtBlobAccountMock.Codeunit.al
create mode 100644 Apps/W1/External File Storage - Azure File Service Connector/app/Entitlements/ExtFileShareConnector.Entitlement.al
create mode 100644 Apps/W1/External File Storage - Azure File Service Connector/app/ExtensionLogo.png
create mode 100644 Apps/W1/External File Storage - Azure File Service Connector/app/README.md
create mode 100644 Apps/W1/External File Storage - Azure File Service Connector/app/app.json
create mode 100644 Apps/W1/External File Storage - Azure File Service Connector/app/data/connector-logo.png
create mode 100644 Apps/W1/External File Storage - Azure File Service Connector/app/permissions/ExtFileShareEdit.PermissionSet.al
create mode 100644 Apps/W1/External File Storage - Azure File Service Connector/app/permissions/ExtFileShareObjects.PermissionSet.al
create mode 100644 Apps/W1/External File Storage - Azure File Service Connector/app/permissions/ExtFileShareRead.PermissionSet.al
create mode 100644 Apps/W1/External File Storage - Azure File Service Connector/app/permissions/FileStorageAdminExtFileShare.PermissionSetExt.al
create mode 100644 Apps/W1/External File Storage - Azure File Service Connector/app/permissions/FileStorageEditExtFileShare.PermissionSetExt.al
create mode 100644 Apps/W1/External File Storage - Azure File Service Connector/app/src/ExtFileShareAccount.Page.al
create mode 100644 Apps/W1/External File Storage - Azure File Service Connector/app/src/ExtFileShareAccount.Table.al
create mode 100644 Apps/W1/External File Storage - Azure File Service Connector/app/src/ExtFileShareAccountWizard.Page.al
create mode 100644 Apps/W1/External File Storage - Azure File Service Connector/app/src/ExtFileShareAuthType.Enum.al
create mode 100644 Apps/W1/External File Storage - Azure File Service Connector/app/src/ExtFileShareConnector.EnumExt.al
create mode 100644 Apps/W1/External File Storage - Azure File Service Connector/app/src/ExtFileShareConnectorImpl.Codeunit.al
create mode 100644 Apps/W1/External File Storage - Azure File Service Connector/test/ExtensionLogo.png
create mode 100644 Apps/W1/External File Storage - Azure File Service Connector/test/README.md
create mode 100644 Apps/W1/External File Storage - Azure File Service Connector/test/app.json
create mode 100644 Apps/W1/External File Storage - Azure File Service Connector/test/src/ExtAzureFileServiceTest.Codeunit.al
create mode 100644 Apps/W1/External File Storage - Azure File Service Connector/test/src/mocks/ExtFileAccountMock.Codeunit.al
create mode 100644 Apps/W1/External File Storage - SharePoint Connector/app/Entitlements/ExtSharePointConnector.Entitlement.al
create mode 100644 Apps/W1/External File Storage - SharePoint Connector/app/ExtensionLogo.png
create mode 100644 Apps/W1/External File Storage - SharePoint Connector/app/README.md
create mode 100644 Apps/W1/External File Storage - SharePoint Connector/app/app.json
create mode 100644 Apps/W1/External File Storage - SharePoint Connector/app/data/connector-logo.png
create mode 100644 Apps/W1/External File Storage - SharePoint Connector/app/permissions/ExtSharePointEdit.PermissionSet.al
create mode 100644 Apps/W1/External File Storage - SharePoint Connector/app/permissions/ExtSharePointObjects.PermissionSet.al
create mode 100644 Apps/W1/External File Storage - SharePoint Connector/app/permissions/ExtSharePointRead.PermissionSet.al
create mode 100644 Apps/W1/External File Storage - SharePoint Connector/app/permissions/FileStorageAdminExtSharePoint.PermissionSetExt.al
create mode 100644 Apps/W1/External File Storage - SharePoint Connector/app/permissions/FileStorageEditExtSharePoint.PermissionSetExt.al
create mode 100644 Apps/W1/External File Storage - SharePoint Connector/app/src/ExtSharePointAccount.Page.al
create mode 100644 Apps/W1/External File Storage - SharePoint Connector/app/src/ExtSharePointAccount.Table.al
create mode 100644 Apps/W1/External File Storage - SharePoint Connector/app/src/ExtSharePointAccountWizard.Page.al
create mode 100644 Apps/W1/External File Storage - SharePoint Connector/app/src/ExtSharePointConnector.EnumExt.al
create mode 100644 Apps/W1/External File Storage - SharePoint Connector/app/src/ExtSharePointConnectorImpl.Codeunit.al
create mode 100644 Apps/W1/External File Storage - SharePoint Connector/test/ExtensionLogo.png
create mode 100644 Apps/W1/External File Storage - SharePoint Connector/test/README.md
create mode 100644 Apps/W1/External File Storage - SharePoint Connector/test/app.json
create mode 100644 Apps/W1/External File Storage - SharePoint Connector/test/src/ExtSharePointConnectorTest.Codeunit.al
create mode 100644 Apps/W1/External File Storage - SharePoint Connector/test/src/mocks/ExtSharePointAccountMock.Codeunit.al
diff --git a/Apps/W1/External File Storage - Azure Blob Service Connector/app/Entitlements/ExtBlobStorageConnector.Entitlement.al b/Apps/W1/External File Storage - Azure Blob Service Connector/app/Entitlements/ExtBlobStorageConnector.Entitlement.al
new file mode 100644
index 0000000000..9233f60fd8
--- /dev/null
+++ b/Apps/W1/External File Storage - Azure Blob Service Connector/app/Entitlements/ExtBlobStorageConnector.Entitlement.al
@@ -0,0 +1,13 @@
+// ------------------------------------------------------------------------------------------------
+// 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;
+
+entitlement "Ext. Blob Storage Connector"
+{
+
+ ObjectEntitlements = "Ext. Blob Stor. - Edit";
+ Type = Implicit;
+}
\ No newline at end of file
diff --git a/Apps/W1/External File Storage - Azure Blob Service Connector/app/ExtensionLogo.png b/Apps/W1/External File Storage - Azure Blob Service Connector/app/ExtensionLogo.png
new file mode 100644
index 0000000000000000000000000000000000000000..30941b354fa335cad3ea5426ac24cadb2ee328e5
GIT binary patch
literal 4681
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
literal 0
HcmV?d00001
diff --git a/Apps/W1/External File Storage - Azure Blob Service Connector/app/README.md b/Apps/W1/External File Storage - Azure Blob Service Connector/app/README.md
new file mode 100644
index 0000000000..9e127e4852
--- /dev/null
+++ b/Apps/W1/External File Storage - Azure Blob Service Connector/app/README.md
@@ -0,0 +1,2 @@
+# External File Storage - Azure Blob Storage Connector
+This connector allows access to Azure Blob Storage Containers.
diff --git a/Apps/W1/External File Storage - Azure Blob Service Connector/app/app.json b/Apps/W1/External File Storage - Azure Blob Service Connector/app/app.json
new file mode 100644
index 0000000000..e79567a968
--- /dev/null
+++ b/Apps/W1/External File Storage - Azure Blob Service Connector/app/app.json
@@ -0,0 +1,37 @@
+{
+ "id": "c9ce86fe-cb70-4b79-be03-d21856b1a4ca",
+ "name": "External File Storage - Azure Blob Service Connector",
+ "publisher": "Microsoft",
+ "brief": "Enables file and folder operations for Azure Blob Service Containers via the External File Storage Module with Business Central.",
+ "description": "This app enables file and folder operations for Azure Blob Service Containers via the External File Storage Module with Business Central.",
+ "version": "26.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=2134520",
+ "url": "https://go.microsoft.com/fwlink/?linkid=724011",
+ "logo": "ExtensionLogo.png",
+ "application": "26.0.0.0",
+ "platform": "26.0.0.0",
+ "internalsVisibleTo": [
+ {
+ "id": "adcda309-4da8-43b8-b05d-d0287462ed42",
+ "name": "External File Storage - Azure Blob Service Connector Tests",
+ "publisher": "Microsoft"
+ }
+ ],
+ "dependencies": [],
+ "screenshots": [],
+ "idRanges": [
+ {
+ "from": 4560,
+ "to": 4569
+ }
+ ],
+ "resourceExposurePolicy": {
+ "allowDebugging": true,
+ "allowDownloadingSource": true,
+ "includeSourceInSymbolFile": true
+ },
+ "contextSensitiveHelpUrl": "https://go.microsoft.com/fwlink/?linkid=2134520",
+ "resourceFolders": ["data"]
+}
\ No newline at end of file
diff --git a/Apps/W1/External File Storage - Azure Blob Service Connector/app/data/connector-logo.png b/Apps/W1/External File Storage - Azure Blob Service Connector/app/data/connector-logo.png
new file mode 100644
index 0000000000000000000000000000000000000000..d94371448c610492afc354a236ae1754c36f48b9
GIT binary patch
literal 2140
zcmd5;c{JNu8@?g7sv5OqG*m}AMNNb1Am~_Xt)`aI`DO&897;`VB9_qciE&ya9X%~-
zrm^p;gi&jVRM06J8GLAxqOlJ_s`l;sMLXY|`D6b7?m73|``r8f&Ut_D^E~glDQ=e?
z6%HLc1OQNQcCzyT00wPgKvo)Byu-_a#E9VGXbY;?8gl^1syW-)coMUOug56f*VJW}
zXU-ebd9P3MuHZblgBz&pl_jb>?lz&tWP}gb!;5E8EU{ZO1{pCkdazllEyR_mTcJC!QsuOe?VbGnr<#T#r&Zv1$;Q;4N-r{@rf
zo)bT@G^o-D8D?
zT}-TOD@~q?(wAKGN;R`0SMq%K+F7)v2=-(QgqttDzj)J&3C6fZaChoIKrjpbC~7}Om0>LUlyoRM_`j#^v9C3
z2}i)`Qy|Y@*^wk00D8}A0?$jpx|1(OBF{m`65wi$94;=i7K_(p`WYrC$|yQqEMHl_Q?DSM__dc8d2@5qEb?;GBGjPLQ4jD;ZcB-Er^nq;766q&gQ2?J-ZIm!rFTBin;A>U9FZIkW-Ya=pU
zR<;(RL8Y(}1fSFFEwf3u2S0ri-uQ)TrZb0}5llu_P;fUI6szXx@DXK4`JL|&2$0lo
z$S`U+N|CB}sYbFiM9be}z8Ffc({r**Kv~;$WQTlmGq=si_hqJFvaID=IcuspU%gB7
zrL8xjym8H{fYS;??a_0&njxPoe*7@=k)b{r2_r44G#G!x(fm_4E%G4Z!Ux5m4x}P>
zdQE{NY1ni~z%Qn>4uO9K;0^DxWU7{gyQP(HwD;wg-%?8_N@>UMt)7X83%I72u$F82
zyosKq(XnVfp2{#RtF=+xDt3xy6b^f!UX$@$v@-0Qg6diNqXpSGog=r=k1YM{-$DKI
zrLJq@;!829RAgKxxFz@&IUkoCT4B|lkMnFQJ
zTheRu=I{%A!m=A4Wqlu)7`GaymC`p#Bfe>1S}AXKJHE!53OCq8S{D=xR1Y%18_I&KlQP?Jp5)?Vb_2xR79jw_9|
z?(c2K$21v?tmf|K#;v_VVFxt0!uf=XOahzrW_dDh{q_1iMyU8v@Q;IJz9aLicZ4v7
SVbm`Ua<;#0S7qz><9`9f9HPqr
literal 0
HcmV?d00001
diff --git a/Apps/W1/External File Storage - Azure Blob Service Connector/app/permissions/ExtBlobStorEdit.PermissionSet.al b/Apps/W1/External File Storage - Azure Blob Service Connector/app/permissions/ExtBlobStorEdit.PermissionSet.al
new file mode 100644
index 0000000000..344a6585cc
--- /dev/null
+++ b/Apps/W1/External File Storage - Azure Blob Service Connector/app/permissions/ExtBlobStorEdit.PermissionSet.al
@@ -0,0 +1,18 @@
+// ------------------------------------------------------------------------------------------------
+// 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;
+
+permissionset 4562 "Ext. Blob Stor. - Edit"
+{
+ Access = Public;
+ Assignable = false;
+ Caption = 'Blob Storage - Edit';
+
+ IncludedPermissionSets = "Ext. Blob Stor. - Read";
+
+ Permissions =
+ tabledata "Ext. Blob Storage Account" = imd;
+}
diff --git a/Apps/W1/External File Storage - Azure Blob Service Connector/app/permissions/ExtBlobStorObjects.PermissionSet.al b/Apps/W1/External File Storage - Azure Blob Service Connector/app/permissions/ExtBlobStorObjects.PermissionSet.al
new file mode 100644
index 0000000000..cf89506c00
--- /dev/null
+++ b/Apps/W1/External File Storage - Azure Blob Service Connector/app/permissions/ExtBlobStorObjects.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.
+// ------------------------------------------------------------------------------------------------
+
+namespace System.ExternalFileStorage;
+
+permissionset 4560 "Ext. Blob Stor. - Objects"
+{
+ Access = Public;
+ Assignable = false;
+ Caption = 'Blob Storage - Objects';
+
+ Permissions =
+ table "Ext. Blob Storage Account" = X,
+ page "Ext. Blob Stor. Account Wizard" = X,
+ page "Ext. Blob Sto Container Lookup" = X,
+ page "Ext. Blob Storage Account" = X;
+}
diff --git a/Apps/W1/External File Storage - Azure Blob Service Connector/app/permissions/ExtBlobStorRead.PermissionSet.al b/Apps/W1/External File Storage - Azure Blob Service Connector/app/permissions/ExtBlobStorRead.PermissionSet.al
new file mode 100644
index 0000000000..183d766d32
--- /dev/null
+++ b/Apps/W1/External File Storage - Azure Blob Service Connector/app/permissions/ExtBlobStorRead.PermissionSet.al
@@ -0,0 +1,18 @@
+// ------------------------------------------------------------------------------------------------
+// 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;
+
+permissionset 4561 "Ext. Blob Stor. - Read"
+{
+ Access = Public;
+ Assignable = false;
+ Caption = 'Blob Storage - Read';
+
+ IncludedPermissionSets = "Ext. Blob Stor. - Objects";
+
+ Permissions =
+ tabledata "Ext. Blob Storage Account" = r;
+}
diff --git a/Apps/W1/External File Storage - Azure Blob Service Connector/app/permissions/FileStorageAdminExtBlobStorage.PermissionSetExt.al b/Apps/W1/External File Storage - Azure Blob Service Connector/app/permissions/FileStorageAdminExtBlobStorage.PermissionSetExt.al
new file mode 100644
index 0000000000..0c91b5322c
--- /dev/null
+++ b/Apps/W1/External File Storage - Azure Blob Service Connector/app/permissions/FileStorageAdminExtBlobStorage.PermissionSetExt.al
@@ -0,0 +1,11 @@
+// ------------------------------------------------------------------------------------------------
+// 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;
+
+permissionsetextension 4560 "File Storage - Admin - Ext. Blob Storage" extends "File Storage - Admin"
+{
+ IncludedPermissionSets = "Ext. Blob Stor. - Edit";
+}
diff --git a/Apps/W1/External File Storage - Azure Blob Service Connector/app/permissions/FileStorageEditExtBlobStorage.PermissionSetExt.al b/Apps/W1/External File Storage - Azure Blob Service Connector/app/permissions/FileStorageEditExtBlobStorage.PermissionSetExt.al
new file mode 100644
index 0000000000..1c70d0195a
--- /dev/null
+++ b/Apps/W1/External File Storage - Azure Blob Service Connector/app/permissions/FileStorageEditExtBlobStorage.PermissionSetExt.al
@@ -0,0 +1,11 @@
+// ------------------------------------------------------------------------------------------------
+// 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;
+
+permissionsetextension 4561 "File Storage - Edit - Ext. Blob Storage" extends "File Storage - Edit"
+{
+ IncludedPermissionSets = "Ext. Blob Stor. - Read";
+}
diff --git a/Apps/W1/External File Storage - Azure Blob Service Connector/app/src/ExtBlobStoConnectorImpl.Codeunit.al b/Apps/W1/External File Storage - Azure Blob Service Connector/app/src/ExtBlobStoConnectorImpl.Codeunit.al
new file mode 100644
index 0000000000..f15c208587
--- /dev/null
+++ b/Apps/W1/External File Storage - Azure Blob Service Connector/app/src/ExtBlobStoConnectorImpl.Codeunit.al
@@ -0,0 +1,508 @@
+// ------------------------------------------------------------------------------------------------
+// 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;
+
+using System.Text;
+using System.Utilities;
+using System.Azure.Storage;
+using System.DataAdministration;
+
+codeunit 4560 "Ext. Blob Sto. Connector Impl." implements "External File Storage Connector"
+{
+ Access = Internal;
+ InherentEntitlements = X;
+ InherentPermissions = X;
+ Permissions = tabledata "Ext. Blob Storage Account" = rimd;
+
+ var
+ ConnectorDescriptionTxt: Label 'Use Azure Blob Storage to store and retrieve files.';
+ NotRegisteredAccountErr: Label 'We could not find the account. Typically, this is because the account has been deleted.';
+ MarkerFileNameTok: Label 'BusinessCentral.FileSystem.txt', Locked = true;
+
+ ///
+ /// Gets a List of Files stored on the provided account.
+ ///
+ /// The file account ID which is used to get the file.
+ /// The file path to list.
+ /// Defines the pagination data.
+ /// A list with all files stored in the path.
+ procedure ListFiles(AccountId: Guid; Path: Text; FilePaginationData: Codeunit "File Pagination Data"; var TempFileAccountContent: Record "File Account Content" temporary)
+ var
+ ABSContainerContent: Record "ABS Container Content";
+ ABSBlobClient: Codeunit "ABS Blob Client";
+ ABSOperationResponse: Codeunit "ABS Operation Response";
+ ABSOptionalParameters: Codeunit "ABS Optional Parameters";
+ begin
+ InitBlobClient(AccountId, ABSBlobClient);
+ CheckPath(Path);
+ InitOptionalParameters(Path, FilePaginationData, ABSOptionalParameters);
+ ABSOptionalParameters.Delimiter('/');
+ ABSOperationResponse := ABSBlobClient.ListBlobs(ABSContainerContent, ABSOptionalParameters);
+ ValidateListingResponse(FilePaginationData, ABSOperationResponse);
+
+ ABSContainerContent.SetFilter("Blob Type", '<>%1', '');
+ ABSContainerContent.SetFilter(Name, '<>%1', MarkerFileNameTok);
+ if not ABSContainerContent.FindSet() then
+ exit;
+
+ repeat
+ TempFileAccountContent.Init();
+ TempFileAccountContent.Name := ABSContainerContent.Name;
+ TempFileAccountContent.Type := TempFileAccountContent.Type::"File";
+ TempFileAccountContent."Parent Directory" := ABSContainerContent."Parent Directory";
+ TempFileAccountContent.Insert();
+ until ABSContainerContent.Next() = 0;
+ end;
+
+ ///
+ /// Gets a file from the provided account.
+ ///
+ /// The file account ID which is used to get the file.
+ /// The file path inside the file account.
+ /// The Stream were the file is read to.
+ procedure GetFile(AccountId: Guid; Path: Text; Stream: InStream)
+ var
+ ABSBlobClient: Codeunit "ABS Blob Client";
+ ABSOperationResponse: Codeunit "ABS Operation Response";
+ begin
+ InitBlobClient(AccountId, ABSBlobClient);
+ ABSOperationResponse := ABSBlobClient.GetBlobAsStream(Path, Stream);
+
+ if ABSOperationResponse.IsSuccessful() then
+ exit;
+
+ Error(ABSOperationResponse.GetError());
+ end;
+
+ ///
+ /// Create a file in the provided account.
+ ///
+ /// The file account ID which is used to send out the file.
+ /// The file path inside the file account.
+ /// The Stream were the file is read from.
+ procedure CreateFile(AccountId: Guid; Path: Text; Stream: InStream)
+ var
+ ABSBlobClient: Codeunit "ABS Blob Client";
+ ABSOperationResponse: Codeunit "ABS Operation Response";
+ begin
+ InitBlobClient(AccountId, ABSBlobClient);
+ ABSOperationResponse := ABSBlobClient.PutBlobBlockBlobStream(Path, Stream);
+
+ if ABSOperationResponse.IsSuccessful() then
+ exit;
+
+ Error(ABSOperationResponse.GetError());
+ end;
+
+ ///
+ /// Copies as file inside the provided account.
+ ///
+ /// The file account ID which is used to send out the file.
+ /// The source file path.
+ /// The target file path.
+ procedure CopyFile(AccountId: Guid; SourcePath: Text; TargetPath: Text)
+ var
+ ABSBlobClient: Codeunit "ABS Blob Client";
+ ABSOperationResponse: Codeunit "ABS Operation Response";
+ begin
+ InitBlobClient(AccountId, ABSBlobClient);
+ ABSOperationResponse := ABSBlobClient.CopyBlob(TargetPath, SourcePath);
+
+ if ABSOperationResponse.IsSuccessful() then
+ exit;
+
+ Error(ABSOperationResponse.GetError());
+ end;
+
+ ///
+ /// Move as file inside the provided account.
+ ///
+ /// The file account ID which is used to send out the file.
+ /// The source file path.
+ /// The target file path.
+ procedure MoveFile(AccountId: Guid; SourcePath: Text; TargetPath: Text)
+ var
+ ABSBlobClient: Codeunit "ABS Blob Client";
+ ABSOperationResponse: Codeunit "ABS Operation Response";
+ begin
+ InitBlobClient(AccountId, ABSBlobClient);
+ ABSOperationResponse := ABSBlobClient.CopyBlob(TargetPath, SourcePath);
+ if not ABSOperationResponse.IsSuccessful() then
+ Error(ABSOperationResponse.GetError());
+
+ ABSOperationResponse := ABSBlobClient.DeleteBlob(SourcePath);
+ if not ABSOperationResponse.IsSuccessful() then
+ Error(ABSOperationResponse.GetError());
+ end;
+
+ ///
+ /// Checks if a file exists on the provided account.
+ ///
+ /// The file account ID which is used to send out the file.
+ /// The file path inside the file account.
+ /// Returns true if the file exists
+ procedure FileExists(AccountId: Guid; Path: Text): Boolean
+ var
+ ABSContainerContent: Record "ABS Container Content";
+ ABSBlobClient: Codeunit "ABS Blob Client";
+ ABSOperationResponse: Codeunit "ABS Operation Response";
+ ABSOptionalParameters: Codeunit "ABS Optional Parameters";
+ begin
+ if Path = '' then
+ exit(false);
+
+ InitBlobClient(AccountId, ABSBlobClient);
+ ABSOptionalParameters.Prefix(Path);
+ ABSOperationResponse := ABSBlobClient.ListBlobs(ABSContainerContent, ABSOptionalParameters);
+ if not ABSOperationResponse.IsSuccessful() then
+ Error(ABSOperationResponse.GetError());
+
+ exit(not ABSContainerContent.IsEmpty());
+ end;
+
+ ///
+ /// Deletes a file exists on the provided account.
+ ///
+ /// The file account ID which is used to send out the file.
+ /// The file path inside the file account.
+ procedure DeleteFile(AccountId: Guid; Path: Text)
+ var
+ ABSBlobClient: Codeunit "ABS Blob Client";
+ ABSOperationResponse: Codeunit "ABS Operation Response";
+ begin
+ InitBlobClient(AccountId, ABSBlobClient);
+ ABSOperationResponse := ABSBlobClient.DeleteBlob(Path);
+
+ if ABSOperationResponse.IsSuccessful() then
+ exit;
+
+ Error(ABSOperationResponse.GetError());
+ end;
+
+ ///
+ /// Gets a List of Directories stored on the provided account.
+ ///
+ /// The file account ID which is used to get the file.
+ /// The file path to list.
+ /// Defines the pagination data.
+ /// A list with all directories stored in the path.
+ procedure ListDirectories(AccountId: Guid; Path: Text; FilePaginationData: Codeunit "File Pagination Data"; var TempFileAccountContent: Record "File Account Content" temporary)
+ var
+ ABSContainerContent: Record "ABS Container Content";
+ ABSBlobClient: Codeunit "ABS Blob Client";
+ ABSOperationResponse: Codeunit "ABS Operation Response";
+ ABSOptionalParameters: Codeunit "ABS Optional Parameters";
+ begin
+ InitBlobClient(AccountId, ABSBlobClient);
+ CheckPath(Path);
+ InitOptionalParameters(Path, FilePaginationData, ABSOptionalParameters);
+ ABSOperationResponse := ABSBlobClient.ListBlobs(ABSContainerContent, ABSOptionalParameters);
+ ValidateListingResponse(FilePaginationData, ABSOperationResponse);
+
+ ABSContainerContent.SetRange("Parent Directory", Path);
+ ABSContainerContent.SetRange("Blob Type", '');
+ if not ABSContainerContent.FindSet() then
+ exit;
+
+ repeat
+ TempFileAccountContent.Init();
+ TempFileAccountContent.Name := ABSContainerContent.Name;
+ TempFileAccountContent.Type := TempFileAccountContent.Type::Directory;
+ TempFileAccountContent."Parent Directory" := ABSContainerContent."Parent Directory";
+ TempFileAccountContent.Insert();
+ until ABSContainerContent.Next() = 0;
+ end;
+
+ ///
+ /// Creates a directory on the provided account.
+ ///
+ /// The file account ID which is used to send out the file.
+ /// The directory path inside the file account.
+ procedure CreateDirectory(AccountId: Guid; Path: Text)
+ var
+ TempBlob: Codeunit "Temp Blob";
+ IStream: InStream;
+ OStream: OutStream;
+ DirectoryAlreadyExistsErr: Label 'Directory already exists.';
+ MarkerFileContentTok: Label 'This is a directory marker file created by Business Central. It is safe to delete it.', Locked = true;
+ begin
+ if DirectoryExists(AccountId, Path) then
+ Error(DirectoryAlreadyExistsErr);
+
+ Path := CombinePath(Path, MarkerFileNameTok);
+ TempBlob.CreateOutStream(OStream);
+ OStream.WriteText(MarkerFileContentTok);
+
+ TempBlob.CreateInStream(IStream);
+ CreateFile(AccountId, Path, IStream);
+ end;
+
+ ///
+ /// Checks if a directory exists on the provided account.
+ ///
+ /// The file account ID which is used to send out the file.
+ /// The directory path inside the file account.
+ /// Returns true if the directory exists
+ procedure DirectoryExists(AccountId: Guid; Path: Text): Boolean
+ var
+ ABSContainerContent: Record "ABS Container Content";
+ ABSBlobClient: Codeunit "ABS Blob Client";
+ ABSOperationResponse: Codeunit "ABS Operation Response";
+ ABSOptionalParameters: Codeunit "ABS Optional Parameters";
+ begin
+ if Path = '' then
+ exit(true);
+
+ InitBlobClient(AccountId, ABSBlobClient);
+ ABSOptionalParameters.Prefix(Path);
+ ABSOptionalParameters.MaxResults(1);
+ ABSOperationResponse := ABSBlobClient.ListBlobs(ABSContainerContent, ABSOptionalParameters);
+ if not ABSOperationResponse.IsSuccessful() then
+ Error(ABSOperationResponse.GetError());
+
+ exit(not ABSContainerContent.IsEmpty());
+ end;
+
+ ///
+ /// Deletes a directory exists on the provided account.
+ ///
+ /// The file account ID which is used to send out the file.
+ /// The directory path inside the file account.
+ procedure DeleteDirectory(AccountId: Guid; Path: Text)
+ var
+ TempFileAccountContent: Record "File Account Content" temporary;
+ FilePaginationData: Codeunit "File Pagination Data";
+ DirectoryMustBeEmptyErr: Label 'Directory is not empty.';
+ begin
+ ListFiles(AccountId, Path, FilePaginationData, TempFileAccountContent);
+ ListDirectories(AccountId, Path, FilePaginationData, TempFileAccountContent);
+ TempFileAccountContent.SetFilter(Name, '<>%1', MarkerFileNameTok);
+ if not TempFileAccountContent.IsEmpty() then
+ Error(DirectoryMustBeEmptyErr);
+
+ DeleteFile(AccountId, CombinePath(Path, MarkerFileNameTok));
+ end;
+
+ ///
+ /// Gets the registered accounts for the Blob Storage connector.
+ ///
+ /// Out parameter holding all the registered accounts for the Blob Storage connector.
+ procedure GetAccounts(var TempAccounts: Record "File Account" temporary)
+ var
+ Account: Record "Ext. Blob Storage Account";
+ begin
+ if not Account.FindSet() then
+ exit;
+
+ repeat
+ TempAccounts."Account Id" := Account.Id;
+ TempAccounts.Name := Account.Name;
+ TempAccounts.Connector := Enum::"Ext. File Storage Connector"::"Blob Storage";
+ TempAccounts.Insert();
+ until Account.Next() = 0;
+ end;
+
+ ///
+ /// Shows accounts information.
+ ///
+ /// The ID of the account to show.
+ procedure ShowAccountInformation(AccountId: Guid)
+ var
+ BlobStorageAccountLocal: Record "Ext. Blob Storage Account";
+ begin
+ if not BlobStorageAccountLocal.Get(AccountId) then
+ Error(NotRegisteredAccountErr);
+
+ BlobStorageAccountLocal.SetRecFilter();
+ Page.Run(Page::"Ext. Blob Storage Account", BlobStorageAccountLocal);
+ end;
+
+ ///
+ /// Register an file account for the Blob Storage connector.
+ ///
+ /// Out parameter holding details of the registered account.
+ /// True if the registration was successful; false - otherwise.
+ procedure RegisterAccount(var TempAccount: Record "File Account" temporary): Boolean
+ var
+ BlobStorageAccountWizard: Page "Ext. Blob Stor. Account Wizard";
+ begin
+ BlobStorageAccountWizard.RunModal();
+
+ exit(BlobStorageAccountWizard.GetAccount(TempAccount));
+ end;
+
+ ///
+ /// Deletes an file account for the Blob Storage connector.
+ ///
+ /// The ID of the Blob Storage account
+ /// True if an account was deleted.
+ procedure DeleteAccount(AccountId: Guid): Boolean
+ var
+ BlobStorageAccountLocal: Record "Ext. Blob Storage Account";
+ begin
+ if BlobStorageAccountLocal.Get(AccountId) then
+ exit(BlobStorageAccountLocal.Delete());
+
+ exit(false);
+ end;
+
+ ///
+ /// Gets a description of the Blob Storage connector.
+ ///
+ /// A short description of the Blob Storage connector.
+ procedure GetDescription(): Text[250]
+ begin
+ exit(ConnectorDescriptionTxt);
+ end;
+
+ ///
+ /// Gets the Blob Storage connector logo.
+ ///
+ /// A base64-formatted image to be used as logo.
+ procedure GetLogoAsBase64(): Text
+ var
+ Base64Convert: Codeunit "Base64 Convert";
+ Stream: InStream;
+ begin
+ NavApp.GetResource('connector-logo.png', Stream);
+ exit(Base64Convert.ToBase64(Stream));
+ end;
+
+ internal procedure IsAccountValid(var TempAccount: Record "Ext. Blob Storage Account" temporary): Boolean
+ begin
+ if TempAccount.Name = '' then
+ exit(false);
+
+ if TempAccount."Storage Account Name" = '' then
+ exit(false);
+
+ if TempAccount."Container Name" = '' then
+ exit(false);
+
+ exit(true);
+ end;
+
+ internal procedure CreateAccount(var AccountToCopy: Record "Ext. Blob Storage Account"; Password: SecretText; var FileAccount: Record "File Account")
+ var
+ NewBlobStorageAccount: Record "Ext. Blob Storage Account";
+ begin
+ NewBlobStorageAccount.TransferFields(AccountToCopy);
+
+ NewBlobStorageAccount.Id := CreateGuid();
+ NewBlobStorageAccount.SetSecret(Password);
+
+ NewBlobStorageAccount.Insert();
+
+ FileAccount."Account Id" := NewBlobStorageAccount.Id;
+ FileAccount.Name := NewBlobStorageAccount.Name;
+ FileAccount.Connector := Enum::"Ext. File Storage Connector"::"Blob Storage";
+ end;
+
+ internal procedure LookUpContainer(var Account: Record "Ext. Blob Storage Account"; AuthType: Enum "Ext. Blob Storage Auth. Type"; Secret: SecretText; var NewContainerName: Text[2048])
+ var
+ ABSContainers: Record "ABS Container";
+ ABSContainerClient: Codeunit "ABS Container Client";
+ StorageServiceAuthorization: Codeunit "Storage Service Authorization";
+ ABSOperationResponse: Codeunit "ABS Operation Response";
+ Authorization: Interface "Storage Service Authorization";
+ begin
+ Account.TestField("Storage Account Name");
+ case AuthType of
+ AuthType::SasToken:
+ Authorization := SetReadySAS(StorageServiceAuthorization, Secret);
+ AuthType::SharedKey:
+ Authorization := StorageServiceAuthorization.CreateSharedKey(Secret);
+ end;
+
+ ABSContainerClient.Initialize(Account."Storage Account Name", Authorization);
+ ABSOperationResponse := ABSContainerClient.ListContainers(ABSContainers);
+ if not ABSOperationResponse.IsSuccessful() then
+ Error(ABSOperationResponse.GetError());
+
+ if not ABSContainers.Get(NewContainerName) then
+ if ABSContainers.FindFirst() then;
+
+ if (Page.RunModal(Page::"Ext. Blob Sto Container Lookup", ABSContainers) <> Action::LookupOK) then
+ exit;
+
+ NewContainerName := ABSContainers.Name;
+ end;
+
+ local procedure InitBlobClient(var AccountId: Guid; var ABSBlobClient: Codeunit "ABS Blob Client")
+ var
+ BlobStorageAccount: Record "Ext. Blob Storage Account";
+ StorageServiceAuthorization: Codeunit "Storage Service Authorization";
+ Authorization: Interface "Storage Service Authorization";
+ AccountDisabledErr: Label 'The account "%1" is disabled.', Comment = '%1 - Account Name';
+ begin
+ BlobStorageAccount.Get(AccountId);
+ if BlobStorageAccount.Disabled then
+ Error(AccountDisabledErr, BlobStorageAccount.Name);
+
+ case BlobStorageAccount."Authorization Type" of
+ "Ext. Blob Storage Auth. Type"::SasToken:
+ Authorization := SetReadySAS(StorageServiceAuthorization, BlobStorageAccount.GetSecret(BlobStorageAccount."Secret Key"));
+ "Ext. Blob Storage Auth. Type"::SharedKey:
+ Authorization := StorageServiceAuthorization.CreateSharedKey(BlobStorageAccount.GetSecret(BlobStorageAccount."Secret Key"));
+ end;
+ ABSBlobClient.Initialize(BlobStorageAccount."Storage Account Name", BlobStorageAccount."Container Name", Authorization);
+ end;
+
+ local procedure CheckPath(var Path: Text)
+ begin
+ if (Path <> '') and not Path.EndsWith(PathSeparator()) then
+ Path += PathSeparator();
+ end;
+
+ local procedure CombinePath(Path: Text; ChildPath: Text): Text
+ begin
+ if Path = '' then
+ exit(ChildPath);
+
+ if not Path.EndsWith(PathSeparator()) then
+ Path += PathSeparator();
+
+ exit(Path + ChildPath);
+ end;
+
+ local procedure InitOptionalParameters(Path: Text; var FilePaginationData: Codeunit "File Pagination Data"; var ABSOptionalParameters: Codeunit "ABS Optional Parameters")
+ begin
+ ABSOptionalParameters.Prefix(Path);
+ ABSOptionalParameters.MaxResults(500);
+ ABSOptionalParameters.NextMarker(FilePaginationData.GetMarker());
+ end;
+
+ local procedure ValidateListingResponse(var FilePaginationData: Codeunit "File Pagination Data"; var ABSOperationResponse: Codeunit "ABS Operation Response")
+ begin
+ if not ABSOperationResponse.IsSuccessful() then
+ Error(ABSOperationResponse.GetError());
+
+ FilePaginationData.SetMarker(ABSOperationResponse.GetNextMarker());
+ FilePaginationData.SetEndOfListing(ABSOperationResponse.GetNextMarker() = '');
+ end;
+
+ local procedure SetReadySAS(var StorageServiceAuthorization: Codeunit "Storage Service Authorization"; Secret: SecretText): Interface System.Azure.Storage."Storage Service Authorization"
+ begin
+ exit(StorageServiceAuthorization.UseReadySAS(Secret));
+ end;
+
+ local procedure PathSeparator(): Text
+ begin
+ exit('/');
+ end;
+
+ [EventSubscriber(ObjectType::Codeunit, Codeunit::"Environment Cleanup", OnClearCompanyConfig, '', false, false)]
+ local procedure EnvironmentCleanup_OnClearCompanyConfig(CompanyName: Text; SourceEnv: Enum "Environment Type"; DestinationEnv: Enum "Environment Type")
+ var
+ ExtBlobStorageAccount: Record "Ext. Blob Storage Account";
+ begin
+ ExtBlobStorageAccount.SetRange(Disabled, false);
+ if ExtBlobStorageAccount.IsEmpty() then
+ exit;
+
+ ExtBlobStorageAccount.ModifyAll(Disabled, true);
+ end;
+}
\ No newline at end of file
diff --git a/Apps/W1/External File Storage - Azure Blob Service Connector/app/src/ExtBlobStoContainerLookup.Page.al b/Apps/W1/External File Storage - Azure Blob Service Connector/app/src/ExtBlobStoContainerLookup.Page.al
new file mode 100644
index 0000000000..c74726ba8e
--- /dev/null
+++ b/Apps/W1/External File Storage - Azure Blob Service Connector/app/src/ExtBlobStoContainerLookup.Page.al
@@ -0,0 +1,33 @@
+// ------------------------------------------------------------------------------------------------
+// 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;
+
+using System.Azure.Storage;
+
+page 4562 "Ext. Blob Sto Container Lookup"
+{
+ ApplicationArea = All;
+ Caption = 'Container Lookup';
+ Editable = false;
+ Extensible = false;
+ PageType = List;
+ SourceTable = "ABS Container";
+ UsageCategory = None;
+
+ layout
+ {
+ area(content)
+ {
+ repeater(General)
+ {
+ field(Name; Rec.Name)
+ {
+ ToolTip = 'Specifies the Name of the container.';
+ }
+ }
+ }
+ }
+}
diff --git a/Apps/W1/External File Storage - Azure Blob Service Connector/app/src/ExtBlobStorAccountWizard.Page.al b/Apps/W1/External File Storage - Azure Blob Service Connector/app/src/ExtBlobStorAccountWizard.Page.al
new file mode 100644
index 0000000000..d8139bebef
--- /dev/null
+++ b/Apps/W1/External File Storage - Azure Blob Service Connector/app/src/ExtBlobStorAccountWizard.Page.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.
+// ------------------------------------------------------------------------------------------------
+
+namespace System.ExternalFileStorage;
+
+Using System.Environment;
+
+///
+/// Displays an account that is being registered via the Blob Storage connector.
+///
+page 4561 "Ext. Blob Stor. Account Wizard"
+{
+ ApplicationArea = All;
+ Caption = 'Setup Azure Blob Storage Account';
+ Editable = true;
+ Extensible = false;
+ PageType = NavigatePage;
+ Permissions = tabledata "Ext. Blob Storage Account" = rimd;
+ SourceTable = "Ext. Blob Storage Account";
+ SourceTableTemporary = true;
+
+ layout
+ {
+ area(Content)
+ {
+ group(TopBanner)
+ {
+ Editable = false;
+ ShowCaption = false;
+ Visible = TopBannerVisible;
+ field(NotDoneIcon; MediaResources."Media Reference")
+ {
+ Editable = false;
+ ShowCaption = false;
+ ToolTip = ' ', Locked = true;
+ }
+ }
+
+ field(NameField; Rec.Name)
+ {
+ Caption = 'Account Name';
+ NotBlank = true;
+ ShowMandatory = true;
+ ToolTip = 'Specifies the name of the Azure Blob Storage account.';
+
+ trigger OnValidate()
+ begin
+ IsNextEnabled := BlobStorageConnectorImpl.IsAccountValid(Rec);
+ end;
+ }
+
+ field(StorageAccountNameField; Rec."Storage Account Name")
+ {
+ Caption = 'Storage Account Name';
+ ShowMandatory = true;
+
+ trigger OnValidate()
+ begin
+ IsNextEnabled := BlobStorageConnectorImpl.IsAccountValid(Rec);
+ end;
+ }
+
+ field("Authorization Type"; Rec."Authorization Type")
+ {
+ }
+
+ field(SecretField; Secret)
+ {
+ Caption = 'Secret';
+ ExtendedDatatype = Masked;
+ ShowMandatory = true;
+ ToolTip = 'Specifies the Shared access signature Token or SharedKey.';
+ }
+
+ field(ContainerNameField; Rec."Container Name")
+ {
+ Caption = 'Container Name';
+ ShowMandatory = true;
+ ToolTip = 'Specifies the container to use of the Storage Blob.';
+
+ trigger OnLookup(var Text: Text): Boolean
+ var
+ BlobStorageConnectorImpl: Codeunit "Ext. Blob Sto. Connector Impl.";
+ NewContainerName: Text[2048];
+ begin
+ CurrPage.Update();
+ NewContainerName := CopyStr(Text, 1, MaxStrLen(NewContainerName));
+ BlobStorageConnectorImpl.LookUpContainer(Rec, Rec."Authorization Type", Secret, NewContainerName);
+ Text := NewContainerName;
+ exit(true);
+ end;
+
+ trigger OnValidate()
+ begin
+ IsNextEnabled := BlobStorageConnectorImpl.IsAccountValid(Rec);
+ end;
+ }
+ }
+ }
+
+ actions
+ {
+ area(processing)
+ {
+ action(Back)
+ {
+ Caption = 'Back';
+ Image = Cancel;
+ InFooterBar = true;
+ ToolTip = 'Move to the previous step.';
+
+ trigger OnAction()
+ begin
+ CurrPage.Close();
+ end;
+ }
+
+ action(Next)
+ {
+ Caption = 'Next';
+ Enabled = IsNextEnabled;
+ Image = NextRecord;
+ InFooterBar = true;
+ ToolTip = 'Move to the next step.';
+
+ trigger OnAction()
+ begin
+ BlobStorageConnectorImpl.CreateAccount(Rec, Secret, BlobStorageAccount);
+ CurrPage.Close();
+ end;
+ }
+ }
+ }
+
+ var
+ BlobStorageAccount: Record "File Account";
+ MediaResources: Record "Media Resources";
+ BlobStorageConnectorImpl: Codeunit "Ext. Blob Sto. Connector Impl.";
+ [NonDebuggable]
+ Secret: Text;
+ IsNextEnabled: Boolean;
+ TopBannerVisible: Boolean;
+
+ trigger OnOpenPage()
+ var
+ AssistedSetupLogoTok: Label 'ASSISTEDSETUP-NOTEXT-400PX.PNG', Locked = true;
+ begin
+ Rec.Init();
+ Rec.Insert();
+
+ if MediaResources.Get(AssistedSetupLogoTok) and (CurrentClientType() = ClientType::Web) then
+ TopBannerVisible := MediaResources."Media Reference".HasValue();
+ end;
+
+ internal procedure GetAccount(var FileAccount: Record "File Account"): Boolean
+ begin
+ if IsNullGuid(BlobStorageAccount."Account Id") then
+ exit(false);
+
+ FileAccount := BlobStorageAccount;
+
+ exit(true);
+ end;
+}
\ No newline at end of file
diff --git a/Apps/W1/External File Storage - Azure Blob Service Connector/app/src/ExtBlobStorageAccount.Page.al b/Apps/W1/External File Storage - Azure Blob Service Connector/app/src/ExtBlobStorageAccount.Page.al
new file mode 100644
index 0000000000..b69415a238
--- /dev/null
+++ b/Apps/W1/External File Storage - Azure Blob Service Connector/app/src/ExtBlobStorageAccount.Page.al
@@ -0,0 +1,80 @@
+// ------------------------------------------------------------------------------------------------
+// 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;
+
+///
+/// Displays an account that was registered via the Blob Storage connector.
+///
+page 4560 "Ext. Blob Storage Account"
+{
+ ApplicationArea = All;
+ Caption = 'Azure Blob Storage Account';
+ DataCaptionExpression = Rec.Name;
+ Extensible = false;
+ InsertAllowed = false;
+ PageType = Card;
+ Permissions = tabledata "Ext. Blob Storage Account" = rimd;
+ SourceTable = "Ext. Blob Storage Account";
+ UsageCategory = None;
+
+ layout
+ {
+ area(Content)
+ {
+ field(NameField; Rec.Name)
+ {
+ NotBlank = true;
+ ShowMandatory = true;
+ }
+ field(StorageAccountNameField; Rec."Storage Account Name") { }
+ field("Authorization Type"; Rec."Authorization Type") { }
+ field(SecretField; Secret)
+ {
+ Caption = 'Password';
+ Editable = SecretEditable;
+ ExtendedDatatype = Masked;
+ ToolTip = 'Specifies the Shared access signature Token or SharedKey.';
+
+ trigger OnValidate()
+ begin
+ Rec.SetSecret(Secret);
+ end;
+ }
+ field(ContainerNameField; Rec."Container Name")
+ {
+ trigger OnLookup(var Text: Text): Boolean
+ var
+ BlobStorageConnectorImpl: Codeunit "Ext. Blob Sto. Connector Impl.";
+ NewContainerName: Text[2048];
+ begin
+ CurrPage.Update();
+ NewContainerName := CopyStr(Text, 1, MaxStrLen(NewContainerName));
+ BlobStorageConnectorImpl.LookUpContainer(Rec, Rec."Authorization Type", Rec.GetSecret(Rec."Secret Key"), NewContainerName);
+ Text := NewContainerName;
+ exit(true);
+ end;
+ }
+ field(DisabledField; Rec.Disabled) { }
+ }
+ }
+
+ var
+ SecretEditable: Boolean;
+ [NonDebuggable]
+ Secret: Text;
+
+ trigger OnOpenPage()
+ begin
+ Rec.SetCurrentKey(Name);
+ end;
+
+ trigger OnAfterGetCurrRecord()
+ begin
+ SecretEditable := CurrPage.Editable();
+ if not IsNullGuid(Rec."Secret Key") then
+ Secret := '***';
+ end;
+}
\ No newline at end of file
diff --git a/Apps/W1/External File Storage - Azure Blob Service Connector/app/src/ExtBlobStorageAccount.Table.al b/Apps/W1/External File Storage - Azure Blob Service Connector/app/src/ExtBlobStorageAccount.Table.al
new file mode 100644
index 0000000000..66a1ba23d4
--- /dev/null
+++ b/Apps/W1/External File Storage - Azure Blob Service Connector/app/src/ExtBlobStorageAccount.Table.al
@@ -0,0 +1,91 @@
+// ------------------------------------------------------------------------------------------------
+// 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;
+
+///
+/// Holds the information for all file accounts that are registered via the Blob Storage connector
+///
+table 4560 "Ext. Blob Storage Account"
+{
+ Caption = 'Azure Blob Storage Account';
+ DataClassification = CustomerContent;
+
+ fields
+ {
+ field(1; "Id"; Guid)
+ {
+ AllowInCustomizations = Never;
+ Caption = 'Primary Key';
+ DataClassification = SystemMetadata;
+ }
+
+ field(2; Name; Text[250])
+ {
+ Caption = 'Account Name';
+ ToolTip = 'Specifies the name of the Storage account connection.';
+ }
+ field(3; "Storage Account Name"; Text[2048])
+ {
+ Caption = 'Storage Account Name';
+ ToolTip = 'Specifies the Azure Storage name.';
+ }
+ field(4; "Container Name"; Text[2048])
+ {
+ Caption = 'Container Name';
+ ToolTip = 'Specifies the Azure Storage Container name.';
+ }
+ field(7; "Authorization Type"; Enum "Ext. Blob Storage Auth. Type")
+ {
+ Access = Internal;
+ Caption = 'Authorization Type';
+ ToolTip = 'The way of authorizing used to access the Blob Storage.';
+ }
+ field(8; "Secret Key"; Guid)
+ {
+ Access = Internal;
+ Caption = 'Secret Key';
+ DataClassification = SystemMetadata;
+ }
+ field(9; Disabled; Boolean)
+ {
+ Caption = 'Disabled';
+ ToolTip = 'Specifies if the account is disabled. This happens automatically when a sandbox is created.';
+ }
+ }
+
+ keys
+ {
+ key(PK; Id)
+ {
+ Clustered = true;
+ }
+ }
+
+ var
+ UnableToGetSecretMsg: Label 'Unable to get Blob Storage secret.';
+ UnableToSetSecretMsg: Label 'Unable to set Blob Storage secret.';
+
+ trigger OnDelete()
+ begin
+ if not IsNullGuid(Rec."Secret Key") then
+ if IsolatedStorage.Delete(Rec."Secret Key") then;
+ end;
+
+ procedure SetSecret(Secret: SecretText)
+ begin
+ if IsNullGuid(Rec."Secret Key") then
+ Rec."Secret Key" := CreateGuid();
+
+ if not IsolatedStorage.Set(Format(Rec."Secret Key"), Secret, DataScope::Company) then
+ Error(UnableToSetSecretMsg);
+ end;
+
+ procedure GetSecret(SecretKey: Guid) Secret: SecretText
+ begin
+ if not IsolatedStorage.Get(Format(SecretKey), DataScope::Company, Secret) then
+ Error(UnableToGetSecretMsg);
+ end;
+}
\ No newline at end of file
diff --git a/Apps/W1/External File Storage - Azure Blob Service Connector/app/src/ExtBlobStorageAuthType.Enum.al b/Apps/W1/External File Storage - Azure Blob Service Connector/app/src/ExtBlobStorageAuthType.Enum.al
new file mode 100644
index 0000000000..876a7ed6fe
--- /dev/null
+++ b/Apps/W1/External File Storage - Azure Blob Service Connector/app/src/ExtBlobStorageAuthType.Enum.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.
+// ------------------------------------------------------------------------------------------------
+
+namespace System.ExternalFileStorage;
+
+enum 4560 "Ext. Blob Storage Auth. Type"
+{
+ Access = Internal;
+
+ value(0; SasToken)
+ {
+ Caption = 'Shared Access Signature';
+ }
+ value(1; SharedKey)
+ {
+ Caption = 'Shared Key';
+ }
+}
diff --git a/Apps/W1/External File Storage - Azure Blob Service Connector/app/src/ExtBlobStorageConnector.EnumExt.al b/Apps/W1/External File Storage - Azure Blob Service Connector/app/src/ExtBlobStorageConnector.EnumExt.al
new file mode 100644
index 0000000000..b93624b36c
--- /dev/null
+++ b/Apps/W1/External File Storage - Azure Blob Service Connector/app/src/ExtBlobStorageConnector.EnumExt.al
@@ -0,0 +1,21 @@
+// ------------------------------------------------------------------------------------------------
+// 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;
+
+///
+/// Enum extension to register the Blob Storage connector.
+///
+enumextension 4560 "Ext. Blob Storage Connector" extends "Ext. File Storage Connector"
+{
+ ///
+ /// The Blob Storage connector.
+ ///
+ value(4560; "Blob Storage")
+ {
+ Caption = 'Blob Storage';
+ Implementation = "External File Storage Connector" = "Ext. Blob Sto. Connector Impl.";
+ }
+}
\ No newline at end of file
diff --git a/Apps/W1/External File Storage - Azure Blob Service Connector/test/ExtensionLogo.png b/Apps/W1/External File Storage - Azure Blob Service Connector/test/ExtensionLogo.png
new file mode 100644
index 0000000000000000000000000000000000000000..30941b354fa335cad3ea5426ac24cadb2ee328e5
GIT binary patch
literal 4681
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
literal 0
HcmV?d00001
diff --git a/Apps/W1/External File Storage - Azure Blob Service Connector/test/README.md b/Apps/W1/External File Storage - Azure Blob Service Connector/test/README.md
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/Apps/W1/External File Storage - Azure Blob Service Connector/test/app.json b/Apps/W1/External File Storage - Azure Blob Service Connector/test/app.json
new file mode 100644
index 0000000000..c181f8e710
--- /dev/null
+++ b/Apps/W1/External File Storage - Azure Blob Service Connector/test/app.json
@@ -0,0 +1,55 @@
+{
+ "id": "adcda309-4da8-43b8-b05d-d0287462ed42",
+ "name": "External File Storage - Azure Blob Service Connector Tests",
+ "publisher": "Microsoft",
+ "brief": "Tests for the External File Storage - Azure Blob Service Connector app",
+ "description": "Tests for the External File Storage - Azure Blob Service Connector app",
+ "version": "26.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=2134520",
+ "url": "https://go.microsoft.com/fwlink/?linkid=724011",
+ "logo": "ExtensionLogo.png",
+ "application": "26.0.0.0",
+ "dependencies": [
+ {
+ "id": "c9ce86fe-cb70-4b79-be03-d21856b1a4ca",
+ "name": "External File Storage - Azure Blob Service Connector",
+ "publisher": "Microsoft",
+ "version": "26.0.0.0"
+ },
+ {
+ "id": "dd0be2ea-f733-4d65-bb34-a28f4624fb14",
+ "name": "Library Assert",
+ "publisher": "Microsoft",
+ "version": "26.0.0.0"
+ },
+ {
+ "id": "e7320ebb-08b3-4406-b1ec-b4927d3e280b",
+ "name": "Any",
+ "publisher": "Microsoft",
+ "version": "26.0.0.0"
+ },
+ {
+ "id": "9856ae4f-d1a7-46ef-89bb-6ef056398228",
+ "name": "System Application Test Library",
+ "publisher": "Microsoft",
+ "version": "26.0.0.0"
+ }
+ ],
+ "screenshots": [],
+ "platform": "26.0.0.0",
+ "idRanges": [
+ {
+ "from": 100000,
+ "to": 150000
+ }
+ ],
+ "target": "OnPrem",
+ "resourceExposurePolicy": {
+ "allowDebugging": true,
+ "allowDownloadingSource": true,
+ "includeSourceInSymbolFile": true
+ },
+ "contextSensitiveHelpUrl": "https://go.microsoft.com/fwlink/?linkid=2134520"
+}
\ No newline at end of file
diff --git a/Apps/W1/External File Storage - Azure Blob Service Connector/test/src/ExtAzureBlobServiceTest.Codeunit.al b/Apps/W1/External File Storage - Azure Blob Service Connector/test/src/ExtAzureBlobServiceTest.Codeunit.al
new file mode 100644
index 0000000000..1f3f76827d
--- /dev/null
+++ b/Apps/W1/External File Storage - Azure Blob Service Connector/test/src/ExtAzureBlobServiceTest.Codeunit.al
@@ -0,0 +1,150 @@
+// ------------------------------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+// ------------------------------------------------------------------------------------------------
+
+codeunit 144566 "Ext. Azure Blob Service Test"
+{
+ Subtype = Test;
+ TestPermissions = Disabled;
+
+ [Test]
+ [Scope('OnPrem')]
+ [HandlerFunctions('AccountRegisterPageHandler')]
+ [TransactionModel(TransactionModel::AutoRollback)]
+ procedure TestMultipleAccountsCanBeRegistered()
+ var
+ FileAccount: Record "File Account";
+ ExtFileConnector: Codeunit "Ext. Blob Sto. Connector Impl.";
+ FileAccounts: TestPage "File Accounts";
+ AccountIds: array[3] of Guid;
+ AccountName: array[3] of Text[250];
+ Index: Integer;
+ begin
+ // [Scenario] Create multiple accounts
+ Initialize();
+
+ // [When] Multiple accounts are registered
+ for Index := 1 to 3 do begin
+ SetBasicAccount();
+
+ Assert.IsTrue(ExtFileConnector.RegisterAccount(FileAccount), 'Failed to register account.');
+ AccountIds[Index] := FileAccount."Account Id";
+ AccountName[Index] := FileAccountMock.Name();
+
+ // [Then] Accounts are retrieved from the GetAccounts method
+ FileAccount.DeleteAll();
+ ExtFileConnector.GetAccounts(FileAccount);
+ Assert.RecordCount(FileAccount, Index);
+ end;
+
+ FileAccounts.OpenView();
+ for Index := 1 to 3 do begin
+ FileAccounts.GoToKey(AccountIds[Index], Enum::"Ext. File Storage Connector"::"Blob Storage");
+ Assert.AreEqual(AccountName[Index], FileAccounts.NameField.Value(), 'A different name was expected.');
+ end;
+ end;
+
+
+ [Test]
+ [Scope('OnPrem')]
+ [HandlerFunctions('AccountRegisterPageHandler')]
+ [TransactionModel(TransactionModel::AutoRollback)]
+ procedure TestEnviromentCleanupDisablesAccounts()
+ var
+ FileAccount: Record "File Account";
+ ExtSharePointAccount: Record "Ext. Blob Storage Account";
+ ExtFileConnector: Codeunit "Ext. Blob Sto. Connector Impl.";
+ EnvironmentTriggers: Codeunit "Environment Triggers";
+ AccountIds: array[3] of Guid;
+ Index: Integer;
+ begin
+ // [Scenario] Create multiple accounts
+ Initialize();
+
+ // [When] Multiple accounts are registered
+ for Index := 1 to 3 do begin
+ SetBasicAccount();
+
+ Assert.IsTrue(ExtFileConnector.RegisterAccount(FileAccount), 'Failed to register account.');
+ AccountIds[Index] := FileAccount."Account Id";
+
+ // [Then] Accounts are retrieved from the GetAccounts method
+ FileAccount.DeleteAll();
+ ExtFileConnector.GetAccounts(FileAccount);
+ Assert.RecordCount(FileAccount, Index);
+ end;
+
+ ExtSharePointAccount.SetRange(Disabled, true);
+ Assert.IsTrue(ExtSharePointAccount.IsEmpty(), 'Accounts are already disabled.');
+
+ EnvironmentTriggers.OnAfterCopyEnvironmentPerCompany(0, Any.AlphabeticText(30), 1, Any.AlphabeticText(30));
+
+ Assert.IsFalse(ExtSharePointAccount.IsEmpty(), 'Accounts are not disabled.');
+ end;
+
+ [Test]
+ [Scope('OnPrem')]
+ [HandlerFunctions('AccountRegisterPageHandler,AccountShowPageHandler')]
+ [TransactionModel(TransactionModel::AutoRollback)]
+ procedure TestShowAccountInformation()
+ var
+ FileAccount: Record "File Account";
+ FileConnector: Codeunit "Ext. Blob Sto. Connector Impl.";
+ begin
+ // [Scenario] Account Information is displayed in the Account page.
+
+ // [Given] An file account
+ Initialize();
+ SetBasicAccount();
+ FileConnector.RegisterAccount(FileAccount);
+
+ // [When] The ShowAccountInformation method is invoked
+ FileConnector.ShowAccountInformation(FileAccount."Account Id");
+
+ // [Then] The account page opens and displays the information
+ // Verify in AccountModalPageHandler
+ end;
+
+ local procedure Initialize()
+ var
+ ExtBlobStorageAccount: Record "Ext. Blob Storage Account";
+ begin
+ ExtBlobStorageAccount.DeleteAll();
+ end;
+
+ local procedure SetBasicAccount()
+ begin
+ FileAccountMock.Name(CopyStr(Any.AlphanumericText(250), 1, 250));
+ FileAccountMock.StorageAccountName(CopyStr(Any.AlphanumericText(250), 1, 250));
+ FileAccountMock.ContainerName(CopyStr(Any.AlphanumericText(250), 1, 250));
+ FileAccountMock.Password('testpassword');
+ end;
+
+ [ModalPageHandler]
+ procedure AccountRegisterPageHandler(var AccountWizard: TestPage "Ext. Blob Stor. Account Wizard")
+ begin
+ // Setup account
+ AccountWizard.NameField.SetValue(FileAccountMock.Name());
+ AccountWizard.StorageAccountNameField.SetValue(FileAccountMock.StorageAccountName());
+ AccountWizard.ContainerNameField.SetValue(FileAccountMock.ContainerName());
+ AccountWizard."Authorization Type".SetValue(FileAccountMock.AuthorizationType());
+ AccountWizard.SecretField.SetValue(FileAccountMock.Password());
+ AccountWizard.Next.Invoke();
+ end;
+
+ [PageHandler]
+ procedure AccountShowPageHandler(var Account: TestPage "Ext. Blob Storage Account")
+ begin
+ // Verify the account
+ Assert.AreEqual(FileAccountMock.Name(), Account.NameField.Value(), 'A different name was expected.');
+ Assert.AreEqual(FileAccountMock.StorageAccountName(), Account.StorageAccountNameField.Value(), 'A different storage account name was expected.');
+ Assert.AreEqual(FileAccountMock.ContainerName(), Account.ContainerNameField.Value(), 'A different container name was expected.');
+ Assert.AreEqual(FileAccountMock.AuthorizationType(), Account."Authorization Type".AsInteger(), 'A different authorization type was expected.');
+ end;
+
+ var
+ Any: Codeunit Any;
+ Assert: Codeunit "Library Assert";
+ FileAccountMock: Codeunit "Ext. Blob Account Mock";
+}
\ No newline at end of file
diff --git a/Apps/W1/External File Storage - Azure Blob Service Connector/test/src/mocks/ExtBlobAccountMock.Codeunit.al b/Apps/W1/External File Storage - Azure Blob Service Connector/test/src/mocks/ExtBlobAccountMock.Codeunit.al
new file mode 100644
index 0000000000..b58825e030
--- /dev/null
+++ b/Apps/W1/External File Storage - Azure Blob Service Connector/test/src/mocks/ExtBlobAccountMock.Codeunit.al
@@ -0,0 +1,67 @@
+// ------------------------------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+// ------------------------------------------------------------------------------------------------
+
+codeunit 144565 "Ext. Blob Account Mock"
+{
+ Access = Internal;
+ SingleInstance = true;
+
+ procedure Name(): Text[250]
+ begin
+ exit(AccName);
+ end;
+
+ procedure Name(Value: Text[250])
+ begin
+ AccName := Value;
+ end;
+
+ procedure StorageAccountName(): Text[250]
+ begin
+ exit(AccStorageAccountName);
+ end;
+
+ procedure StorageAccountName(Value: Text[250])
+ begin
+ AccStorageAccountName := Value;
+ end;
+
+ procedure ContainerName(): Text[250]
+ begin
+ exit(AccContainerName);
+ end;
+
+ procedure ContainerName(Value: Text[250])
+ begin
+ AccContainerName := Value;
+ end;
+
+ procedure Password(): Text
+ begin
+ exit(AccPassword);
+ end;
+
+ procedure Password(Value: Text)
+ begin
+ AccPassword := Value;
+ end;
+
+ procedure AuthorizationType(Value: Enum "Ext. Blob Storage Auth. Type")
+ begin
+ AccAuthorizationType := Value;
+ end;
+
+ procedure AuthorizationType(): Enum "Ext. Blob Storage Auth. Type"
+ begin
+ exit(AccAuthorizationType);
+ end;
+
+ var
+ AccName: Text[250];
+ AccStorageAccountName: Text[250];
+ AccContainerName: Text[250];
+ AccPassword: Text;
+ AccAuthorizationType: Enum "Ext. Blob Storage Auth. Type";
+}
\ No newline at end of file
diff --git a/Apps/W1/External File Storage - Azure File Service Connector/app/Entitlements/ExtFileShareConnector.Entitlement.al b/Apps/W1/External File Storage - Azure File Service Connector/app/Entitlements/ExtFileShareConnector.Entitlement.al
new file mode 100644
index 0000000000..d9d40915a7
--- /dev/null
+++ b/Apps/W1/External File Storage - Azure File Service Connector/app/Entitlements/ExtFileShareConnector.Entitlement.al
@@ -0,0 +1,13 @@
+// ------------------------------------------------------------------------------------------------
+// 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;
+
+entitlement "Ext. File Share Connector"
+{
+
+ ObjectEntitlements = "Ext. File Share - Edit";
+ Type = Implicit;
+}
\ No newline at end of file
diff --git a/Apps/W1/External File Storage - Azure File Service Connector/app/ExtensionLogo.png b/Apps/W1/External File Storage - Azure File Service Connector/app/ExtensionLogo.png
new file mode 100644
index 0000000000000000000000000000000000000000..30941b354fa335cad3ea5426ac24cadb2ee328e5
GIT binary patch
literal 4681
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
literal 0
HcmV?d00001
diff --git a/Apps/W1/External File Storage - Azure File Service Connector/app/README.md b/Apps/W1/External File Storage - Azure File Service Connector/app/README.md
new file mode 100644
index 0000000000..1ec1f8f8ff
--- /dev/null
+++ b/Apps/W1/External File Storage - Azure File Service Connector/app/README.md
@@ -0,0 +1,2 @@
+# External File Storage - Azure File Share Connector
+This connector allows access to Azure File Shares.
diff --git a/Apps/W1/External File Storage - Azure File Service Connector/app/app.json b/Apps/W1/External File Storage - Azure File Service Connector/app/app.json
new file mode 100644
index 0000000000..0817284637
--- /dev/null
+++ b/Apps/W1/External File Storage - Azure File Service Connector/app/app.json
@@ -0,0 +1,37 @@
+{
+ "id": "79447b11-8301-4d02-a546-2261eb811296",
+ "name": "External File Storage - Azure File Service Connector",
+ "publisher": "Microsoft",
+ "brief": "Enables file and folder operations for Azure File Services via the External File Storage Module with Business Central.",
+ "description": "This app enables file and folder operations for Azure File Services via the External File Storage Module with Business Central.",
+ "version": "26.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=2134520",
+ "url": "https://go.microsoft.com/fwlink/?linkid=724011",
+ "logo": "ExtensionLogo.png",
+ "application": "26.0.0.0",
+ "platform": "26.0.0.0",
+ "internalsVisibleTo": [
+ {
+ "id": "80ef626f-e8de-4050-b144-0e3d4993a718",
+ "name": "External File Storage - Azure File Service Connector Tests",
+ "publisher": "Microsoft"
+ }
+ ],
+ "dependencies": [],
+ "screenshots": [],
+ "idRanges": [
+ {
+ "from": 4570,
+ "to": 4579
+ }
+ ],
+ "resourceExposurePolicy": {
+ "allowDebugging": true,
+ "allowDownloadingSource": true,
+ "includeSourceInSymbolFile": true
+ },
+ "contextSensitiveHelpUrl": "https://go.microsoft.com/fwlink/?linkid=2134520",
+ "resourceFolders": ["data"]
+}
\ No newline at end of file
diff --git a/Apps/W1/External File Storage - Azure File Service Connector/app/data/connector-logo.png b/Apps/W1/External File Storage - Azure File Service Connector/app/data/connector-logo.png
new file mode 100644
index 0000000000000000000000000000000000000000..98f852e166fe7ca4d8c8c188feeee78c2c3dd210
GIT binary patch
literal 4430
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
TiwojUAwW;d=w6kEW90t;R3DOa
literal 0
HcmV?d00001
diff --git a/Apps/W1/External File Storage - Azure File Service Connector/app/permissions/ExtFileShareEdit.PermissionSet.al b/Apps/W1/External File Storage - Azure File Service Connector/app/permissions/ExtFileShareEdit.PermissionSet.al
new file mode 100644
index 0000000000..85f14aed8f
--- /dev/null
+++ b/Apps/W1/External File Storage - Azure File Service Connector/app/permissions/ExtFileShareEdit.PermissionSet.al
@@ -0,0 +1,18 @@
+// ------------------------------------------------------------------------------------------------
+// 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;
+
+permissionset 4572 "Ext. File Share - Edit"
+{
+ Access = Public;
+ Assignable = false;
+ Caption = 'File Share - Edit';
+
+ IncludedPermissionSets = "Ext. File Share - Read";
+
+ Permissions =
+ tabledata "Ext. File Share Account" = imd;
+}
diff --git a/Apps/W1/External File Storage - Azure File Service Connector/app/permissions/ExtFileShareObjects.PermissionSet.al b/Apps/W1/External File Storage - Azure File Service Connector/app/permissions/ExtFileShareObjects.PermissionSet.al
new file mode 100644
index 0000000000..405e2c3d4e
--- /dev/null
+++ b/Apps/W1/External File Storage - Azure File Service Connector/app/permissions/ExtFileShareObjects.PermissionSet.al
@@ -0,0 +1,18 @@
+// ------------------------------------------------------------------------------------------------
+// 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;
+
+permissionset 4570 "Ext. File Share - Objects"
+{
+ Access = Public;
+ Assignable = false;
+ Caption = 'File Share - Objects';
+
+ Permissions =
+ table "Ext. File Share Account" = X,
+ page "Ext. File Share Account Wizard" = X,
+ page "Ext. File Share Account" = X;
+}
diff --git a/Apps/W1/External File Storage - Azure File Service Connector/app/permissions/ExtFileShareRead.PermissionSet.al b/Apps/W1/External File Storage - Azure File Service Connector/app/permissions/ExtFileShareRead.PermissionSet.al
new file mode 100644
index 0000000000..58377dff9f
--- /dev/null
+++ b/Apps/W1/External File Storage - Azure File Service Connector/app/permissions/ExtFileShareRead.PermissionSet.al
@@ -0,0 +1,18 @@
+// ------------------------------------------------------------------------------------------------
+// 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;
+
+permissionset 4571 "Ext. File Share - Read"
+{
+ Access = Public;
+ Assignable = false;
+ Caption = 'File Share - Read';
+
+ IncludedPermissionSets = "Ext. File Share - Objects";
+
+ Permissions =
+ tabledata "Ext. File Share Account" = r;
+}
diff --git a/Apps/W1/External File Storage - Azure File Service Connector/app/permissions/FileStorageAdminExtFileShare.PermissionSetExt.al b/Apps/W1/External File Storage - Azure File Service Connector/app/permissions/FileStorageAdminExtFileShare.PermissionSetExt.al
new file mode 100644
index 0000000000..ccefc7bcda
--- /dev/null
+++ b/Apps/W1/External File Storage - Azure File Service Connector/app/permissions/FileStorageAdminExtFileShare.PermissionSetExt.al
@@ -0,0 +1,11 @@
+// ------------------------------------------------------------------------------------------------
+// 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;
+
+permissionsetextension 4570 "File Storage - Admin - Ext. File Share" extends "File Storage - Admin"
+{
+ IncludedPermissionSets = "Ext. File Share - Edit";
+}
diff --git a/Apps/W1/External File Storage - Azure File Service Connector/app/permissions/FileStorageEditExtFileShare.PermissionSetExt.al b/Apps/W1/External File Storage - Azure File Service Connector/app/permissions/FileStorageEditExtFileShare.PermissionSetExt.al
new file mode 100644
index 0000000000..ef4938c666
--- /dev/null
+++ b/Apps/W1/External File Storage - Azure File Service Connector/app/permissions/FileStorageEditExtFileShare.PermissionSetExt.al
@@ -0,0 +1,11 @@
+// ------------------------------------------------------------------------------------------------
+// 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;
+
+permissionsetextension 4571 "File Storage - Edit - Ext. File Share" extends "File Storage - Edit"
+{
+ IncludedPermissionSets = "Ext. File Share - Read";
+}
diff --git a/Apps/W1/External File Storage - Azure File Service Connector/app/src/ExtFileShareAccount.Page.al b/Apps/W1/External File Storage - Azure File Service Connector/app/src/ExtFileShareAccount.Page.al
new file mode 100644
index 0000000000..d01de2a1ae
--- /dev/null
+++ b/Apps/W1/External File Storage - Azure File Service Connector/app/src/ExtFileShareAccount.Page.al
@@ -0,0 +1,67 @@
+// ------------------------------------------------------------------------------------------------
+// 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;
+
+///
+/// Displays an account that was registered via the File Share connector.
+///
+page 4570 "Ext. File Share Account"
+{
+ ApplicationArea = All;
+ Caption = 'Azure File Share Account';
+ DataCaptionExpression = Rec.Name;
+ Extensible = false;
+ InsertAllowed = false;
+ PageType = Card;
+ Permissions = tabledata "Ext. File Share Account" = rimd;
+ SourceTable = "Ext. File Share Account";
+ UsageCategory = None;
+
+ layout
+ {
+ area(Content)
+ {
+ field(NameField; Rec.Name)
+ {
+ NotBlank = true;
+ ShowMandatory = true;
+ }
+ field(StorageAccountNameField; Rec."Storage Account Name") { }
+ field("Authorization Type"; Rec."Authorization Type") { }
+ field(SecretField; Secret)
+ {
+ Caption = 'Password';
+ Editable = SecretEditable;
+ ExtendedDatatype = Masked;
+ ToolTip = 'Specifies the Shared access signature Token or SharedKey.';
+
+ trigger OnValidate()
+ begin
+ Rec.SetSecret(Secret);
+ end;
+ }
+ field(FileShareNameField; Rec."File Share Name") { }
+ field(DisabledField; Rec.Disabled) { }
+ }
+ }
+
+ var
+ SecretEditable: Boolean;
+ [NonDebuggable]
+ Secret: Text;
+
+ trigger OnOpenPage()
+ begin
+ Rec.SetCurrentKey(Name);
+ end;
+
+ trigger OnAfterGetCurrRecord()
+ begin
+ SecretEditable := CurrPage.Editable();
+ if not IsNullGuid(Rec."Secret Key") then
+ Secret := '***';
+ end;
+}
\ No newline at end of file
diff --git a/Apps/W1/External File Storage - Azure File Service Connector/app/src/ExtFileShareAccount.Table.al b/Apps/W1/External File Storage - Azure File Service Connector/app/src/ExtFileShareAccount.Table.al
new file mode 100644
index 0000000000..8e69d42133
--- /dev/null
+++ b/Apps/W1/External File Storage - Azure File Service Connector/app/src/ExtFileShareAccount.Table.al
@@ -0,0 +1,89 @@
+// ------------------------------------------------------------------------------------------------
+// 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;
+
+///
+/// Holds the information for all file accounts that are registered via the File Share connector
+///
+table 4570 "Ext. File Share Account"
+{
+ Caption = 'Azure File Share Account';
+ DataClassification = CustomerContent;
+
+ fields
+ {
+ field(1; "Id"; Guid)
+ {
+ AllowInCustomizations = Never;
+ Caption = 'Primary Key';
+ DataClassification = SystemMetadata;
+ }
+ field(2; Name; Text[250])
+ {
+ Caption = 'Account Name';
+ ToolTip = 'Specifies the name of the storage account connection.';
+ }
+ field(3; "Storage Account Name"; Text[2048])
+ {
+ Caption = 'Storage Account Name';
+ ToolTip = 'Specifies the Azure Storage name.';
+ }
+ field(4; "File Share Name"; Text[2048])
+ {
+ Caption = 'File Share Name';
+ ToolTip = 'Specifies the Azure File Share name.';
+ }
+ field(7; "Authorization Type"; Enum "Ext. File Share Auth. Type")
+ {
+ Access = Internal;
+ Caption = 'Authorization Type';
+ ToolTip = 'The way of authorizing used to access the Blob Storage.';
+ }
+ field(8; "Secret Key"; Guid)
+ {
+ Access = Internal;
+ DataClassification = SystemMetadata;
+ }
+ field(9; Disabled; Boolean)
+ {
+ Caption = 'Disabled';
+ ToolTip = 'Specifies if the account is disabled. This happens automatically when a sandbox is created.';
+ }
+ }
+
+ keys
+ {
+ key(PK; Id)
+ {
+ Clustered = true;
+ }
+ }
+
+ var
+ UnableToGetSecretMsg: Label 'Unable to get File Share Account secret.';
+ UnableToSetSecretMsg: Label 'Unable to set File Share Account secret.';
+
+ trigger OnDelete()
+ begin
+ if not IsNullGuid(Rec."Secret Key") then
+ if IsolatedStorage.Delete(Rec."Secret Key") then;
+ end;
+
+ procedure SetSecret(Secret: SecretText)
+ begin
+ if IsNullGuid(Rec."Secret Key") then
+ Rec."Secret Key" := CreateGuid();
+
+ if not IsolatedStorage.Set(Format(Rec."Secret Key"), Secret, DataScope::Company) then
+ Error(UnableToSetSecretMsg);
+ end;
+
+ procedure GetSecret(SecretKey: Guid) Secret: SecretText
+ begin
+ if not IsolatedStorage.Get(Format(SecretKey), DataScope::Company, Secret) then
+ Error(UnableToGetSecretMsg);
+ end;
+}
\ No newline at end of file
diff --git a/Apps/W1/External File Storage - Azure File Service Connector/app/src/ExtFileShareAccountWizard.Page.al b/Apps/W1/External File Storage - Azure File Service Connector/app/src/ExtFileShareAccountWizard.Page.al
new file mode 100644
index 0000000000..e3fbc70f18
--- /dev/null
+++ b/Apps/W1/External File Storage - Azure File Service Connector/app/src/ExtFileShareAccountWizard.Page.al
@@ -0,0 +1,154 @@
+// ------------------------------------------------------------------------------------------------
+// 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;
+
+Using System.Environment;
+
+///
+/// Displays an account that is being registered via the File Share connector.
+///
+page 4571 "Ext. File Share Account Wizard"
+{
+ ApplicationArea = All;
+ Caption = 'Setup Azure File Share Account';
+ Editable = true;
+ Extensible = false;
+ PageType = NavigatePage;
+ Permissions = tabledata "Ext. File Share Account" = rimd;
+ SourceTable = "Ext. File Share Account";
+ SourceTableTemporary = true;
+
+ layout
+ {
+ area(Content)
+ {
+ group(TopBanner)
+ {
+ Editable = false;
+ ShowCaption = false;
+ Visible = TopBannerVisible;
+ field(NotDoneIcon; MediaResources."Media Reference")
+ {
+ Editable = false;
+ ShowCaption = false;
+ ToolTip = ' ', Locked = true;
+ }
+ }
+
+ field(NameField; Rec.Name)
+ {
+ Caption = 'Account Name';
+ NotBlank = true;
+ ShowMandatory = true;
+ ToolTip = 'Specifies the name of the Azure File Share account.';
+
+ trigger OnValidate()
+ begin
+ IsNextEnabled := FileShareConnectorImpl.IsAccountValid(Rec);
+ end;
+ }
+
+ field(StorageAccountNameField; Rec."Storage Account Name")
+ {
+ Caption = 'Storage Account Name';
+ ShowMandatory = true;
+
+ trigger OnValidate()
+ begin
+ IsNextEnabled := FileShareConnectorImpl.IsAccountValid(Rec);
+ end;
+ }
+
+ field("Authorization Type"; Rec."Authorization Type")
+ {
+ }
+
+ field(SecretField; Secret)
+ {
+ Caption = 'Secret';
+ ExtendedDatatype = Masked;
+ ShowMandatory = true;
+ ToolTip = 'Specifies the Shared access signature Token or SharedKey.';
+ }
+
+ field(FileShareNameField; Rec."File Share Name")
+ {
+ Caption = 'File Share Name';
+ ShowMandatory = true;
+ ToolTip = 'Specifies the file share to use of the storage account.';
+
+ trigger OnValidate()
+ begin
+ IsNextEnabled := FileShareConnectorImpl.IsAccountValid(Rec);
+ end;
+ }
+ }
+ }
+
+ actions
+ {
+ area(processing)
+ {
+ action(Back)
+ {
+ Caption = 'Back';
+ Image = Cancel;
+ InFooterBar = true;
+ ToolTip = 'Move to the previous step.';
+
+ trigger OnAction()
+ begin
+ CurrPage.Close();
+ end;
+ }
+
+ action(Next)
+ {
+ Caption = 'Next';
+ Enabled = IsNextEnabled;
+ Image = NextRecord;
+ InFooterBar = true;
+ ToolTip = 'Move to the next step.';
+
+ trigger OnAction()
+ begin
+ FileShareConnectorImpl.CreateAccount(Rec, Secret, FileShareAccount);
+ CurrPage.Close();
+ end;
+ }
+ }
+ }
+
+ var
+ FileShareAccount: Record "File Account";
+ MediaResources: Record "Media Resources";
+ FileShareConnectorImpl: Codeunit "Ext. File Share Connector Impl";
+ [NonDebuggable]
+ Secret: Text;
+ IsNextEnabled: Boolean;
+ TopBannerVisible: Boolean;
+
+ trigger OnOpenPage()
+ var
+ AssistedSetupLogoTok: Label 'ASSISTEDSETUP-NOTEXT-400PX.PNG', Locked = true;
+ begin
+ Rec.Init();
+ Rec.Insert();
+
+ if MediaResources.Get(AssistedSetupLogoTok) and (CurrentClientType() = ClientType::Web) then
+ TopBannerVisible := MediaResources."Media Reference".HasValue();
+ end;
+
+ internal procedure GetAccount(var FileAccount: Record "File Account"): Boolean
+ begin
+ if IsNullGuid(FileShareAccount."Account Id") then
+ exit(false);
+
+ FileAccount := FileShareAccount;
+
+ exit(true);
+ end;
+}
\ No newline at end of file
diff --git a/Apps/W1/External File Storage - Azure File Service Connector/app/src/ExtFileShareAuthType.Enum.al b/Apps/W1/External File Storage - Azure File Service Connector/app/src/ExtFileShareAuthType.Enum.al
new file mode 100644
index 0000000000..88b9376c58
--- /dev/null
+++ b/Apps/W1/External File Storage - Azure File Service Connector/app/src/ExtFileShareAuthType.Enum.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.
+// ------------------------------------------------------------------------------------------------
+
+namespace System.ExternalFileStorage;
+
+enum 4570 "Ext. File Share Auth. Type"
+{
+ Access = Internal;
+
+ value(0; SasToken)
+ {
+ Caption = 'Shared Access Signature';
+ }
+ value(1; SharedKey)
+ {
+ Caption = 'Shared Key';
+ }
+}
diff --git a/Apps/W1/External File Storage - Azure File Service Connector/app/src/ExtFileShareConnector.EnumExt.al b/Apps/W1/External File Storage - Azure File Service Connector/app/src/ExtFileShareConnector.EnumExt.al
new file mode 100644
index 0000000000..7930b74146
--- /dev/null
+++ b/Apps/W1/External File Storage - Azure File Service Connector/app/src/ExtFileShareConnector.EnumExt.al
@@ -0,0 +1,21 @@
+// ------------------------------------------------------------------------------------------------
+// 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;
+
+///
+/// Enum extension to register the File Share connector.
+///
+enumextension 4570 "Ext. File Share Connector" extends "Ext. File Storage Connector"
+{
+ ///
+ /// The File Share connector.
+ ///
+ value(4570; "File Share")
+ {
+ Caption = 'File Share';
+ Implementation = "External File Storage Connector" = "Ext. File Share Connector Impl";
+ }
+}
\ No newline at end of file
diff --git a/Apps/W1/External File Storage - Azure File Service Connector/app/src/ExtFileShareConnectorImpl.Codeunit.al b/Apps/W1/External File Storage - Azure File Service Connector/app/src/ExtFileShareConnectorImpl.Codeunit.al
new file mode 100644
index 0000000000..3038fa649c
--- /dev/null
+++ b/Apps/W1/External File Storage - Azure File Service Connector/app/src/ExtFileShareConnectorImpl.Codeunit.al
@@ -0,0 +1,472 @@
+// ------------------------------------------------------------------------------------------------
+// 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;
+
+using System.Text;
+using System.Azure.Storage;
+using System.Azure.Storage.Files;
+using System.DataAdministration;
+
+codeunit 4570 "Ext. File Share Connector Impl" implements "External File Storage Connector"
+{
+ Access = Internal;
+ InherentEntitlements = X;
+ InherentPermissions = X;
+ Permissions = tabledata "Ext. File Share Account" = rimd;
+
+ var
+ ConnectorDescriptionTxt: Label 'Use Azure File Share to store and retrieve files.';
+ NotRegisteredAccountErr: Label 'We could not find the account. Typically, this is because the account has been deleted.';
+ NotFoundTok: Label '404', Locked = true;
+
+ ///
+ /// Gets a List of Files stored on the provided account.
+ ///
+ /// The file account ID which is used to get the file.
+ /// The file path to list.
+ /// Defines the pagination data.
+ /// A list with all files stored in the path.
+ procedure ListFiles(AccountId: Guid; Path: Text; FilePaginationData: Codeunit "File Pagination Data"; var TempFileAccountContent: Record "File Account Content" temporary)
+ var
+ AFSDirectoryContent: Record "AFS Directory Content";
+ begin
+ GetDirectoryContent(AccountId, Path, FilePaginationData, AFSDirectoryContent);
+
+ AFSDirectoryContent.SetRange("Parent Directory", Path);
+ AFSDirectoryContent.SetRange("Resource Type", AFSDirectoryContent."Resource Type"::File);
+ if not AFSDirectoryContent.FindSet() then
+ exit;
+
+ repeat
+ TempFileAccountContent.Init();
+ TempFileAccountContent.Name := AFSDirectoryContent.Name;
+ TempFileAccountContent.Type := TempFileAccountContent.Type::"File";
+ TempFileAccountContent."Parent Directory" := AFSDirectoryContent."Parent Directory";
+ TempFileAccountContent.Insert();
+ until AFSDirectoryContent.Next() = 0;
+ end;
+
+ ///
+ /// Gets a file from the provided account.
+ ///
+ /// The file account ID which is used to get the file.
+ /// The file path inside the file account.
+ /// The Stream were the file is read to.
+ procedure GetFile(AccountId: Guid; Path: Text; Stream: InStream)
+ var
+ AFSFileClient: Codeunit "AFS File Client";
+ AFSOperationResponse: Codeunit "AFS Operation Response";
+ begin
+ InitFileClient(AccountId, AFSFileClient);
+ AFSOperationResponse := AFSFileClient.GetFileAsStream(Path, Stream);
+
+ if AFSOperationResponse.IsSuccessful() then
+ exit;
+
+ Error(AFSOperationResponse.GetError());
+ end;
+
+ ///
+ /// Create a file in the provided account.
+ ///
+ /// The file account ID which is used to send out the file.
+ /// The file path inside the file account.
+ /// The Stream were the file is read from.
+ procedure CreateFile(AccountId: Guid; Path: Text; Stream: InStream)
+ var
+ AFSFileClient: Codeunit "AFS File Client";
+ AFSOperationResponse: Codeunit "AFS Operation Response";
+ begin
+ InitFileClient(AccountId, AFSFileClient);
+
+ AFSOperationResponse := AFSFileClient.CreateFile(Path, Stream);
+ if not AFSOperationResponse.IsSuccessful() then
+ Error(AFSOperationResponse.GetError());
+
+ AFSOperationResponse := AFSFileClient.PutFileStream(Path, Stream);
+ if not AFSOperationResponse.IsSuccessful() then
+ Error(AFSOperationResponse.GetError());
+ end;
+
+ ///
+ /// Copies as file inside the provided account.
+ ///
+ /// The file account ID which is used to send out the file.
+ /// The source file path.
+ /// The target file path.
+ procedure CopyFile(AccountId: Guid; SourcePath: Text; TargetPath: Text)
+ var
+ AFSFileClient: Codeunit "AFS File Client";
+ AFSOperationResponse: Codeunit "AFS Operation Response";
+ begin
+ InitFileClient(AccountId, AFSFileClient);
+ AFSOperationResponse := AFSFileClient.CopyFile(TargetPath, SourcePath);
+
+ if AFSOperationResponse.IsSuccessful() then
+ exit;
+
+ Error(AFSOperationResponse.GetError());
+ end;
+
+ ///
+ /// Move as file inside the provided account.
+ ///
+ /// The file account ID which is used to send out the file.
+ /// The source file path.
+ /// The target file path.
+ procedure MoveFile(AccountId: Guid; SourcePath: Text; TargetPath: Text)
+ var
+ AFSFileClient: Codeunit "AFS File Client";
+ AFSOperationResponse: Codeunit "AFS Operation Response";
+ begin
+ InitFileClient(AccountId, AFSFileClient);
+ AFSOperationResponse := AFSFileClient.RenameFile(TargetPath, SourcePath);
+ if not AFSOperationResponse.IsSuccessful() then
+ Error(AFSOperationResponse.GetError());
+ end;
+
+ ///
+ /// Checks if a file exists on the provided account.
+ ///
+ /// The file account ID which is used to send out the file.
+ /// The file path inside the file account.
+ /// Returns true if the file exists
+ procedure FileExists(AccountId: Guid; Path: Text): Boolean
+ var
+ AFSFileClient: Codeunit "AFS File Client";
+ AFSOperationResponse: Codeunit "AFS Operation Response";
+ AFSOptionalParameters: Codeunit "AFS Optional Parameters";
+ TargetText: Text;
+ begin
+ if Path = '' then
+ exit(false);
+
+ InitFileClient(AccountId, AFSFileClient);
+ AFSOptionalParameters.Range(0, 1);
+
+ AFSOperationResponse := AFSFileClient.GetFileAsText(Path, TargetText, AFSOptionalParameters);
+ if AFSOperationResponse.IsSuccessful() then
+ exit(true);
+
+ if AFSOperationResponse.GetError().Contains(NotFoundTok) then
+ exit(false);
+
+ Error(AFSOperationResponse.GetError());
+ end;
+
+ ///
+ /// Deletes a file exists on the provided account.
+ ///
+ /// The file account ID which is used to send out the file.
+ /// The file path inside the file account.
+ procedure DeleteFile(AccountId: Guid; Path: Text)
+ var
+ AFSFileClient: Codeunit "AFS File Client";
+ AFSOperationResponse: Codeunit "AFS Operation Response";
+ begin
+ InitFileClient(AccountId, AFSFileClient);
+ AFSOperationResponse := AFSFileClient.DeleteFile(Path);
+
+ if AFSOperationResponse.IsSuccessful() then
+ exit;
+
+ Error(AFSOperationResponse.GetError());
+ end;
+
+ ///
+ /// Gets a List of Directories stored on the provided account.
+ ///
+ /// The file account ID which is used to get the file.
+ /// The file path to list.
+ /// Defines the pagination data.
+ /// A list with all directories stored in the path.
+ procedure ListDirectories(AccountId: Guid; Path: Text; FilePaginationData: Codeunit "File Pagination Data"; var TempFileAccountContent: Record "File Account Content" temporary)
+ var
+ AFSDirectoryContent: Record "AFS Directory Content";
+ begin
+ GetDirectoryContent(AccountId, Path, FilePaginationData, AFSDirectoryContent);
+
+ AFSDirectoryContent.SetRange("Parent Directory", Path);
+ AFSDirectoryContent.SetRange("Resource Type", AFSDirectoryContent."Resource Type"::Directory);
+ if not AFSDirectoryContent.FindSet() then
+ exit;
+
+ repeat
+ TempFileAccountContent.Init();
+ TempFileAccountContent.Name := AFSDirectoryContent.Name;
+ TempFileAccountContent.Type := TempFileAccountContent.Type::Directory;
+ TempFileAccountContent."Parent Directory" := AFSDirectoryContent."Parent Directory";
+ TempFileAccountContent.Insert();
+ until AFSDirectoryContent.Next() = 0;
+ end;
+
+ ///
+ /// Creates a directory on the provided account.
+ ///
+ /// The file account ID which is used to send out the file.
+ /// The directory path inside the file account.
+ procedure CreateDirectory(AccountId: Guid; Path: Text)
+ var
+ AFSFileClient: Codeunit "AFS File Client";
+ AFSOperationResponse: Codeunit "AFS Operation Response";
+ DirectoryAlreadyExistsErr: Label 'Directory already exists.';
+ begin
+ if DirectoryExists(AccountId, Path) then
+ Error(DirectoryAlreadyExistsErr);
+
+ InitFileClient(AccountId, AFSFileClient);
+ AFSOperationResponse := AFSFileClient.CreateDirectory(Path);
+ if not AFSOperationResponse.IsSuccessful() then
+ Error(AFSOperationResponse.GetError());
+ end;
+
+ ///
+ /// Checks if a directory exists on the provided account.
+ ///
+ /// The file account ID which is used to send out the file.
+ /// The directory path inside the file account.
+ /// Returns true if the directory exists
+ procedure DirectoryExists(AccountId: Guid; Path: Text): Boolean
+ var
+ AFSDirectoryContent: Record "AFS Directory Content";
+ AFSFileClient: Codeunit "AFS File Client";
+ AFSOperationResponse: Codeunit "AFS Operation Response";
+ AFSOptionalParameters: Codeunit "AFS Optional Parameters";
+ begin
+ if Path = '' then
+ exit(true);
+
+ InitFileClient(AccountId, AFSFileClient);
+ AFSOptionalParameters.MaxResults(1);
+ AFSOperationResponse := AFSFileClient.ListDirectory(CopyStr(Path, 1, 2048), AFSDirectoryContent, AFSOptionalParameters);
+ if AFSOperationResponse.IsSuccessful() then
+ exit(not AFSDirectoryContent.IsEmpty());
+
+ if AFSOperationResponse.GetError().Contains(NotFoundTok) then
+ exit(false)
+ else
+ Error(AFSOperationResponse.GetError());
+
+ end;
+
+ ///
+ /// Deletes a directory exists on the provided account.
+ ///
+ /// The file account ID which is used to send out the file.
+ /// The directory path inside the file account.
+ procedure DeleteDirectory(AccountId: Guid; Path: Text)
+ var
+ AFSFileClient: Codeunit "AFS File Client";
+ AFSOperationResponse: Codeunit "AFS Operation Response";
+ begin
+ InitFileClient(AccountId, AFSFileClient);
+ AFSOperationResponse := AFSFileClient.DeleteDirectory(Path);
+
+ if AFSOperationResponse.IsSuccessful() then
+ exit;
+
+ Error(AFSOperationResponse.GetError());
+ end;
+
+ ///
+ /// Gets the registered accounts for the File Share connector.
+ ///
+ /// Out parameter holding all the registered accounts for the File Share connector.
+ procedure GetAccounts(var TempAccounts: Record "File Account" temporary)
+ var
+ Account: Record "Ext. File Share Account";
+ begin
+ if not Account.FindSet() then
+ exit;
+
+ repeat
+ TempAccounts."Account Id" := Account.Id;
+ TempAccounts.Name := Account.Name;
+ TempAccounts.Connector := Enum::"Ext. File Storage Connector"::"File Share";
+ TempAccounts.Insert();
+ until Account.Next() = 0;
+ end;
+
+ ///
+ /// Shows accounts information.
+ ///
+ /// The ID of the account to show.
+ procedure ShowAccountInformation(AccountId: Guid)
+ var
+ FileShareAccountLocal: Record "Ext. File Share Account";
+ begin
+ if not FileShareAccountLocal.Get(AccountId) then
+ Error(NotRegisteredAccountErr);
+
+ FileShareAccountLocal.SetRecFilter();
+ Page.Run(Page::"Ext. File Share Account", FileShareAccountLocal);
+ end;
+
+ ///
+ /// Register an file account for the File Share connector.
+ ///
+ /// Out parameter holding details of the registered account.
+ /// True if the registration was successful; false - otherwise.
+ procedure RegisterAccount(var TempAccount: Record "File Account" temporary): Boolean
+ var
+ FileShareAccountWizard: Page "Ext. File Share Account Wizard";
+ begin
+ FileShareAccountWizard.RunModal();
+
+ exit(FileShareAccountWizard.GetAccount(TempAccount));
+ end;
+
+ ///
+ /// Deletes an file account for the File Share connector.
+ ///
+ /// The ID of the File Share account
+ /// True if an account was deleted.
+ procedure DeleteAccount(AccountId: Guid): Boolean
+ var
+ FileShareAccountLocal: Record "Ext. File Share Account";
+ begin
+ if FileShareAccountLocal.Get(AccountId) then
+ exit(FileShareAccountLocal.Delete());
+
+ exit(false);
+ end;
+
+ ///
+ /// Gets a description of the File Share connector.
+ ///
+ /// A short description of the File Share connector.
+ procedure GetDescription(): Text[250]
+ begin
+ exit(ConnectorDescriptionTxt);
+ end;
+
+ ///
+ /// Gets the File Share connector logo.
+ ///
+ /// A base64-formatted image to be used as logo.
+ procedure GetLogoAsBase64(): Text
+ var
+ Base64Convert: Codeunit "Base64 Convert";
+ Stream: InStream;
+ begin
+ NavApp.GetResource('connector-logo.png', Stream);
+ exit(Base64Convert.ToBase64(Stream));
+ end;
+
+ internal procedure IsAccountValid(var Account: Record "Ext. File Share Account" temporary): Boolean
+ begin
+ if Account.Name = '' then
+ exit(false);
+
+ if Account."Storage Account Name" = '' then
+ exit(false);
+
+ if Account."File Share Name" = '' then
+ exit(false);
+
+ exit(true);
+ end;
+
+ internal procedure CreateAccount(var AccountToCopy: Record "Ext. File Share Account"; Password: SecretText; var TempFileAccount: Record "File Account" temporary)
+ var
+ NewFileShareAccount: Record "Ext. File Share Account";
+ begin
+ NewFileShareAccount.TransferFields(AccountToCopy);
+
+ NewFileShareAccount.Id := CreateGuid();
+ NewFileShareAccount.SetSecret(Password);
+
+ NewFileShareAccount.Insert();
+
+ TempFileAccount."Account Id" := NewFileShareAccount.Id;
+ TempFileAccount.Name := NewFileShareAccount.Name;
+ TempFileAccount.Connector := Enum::"Ext. File Storage Connector"::"File Share";
+ end;
+
+ local procedure InitFileClient(var AccountId: Guid; var AFSFileClient: Codeunit "AFS File Client")
+ var
+ FileShareAccount: Record "Ext. File Share Account";
+ StorageServiceAuthorization: Codeunit "Storage Service Authorization";
+ Authorization: Interface "Storage Service Authorization";
+ AccountDisabledErr: Label 'The account "%1" is disabled.', Comment = '%1 - Account Name';
+ begin
+ FileShareAccount.Get(AccountId);
+ if FileShareAccount.Disabled then
+ Error(AccountDisabledErr, FileShareAccount.Name);
+
+ case FileShareAccount."Authorization Type" of
+ FileShareAccount."Authorization Type"::SasToken:
+ Authorization := SetReadySAS(StorageServiceAuthorization, FileShareAccount.GetSecret(FileShareAccount."Secret Key"));
+ FileShareAccount."Authorization Type"::SharedKey:
+ Authorization := StorageServiceAuthorization.CreateSharedKey(FileShareAccount.GetSecret(FileShareAccount."Secret Key"));
+ end;
+
+ AFSFileClient.Initialize(FileShareAccount."Storage Account Name", FileShareAccount."File Share Name", Authorization);
+ end;
+
+ local procedure CheckPath(var Path: Text)
+ var
+ PathToLongErr: Label 'The path is too long. The maximum length is 2048 characters.';
+ begin
+ if (Path <> '') and not Path.EndsWith(PathSeparator()) then
+ Path += PathSeparator();
+
+ if StrLen(Path) > 2048 then
+ Error(PathToLongErr);
+ end;
+
+ local procedure InitOptionalParameters(var FilePaginationData: Codeunit "File Pagination Data"; var AFSOptionalParameters: Codeunit "AFS Optional Parameters")
+ begin
+ AFSOptionalParameters.MaxResults(500);
+ AFSOptionalParameters.Marker(FilePaginationData.GetMarker());
+ end;
+
+ local procedure ValidateListingResponse(var FilePaginationData: Codeunit "File Pagination Data"; var AFSOperationResponse: Codeunit "AFS Operation Response")
+ begin
+ if not AFSOperationResponse.IsSuccessful() then
+ Error(AFSOperationResponse.GetError());
+
+ FilePaginationData.SetEndOfListing(true);
+ end;
+
+ local procedure GetDirectoryContent(var AccountId: Guid; var PassedPath: Text; var FilePaginationData: Codeunit "File Pagination Data"; var AFSDirectoryContent: Record "AFS Directory Content")
+ var
+ AFSFileClient: Codeunit "AFS File Client";
+ AFSOperationResponse: Codeunit "AFS Operation Response";
+ AFSOptionalParameters: Codeunit "AFS Optional Parameters";
+ Path: Text[2048];
+ begin
+ InitFileClient(AccountId, AFSFileClient);
+ CheckPath(PassedPath);
+ InitOptionalParameters(FilePaginationData, AFSOptionalParameters);
+ Path := CopyStr(PassedPath, 1, MaxStrLen(Path));
+ AFSOperationResponse := AFSFileClient.ListDirectory(Path, AFSDirectoryContent, AFSOptionalParameters);
+ PassedPath := Path;
+ ValidateListingResponse(FilePaginationData, AFSOperationResponse);
+ end;
+
+ local procedure SetReadySAS(var StorageServiceAuthorization: Codeunit "Storage Service Authorization"; Secret: SecretText): Interface System.Azure.Storage."Storage Service Authorization"
+ begin
+ exit(StorageServiceAuthorization.UseReadySAS(Secret));
+ end;
+
+ local procedure PathSeparator(): Text
+ begin
+ exit('/');
+ end;
+
+ [EventSubscriber(ObjectType::Codeunit, Codeunit::"Environment Cleanup", OnClearCompanyConfig, '', false, false)]
+ local procedure EnvironmentCleanup_OnClearCompanyConfig(CompanyName: Text; SourceEnv: Enum "Environment Type"; DestinationEnv: Enum "Environment Type")
+ var
+ ExtFileShareAccount: Record "Ext. File Share Account";
+ begin
+ ExtFileShareAccount.SetRange(Disabled, false);
+ if ExtFileShareAccount.IsEmpty() then
+ exit;
+
+ ExtFileShareAccount.ModifyAll(Disabled, true);
+ end;
+}
\ No newline at end of file
diff --git a/Apps/W1/External File Storage - Azure File Service Connector/test/ExtensionLogo.png b/Apps/W1/External File Storage - Azure File Service Connector/test/ExtensionLogo.png
new file mode 100644
index 0000000000000000000000000000000000000000..30941b354fa335cad3ea5426ac24cadb2ee328e5
GIT binary patch
literal 4681
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
literal 0
HcmV?d00001
diff --git a/Apps/W1/External File Storage - Azure File Service Connector/test/README.md b/Apps/W1/External File Storage - Azure File Service Connector/test/README.md
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/Apps/W1/External File Storage - Azure File Service Connector/test/app.json b/Apps/W1/External File Storage - Azure File Service Connector/test/app.json
new file mode 100644
index 0000000000..a6eadeb709
--- /dev/null
+++ b/Apps/W1/External File Storage - Azure File Service Connector/test/app.json
@@ -0,0 +1,55 @@
+{
+ "id": "80ef626f-e8de-4050-b144-0e3d4993a718",
+ "name": "External File Storage - Azure File Service Connector Tests",
+ "publisher": "Microsoft",
+ "brief": "Tests for the External File Storage - Azure File Service Connector app",
+ "description": "Tests for the External File Storage - Azure File Service Connector app",
+ "version": "26.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=2134520",
+ "url": "https://go.microsoft.com/fwlink/?linkid=724011",
+ "logo": "ExtensionLogo.png",
+ "application": "26.0.0.0",
+ "dependencies": [
+ {
+ "id": "79447b11-8301-4d02-a546-2261eb811296",
+ "name": "External File Storage - Azure File Service Connector",
+ "publisher": "Microsoft",
+ "version": "26.0.0.0"
+ },
+ {
+ "id": "dd0be2ea-f733-4d65-bb34-a28f4624fb14",
+ "name": "Library Assert",
+ "publisher": "Microsoft",
+ "version": "26.0.0.0"
+ },
+ {
+ "id": "e7320ebb-08b3-4406-b1ec-b4927d3e280b",
+ "name": "Any",
+ "publisher": "Microsoft",
+ "version": "26.0.0.0"
+ },
+ {
+ "id": "9856ae4f-d1a7-46ef-89bb-6ef056398228",
+ "name": "System Application Test Library",
+ "publisher": "Microsoft",
+ "version": "26.0.0.0"
+ }
+ ],
+ "screenshots": [],
+ "platform": "26.0.0.0",
+ "idRanges": [
+ {
+ "from": 100000,
+ "to": 150000
+ }
+ ],
+ "target": "OnPrem",
+ "resourceExposurePolicy": {
+ "allowDebugging": true,
+ "allowDownloadingSource": true,
+ "includeSourceInSymbolFile": true
+ },
+ "contextSensitiveHelpUrl": "https://go.microsoft.com/fwlink/?linkid=2134520"
+}
\ No newline at end of file
diff --git a/Apps/W1/External File Storage - Azure File Service Connector/test/src/ExtAzureFileServiceTest.Codeunit.al b/Apps/W1/External File Storage - Azure File Service Connector/test/src/ExtAzureFileServiceTest.Codeunit.al
new file mode 100644
index 0000000000..33f42c9f47
--- /dev/null
+++ b/Apps/W1/External File Storage - Azure File Service Connector/test/src/ExtAzureFileServiceTest.Codeunit.al
@@ -0,0 +1,150 @@
+// ------------------------------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+// ------------------------------------------------------------------------------------------------
+
+codeunit 144571 "Ext. Azure File Service Test"
+{
+ Subtype = Test;
+ TestPermissions = Disabled;
+
+ [Test]
+ [Scope('OnPrem')]
+ [HandlerFunctions('AccountRegisterPageHandler')]
+ [TransactionModel(TransactionModel::AutoRollback)]
+ procedure TestMultipleAccountsCanBeRegistered()
+ var
+ FileAccount: Record "File Account";
+ ExtFileConnector: Codeunit "Ext. File Share Connector Impl";
+ FileAccounts: TestPage "File Accounts";
+ AccountIds: array[3] of Guid;
+ AccountName: array[3] of Text[250];
+ Index: Integer;
+ begin
+ // [Scenario] Create multiple accounts
+ Initialize();
+
+ // [When] Multiple accounts are registered
+ for Index := 1 to 3 do begin
+ SetBasicAccount();
+
+ Assert.IsTrue(ExtFileConnector.RegisterAccount(FileAccount), 'Failed to register account.');
+ AccountIds[Index] := FileAccount."Account Id";
+ AccountName[Index] := FileAccountMock.Name();
+
+ // [Then] Accounts are retrieved from the GetAccounts method
+ FileAccount.DeleteAll();
+ ExtFileConnector.GetAccounts(FileAccount);
+ Assert.RecordCount(FileAccount, Index);
+ end;
+
+ FileAccounts.OpenView();
+ for Index := 1 to 3 do begin
+ FileAccounts.GoToKey(AccountIds[Index], Enum::"Ext. File Storage Connector"::"File Share");
+ Assert.AreEqual(AccountName[Index], FileAccounts.NameField.Value(), 'A different name was expected.');
+ end;
+ end;
+
+
+ [Test]
+ [Scope('OnPrem')]
+ [HandlerFunctions('AccountRegisterPageHandler')]
+ [TransactionModel(TransactionModel::AutoRollback)]
+ procedure TestEnviromentCleanupDisablesAccounts()
+ var
+ FileAccount: Record "File Account";
+ ExtSharePointAccount: Record "Ext. File Share Account";
+ ExtFileConnector: Codeunit "Ext. File Share Connector Impl";
+ EnvironmentTriggers: Codeunit "Environment Triggers";
+ AccountIds: array[3] of Guid;
+ Index: Integer;
+ begin
+ // [Scenario] Create multiple accounts
+ Initialize();
+
+ // [When] Multiple accounts are registered
+ for Index := 1 to 3 do begin
+ SetBasicAccount();
+
+ Assert.IsTrue(ExtFileConnector.RegisterAccount(FileAccount), 'Failed to register account.');
+ AccountIds[Index] := FileAccount."Account Id";
+
+ // [Then] Accounts are retrieved from the GetAccounts method
+ FileAccount.DeleteAll();
+ ExtFileConnector.GetAccounts(FileAccount);
+ Assert.RecordCount(FileAccount, Index);
+ end;
+
+ ExtSharePointAccount.SetRange(Disabled, true);
+ Assert.IsTrue(ExtSharePointAccount.IsEmpty(), 'Accounts are already disabled.');
+
+ EnvironmentTriggers.OnAfterCopyEnvironmentPerCompany(0, Any.AlphabeticText(30), 1, Any.AlphabeticText(30));
+
+ Assert.IsFalse(ExtSharePointAccount.IsEmpty(), 'Accounts are not disabled.');
+ end;
+
+ [Test]
+ [Scope('OnPrem')]
+ [HandlerFunctions('AccountRegisterPageHandler,AccountShowPageHandler')]
+ [TransactionModel(TransactionModel::AutoRollback)]
+ procedure TestShowAccountInformation()
+ var
+ FileAccount: Record "File Account";
+ FileConnector: Codeunit "Ext. File Share Connector Impl";
+ begin
+ // [Scenario] Account Information is displayed in the Account page.
+
+ // [Given] An file account
+ Initialize();
+ SetBasicAccount();
+ FileConnector.RegisterAccount(FileAccount);
+
+ // [When] The ShowAccountInformation method is invoked
+ FileConnector.ShowAccountInformation(FileAccount."Account Id");
+
+ // [Then] The account page opens and displays the information
+ // Verify in AccountModalPageHandler
+ end;
+
+ local procedure Initialize()
+ var
+ ExtFileShareAccount: Record "Ext. File Share Account";
+ begin
+ ExtFileShareAccount.DeleteAll();
+ end;
+
+ local procedure SetBasicAccount()
+ begin
+ FileAccountMock.Name(CopyStr(Any.AlphanumericText(250), 1, 250));
+ FileAccountMock.StorageAccountName(CopyStr(Any.AlphanumericText(250), 1, 250));
+ FileAccountMock.FileShareName(CopyStr(Any.AlphanumericText(250), 1, 250));
+ FileAccountMock.Password('testpassword');
+ end;
+
+ [ModalPageHandler]
+ procedure AccountRegisterPageHandler(var AccountWizard: TestPage "Ext. File Share Account Wizard")
+ begin
+ // Setup account
+ AccountWizard.NameField.SetValue(FileAccountMock.Name());
+ AccountWizard.StorageAccountNameField.SetValue(FileAccountMock.StorageAccountName());
+ AccountWizard.FileShareNameField.SetValue(FileAccountMock.FileShareName());
+ AccountWizard."Authorization Type".SetValue(FileAccountMock.AuthorizationType());
+ AccountWizard.SecretField.SetValue(FileAccountMock.Password());
+ AccountWizard.Next.Invoke();
+ end;
+
+ [PageHandler]
+ procedure AccountShowPageHandler(var Account: TestPage "Ext. File Share Account")
+ begin
+ // Verify the account
+ Assert.AreEqual(FileAccountMock.Name(), Account.NameField.Value(), 'A different name was expected.');
+ Assert.AreEqual(FileAccountMock.StorageAccountName(), Account.StorageAccountNameField.Value(), 'A different storage account name was expected.');
+ Assert.AreEqual(FileAccountMock.FileShareName(), Account.FileShareNameField.Value(), 'A different file share name was expected.');
+ Assert.AreEqual(FileAccountMock.AuthorizationType(), Account."Authorization Type".AsInteger(), 'A different authorization type was expected.');
+ end;
+
+ var
+ Any: Codeunit Any;
+ Assert: Codeunit "Library Assert";
+ FileAccountMock: Codeunit "Ext. File Account Mock";
+}
\ No newline at end of file
diff --git a/Apps/W1/External File Storage - Azure File Service Connector/test/src/mocks/ExtFileAccountMock.Codeunit.al b/Apps/W1/External File Storage - Azure File Service Connector/test/src/mocks/ExtFileAccountMock.Codeunit.al
new file mode 100644
index 0000000000..f397be5cd0
--- /dev/null
+++ b/Apps/W1/External File Storage - Azure File Service Connector/test/src/mocks/ExtFileAccountMock.Codeunit.al
@@ -0,0 +1,68 @@
+// ------------------------------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+// ------------------------------------------------------------------------------------------------
+
+codeunit 144570 "Ext. File Account Mock"
+{
+ Access = Internal;
+ SingleInstance = true;
+
+ procedure Name(): Text[250]
+ begin
+ exit(AccName);
+ end;
+
+ procedure Name(Value: Text[250])
+ begin
+ AccName := Value;
+ end;
+
+ procedure StorageAccountName(): Text[250]
+ begin
+ exit(AccStorageAccountName);
+ end;
+
+ procedure StorageAccountName(Value: Text[250])
+ begin
+ AccStorageAccountName := Value;
+ end;
+
+
+ procedure FileShareName(): Text[250]
+ begin
+ exit(AccFileShareName);
+ end;
+
+ procedure FileShareName(Value: Text[250])
+ begin
+ AccFileShareName := Value;
+ end;
+
+ procedure Password(): Text
+ begin
+ exit(AccPassword);
+ end;
+
+ procedure Password(Value: Text)
+ begin
+ AccPassword := Value;
+ end;
+
+ procedure AuthorizationType(Value: Enum "Ext. File Share Auth. Type")
+ begin
+ AccAuthorizationType := Value;
+ end;
+
+ procedure AuthorizationType(): Enum "Ext. File Share Auth. Type"
+ begin
+ exit(AccAuthorizationType);
+ end;
+
+ var
+ AccName: Text[250];
+ AccStorageAccountName: Text[250];
+ AccFileShareName: Text[250];
+ AccPassword: Text;
+ AccAuthorizationType: Enum "Ext. File Share Auth. Type";
+}
\ No newline at end of file
diff --git a/Apps/W1/External File Storage - SharePoint Connector/app/Entitlements/ExtSharePointConnector.Entitlement.al b/Apps/W1/External File Storage - SharePoint Connector/app/Entitlements/ExtSharePointConnector.Entitlement.al
new file mode 100644
index 0000000000..b7efbd1973
--- /dev/null
+++ b/Apps/W1/External File Storage - SharePoint Connector/app/Entitlements/ExtSharePointConnector.Entitlement.al
@@ -0,0 +1,13 @@
+// ------------------------------------------------------------------------------------------------
+// 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;
+
+entitlement "Ext. SharePoint Connector"
+{
+
+ ObjectEntitlements = "Ext. SharePoint - Edit";
+ Type = Implicit;
+}
\ No newline at end of file
diff --git a/Apps/W1/External File Storage - SharePoint Connector/app/ExtensionLogo.png b/Apps/W1/External File Storage - SharePoint Connector/app/ExtensionLogo.png
new file mode 100644
index 0000000000000000000000000000000000000000..30941b354fa335cad3ea5426ac24cadb2ee328e5
GIT binary patch
literal 4681
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
literal 0
HcmV?d00001
diff --git a/Apps/W1/External File Storage - SharePoint Connector/app/README.md b/Apps/W1/External File Storage - SharePoint Connector/app/README.md
new file mode 100644
index 0000000000..121e78fdc0
--- /dev/null
+++ b/Apps/W1/External File Storage - SharePoint Connector/app/README.md
@@ -0,0 +1,3 @@
+# External File Storage - SharePoint Connector
+This connector allows access to Share Point Files and Folder.
+A proper App Registration with Sites.ReadWrite.All permission is needed.
\ No newline at end of file
diff --git a/Apps/W1/External File Storage - SharePoint Connector/app/app.json b/Apps/W1/External File Storage - SharePoint Connector/app/app.json
new file mode 100644
index 0000000000..696c2d7a7f
--- /dev/null
+++ b/Apps/W1/External File Storage - SharePoint Connector/app/app.json
@@ -0,0 +1,37 @@
+{
+ "id": "34bfcef7-f8ed-449f-94be-74024cadba3b",
+ "name": "External File Storage - SharePoint Connector",
+ "publisher": "Microsoft",
+ "brief": "Enables file and folder operations for SharePoint folders and files via the External File Storage Module with Business Central.",
+ "description": "This app enables file and folder operations for SharePoint folders and files via the External File Storage Module with Business Central.",
+ "version": "26.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=2134520",
+ "url": "https://go.microsoft.com/fwlink/?linkid=724011",
+ "logo": "ExtensionLogo.png",
+ "application": "26.0.0.0",
+ "platform": "26.0.0.0",
+ "internalsVisibleTo": [
+ {
+ "id": "b072f3f0-db0e-4331-b30d-4c0ebbcde681",
+ "name": "External File Storage - SharePoint Connector Tests",
+ "publisher": "Microsoft"
+ }
+ ],
+ "dependencies": [],
+ "screenshots": [],
+ "idRanges": [
+ {
+ "from": 4580,
+ "to": 4589
+ }
+ ],
+ "resourceExposurePolicy": {
+ "allowDebugging": true,
+ "allowDownloadingSource": true,
+ "includeSourceInSymbolFile": true
+ },
+ "contextSensitiveHelpUrl": "https://go.microsoft.com/fwlink/?linkid=2134520",
+ "resourceFolders": ["data"]
+}
\ No newline at end of file
diff --git a/Apps/W1/External File Storage - SharePoint Connector/app/data/connector-logo.png b/Apps/W1/External File Storage - SharePoint Connector/app/data/connector-logo.png
new file mode 100644
index 0000000000000000000000000000000000000000..3c4d410a3714e5411eaec2aeab2f8493458bc3b8
GIT binary patch
literal 13403
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
+/// Displays an account that was registered via the SharePoint connector.
+///
+page 4580 "Ext. SharePoint Account"
+{
+ ApplicationArea = All;
+ Caption = 'SharePoint Account';
+ DataCaptionExpression = Rec.Name;
+ Extensible = false;
+ InsertAllowed = false;
+ PageType = Card;
+ Permissions = tabledata "Ext. SharePoint Account" = rimd;
+ SourceTable = "Ext. SharePoint Account";
+ UsageCategory = None;
+
+ layout
+ {
+ area(Content)
+ {
+ field(NameField; Rec.Name)
+ {
+ NotBlank = true;
+ ShowMandatory = true;
+ }
+ field("Tenant Id"; Rec."Tenant Id") { }
+ field("Client Id"; Rec."Client Id") { }
+ field(SecretField; ClientSecret)
+ {
+ Caption = 'Password';
+ Editable = ClientSecretEditable;
+ ExtendedDatatype = Masked;
+ ToolTip = 'Specifies the the Client Secret of the App Registration.';
+
+ trigger OnValidate()
+ begin
+ Rec.SetClientSecret(ClientSecret);
+ end;
+ }
+ field("SharePoint Url"; Rec."SharePoint Url") { }
+ field("Base Relative Folder Path"; Rec."Base Relative Folder Path") { }
+ field(Disabled; Rec.Disabled) { }
+ }
+ }
+
+ var
+ ClientSecretEditable: Boolean;
+ [NonDebuggable]
+ ClientSecret: Text;
+
+ trigger OnOpenPage()
+ begin
+ Rec.SetCurrentKey(Name);
+ end;
+
+ trigger OnAfterGetCurrRecord()
+ begin
+ ClientSecretEditable := CurrPage.Editable();
+ if not IsNullGuid(Rec."Client Secret Key") then
+ ClientSecret := '***';
+ end;
+}
\ No newline at end of file
diff --git a/Apps/W1/External File Storage - SharePoint Connector/app/src/ExtSharePointAccount.Table.al b/Apps/W1/External File Storage - SharePoint Connector/app/src/ExtSharePointAccount.Table.al
new file mode 100644
index 0000000000..a175e1c7dc
--- /dev/null
+++ b/Apps/W1/External File Storage - SharePoint Connector/app/src/ExtSharePointAccount.Table.al
@@ -0,0 +1,95 @@
+// ------------------------------------------------------------------------------------------------
+// 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;
+
+///
+/// Holds the information for all file accounts that are registered via the SharePoint connector
+///
+table 4580 "Ext. SharePoint Account"
+{
+ Caption = 'SharePoint Account';
+ DataClassification = CustomerContent;
+
+ fields
+ {
+ field(1; "Id"; Guid)
+ {
+ AllowInCustomizations = Never;
+ Caption = 'Primary Key';
+ DataClassification = SystemMetadata;
+ }
+ field(2; Name; Text[250])
+ {
+ Caption = 'Account Name';
+ ToolTip = 'Specifies the name of the storage account connection.';
+ }
+ field(4; "SharePoint Url"; Text[2048])
+ {
+ Caption = 'SharePoint Url';
+ ToolTip = 'Specifies the the url to your SharePoint site.';
+ }
+ field(5; "Base Relative Folder Path"; Text[2048])
+ {
+ Caption = 'Base Relative Folder Path';
+ ToolTip = 'Specifies the folder path relative to the site collection. Start with the document library or folder name (e.g., Shared Documents/Reports). This path can be copied from the URL of the folder in SharePoint after the site collection (e.g., /Shared Documents/Reports from https://mysharepoint.sharepoint.com/sites/ProjectX/Shared%20Documents/Reports).';
+ }
+ field(6; "Tenant Id"; Guid)
+ {
+ Access = Internal;
+ Caption = 'Tenant Id';
+ ToolTip = 'Specifies the Tenant Id of the App Registration.';
+ }
+ field(7; "Client Id"; Guid)
+ {
+ Access = Internal;
+ Caption = 'Client Id';
+ ToolTip = 'Specifies the the Client Id of the App Registration.';
+ }
+ field(8; "Client Secret Key"; Guid)
+ {
+ Access = Internal;
+ DataClassification = SystemMetadata;
+ }
+ field(9; Disabled; Boolean)
+ {
+ Caption = 'Disabled';
+ ToolTip = 'Specifies if the account is disabled. This happens automatically when a sandbox is created.';
+ }
+ }
+
+ keys
+ {
+ key(PK; Id)
+ {
+ Clustered = true;
+ }
+ }
+
+ var
+ UnableToGetClientMsg: Label 'Unable to get SharePoint Account Client Secret.';
+ UnableToSetClientSecretMsg: Label 'Unable to set SharePoint Client Secret.';
+
+ trigger OnDelete()
+ begin
+ if not IsNullGuid(Rec."Client Secret Key") then
+ if IsolatedStorage.Delete(Rec."Client Secret Key") then;
+ end;
+
+ procedure SetClientSecret(ClientSecret: SecretText)
+ begin
+ if IsNullGuid(Rec."Client Secret Key") then
+ Rec."Client Secret Key" := CreateGuid();
+
+ if not IsolatedStorage.Set(Format(Rec."Client Secret Key"), ClientSecret, DataScope::Company) then
+ Error(UnableToSetClientSecretMsg);
+ end;
+
+ procedure GetClientSecret(ClientSecretKey: Guid) ClientSecret: SecretText
+ begin
+ if not IsolatedStorage.Get(Format(ClientSecretKey), DataScope::Company, ClientSecret) then
+ Error(UnableToGetClientMsg);
+ end;
+}
\ No newline at end of file
diff --git a/Apps/W1/External File Storage - SharePoint Connector/app/src/ExtSharePointAccountWizard.Page.al b/Apps/W1/External File Storage - SharePoint Connector/app/src/ExtSharePointAccountWizard.Page.al
new file mode 100644
index 0000000000..1757e06e6c
--- /dev/null
+++ b/Apps/W1/External File Storage - SharePoint Connector/app/src/ExtSharePointAccountWizard.Page.al
@@ -0,0 +1,169 @@
+// ------------------------------------------------------------------------------------------------
+// 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;
+
+Using System.Environment;
+
+///
+/// Displays an account that is being registered via the SharePoint connector.
+///
+page 4581 "Ext. SharePoint Account Wizard"
+{
+ ApplicationArea = All;
+ Caption = 'Setup SharePoint Account';
+ Editable = true;
+ Extensible = false;
+ PageType = NavigatePage;
+ Permissions = tabledata "Ext. SharePoint Account" = rimd;
+ SourceTable = "Ext. SharePoint Account";
+ SourceTableTemporary = true;
+
+ layout
+ {
+ area(Content)
+ {
+ group(TopBanner)
+ {
+ Editable = false;
+ ShowCaption = false;
+ Visible = TopBannerVisible;
+ field(NotDoneIcon; MediaResources."Media Reference")
+ {
+ Editable = false;
+ ShowCaption = false;
+ ToolTip = ' ', Locked = true;
+ }
+ }
+
+ field(NameField; Rec.Name)
+ {
+ Caption = 'Account Name';
+ NotBlank = true;
+ ShowMandatory = true;
+ ToolTip = 'Specifies the name of the Azure SharePoint account.';
+
+ trigger OnValidate()
+ begin
+ IsNextEnabled := SharePointConnectorImpl.IsAccountValid(Rec);
+ end;
+ }
+
+ field("Tenant Id"; Rec."Tenant Id")
+ {
+ ShowMandatory = true;
+
+ trigger OnValidate()
+ begin
+ IsNextEnabled := SharePointConnectorImpl.IsAccountValid(Rec);
+ end;
+ }
+
+ field("Client Id"; Rec."Client Id")
+ {
+ ShowMandatory = true;
+
+ trigger OnValidate()
+ begin
+ IsNextEnabled := SharePointConnectorImpl.IsAccountValid(Rec);
+ end;
+ }
+
+ field(ClientSecretField; ClientSecret)
+ {
+ Caption = 'Client Secret';
+ ExtendedDatatype = Masked;
+ ShowMandatory = true;
+ ToolTip = 'Specifies the Client Secret of the App Registration.';
+ }
+
+ field("SharePoint Url"; Rec."SharePoint Url")
+ {
+ Caption = 'SharePoint Name';
+ ShowMandatory = true;
+ ToolTip = 'Specifies the SharePoint to use of the storage account.';
+
+ trigger OnValidate()
+ begin
+ IsNextEnabled := SharePointConnectorImpl.IsAccountValid(Rec);
+ end;
+ }
+
+ field("Base Relative Folder Path"; Rec."Base Relative Folder Path")
+ {
+ ShowMandatory = true;
+
+ trigger OnValidate()
+ begin
+ IsNextEnabled := SharePointConnectorImpl.IsAccountValid(Rec);
+ end;
+ }
+ }
+ }
+
+ actions
+ {
+ area(processing)
+ {
+ action(Back)
+ {
+ Caption = 'Back';
+ Image = Cancel;
+ InFooterBar = true;
+ ToolTip = 'Move to previous step.';
+
+ trigger OnAction()
+ begin
+ CurrPage.Close();
+ end;
+ }
+
+ action(Next)
+ {
+ Caption = 'Next';
+ Enabled = IsNextEnabled;
+ Image = NextRecord;
+ InFooterBar = true;
+ ToolTip = 'Move to next step.';
+
+ trigger OnAction()
+ begin
+ SharePointConnectorImpl.CreateAccount(Rec, ClientSecret, SharePointAccount);
+ CurrPage.Close();
+ end;
+ }
+ }
+ }
+
+ var
+ SharePointAccount: Record "File Account";
+ MediaResources: Record "Media Resources";
+ SharePointConnectorImpl: Codeunit "Ext. SharePoint Connector Impl";
+ [NonDebuggable]
+ ClientSecret: Text;
+ IsNextEnabled: Boolean;
+ TopBannerVisible: Boolean;
+
+ trigger OnOpenPage()
+ var
+ AssistedSetupLogoTok: Label 'ASSISTEDSETUP-NOTEXT-400PX.PNG', Locked = true;
+ begin
+ Rec.Init();
+ Rec.Insert();
+
+ if MediaResources.Get(AssistedSetupLogoTok) and (CurrentClientType() = ClientType::Web) then
+ TopBannerVisible := MediaResources."Media Reference".HasValue();
+ end;
+
+ internal procedure GetAccount(var FileAccount: Record "File Account"): Boolean
+ begin
+ if IsNullGuid(SharePointAccount."Account Id") then
+ exit(false);
+
+ FileAccount := SharePointAccount;
+
+ exit(true);
+ end;
+}
\ No newline at end of file
diff --git a/Apps/W1/External File Storage - SharePoint Connector/app/src/ExtSharePointConnector.EnumExt.al b/Apps/W1/External File Storage - SharePoint Connector/app/src/ExtSharePointConnector.EnumExt.al
new file mode 100644
index 0000000000..cafd9b5652
--- /dev/null
+++ b/Apps/W1/External File Storage - SharePoint Connector/app/src/ExtSharePointConnector.EnumExt.al
@@ -0,0 +1,21 @@
+// ------------------------------------------------------------------------------------------------
+// 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;
+
+///
+/// Enum extension to register the SharePoint connector.
+///
+enumextension 4580 "Ext. SharePoint Connector" extends "Ext. File Storage Connector"
+{
+ ///
+ /// The SharePoint connector.
+ ///
+ value(4580; "SharePoint")
+ {
+ Caption = 'SharePoint';
+ Implementation = "External File Storage Connector" = "Ext. SharePoint Connector Impl";
+ }
+}
\ No newline at end of file
diff --git a/Apps/W1/External File Storage - SharePoint Connector/app/src/ExtSharePointConnectorImpl.Codeunit.al b/Apps/W1/External File Storage - SharePoint Connector/app/src/ExtSharePointConnectorImpl.Codeunit.al
new file mode 100644
index 0000000000..aece559dad
--- /dev/null
+++ b/Apps/W1/External File Storage - SharePoint Connector/app/src/ExtSharePointConnectorImpl.Codeunit.al
@@ -0,0 +1,458 @@
+// ------------------------------------------------------------------------------------------------
+// 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;
+
+using System.Text;
+using System.Integration.Sharepoint;
+using System.Utilities;
+using System.DataAdministration;
+
+codeunit 4580 "Ext. SharePoint Connector Impl" implements "External File Storage Connector"
+{
+ Access = Internal;
+ InherentEntitlements = X;
+ InherentPermissions = X;
+ Permissions = tabledata "Ext. SharePoint Account" = rimd;
+
+ var
+ ConnectorDescriptionTxt: Label 'Use SharePoint to store and retrieve files.', MaxLength = 250;
+ NotRegisteredAccountErr: Label 'We could not find the account. Typically, this is because the account has been deleted.';
+
+ ///
+ /// Gets a List of Files stored on the provided account.
+ ///
+ /// The file account ID which is used to get the file.
+ /// The file path to list.
+ /// Defines the pagination data.
+ /// A list with all files stored in the path.
+ procedure ListFiles(AccountId: Guid; Path: Text; FilePaginationData: Codeunit "File Pagination Data"; var TempFileAccountContent: Record "File Account Content" temporary)
+ var
+ SharePointFile: Record "SharePoint File";
+ SharePointClient: Codeunit "SharePoint Client";
+ OrginalPath: Text;
+ begin
+ OrginalPath := Path;
+ InitPath(AccountId, Path);
+ InitSharePointClient(AccountId, SharePointClient);
+ if not SharePointClient.GetFolderFilesByServerRelativeUrl(Path, SharePointFile) then
+ ShowError(SharePointClient);
+
+ FilePaginationData.SetEndOfListing(true);
+
+ if not SharePointFile.FindSet() then
+ exit;
+
+ repeat
+ TempFileAccountContent.Init();
+ TempFileAccountContent.Name := SharePointFile.Name;
+ TempFileAccountContent.Type := TempFileAccountContent.Type::"File";
+ TempFileAccountContent."Parent Directory" := CopyStr(OrginalPath, 1, MaxStrLen(TempFileAccountContent."Parent Directory"));
+ TempFileAccountContent.Insert();
+ until SharePointFile.Next() = 0;
+ end;
+
+ ///
+ /// Gets a file from the provided account.
+ ///
+ /// The file account ID which is used to get the file.
+ /// The file path inside the file account.
+ /// The Stream were the file is read to.
+ procedure GetFile(AccountId: Guid; Path: Text; Stream: InStream)
+ var
+ SharePointClient: Codeunit "SharePoint Client";
+ Content: HttpContent;
+ TempBlobStream: InStream;
+ begin
+ InitPath(AccountId, Path);
+ InitSharePointClient(AccountId, SharePointClient);
+
+ if not SharePointClient.DownloadFileContentByServerRelativeUrl(Path, TempBlobStream) then
+ ShowError(SharePointClient);
+
+ // Platform fix: For some reason the Stream from DownloadFileContentByServerRelativeUrl dies after leaving the interface
+ Content.WriteFrom(TempBlobStream);
+ Content.ReadAs(Stream);
+ end;
+
+ ///
+ /// Create a file in the provided account.
+ ///
+ /// The file account ID which is used to send out the file.
+ /// The file path inside the file account.
+ /// The Stream were the file is read from.
+ procedure CreateFile(AccountId: Guid; Path: Text; Stream: InStream)
+ var
+ SharePointFile: Record "SharePoint File";
+ SharePointClient: Codeunit "SharePoint Client";
+ ParentPath, FileName : Text;
+ begin
+ InitPath(AccountId, Path);
+ InitSharePointClient(AccountId, SharePointClient);
+ SplitPath(Path, ParentPath, FileName);
+ if SharePointClient.AddFileToFolder(ParentPath, FileName, Stream, SharePointFile, false) then
+ exit;
+
+ ShowError(SharePointClient);
+ end;
+
+ ///
+ /// Copies as file inside the provided account.
+ ///
+ /// The file account ID which is used to send out the file.
+ /// The source file path.
+ /// The target file path.
+ procedure CopyFile(AccountId: Guid; SourcePath: Text; TargetPath: Text)
+ var
+ TempBlob: Codeunit "Temp Blob";
+ Stream: InStream;
+ begin
+ TempBlob.CreateInStream(Stream);
+
+ GetFile(AccountId, SourcePath, Stream);
+ CreateFile(AccountId, TargetPath, Stream);
+ end;
+
+ ///
+ /// Move as file inside the provided account.
+ ///
+ /// The file account ID which is used to send out the file.
+ /// The source file path.
+ /// The target file path.
+ procedure MoveFile(AccountId: Guid; SourcePath: Text; TargetPath: Text)
+ var
+ Stream: InStream;
+ begin
+ GetFile(AccountId, SourcePath, Stream);
+ CreateFile(AccountId, TargetPath, Stream);
+ DeleteFile(AccountId, SourcePath);
+ end;
+
+ ///
+ /// Checks if a file exists on the provided account.
+ ///
+ /// The file account ID which is used to send out the file.
+ /// The file path inside the file account.
+ /// Returns true if the file exists
+ procedure FileExists(AccountId: Guid; Path: Text): Boolean
+ var
+ SharePointFile: Record "SharePoint File";
+ SharePointClient: Codeunit "SharePoint Client";
+ begin
+ InitPath(AccountId, Path);
+ InitSharePointClient(AccountId, SharePointClient);
+ if not SharePointClient.GetFolderFilesByServerRelativeUrl(GetParentPath(Path), SharePointFile) then
+ ShowError(SharePointClient);
+
+ SharePointFile.SetRange(Name, GetFileName(Path));
+ exit(not SharePointFile.IsEmpty());
+ end;
+
+ ///
+ /// Deletes a file exists on the provided account.
+ ///
+ /// The file account ID which is used to send out the file.
+ /// The file path inside the file account.
+ procedure DeleteFile(AccountId: Guid; Path: Text)
+ var
+ SharePointClient: Codeunit "SharePoint Client";
+ begin
+ InitPath(AccountId, Path);
+ InitSharePointClient(AccountId, SharePointClient);
+ if SharePointClient.DeleteFileByServerRelativeUrl(Path) then
+ exit;
+
+ ShowError(SharePointClient);
+ end;
+
+ ///
+ /// Gets a List of Directories stored on the provided account.
+ ///
+ /// The file account ID which is used to get the file.
+ /// The file path to list.
+ /// Defines the pagination data.
+ /// A list with all directories stored in the path.
+ procedure ListDirectories(AccountId: Guid; Path: Text; FilePaginationData: Codeunit "File Pagination Data"; var TempFileAccountContent: Record "File Account Content" temporary)
+ var
+ SharePointFolder: Record "SharePoint Folder";
+ SharePointClient: Codeunit "SharePoint Client";
+ OrginalPath: Text;
+ begin
+ OrginalPath := Path;
+ InitPath(AccountId, Path);
+ InitSharePointClient(AccountId, SharePointClient);
+ if not SharePointClient.GetSubFoldersByServerRelativeUrl(Path, SharePointFolder) then
+ ShowError(SharePointClient);
+
+ FilePaginationData.SetEndOfListing(true);
+
+ if not SharePointFolder.FindSet() then
+ exit;
+
+ repeat
+ TempFileAccountContent.Init();
+ TempFileAccountContent.Name := SharePointFolder.Name;
+ TempFileAccountContent.Type := TempFileAccountContent.Type::Directory;
+ TempFileAccountContent."Parent Directory" := CopyStr(OrginalPath, 1, MaxStrLen(TempFileAccountContent."Parent Directory"));
+ TempFileAccountContent.Insert();
+ until SharePointFolder.Next() = 0;
+ end;
+
+ ///
+ /// Creates a directory on the provided account.
+ ///
+ /// The file account ID which is used to send out the file.
+ /// The directory path inside the file account.
+ procedure CreateDirectory(AccountId: Guid; Path: Text)
+ var
+ SharePointFolder: Record "SharePoint Folder";
+ SharePointClient: Codeunit "SharePoint Client";
+ begin
+ InitPath(AccountId, Path);
+ InitSharePointClient(AccountId, SharePointClient);
+ if SharePointClient.CreateFolder(Path, SharePointFolder) then
+ exit;
+
+ ShowError(SharePointClient);
+ end;
+
+ ///
+ /// Checks if a directory exists on the provided account.
+ ///
+ /// The file account ID which is used to send out the file.
+ /// The directory path inside the file account.
+ /// Returns true if the directory exists
+ procedure DirectoryExists(AccountId: Guid; Path: Text): Boolean
+ var
+ SharePointFolder: Record "SharePoint Folder";
+ SharePointClient: Codeunit "SharePoint Client";
+ begin
+ InitPath(AccountId, Path);
+ InitSharePointClient(AccountId, SharePointClient);
+ if SharePointClient.GetSubFoldersByServerRelativeUrl(Path, SharePointFolder) then
+ exit;
+
+ ShowError(SharePointClient);
+ end;
+
+ ///
+ /// Deletes a directory exists on the provided account.
+ ///
+ /// The file account ID which is used to send out the file.
+ /// The directory path inside the file account.
+ procedure DeleteDirectory(AccountId: Guid; Path: Text)
+ var
+ SharePointClient: Codeunit "SharePoint Client";
+ begin
+ InitPath(AccountId, Path);
+ InitSharePointClient(AccountId, SharePointClient);
+ if SharePointClient.DeleteFolderByServerRelativeUrl(Path) then
+ exit;
+
+ ShowError(SharePointClient);
+ end;
+
+ ///
+ /// Gets the registered accounts for the SharePoint connector.
+ ///
+ /// Out parameter holding all the registered accounts for the SharePoint connector.
+ procedure GetAccounts(var TempAccounts: Record "File Account" temporary)
+ var
+ Account: Record "Ext. SharePoint Account";
+ begin
+ if not Account.FindSet() then
+ exit;
+
+ repeat
+ TempAccounts."Account Id" := Account.Id;
+ TempAccounts.Name := Account.Name;
+ TempAccounts.Connector := Enum::"Ext. File Storage Connector"::"SharePoint";
+ TempAccounts.Insert();
+ until Account.Next() = 0;
+ end;
+
+ ///
+ /// Shows accounts information.
+ ///
+ /// The ID of the account to show.
+ procedure ShowAccountInformation(AccountId: Guid)
+ var
+ SharePointAccountLocal: Record "Ext. SharePoint Account";
+ begin
+ if not SharePointAccountLocal.Get(AccountId) then
+ Error(NotRegisteredAccountErr);
+
+ SharePointAccountLocal.SetRecFilter();
+ Page.Run(Page::"Ext. SharePoint Account", SharePointAccountLocal);
+ end;
+
+ ///
+ /// Register an file account for the SharePoint connector.
+ ///
+ /// Out parameter holding details of the registered account.
+ /// True if the registration was successful; false - otherwise.
+ procedure RegisterAccount(var TempAccount: Record "File Account" temporary): Boolean
+ var
+ SharePointAccountWizard: Page "Ext. SharePoint Account Wizard";
+ begin
+ SharePointAccountWizard.RunModal();
+
+ exit(SharePointAccountWizard.GetAccount(TempAccount));
+ end;
+
+ ///
+ /// Deletes an file account for the SharePoint connector.
+ ///
+ /// The ID of the SharePoint account
+ /// True if an account was deleted.
+ procedure DeleteAccount(AccountId: Guid): Boolean
+ var
+ SharePointAccountLocal: Record "Ext. SharePoint Account";
+ begin
+ if SharePointAccountLocal.Get(AccountId) then
+ exit(SharePointAccountLocal.Delete());
+
+ exit(false);
+ end;
+
+ ///
+ /// Gets a description of the SharePoint connector.
+ ///
+ /// A short description of the SharePoint connector.
+ procedure GetDescription(): Text[250]
+ begin
+ exit(ConnectorDescriptionTxt);
+ end;
+
+ ///
+ /// Gets the SharePoint connector logo.
+ ///
+ /// A base64-formatted image to be used as logo.
+ procedure GetLogoAsBase64(): Text
+ var
+ Base64Convert: Codeunit "Base64 Convert";
+ Stream: InStream;
+ begin
+ NavApp.GetResource('connector-logo.png', Stream);
+ exit(Base64Convert.ToBase64(Stream));
+ end;
+
+ internal procedure IsAccountValid(var TempAccount: Record "Ext. SharePoint Account" temporary): Boolean
+ begin
+ if TempAccount.Name = '' then
+ exit(false);
+
+ if IsNullGuid(TempAccount."Client Id") then
+ exit(false);
+
+ if IsNullGuid(TempAccount."Tenant Id") then
+ exit(false);
+
+ if TempAccount."SharePoint Url" = '' then
+ exit(false);
+
+ if TempAccount."Base Relative Folder Path" = '' then
+ exit(false);
+
+ exit(true);
+ end;
+
+ internal procedure CreateAccount(var AccountToCopy: Record "Ext. SharePoint Account"; Password: SecretText; var TempFileAccount: Record "File Account" temporary)
+ var
+ NewExtSharePointAccount: Record "Ext. SharePoint Account";
+ begin
+ NewExtSharePointAccount.TransferFields(AccountToCopy);
+
+ NewExtSharePointAccount.Id := CreateGuid();
+ NewExtSharePointAccount.SetClientSecret(Password);
+
+ NewExtSharePointAccount.Insert();
+
+ TempFileAccount."Account Id" := NewExtSharePointAccount.Id;
+ TempFileAccount.Name := NewExtSharePointAccount.Name;
+ TempFileAccount.Connector := Enum::"Ext. File Storage Connector"::"SharePoint";
+ end;
+
+ local procedure InitSharePointClient(var AccountId: Guid; var SharePointClient: Codeunit "SharePoint Client")
+ var
+ SharePointAccount: Record "Ext. SharePoint Account";
+ SharePointAuth: Codeunit "SharePoint Auth.";
+ SharePointAuthorization: Interface "SharePoint Authorization";
+ Scopes: List of [Text];
+ AccountDisabledErr: Label 'The account "%1" is disabled.', Comment = '%1 - Account Name';
+ begin
+ SharePointAccount.Get(AccountId);
+ if SharePointAccount.Disabled then
+ Error(AccountDisabledErr, SharePointAccount.Name);
+
+ Scopes.Add('00000003-0000-0ff1-ce00-000000000000/.default');
+ SharePointAuthorization := SharePointAuth.CreateAuthorizationCode(Format(SharePointAccount."Tenant Id", 0, 4), Format(SharePointAccount."Client Id", 0, 4), SharePointAccount.GetClientSecret(SharePointAccount."Client Secret Key"), Scopes);
+ SharePointClient.Initialize(SharePointAccount."SharePoint Url", SharePointAuthorization);
+ end;
+
+ local procedure PathSeparator(): Text
+ begin
+ exit('/');
+ end;
+
+ local procedure ShowError(var SharePointClient: Codeunit "SharePoint Client")
+ var
+ ErrorOccuredErr: Label 'An error occured.\%1', Comment = '%1 - Error message from sharepoint';
+ begin
+ Error(ErrorOccuredErr, SharePointClient.GetDiagnostics().GetErrorMessage());
+ end;
+
+ local procedure GetParentPath(Path: Text) ParentPath: Text
+ begin
+ if (Path.TrimEnd(PathSeparator()).Contains(PathSeparator())) then
+ ParentPath := Path.TrimEnd(PathSeparator()).Substring(1, Path.LastIndexOf(PathSeparator()));
+ end;
+
+ local procedure GetFileName(Path: Text) FileName: Text
+ begin
+ if (Path.TrimEnd(PathSeparator()).Contains(PathSeparator())) then
+ FileName := Path.TrimEnd(PathSeparator()).Substring(Path.LastIndexOf(PathSeparator()) + 1);
+ end;
+
+ local procedure InitPath(AccountId: Guid; var Path: Text)
+ var
+ SharePointAccount: Record "Ext. SharePoint Account";
+ begin
+ SharePointAccount.Get(AccountId);
+ Path := CombinePath(SharePointAccount."Base Relative Folder Path", Path);
+ end;
+
+ local procedure CombinePath(Parent: Text; Child: Text): Text
+ begin
+ if Parent = '' then
+ exit(Child);
+
+ if Child = '' then
+ exit(Parent);
+
+ if not Parent.EndsWith(PathSeparator()) then
+ Parent += PathSeparator();
+
+ exit(Parent + Child);
+ end;
+
+ local procedure SplitPath(Path: Text; var ParentPath: Text; var FileName: Text)
+ begin
+ ParentPath := Path.TrimEnd(PathSeparator()).Substring(1, Path.LastIndexOf(PathSeparator()));
+ FileName := Path.TrimEnd(PathSeparator()).Substring(Path.LastIndexOf(PathSeparator()) + 1);
+ end;
+
+ [EventSubscriber(ObjectType::Codeunit, Codeunit::"Environment Cleanup", OnClearCompanyConfig, '', false, false)]
+ local procedure EnvironmentCleanup_OnClearCompanyConfig(CompanyName: Text; SourceEnv: Enum "Environment Type"; DestinationEnv: Enum "Environment Type")
+ var
+ ExtSharePointAccount: Record "Ext. SharePoint Account";
+ begin
+ ExtSharePointAccount.SetRange(Disabled, false);
+ if ExtSharePointAccount.IsEmpty() then
+ exit;
+
+ ExtSharePointAccount.ModifyAll(Disabled, true);
+ end;
+}
\ No newline at end of file
diff --git a/Apps/W1/External File Storage - SharePoint Connector/test/ExtensionLogo.png b/Apps/W1/External File Storage - SharePoint Connector/test/ExtensionLogo.png
new file mode 100644
index 0000000000000000000000000000000000000000..30941b354fa335cad3ea5426ac24cadb2ee328e5
GIT binary patch
literal 4681
zcmc&&XHyeg(#9JF|QCI_JtTHP)d9vw?|-h^X~+HO+3~{=Y^}dgJd|
zZsgwtiU3`kU?L()hJOt(_A~a9h=`t`r>Slc{(PrmE|9&MA$7@|AnwlN2KOad*5-LZ
zZg_ubG>D{{Px4B^#6TNkLBhxOAsX)Ol$^SOw
zvZLIccJIo2ZC;ipTEG~N!1t+4MgO(1ocz=&{>BE|@yR*aROgK4^XH9?bgQ9eo|T$2
z6l!y3C1rpGY)!vF5S|kLVc?dAO{n^NaC4y7A$5xyq{4&2xU?mDNSr8?LDuG_>zCyZ
zMmd-N3i^d_=BK?~+&*xxd{s;*BZe3@#i{;k|B?t7ib{8VhQ+kos)UnRWw0-ve7c@IZGQGChp2H5uiLtFoF>!=+5h+)Vqd2MalH&qcY*VhHwShqLNQkSQmz
zx(Ux}b=r7Ru;1fj4p|MrOTgaM>+V_Y(V<`iV8%YaffoQ7ig`lDj1D3*#c=}U&uONF
zIosINLm%p|KD1ud%PQw=_(nCI1iJ?SpJ^rs?0J#i5i@Spf$_d12So!H2%bp+8Yki3
zOZKd6gMdtjd8RlWd^@)buBzb>*eI4V2Xy}JnUuSSLY37i!MjayWbs(Phvq}rPMnL|
z7~lhWMAlXx*%FN~UX?Cz_uER%#+@*={-i(v?>O2o9=HHlZi-kIAD>hv7MqSVvz)wJ
z&bUJd#saK=;uqRYVwa`s#nGLmYC)36pI9Zq3RL0fnkg;kUYbW^fuu?8cPR@d5>VsX`?mBJ+fjqc}G7!Vve%fub#3v@{_W`-dHq{~Qo!o)BeL;?
z96pVT_Vi3$=LctJzu6idoi2h3Tu9=0sdXV8kQ3hS$irlnFwGZg&
zYtn8zVY&n5z!QI|--Wkqz%0K*@A5~O92Al5HZvVWsr-1)+X_V(3Fd3DrI?%F45s+4
zFdGV%nI4@7{H-?wJ!14$FIC_?Hy7XX-6>K;9%3s_5#1rjova{p+y2)PrONtXL00@|
zGp~5hVnCyO9vu4g_a8GFJv7wbqpSTOAhi>0c0OGG4{?>5t}6?@^9w4c$KMvBjstDhHvv{k
zO>{5P>{Dc|X5UeYg(W-+esE8*5QTc|VX_6!7<8*LvW2@=xS6Ftd*}}HQE6iQM@yP<
zNk1*p(|qo$Z3yyr{KbGa!u|Khypj`Py#8>qdWl`xq4XNe1qtIYWp_nVD6%C#k~q6E
zU;>rmoT;E_i7?~!V#(t}tHbXg)>I=j;z05@jNhAu(MnXDe!IfoGkU#!c)6=HB4W>K
ze^)$Lz;f?rX(P_L_>$bgeN1)?yoe-uC|hK|uH4uY)~)KSHx2)mhMoHg5)057xya|r
zbJ;?Niu!Z|4YmksTLwc~&M6fqY{a6#B8XILkE0jd5a@{`kw|dWA0@A`wyXA2bd
zv}ejN*gxL8qI{w>f!MHcOBhWGZ`}>Uf8`Z$^G@G#vF?oeqTb$#
zH(Hdn!S7rwCgddLI%tYUEB<*lW=&~yTI%7hterNbSr>pTc=utO!FoNiBjxCvCu`tU
ztDMINoR6c<2Y!%BEoh1l9znkjW84`53iW2H0T>PbrBaPSdr1cm7>(w*An~TXhp(L#
zT_tPF$p3Tfih|r3TD@OH1P;sA&{C{Q;k<~ddd0bJ^wqEI8bd
zS3v)O1XpB8#Wr8vkoQw1fJ;~`te4A!9*}T^D5nvLwY90S`mtsc!4+Ghv
zmZrJbpWu24>Yux5vG1rUR$14OtkL5k`wPRc^#_)dmn9m<3yX?tJ!F^P9r{iM5~G3>
zB*^@)U-tp)Jd0EXSa^6vOFD-}NTS{o&6XodVO&q>u
zNwD^2z@TER$ON7f*e?m0u8S+Zb_eGjzveFHk{v1jzGk+@4HuME^3zPahz6qAS17eMw(}tO0%*2QC>t*bDe1S!Q0S?S4!Wv15@KAse9@I`
zfv5_*ZT%ugk0x?&a@%lGI_UDv6N05JzH@%v^DyL}t>2yu95L!!-o~h7*M7DQVFrTTIrA*=ncA
z<&EB8j%cTd9$moRbMHO&kIXE@inc|W;x&bBr{-P3j}CIrgbc^hPO%;pnv%>-nGhxw
ze4GYhi0ZCWz&m!%W!AkbMleL&D^{TGlor)N{-C_=NiDPFa;r6Fzq+A)AYq`OMEY`v
ztdQKVi|}b(Q40*}t5Y~_9kmU4?BxI2^XnQJAWT;djD3sRM4?bpo3?LxR(lCFTr9SV
z3Isl!4PbVKRWPdbrp=&E^Mv|e4)D#5RS!ek}pFm#AF^=S;m
zVK^3%vFiC+%t1nAUW!%X%XFB3!?aGz4guR_WNC*k!Xk~MJ`NvhW#Xf&kP1pZTg1Dx
z&n5TAPpHd!IWK0L$7+#Y-rQD7Ar-XZa#B5p1Ex?JV@^F`e4>Xn(dfFqJa))@C+Wi4
z%lXmd)U-_?a;tVjAGv%j^U=e1$>DI~{Q-HzC%{a2MBE*94r9S`r|
zN9NOQ42!LAJ>!n}7OpX(qtna!HGzMy0@JJu+8;i%Q{E(O!YL-ap3PaYxtj~j*(fYN
zb!@^#7=QVa2}ib{X0BY@-yS!5^Xh#_MZzEG<1*>td?-nD^4#pk;#PmfUWrC`$<9#n
zx+`BXRT<`Uwz!N4XEKc$rn-{^TvC{A$aBxy+rx>I;?2eig5mhYKkhcNHu^
zFiDEsnxUB3zLW#i)6R_ekB_A3#a>%~GI{)L>&g8h=d3?UHBiKQFW>Zqtz(EbK$O^T
zp4_`YhniACTR6k#;;-?@`Jssg$5l~8_aY|}h=7#17ay$?n)WR-S_TJCw1(P4
zRcU?Bt#zek%fF?h6>ntM*f0f6Fqw5Z{E2*9tC#HtnhtJ2zYIXCfDanbnbU6Dutl6v
zBpbGWh5F%9wAQ>9DX>+#$4hiP@U6fp%Nc`%pe$nQtXDhYYrL7B&8g~8Rl}SQ)qLt{
zY`(=0lCU>c^!y8KNJEpRqjP`Hau-^Pp2T^uY+X#&dnOE_d8^avU<%%P6}89R{XuJw
zw!loi=R8y4syicQav~2+ne{qv`<|3*rZua7yXU#$7=3uWfr*gvK?~3sUV6?su*u8#
z`KO)yuh{WB@!@fO3TrLuT$GBs4`tHj%VC9u^B^dC7k-4%_3(9Y_coZSH!TZy7y|YT
z7INN7W6BzcY_N;X=Ah`zuk6|i+Q~?XxNoD-$kn;&WNK7EdH`KtFDZLd$=9_7_7sge
z@q0qX(NDY<-wLALGFi@H=6AGGqq