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 0000000000..30941b354f
Binary files /dev/null and b/Apps/W1/External File Storage - Azure Blob Service Connector/app/ExtensionLogo.png differ
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 0000000000..d94371448c
Binary files /dev/null and b/Apps/W1/External File Storage - Azure Blob Service Connector/app/data/connector-logo.png differ
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 0000000000..30941b354f
Binary files /dev/null and b/Apps/W1/External File Storage - Azure Blob Service Connector/test/ExtensionLogo.png differ
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 0000000000..30941b354f
Binary files /dev/null and b/Apps/W1/External File Storage - Azure File Service Connector/app/ExtensionLogo.png differ
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 0000000000..98f852e166
Binary files /dev/null and b/Apps/W1/External File Storage - Azure File Service Connector/app/data/connector-logo.png differ
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 0000000000..30941b354f
Binary files /dev/null and b/Apps/W1/External File Storage - Azure File Service Connector/test/ExtensionLogo.png differ
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 0000000000..30941b354f
Binary files /dev/null and b/Apps/W1/External File Storage - SharePoint Connector/app/ExtensionLogo.png differ
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 0000000000..3c4d410a37
Binary files /dev/null and b/Apps/W1/External File Storage - SharePoint Connector/app/data/connector-logo.png differ
diff --git a/Apps/W1/External File Storage - SharePoint Connector/app/permissions/ExtSharePointEdit.PermissionSet.al b/Apps/W1/External File Storage - SharePoint Connector/app/permissions/ExtSharePointEdit.PermissionSet.al
new file mode 100644
index 0000000000..233b0008bb
--- /dev/null
+++ b/Apps/W1/External File Storage - SharePoint Connector/app/permissions/ExtSharePointEdit.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 4582 "Ext. SharePoint - Edit"
+{
+ Access = Public;
+ Assignable = false;
+ Caption = 'SharePoint - Edit';
+
+ IncludedPermissionSets = "Ext. SharePoint - Read";
+
+ Permissions =
+ tabledata "Ext. SharePoint Account" = imd;
+}
diff --git a/Apps/W1/External File Storage - SharePoint Connector/app/permissions/ExtSharePointObjects.PermissionSet.al b/Apps/W1/External File Storage - SharePoint Connector/app/permissions/ExtSharePointObjects.PermissionSet.al
new file mode 100644
index 0000000000..b961d80baf
--- /dev/null
+++ b/Apps/W1/External File Storage - SharePoint Connector/app/permissions/ExtSharePointObjects.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 4580 "Ext. SharePoint - Objects"
+{
+ Access = Public;
+ Assignable = false;
+ Caption = 'SharePoint - Objects';
+
+ Permissions =
+ table "Ext. SharePoint Account" = X,
+ page "Ext. SharePoint Account Wizard" = X,
+ page "Ext. SharePoint Account" = X;
+}
diff --git a/Apps/W1/External File Storage - SharePoint Connector/app/permissions/ExtSharePointRead.PermissionSet.al b/Apps/W1/External File Storage - SharePoint Connector/app/permissions/ExtSharePointRead.PermissionSet.al
new file mode 100644
index 0000000000..95cd2cfe6e
--- /dev/null
+++ b/Apps/W1/External File Storage - SharePoint Connector/app/permissions/ExtSharePointRead.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 4581 "Ext. SharePoint - Read"
+{
+ Access = Public;
+ Assignable = false;
+ Caption = 'SharePoint - Read';
+
+ IncludedPermissionSets = "Ext. SharePoint - Objects";
+
+ Permissions =
+ tabledata "Ext. SharePoint Account" = r;
+}
diff --git a/Apps/W1/External File Storage - SharePoint Connector/app/permissions/FileStorageAdminExtSharePoint.PermissionSetExt.al b/Apps/W1/External File Storage - SharePoint Connector/app/permissions/FileStorageAdminExtSharePoint.PermissionSetExt.al
new file mode 100644
index 0000000000..695880b292
--- /dev/null
+++ b/Apps/W1/External File Storage - SharePoint Connector/app/permissions/FileStorageAdminExtSharePoint.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 4580 "File Storage - Admin - Ext. SharePoint" extends "File Storage - Admin"
+{
+ IncludedPermissionSets = "Ext. SharePoint - Edit";
+}
diff --git a/Apps/W1/External File Storage - SharePoint Connector/app/permissions/FileStorageEditExtSharePoint.PermissionSetExt.al b/Apps/W1/External File Storage - SharePoint Connector/app/permissions/FileStorageEditExtSharePoint.PermissionSetExt.al
new file mode 100644
index 0000000000..1600e2563f
--- /dev/null
+++ b/Apps/W1/External File Storage - SharePoint Connector/app/permissions/FileStorageEditExtSharePoint.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 4581 "File Storage - Edit - Ext. SharePoint" extends "File Storage - Edit"
+{
+ IncludedPermissionSets = "Ext. SharePoint - Read";
+}
diff --git a/Apps/W1/External File Storage - SharePoint Connector/app/src/ExtSharePointAccount.Page.al b/Apps/W1/External File Storage - SharePoint Connector/app/src/ExtSharePointAccount.Page.al
new file mode 100644
index 0000000000..2bf9f69569
--- /dev/null
+++ b/Apps/W1/External File Storage - SharePoint Connector/app/src/ExtSharePointAccount.Page.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.
+// ------------------------------------------------------------------------------------------------
+
+namespace System.ExternalFileStorage;
+
+///
+/// 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 0000000000..30941b354f
Binary files /dev/null and b/Apps/W1/External File Storage - SharePoint Connector/test/ExtensionLogo.png differ
diff --git a/Apps/W1/External File Storage - SharePoint Connector/test/README.md b/Apps/W1/External File Storage - SharePoint Connector/test/README.md
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/Apps/W1/External File Storage - SharePoint Connector/test/app.json b/Apps/W1/External File Storage - SharePoint Connector/test/app.json
new file mode 100644
index 0000000000..be07b1676a
--- /dev/null
+++ b/Apps/W1/External File Storage - SharePoint Connector/test/app.json
@@ -0,0 +1,55 @@
+{
+ "id": "b072f3f0-db0e-4331-b30d-4c0ebbcde681",
+ "name": "External File Storage - SharePoint Connector Tests",
+ "publisher": "Microsoft",
+ "brief": "Tests for the External File Storage - SharePoint Connector app",
+ "description": "Tests for the External File Storage - SharePoint 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": "34bfcef7-f8ed-449f-94be-74024cadba3b",
+ "name": "External File Storage - SharePoint 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 - SharePoint Connector/test/src/ExtSharePointConnectorTest.Codeunit.al b/Apps/W1/External File Storage - SharePoint Connector/test/src/ExtSharePointConnectorTest.Codeunit.al
new file mode 100644
index 0000000000..f9b97f721a
--- /dev/null
+++ b/Apps/W1/External File Storage - SharePoint Connector/test/src/ExtSharePointConnectorTest.Codeunit.al
@@ -0,0 +1,153 @@
+// ------------------------------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+// ------------------------------------------------------------------------------------------------
+
+codeunit 144581 "Ext. SharePoint Connector Test"
+{
+ Subtype = Test;
+ TestPermissions = Disabled;
+
+ [Test]
+ [Scope('OnPrem')]
+ [HandlerFunctions('AccountRegisterPageHandler')]
+ [TransactionModel(TransactionModel::AutoRollback)]
+ procedure TestMultipleAccountsCanBeRegistered()
+ var
+ FileAccount: Record "File Account";
+ ExtFileConnector: Codeunit "Ext. SharePoint 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"::SharePoint);
+ 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. SharePoint Account";
+ ExtFileConnector: Codeunit "Ext. SharePoint 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. SharePoint 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
+ ExtSharePointAccount: Record "Ext. SharePoint Account";
+ begin
+ ExtSharePointAccount.DeleteAll();
+ end;
+
+ local procedure SetBasicAccount()
+ begin
+ FileAccountMock.Name(CopyStr(Any.AlphanumericText(250), 1, 250));
+ FileAccountMock.SharePointUrl(CopyStr(Any.AlphanumericText(250), 1, 250));
+ FileAccountMock.BaseRelativeFolderPath(CopyStr(Any.AlphanumericText(250), 1, 250));
+ FileAccountMock.TenantId(CreateGuid());
+ FileAccountMock.ClientId(CreateGuid());
+ FileAccountMock.Password('testpassword');
+ end;
+
+ [ModalPageHandler]
+ procedure AccountRegisterPageHandler(var AccountWizard: TestPage "Ext. SharePoint Account Wizard")
+ begin
+ // Setup account
+ AccountWizard.NameField.SetValue(FileAccountMock.Name());
+ AccountWizard."SharePoint Url".SetValue(FileAccountMock.SharePointUrl());
+ AccountWizard."Base Relative Folder Path".SetValue(FileAccountMock.BaseRelativeFolderPath());
+ AccountWizard."Tenant Id".SetValue(FileAccountMock.TenantId());
+ AccountWizard."Client Id".SetValue(FileAccountMock.ClientId());
+ AccountWizard.ClientSecretField.SetValue(FileAccountMock.Password());
+ AccountWizard.Next.Invoke();
+ end;
+
+ [PageHandler]
+ procedure AccountShowPageHandler(var Account: TestPage "Ext. SharePoint Account")
+ begin
+ // Verify the account
+ Assert.AreEqual(FileAccountMock.Name(), Account.NameField.Value(), 'A different name was expected.');
+ Assert.AreEqual(FileAccountMock.SharePointUrl(), Account."SharePoint Url".Value(), 'A different sharepoint url was expected.');
+ Assert.AreEqual(FileAccountMock.BaseRelativeFolderPath(), Account."Base Relative Folder Path".Value(), 'A different base relative folder path was expected.');
+ Assert.AreEqual(Format(FileAccountMock.TenantId()).ToLower(), Format(Account."Tenant Id").ToLower(), 'A different tenant id was expected.');
+ Assert.AreEqual(Format(FileAccountMock.ClientId()).ToLower(), Format(Account."Client Id").ToLower(), 'A different client id was expected.');
+ end;
+
+ var
+ Any: Codeunit Any;
+ Assert: Codeunit "Library Assert";
+ FileAccountMock: Codeunit "Ext. SharePoint Account Mock";
+}
\ No newline at end of file
diff --git a/Apps/W1/External File Storage - SharePoint Connector/test/src/mocks/ExtSharePointAccountMock.Codeunit.al b/Apps/W1/External File Storage - SharePoint Connector/test/src/mocks/ExtSharePointAccountMock.Codeunit.al
new file mode 100644
index 0000000000..714cdb073d
--- /dev/null
+++ b/Apps/W1/External File Storage - SharePoint Connector/test/src/mocks/ExtSharePointAccountMock.Codeunit.al
@@ -0,0 +1,79 @@
+// ------------------------------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+// ------------------------------------------------------------------------------------------------
+
+codeunit 144580 "Ext. SharePoint Account Mock"
+{
+ Access = Internal;
+ SingleInstance = true;
+
+ procedure Name(): Text[250]
+ begin
+ exit(AccName);
+ end;
+
+ procedure Name(Value: Text[250])
+ begin
+ AccName := Value;
+ end;
+
+ procedure SharePointUrl(): Text[250]
+ begin
+ exit(AccSharePointUrl);
+ end;
+
+ procedure SharePointUrl(Value: Text[250])
+ begin
+ AccSharePointUrl := Value;
+ end;
+
+
+ procedure BaseRelativeFolderPath(): Text[250]
+ begin
+ exit(AccBaseRelativeFolderPath);
+ end;
+
+ procedure BaseRelativeFolderPath(Value: Text[250])
+ begin
+ AccBaseRelativeFolderPath := Value;
+ end;
+
+ procedure Password(): Text
+ begin
+ exit(AccPassword);
+ end;
+
+ procedure Password(Value: Text)
+ begin
+ AccPassword := Value;
+ end;
+
+ procedure ClientId(): Guid
+ begin
+ exit(AccClientId);
+ end;
+
+ procedure ClientId(Value: Guid)
+ begin
+ AccClientId := Value;
+ end;
+
+ procedure TenantId(): Guid
+ begin
+ exit(AccTenantId);
+ end;
+
+ procedure TenantId(Value: Guid)
+ begin
+ AccTenantId := Value;
+ end;
+
+ var
+ AccName: Text[250];
+ AccSharePointUrl: Text[250];
+ AccBaseRelativeFolderPath: Text[250];
+ AccPassword: Text;
+ AccTenantId: Guid;
+ AccClientId: Guid;
+}
\ No newline at end of file
diff --git a/Build/projects/1st Party Apps Tests (W1)/.AL-Go/settings.json b/Build/projects/1st Party Apps Tests (W1)/.AL-Go/settings.json
index 67ee6bfadc..372f66451a 100644
--- a/Build/projects/1st Party Apps Tests (W1)/.AL-Go/settings.json
+++ b/Build/projects/1st Party Apps Tests (W1)/.AL-Go/settings.json
@@ -58,7 +58,10 @@
"..\\..\\..\\Apps\\W1\\PowerBIReports\\test-library",
"..\\..\\..\\Apps\\W1\\EDocumentConnectors\\Avalara\\test",
"..\\..\\..\\Apps\\W1\\EDocumentConnectors\\Logiq\\test",
- "..\\..\\..\\Apps\\W1\\EDocumentConnectors\\Microsoft365\\test"
+ "..\\..\\..\\Apps\\W1\\EDocumentConnectors\\Microsoft365\\test",
+ "..\\..\\..\\Apps\\W1\\External File Storage - Azure Blob Service Connector\\test",
+ "..\\..\\..\\Apps\\W1\\External File Storage - Azure File Service Connector\\test",
+ "..\\..\\..\\Apps\\W1\\External File Storage - SharePoint Connector\\test"
],
"doNotRunTests": false
}
\ No newline at end of file