From 768a064797867137202d0f99874217c8b67ff6df Mon Sep 17 00:00:00 2001 From: Tine Staric Date: Mon, 15 Dec 2025 11:51:39 +0100 Subject: [PATCH 1/7] Add support for Microsoft Graph API in SharePoint Connector - Introduced a new field "Use Graph API" in Ext. SharePoint Account table and page to toggle between SharePoint REST API and Microsoft Graph API. - Updated tooltips for clarity on folder paths for both APIs. - Refactored Ext. SharePoint Connector implementation to utilize Graph API for file operations, including ListFiles, GetFile, CreateFile, CopyFile, MoveFile, FileExists, and DeleteFile. - Created Ext. SharePoint Graph Helper codeunit to encapsulate Graph API logic for file and directory operations. - Added Ext. SharePoint REST Helper codeunit for existing REST API operations. - Ensured compatibility with existing functionality while providing an option to leverage Microsoft Graph API for enhanced performance and capabilities. --- .../App/src/ExtSharePointAccount.Page.al | 1 + .../App/src/ExtSharePointAccount.Table.al | 12 +- .../src/ExtSharePointAccountWizard.Page.al | 8 +- .../ExtSharePointConnectorImpl.Codeunit.al | 269 +++---------- .../src/ExtSharePointGraphHelper.Codeunit.al | 369 ++++++++++++++++++ .../src/ExtSharePointRestHelper.Codeunit.al | 287 ++++++++++++++ 6 files changed, 730 insertions(+), 216 deletions(-) create mode 100644 src/Apps/W1/External File Storage - SharePoint Connector/App/src/ExtSharePointGraphHelper.Codeunit.al create mode 100644 src/Apps/W1/External File Storage - SharePoint Connector/App/src/ExtSharePointRestHelper.Codeunit.al diff --git a/src/Apps/W1/External File Storage - SharePoint Connector/App/src/ExtSharePointAccount.Page.al b/src/Apps/W1/External File Storage - SharePoint Connector/App/src/ExtSharePointAccount.Page.al index 6d7ab60256..35ea3bc919 100644 --- a/src/Apps/W1/External File Storage - SharePoint Connector/App/src/ExtSharePointAccount.Page.al +++ b/src/Apps/W1/External File Storage - SharePoint Connector/App/src/ExtSharePointAccount.Page.al @@ -97,6 +97,7 @@ page 4580 "Ext. SharePoint Account" } } field(Disabled; Rec.Disabled) { } + field("Use Graph API"; Rec."Use Graph API") { } } } diff --git a/src/Apps/W1/External File Storage - SharePoint Connector/App/src/ExtSharePointAccount.Table.al b/src/Apps/W1/External File Storage - SharePoint Connector/App/src/ExtSharePointAccount.Table.al index ebc2ac2128..b43c69af66 100644 --- a/src/Apps/W1/External File Storage - SharePoint Connector/App/src/ExtSharePointAccount.Table.al +++ b/src/Apps/W1/External File Storage - SharePoint Connector/App/src/ExtSharePointAccount.Table.al @@ -34,7 +34,7 @@ table 4580 "Ext. SharePoint Account" 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).'; + ToolTip = 'Specifies the base folder path. For SharePoint REST API: Use the full server-relative path including the site (e.g., /sites/ProjectX/Shared Documents/Reports). For Microsoft Graph API: Use only the path relative to the document library (e.g., Reports). When using Graph API, the path should not include the site or document library root, only the folders within the library.'; } field(6; "Tenant Id"; Guid) { @@ -50,6 +50,7 @@ table 4580 "Ext. SharePoint Account" } field(8; "Client Secret Key"; Guid) { + Caption = 'Client Secret Key'; Access = Internal; DataClassification = SystemMetadata; } @@ -66,14 +67,23 @@ table 4580 "Ext. SharePoint Account" } field(11; "Certificate Key"; Guid) { + Caption = 'Certificate Key'; Access = Internal; + AllowInCustomizations = Never; DataClassification = SystemMetadata; } field(12; "Certificate Password Key"; Guid) { + Caption = 'Certificate Password Key'; Access = Internal; + AllowInCustomizations = Never; DataClassification = SystemMetadata; } + field(13; "Use Graph API"; Boolean) + { + Caption = 'Use Microsoft Graph API'; + ToolTip = 'Specifies whether to use Microsoft Graph API or SharePoint REST API. Microsoft Graph API supports downloading files larger than 150 MB through chunked transfers. Note: Requires Microsoft Graph permissions (Sites.ReadWrite.All) configured in your app registration instead of SharePoint permissions.'; + } } keys diff --git a/src/Apps/W1/External File Storage - SharePoint Connector/App/src/ExtSharePointAccountWizard.Page.al b/src/Apps/W1/External File Storage - SharePoint Connector/App/src/ExtSharePointAccountWizard.Page.al index 10fef6fb73..baff31f23b 100644 --- a/src/Apps/W1/External File Storage - SharePoint Connector/App/src/ExtSharePointAccountWizard.Page.al +++ b/src/Apps/W1/External File Storage - SharePoint Connector/App/src/ExtSharePointAccountWizard.Page.al @@ -43,7 +43,6 @@ page 4581 "Ext. SharePoint Account Wizard" Caption = 'Account Name'; NotBlank = true; ShowMandatory = true; - ToolTip = 'Specifies a descriptive name for this SharePoint storage account connection.'; trigger OnValidate() begin @@ -54,7 +53,6 @@ page 4581 "Ext. SharePoint Account Wizard" field("Tenant Id"; Rec."Tenant Id") { ShowMandatory = true; - ToolTip = 'Specifies the Microsoft Entra ID Tenant ID (Directory ID) where your SharePoint site and app registration are located.'; trigger OnValidate() begin @@ -65,7 +63,6 @@ page 4581 "Ext. SharePoint Account Wizard" field("Client Id"; Rec."Client Id") { ShowMandatory = true; - ToolTip = 'Specifies the Client ID (Application ID) of the App Registration in Microsoft Entra ID.'; trigger OnValidate() begin @@ -75,7 +72,6 @@ page 4581 "Ext. SharePoint Account Wizard" field("Authentication Type"; Rec."Authentication Type") { - ToolTip = 'Specifies the authentication flow used for this SharePoint account. Client Secret uses User grant flow, which means that the user must sign in when using this account. Certificate uses Client credentials flow, which means that the user does not need to sign in when using this account.'; trigger OnValidate() begin UpdateAuthTypeVisibility(); @@ -144,6 +140,10 @@ page 4581 "Ext. SharePoint Account Wizard" IsNextEnabled := SharePointConnectorImpl.IsAccountValid(Rec); end; } + + field("Use Graph API"; Rec."Use Graph API") + { + } } } diff --git a/src/Apps/W1/External File Storage - SharePoint Connector/App/src/ExtSharePointConnectorImpl.Codeunit.al b/src/Apps/W1/External File Storage - SharePoint Connector/App/src/ExtSharePointConnectorImpl.Codeunit.al index 6411930b0b..455b923457 100644 --- a/src/Apps/W1/External File Storage - SharePoint Connector/App/src/ExtSharePointConnectorImpl.Codeunit.al +++ b/src/Apps/W1/External File Storage - SharePoint Connector/App/src/ExtSharePointConnectorImpl.Codeunit.al @@ -5,10 +5,8 @@ namespace System.ExternalFileStorage; -using System.DataAdministration; -using System.Integration.Sharepoint; using System.Text; -using System.Utilities; +using System.DataAdministration; codeunit 4580 "Ext. SharePoint Connector Impl" implements "External File Storage Connector" { @@ -18,9 +16,13 @@ codeunit 4580 "Ext. SharePoint Connector Impl" implements "External File Storage Permissions = tabledata "Ext. SharePoint Account" = rimd; var + RestHelper: Codeunit "Ext. SharePoint REST Helper"; + GraphHelper: Codeunit "Ext. SharePoint Graph Helper"; 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.'; + #region File Operations + /// /// Gets a List of Files stored on the provided account. /// @@ -29,29 +31,11 @@ codeunit 4580 "Ext. SharePoint Connector Impl" implements "External File Storage /// 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; + if GetUseGraphAPI(AccountId) then + GraphHelper.ListFiles(AccountId, Path, FilePaginationData, TempFileAccountContent) + else + RestHelper.ListFiles(AccountId, Path, FilePaginationData, TempFileAccountContent); end; /// @@ -61,20 +45,11 @@ codeunit 4580 "Ext. SharePoint Connector Impl" implements "External File Storage /// 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); + if GetUseGraphAPI(AccountId) then + GraphHelper.GetFile(AccountId, Path, Stream) + else + RestHelper.GetFile(AccountId, Path, Stream); end; /// @@ -84,18 +59,11 @@ codeunit 4580 "Ext. SharePoint Connector Impl" implements "External File Storage /// 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); + if GetUseGraphAPI(AccountId) then + GraphHelper.CreateFile(AccountId, Path, Stream) + else + RestHelper.CreateFile(AccountId, Path, Stream); end; /// @@ -105,14 +73,11 @@ codeunit 4580 "Ext. SharePoint Connector Impl" implements "External File Storage /// 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); + if GetUseGraphAPI(AccountId) then + GraphHelper.CopyFile(AccountId, SourcePath, TargetPath) + else + RestHelper.CopyFile(AccountId, SourcePath, TargetPath); end; /// @@ -122,12 +87,11 @@ codeunit 4580 "Ext. SharePoint Connector Impl" implements "External File Storage /// 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); + if GetUseGraphAPI(AccountId) then + GraphHelper.MoveFile(AccountId, SourcePath, TargetPath) + else + RestHelper.MoveFile(AccountId, SourcePath, TargetPath); end; /// @@ -137,17 +101,11 @@ codeunit 4580 "Ext. SharePoint Connector Impl" implements "External File Storage /// 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()); + if GetUseGraphAPI(AccountId) then + exit(GraphHelper.FileExists(AccountId, Path)) + else + exit(RestHelper.FileExists(AccountId, Path)); end; /// @@ -156,15 +114,11 @@ codeunit 4580 "Ext. SharePoint Connector Impl" implements "External File Storage /// 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); + if GetUseGraphAPI(AccountId) then + GraphHelper.DeleteFile(AccountId, Path) + else + RestHelper.DeleteFile(AccountId, Path); end; /// @@ -175,29 +129,11 @@ codeunit 4580 "Ext. SharePoint Connector Impl" implements "External File Storage /// 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; + if GetUseGraphAPI(AccountId) then + GraphHelper.ListDirectories(AccountId, Path, FilePaginationData, TempFileAccountContent) + else + RestHelper.ListDirectories(AccountId, Path, FilePaginationData, TempFileAccountContent); end; /// @@ -206,16 +142,11 @@ codeunit 4580 "Ext. SharePoint Connector Impl" implements "External File Storage /// 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); + if GetUseGraphAPI(AccountId) then + GraphHelper.CreateDirectory(AccountId, Path) + else + RestHelper.CreateDirectory(AccountId, Path); end; /// @@ -225,16 +156,11 @@ codeunit 4580 "Ext. SharePoint Connector Impl" implements "External File Storage /// The directory path inside the file account. /// Returns true if the directory exists procedure DirectoryExists(AccountId: Guid; Path: Text) Result: Boolean - var - SharePointClient: Codeunit "SharePoint Client"; begin - InitPath(AccountId, Path); - InitSharePointClient(AccountId, SharePointClient); - - Result := SharePointClient.FolderExistsByServerRelativeUrl(Path); - - if not SharePointClient.GetDiagnostics().IsSuccessStatusCode() then - ShowError(SharePointClient); + if GetUseGraphAPI(AccountId) then + exit(GraphHelper.DirectoryExists(AccountId, Path)) + else + exit(RestHelper.DirectoryExists(AccountId, Path)); end; /// @@ -243,15 +169,21 @@ codeunit 4580 "Ext. SharePoint Connector Impl" implements "External File Storage /// 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; + if GetUseGraphAPI(AccountId) then + GraphHelper.DeleteDirectory(AccountId, Path) + else + RestHelper.DeleteDirectory(AccountId, Path); + end; + + #endregion - ShowError(SharePointClient); + local procedure GetUseGraphAPI(AccountId: Guid): Boolean + var + SharePointAccount: Record "Ext. SharePoint Account"; + begin + SharePointAccount.Get(AccountId); + exit(SharePointAccount."Use Graph API"); end; /// @@ -383,91 +315,6 @@ codeunit 4580 "Ext. SharePoint Connector Impl" implements "External File Storage 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'); - - case SharePointAccount."Authentication Type" of - Enum::"Ext. SharePoint Auth Type"::"Client Secret": - SharePointAuthorization := SharePointAuth.CreateAuthorizationCode( - Format(SharePointAccount."Tenant Id", 0, 4), - Format(SharePointAccount."Client Id", 0, 4), - SharePointAccount.GetClientSecret(SharePointAccount."Client Secret Key"), - Scopes); - Enum::"Ext. SharePoint Auth Type"::Certificate: - SharePointAuthorization := SharePointAuth.CreateClientCredentials( - Format(SharePointAccount."Tenant Id", 0, 4), - Format(SharePointAccount."Client Id", 0, 4), - SharePointAccount.GetCertificate(SharePointAccount."Certificate Key"), - SharePointAccount.GetCertificatePassword(SharePointAccount."Certificate Password Key"), - Scopes); - end; - - 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 diff --git a/src/Apps/W1/External File Storage - SharePoint Connector/App/src/ExtSharePointGraphHelper.Codeunit.al b/src/Apps/W1/External File Storage - SharePoint Connector/App/src/ExtSharePointGraphHelper.Codeunit.al new file mode 100644 index 0000000000..e98d9b9cfc --- /dev/null +++ b/src/Apps/W1/External File Storage - SharePoint Connector/App/src/ExtSharePointGraphHelper.Codeunit.al @@ -0,0 +1,369 @@ +// ------------------------------------------------------------------------------------------------ +// 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.Integration.Graph.Authorization; +using System.Integration.Sharepoint; +using System.Utilities; +using System.Integration.Graph; + +/// +/// Helper implementation for SharePoint file operations using Microsoft Graph API. +/// This codeunit contains the actual Graph API logic, called by the main connector based on account settings. +/// +codeunit 4581 "Ext. SharePoint Graph Helper" +{ + Access = Internal; + InherentEntitlements = X; + InherentPermissions = X; + Permissions = tabledata "Ext. SharePoint Account" = rimd; + + + var + SharePointGraphClient: Codeunit "SharePoint Graph Client"; + AccountDisabledErr: Label 'The account "%1" is disabled.', Comment = '%1 - Account Name'; + ErrorOccurredErr: Label 'An error occurred.\%1', Comment = '%1 - Error message from Graph API'; + + #region File Operations + + internal procedure ListFiles(AccountId: Guid; Path: Text; FilePaginationData: Codeunit "File Pagination Data"; var TempFileAccountContent: Record "File Account Content" temporary) + var + GraphDriveItem: Record "SharePoint Graph Drive Item"; + OriginalPath: Text; + begin + OriginalPath := Path; + InitPath(AccountId, Path); + InitializeGraphClient(AccountId); + + // List children in the directory + Path := Path.TrimEnd('/'); + if Path = '' then + Path := '/'; + + SharePointGraphClient.GetItemsByPath(Path, GraphDriveItem); + + if GraphDriveItem.FindSet() then + repeat + // Only include files (not folders) + if not GraphDriveItem.IsFolder then begin + TempFileAccountContent.Init(); + TempFileAccountContent.Name := GraphDriveItem.Name; + TempFileAccountContent.Type := TempFileAccountContent.Type::"File"; + TempFileAccountContent."Parent Directory" := CopyStr(OriginalPath, 1, MaxStrLen(TempFileAccountContent."Parent Directory")); + TempFileAccountContent.Insert(); + end; + until GraphDriveItem.Next() = 0; + + FilePaginationData.SetEndOfListing(true); + end; + + internal procedure GetFile(AccountId: Guid; Path: Text; Stream: InStream) + var + TempBlob: Codeunit "Temp Blob"; + Response: Codeunit "SharePoint Graph Response"; + Content: HttpContent; + begin + InitPath(AccountId, Path); + InitializeGraphClient(AccountId); + + // Use chunked download for all files to handle >150MB files + Response := SharePointGraphClient.DownloadLargeFileByPath(Path, TempBlob); + + if not Response.IsSuccessful() then + ShowError(Response); + + // TempBlob is cleared after the procedure. HttpContent "hack" keeps the data. + Content.WriteFrom(TempBlob.CreateInStream()); + Content.ReadAs(Stream); + end; + + internal procedure CreateFile(AccountId: Guid; Path: Text; Stream: InStream) + var + GraphDriveItem: Record "SharePoint Graph Drive Item"; + Response: Codeunit "SharePoint Graph Response"; + FileName: Text; + FolderPath: Text; + begin + InitPath(AccountId, Path); + InitializeGraphClient(AccountId); + + // Split path into folder and filename + SplitPath(Path, FolderPath, FileName); + + // Use chunked upload (handles files of all sizes) + Response := SharePointGraphClient.UploadLargeFile(FolderPath, FileName, Stream, GraphDriveItem); + + if not Response.IsSuccessful() then + ShowError(Response); + end; + + internal procedure CopyFile(AccountId: Guid; SourcePath: Text; TargetPath: Text) + var + Response: Codeunit "SharePoint Graph Response"; + FileName: Text; + TargetFolderPath: Text; + begin + InitPath(AccountId, SourcePath); + InitPath(AccountId, TargetPath); + InitializeGraphClient(AccountId); + + // Split destination path into folder and filename + SplitPath(TargetPath, TargetFolderPath, FileName); + + // Use native Graph API copy operation + Response := SharePointGraphClient.CopyItemByPath(SourcePath, TargetFolderPath, FileName); + + if not Response.IsSuccessful() then + ShowError(Response); + end; + + internal procedure MoveFile(AccountId: Guid; SourcePath: Text; TargetPath: Text) + var + Response: Codeunit "SharePoint Graph Response"; + FileName: Text; + TargetFolderPath: Text; + begin + InitPath(AccountId, SourcePath); + InitPath(AccountId, TargetPath); + InitializeGraphClient(AccountId); + + // Split destination path into folder and filename + SplitPath(TargetPath, TargetFolderPath, FileName); + + // Use native Graph API move operation + Response := SharePointGraphClient.MoveItemByPath(SourcePath, TargetFolderPath, FileName); + + if not Response.IsSuccessful() then + ShowError(Response); + end; + + internal procedure FileExists(AccountId: Guid; Path: Text): Boolean + var + Response: Codeunit "SharePoint Graph Response"; + Exists: Boolean; + begin + InitPath(AccountId, Path); + InitializeGraphClient(AccountId); + + Response := SharePointGraphClient.ItemExistsByPath(Path, Exists); + + if not Response.IsSuccessful() then + ShowError(Response); + + exit(Exists); + end; + + internal procedure DeleteFile(AccountId: Guid; Path: Text) + var + Response: Codeunit "SharePoint Graph Response"; + begin + InitPath(AccountId, Path); + InitializeGraphClient(AccountId); + + Response := SharePointGraphClient.DeleteItemByPath(Path); + + if not Response.IsSuccessful() then + ShowError(Response); + end; + + #endregion + + #region Directory Operations + + internal procedure ListDirectories(AccountId: Guid; Path: Text; FilePaginationData: Codeunit "File Pagination Data"; var TempFileAccountContent: Record "File Account Content" temporary) + var + TempGraphDriveItem: Record "SharePoint Graph Drive Item" temporary; + OriginalPath: Text; + begin + OriginalPath := Path; + InitPath(AccountId, Path); + InitializeGraphClient(AccountId); + + // List children in the directory + Path := Path.TrimEnd('/'); + if Path = '' then + Path := '/'; + + SharePointGraphClient.GetItemsByPath(Path, TempGraphDriveItem); + + if TempGraphDriveItem.FindSet() then + repeat + // Only include folders + if TempGraphDriveItem.IsFolder then begin + TempFileAccountContent.Init(); + TempFileAccountContent.Name := TempGraphDriveItem.Name; + TempFileAccountContent.Type := TempFileAccountContent.Type::Directory; + TempFileAccountContent."Parent Directory" := CopyStr(OriginalPath, 1, MaxStrLen(TempFileAccountContent."Parent Directory")); + TempFileAccountContent.Insert(); + end; + until TempGraphDriveItem.Next() = 0; + + FilePaginationData.SetEndOfListing(true); + end; + + internal procedure CreateDirectory(AccountId: Guid; Path: Text) + var + GraphDriveItem: Record "SharePoint Graph Drive Item"; + Response: Codeunit "SharePoint Graph Response"; + ParentPath: Text; + FolderName: Text; + begin + InitPath(AccountId, Path); + InitializeGraphClient(AccountId); + + // Split path into parent path and folder name + SplitPath(Path, ParentPath, FolderName); + + Response := SharePointGraphClient.CreateFolder(ParentPath, FolderName, GraphDriveItem); + + if not Response.IsSuccessful() then + ShowError(Response); + end; + + internal procedure DirectoryExists(AccountId: Guid; Path: Text): Boolean + var + Response: Codeunit "SharePoint Graph Response"; + Exists: Boolean; + begin + InitPath(AccountId, Path); + InitializeGraphClient(AccountId); + + Response := SharePointGraphClient.ItemExistsByPath(Path, Exists); + + if not Response.IsSuccessful() then + ShowError(Response); + + exit(Exists); + end; + + internal procedure DeleteDirectory(AccountId: Guid; Path: Text) + var + Response: Codeunit "SharePoint Graph Response"; + begin + InitPath(AccountId, Path); + InitializeGraphClient(AccountId); + + Response := SharePointGraphClient.DeleteItemByPath(Path); + + if not Response.IsSuccessful() then + ShowError(Response); + end; + + #endregion + + #region Helper Methods + + local procedure InitializeGraphClient(AccountId: Guid) + var + SharePointAccount: Record "Ext. SharePoint Account"; + GraphAuthorization: Codeunit "Graph Authorization"; + GraphAuthInterface: Interface "Graph Authorization"; + ClientSecret: SecretText; + Certificate: SecretText; + CertificatePassword: SecretText; + Scopes: List of [Text]; + begin + // Get and validate account + SharePointAccount.Get(AccountId); + if SharePointAccount.Disabled then + Error(AccountDisabledErr, SharePointAccount.Name); + + // Add required SharePoint scopes + Scopes.Add('https://graph.microsoft.com/.default'); + + // Create authorization based on authentication type + case SharePointAccount."Authentication Type" of + SharePointAccount."Authentication Type"::"Client Secret": + begin + ClientSecret := SharePointAccount.GetClientSecret(SharePointAccount."Client Secret Key"); + GraphAuthInterface := GraphAuthorization.CreateAuthorizationWithClientCredentials( + Format(SharePointAccount."Tenant Id", 0, 4), + Format(SharePointAccount."Client Id", 0, 4), + ClientSecret, + Scopes); + end; + SharePointAccount."Authentication Type"::Certificate: + begin + Certificate := SharePointAccount.GetCertificate(SharePointAccount."Certificate Key"); + CertificatePassword := SharePointAccount.GetCertificatePassword(SharePointAccount."Certificate Password Key"); + GraphAuthInterface := GraphAuthorization.CreateAuthorizationWithClientCredentials( + Format(SharePointAccount."Tenant Id", 0, 4), + Format(SharePointAccount."Client Id", 0, 4), + Certificate, + CertificatePassword, + Scopes); + end; + end; + + // Initialize SharePoint Graph Client with Site URL and authorization + SharePointGraphClient.Initialize(SharePointAccount."SharePoint Url", GraphAuthInterface); + end; + + local procedure ShowError(Response: Codeunit "SharePoint Graph Response") + begin + Error(ErrorOccurredErr, Response.GetError()); + end; + + local procedure SplitPath(FullPath: Text; var FolderPath: Text; var ItemName: Text) + var + LastSlashPos: Integer; + begin + // Find the last slash to split path + LastSlashPos := StrPos(ReverseString(FullPath), '/'); + + if LastSlashPos = 0 then begin + // No slash found - item is in root + FolderPath := '/'; + ItemName := FullPath; + end else begin + LastSlashPos := StrLen(FullPath) - LastSlashPos + 1; + FolderPath := CopyStr(FullPath, 1, LastSlashPos - 1); + ItemName := CopyStr(FullPath, LastSlashPos + 1); + + if FolderPath = '' then + FolderPath := '/'; + end; + end; + + local procedure ReverseString(InputString: Text): Text + var + ReversedString: Text; + i: Integer; + begin + for i := StrLen(InputString) downto 1 do + ReversedString := ReversedString + CopyStr(InputString, i, 1); + exit(ReversedString); + end; + + local procedure PathSeparator(): Text + begin + exit('/'); + 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; + + #endregion +} diff --git a/src/Apps/W1/External File Storage - SharePoint Connector/App/src/ExtSharePointRestHelper.Codeunit.al b/src/Apps/W1/External File Storage - SharePoint Connector/App/src/ExtSharePointRestHelper.Codeunit.al new file mode 100644 index 0000000000..6e8a2447e4 --- /dev/null +++ b/src/Apps/W1/External File Storage - SharePoint Connector/App/src/ExtSharePointRestHelper.Codeunit.al @@ -0,0 +1,287 @@ +// ------------------------------------------------------------------------------------------------ +// 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.Integration.Sharepoint; +using System.Utilities; + +/// +/// Helper implementation for SharePoint file operations using SharePoint REST API. +/// This codeunit contains the actual REST API logic, called by the main connector based on account settings. +/// +codeunit 4582 "Ext. SharePoint REST Helper" +{ + Access = Internal; + InherentEntitlements = X; + InherentPermissions = X; + Permissions = tabledata "Ext. SharePoint Account" = rimd; + + + #region File Operations + + internal 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"; + OriginalPath: Text; + begin + OriginalPath := 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(OriginalPath, 1, MaxStrLen(TempFileAccountContent."Parent Directory")); + TempFileAccountContent.Insert(); + until SharePointFile.Next() = 0; + end; + + internal 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; + + internal 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; + + internal 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; + + internal procedure MoveFile(AccountId: Guid; SourcePath: Text; TargetPath: Text) + var + Stream: InStream; + begin + GetFile(AccountId, SourcePath, Stream); + CreateFile(AccountId, TargetPath, Stream); + DeleteFile(AccountId, SourcePath); + end; + + internal 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; + + internal 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; + + #endregion + + #region Directory Operations + + internal 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"; + OriginalPath: Text; + begin + OriginalPath := 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(OriginalPath, 1, MaxStrLen(TempFileAccountContent."Parent Directory")); + TempFileAccountContent.Insert(); + until SharePointFolder.Next() = 0; + end; + + internal 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; + + internal procedure DirectoryExists(AccountId: Guid; Path: Text) Result: Boolean + var + SharePointClient: Codeunit "SharePoint Client"; + begin + InitPath(AccountId, Path); + InitSharePointClient(AccountId, SharePointClient); + + Result := SharePointClient.FolderExistsByServerRelativeUrl(Path); + + if not SharePointClient.GetDiagnostics().IsSuccessStatusCode() then + ShowError(SharePointClient); + end; + + internal 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; + + #endregion + + #region Helper Methods + + 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'); + + case SharePointAccount."Authentication Type" of + Enum::"Ext. SharePoint Auth Type"::"Client Secret": + SharePointAuthorization := SharePointAuth.CreateAuthorizationCode( + Format(SharePointAccount."Tenant Id", 0, 4), + Format(SharePointAccount."Client Id", 0, 4), + SharePointAccount.GetClientSecret(SharePointAccount."Client Secret Key"), + Scopes); + Enum::"Ext. SharePoint Auth Type"::Certificate: + SharePointAuthorization := SharePointAuth.CreateClientCredentials( + Format(SharePointAccount."Tenant Id", 0, 4), + Format(SharePointAccount."Client Id", 0, 4), + SharePointAccount.GetCertificate(SharePointAccount."Certificate Key"), + SharePointAccount.GetCertificatePassword(SharePointAccount."Certificate Password Key"), + Scopes); + end; + + SharePointClient.Initialize(SharePointAccount."SharePoint Url", SharePointAuthorization); + end; + + local procedure PathSeparator(): Text + begin + exit('/'); + end; + + local procedure ShowError(var SharePointClient: Codeunit "SharePoint Client") + var + ErrorOccurredErr: Label 'An error occurred.\%1', Comment = '%1 - Error message from SharePoint'; + begin + Error(ErrorOccurredErr, 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; + + #endregion +} From 8e7bd9104bd473dbf1e8f53888e298626ebebe76 Mon Sep 17 00:00:00 2001 From: Tine Staric Date: Mon, 15 Dec 2025 12:04:23 +0100 Subject: [PATCH 2/7] Fix parameter assignment for certificate in SetParameters method --- .../src/Authorization/GraphAuthClientCredentials.Codeunit.al | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/System Application/App/MicrosoftGraph/src/Authorization/GraphAuthClientCredentials.Codeunit.al b/src/System Application/App/MicrosoftGraph/src/Authorization/GraphAuthClientCredentials.Codeunit.al index 2cc65ce240..d2a155e973 100644 --- a/src/System Application/App/MicrosoftGraph/src/Authorization/GraphAuthClientCredentials.Codeunit.al +++ b/src/System Application/App/MicrosoftGraph/src/Authorization/GraphAuthClientCredentials.Codeunit.al @@ -36,7 +36,7 @@ codeunit 9357 "Graph Auth. Client Credentials" implements "Graph Authorization" ClientCredentialsType := ClientCredentialsType::Certificate; AadTenantId := NewAadTenantId; ClientId := NewClientId; - Certificate := Certificate; + Certificate := NewCertificate; CertificatePassword := NewCertificatePassword; Scopes := NewScopes; end; From a57ebeac83bb3e3f8e2e7665efa4f64a0f1226bc Mon Sep 17 00:00:00 2001 From: Tine Staric Date: Sun, 28 Dec 2025 11:52:45 +0100 Subject: [PATCH 3/7] Refactor SharePoint file operations to use SharePointAccount record instead of AccountId --- .../ExtSharePointConnectorImpl.Codeunit.al | 107 +++++++++++------- .../src/ExtSharePointGraphHelper.Codeunit.al | 79 ++++++------- .../src/ExtSharePointRestHelper.Codeunit.al | 77 ++++++------- 3 files changed, 139 insertions(+), 124 deletions(-) diff --git a/src/Apps/W1/External File Storage - SharePoint Connector/App/src/ExtSharePointConnectorImpl.Codeunit.al b/src/Apps/W1/External File Storage - SharePoint Connector/App/src/ExtSharePointConnectorImpl.Codeunit.al index 455b923457..2cd6af703f 100644 --- a/src/Apps/W1/External File Storage - SharePoint Connector/App/src/ExtSharePointConnectorImpl.Codeunit.al +++ b/src/Apps/W1/External File Storage - SharePoint Connector/App/src/ExtSharePointConnectorImpl.Codeunit.al @@ -31,11 +31,14 @@ codeunit 4580 "Ext. SharePoint Connector Impl" implements "External File Storage /// 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 + SharePointAccount: Record "Ext. SharePoint Account"; begin - if GetUseGraphAPI(AccountId) then - GraphHelper.ListFiles(AccountId, Path, FilePaginationData, TempFileAccountContent) + SharePointAccount.Get(AccountId); + if SharePointAccount."Use Graph API" then + GraphHelper.ListFiles(SharePointAccount, Path, FilePaginationData, TempFileAccountContent) else - RestHelper.ListFiles(AccountId, Path, FilePaginationData, TempFileAccountContent); + RestHelper.ListFiles(SharePointAccount, Path, FilePaginationData, TempFileAccountContent); end; /// @@ -45,11 +48,14 @@ codeunit 4580 "Ext. SharePoint Connector Impl" implements "External File Storage /// The file path inside the file account. /// The Stream were the file is read to. procedure GetFile(AccountId: Guid; Path: Text; Stream: InStream) + var + SharePointAccount: Record "Ext. SharePoint Account"; begin - if GetUseGraphAPI(AccountId) then - GraphHelper.GetFile(AccountId, Path, Stream) + SharePointAccount.Get(AccountId); + if SharePointAccount."Use Graph API" then + GraphHelper.GetFile(SharePointAccount, Path, Stream) else - RestHelper.GetFile(AccountId, Path, Stream); + RestHelper.GetFile(SharePointAccount, Path, Stream); end; /// @@ -59,11 +65,14 @@ codeunit 4580 "Ext. SharePoint Connector Impl" implements "External File Storage /// The file path inside the file account. /// The Stream were the file is read from. procedure CreateFile(AccountId: Guid; Path: Text; Stream: InStream) + var + SharePointAccount: Record "Ext. SharePoint Account"; begin - if GetUseGraphAPI(AccountId) then - GraphHelper.CreateFile(AccountId, Path, Stream) + SharePointAccount.Get(AccountId); + if SharePointAccount."Use Graph API" then + GraphHelper.CreateFile(SharePointAccount, Path, Stream) else - RestHelper.CreateFile(AccountId, Path, Stream); + RestHelper.CreateFile(SharePointAccount, Path, Stream); end; /// @@ -73,11 +82,14 @@ codeunit 4580 "Ext. SharePoint Connector Impl" implements "External File Storage /// The source file path. /// The target file path. procedure CopyFile(AccountId: Guid; SourcePath: Text; TargetPath: Text) + var + SharePointAccount: Record "Ext. SharePoint Account"; begin - if GetUseGraphAPI(AccountId) then - GraphHelper.CopyFile(AccountId, SourcePath, TargetPath) + SharePointAccount.Get(AccountId); + if SharePointAccount."Use Graph API" then + GraphHelper.CopyFile(SharePointAccount, SourcePath, TargetPath) else - RestHelper.CopyFile(AccountId, SourcePath, TargetPath); + RestHelper.CopyFile(SharePointAccount, SourcePath, TargetPath); end; /// @@ -87,11 +99,14 @@ codeunit 4580 "Ext. SharePoint Connector Impl" implements "External File Storage /// The source file path. /// The target file path. procedure MoveFile(AccountId: Guid; SourcePath: Text; TargetPath: Text) + var + SharePointAccount: Record "Ext. SharePoint Account"; begin - if GetUseGraphAPI(AccountId) then - GraphHelper.MoveFile(AccountId, SourcePath, TargetPath) + SharePointAccount.Get(AccountId); + if SharePointAccount."Use Graph API" then + GraphHelper.MoveFile(SharePointAccount, SourcePath, TargetPath) else - RestHelper.MoveFile(AccountId, SourcePath, TargetPath); + RestHelper.MoveFile(SharePointAccount, SourcePath, TargetPath); end; /// @@ -101,11 +116,14 @@ codeunit 4580 "Ext. SharePoint Connector Impl" implements "External File Storage /// The file path inside the file account. /// Returns true if the file exists procedure FileExists(AccountId: Guid; Path: Text): Boolean + var + SharePointAccount: Record "Ext. SharePoint Account"; begin - if GetUseGraphAPI(AccountId) then - exit(GraphHelper.FileExists(AccountId, Path)) + SharePointAccount.Get(AccountId); + if SharePointAccount."Use Graph API" then + exit(GraphHelper.FileExists(SharePointAccount, Path)) else - exit(RestHelper.FileExists(AccountId, Path)); + exit(RestHelper.FileExists(SharePointAccount, Path)); end; /// @@ -114,11 +132,14 @@ codeunit 4580 "Ext. SharePoint Connector Impl" implements "External File Storage /// 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 + SharePointAccount: Record "Ext. SharePoint Account"; begin - if GetUseGraphAPI(AccountId) then - GraphHelper.DeleteFile(AccountId, Path) + SharePointAccount.Get(AccountId); + if SharePointAccount."Use Graph API" then + GraphHelper.DeleteFile(SharePointAccount, Path) else - RestHelper.DeleteFile(AccountId, Path); + RestHelper.DeleteFile(SharePointAccount, Path); end; /// @@ -129,11 +150,14 @@ codeunit 4580 "Ext. SharePoint Connector Impl" implements "External File Storage /// 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 + SharePointAccount: Record "Ext. SharePoint Account"; begin - if GetUseGraphAPI(AccountId) then - GraphHelper.ListDirectories(AccountId, Path, FilePaginationData, TempFileAccountContent) + SharePointAccount.Get(AccountId); + if SharePointAccount."Use Graph API" then + GraphHelper.ListDirectories(SharePointAccount, Path, FilePaginationData, TempFileAccountContent) else - RestHelper.ListDirectories(AccountId, Path, FilePaginationData, TempFileAccountContent); + RestHelper.ListDirectories(SharePointAccount, Path, FilePaginationData, TempFileAccountContent); end; /// @@ -142,11 +166,14 @@ codeunit 4580 "Ext. SharePoint Connector Impl" implements "External File Storage /// 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 + SharePointAccount: Record "Ext. SharePoint Account"; begin - if GetUseGraphAPI(AccountId) then - GraphHelper.CreateDirectory(AccountId, Path) + SharePointAccount.Get(AccountId); + if SharePointAccount."Use Graph API" then + GraphHelper.CreateDirectory(SharePointAccount, Path) else - RestHelper.CreateDirectory(AccountId, Path); + RestHelper.CreateDirectory(SharePointAccount, Path); end; /// @@ -156,11 +183,14 @@ codeunit 4580 "Ext. SharePoint Connector Impl" implements "External File Storage /// The directory path inside the file account. /// Returns true if the directory exists procedure DirectoryExists(AccountId: Guid; Path: Text) Result: Boolean + var + SharePointAccount: Record "Ext. SharePoint Account"; begin - if GetUseGraphAPI(AccountId) then - exit(GraphHelper.DirectoryExists(AccountId, Path)) + SharePointAccount.Get(AccountId); + if SharePointAccount."Use Graph API" then + exit(GraphHelper.DirectoryExists(SharePointAccount, Path)) else - exit(RestHelper.DirectoryExists(AccountId, Path)); + exit(RestHelper.DirectoryExists(SharePointAccount, Path)); end; /// @@ -169,23 +199,18 @@ codeunit 4580 "Ext. SharePoint Connector Impl" implements "External File Storage /// 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) - begin - if GetUseGraphAPI(AccountId) then - GraphHelper.DeleteDirectory(AccountId, Path) - else - RestHelper.DeleteDirectory(AccountId, Path); - end; - - #endregion - - local procedure GetUseGraphAPI(AccountId: Guid): Boolean var SharePointAccount: Record "Ext. SharePoint Account"; begin SharePointAccount.Get(AccountId); - exit(SharePointAccount."Use Graph API"); + if SharePointAccount."Use Graph API" then + GraphHelper.DeleteDirectory(SharePointAccount, Path) + else + RestHelper.DeleteDirectory(SharePointAccount, Path); end; + #endregion + /// /// Gets the registered accounts for the SharePoint connector. /// diff --git a/src/Apps/W1/External File Storage - SharePoint Connector/App/src/ExtSharePointGraphHelper.Codeunit.al b/src/Apps/W1/External File Storage - SharePoint Connector/App/src/ExtSharePointGraphHelper.Codeunit.al index e98d9b9cfc..e33f09487c 100644 --- a/src/Apps/W1/External File Storage - SharePoint Connector/App/src/ExtSharePointGraphHelper.Codeunit.al +++ b/src/Apps/W1/External File Storage - SharePoint Connector/App/src/ExtSharePointGraphHelper.Codeunit.al @@ -29,14 +29,14 @@ codeunit 4581 "Ext. SharePoint Graph Helper" #region File Operations - internal procedure ListFiles(AccountId: Guid; Path: Text; FilePaginationData: Codeunit "File Pagination Data"; var TempFileAccountContent: Record "File Account Content" temporary) + internal procedure ListFiles(SharePointAccount: Record "Ext. SharePoint Account"; Path: Text; FilePaginationData: Codeunit "File Pagination Data"; var TempFileAccountContent: Record "File Account Content" temporary) var GraphDriveItem: Record "SharePoint Graph Drive Item"; OriginalPath: Text; begin OriginalPath := Path; - InitPath(AccountId, Path); - InitializeGraphClient(AccountId); + InitPath(SharePointAccount, Path); + InitializeGraphClient(SharePointAccount); // List children in the directory Path := Path.TrimEnd('/'); @@ -60,14 +60,14 @@ codeunit 4581 "Ext. SharePoint Graph Helper" FilePaginationData.SetEndOfListing(true); end; - internal procedure GetFile(AccountId: Guid; Path: Text; Stream: InStream) + internal procedure GetFile(SharePointAccount: Record "Ext. SharePoint Account"; Path: Text; Stream: InStream) var TempBlob: Codeunit "Temp Blob"; Response: Codeunit "SharePoint Graph Response"; Content: HttpContent; begin - InitPath(AccountId, Path); - InitializeGraphClient(AccountId); + InitPath(SharePointAccount, Path); + InitializeGraphClient(SharePointAccount); // Use chunked download for all files to handle >150MB files Response := SharePointGraphClient.DownloadLargeFileByPath(Path, TempBlob); @@ -80,15 +80,15 @@ codeunit 4581 "Ext. SharePoint Graph Helper" Content.ReadAs(Stream); end; - internal procedure CreateFile(AccountId: Guid; Path: Text; Stream: InStream) + internal procedure CreateFile(SharePointAccount: Record "Ext. SharePoint Account"; Path: Text; Stream: InStream) var GraphDriveItem: Record "SharePoint Graph Drive Item"; Response: Codeunit "SharePoint Graph Response"; FileName: Text; FolderPath: Text; begin - InitPath(AccountId, Path); - InitializeGraphClient(AccountId); + InitPath(SharePointAccount, Path); + InitializeGraphClient(SharePointAccount); // Split path into folder and filename SplitPath(Path, FolderPath, FileName); @@ -100,15 +100,15 @@ codeunit 4581 "Ext. SharePoint Graph Helper" ShowError(Response); end; - internal procedure CopyFile(AccountId: Guid; SourcePath: Text; TargetPath: Text) + internal procedure CopyFile(SharePointAccount: Record "Ext. SharePoint Account"; SourcePath: Text; TargetPath: Text) var Response: Codeunit "SharePoint Graph Response"; FileName: Text; TargetFolderPath: Text; begin - InitPath(AccountId, SourcePath); - InitPath(AccountId, TargetPath); - InitializeGraphClient(AccountId); + InitPath(SharePointAccount, SourcePath); + InitPath(SharePointAccount, TargetPath); + InitializeGraphClient(SharePointAccount); // Split destination path into folder and filename SplitPath(TargetPath, TargetFolderPath, FileName); @@ -120,15 +120,15 @@ codeunit 4581 "Ext. SharePoint Graph Helper" ShowError(Response); end; - internal procedure MoveFile(AccountId: Guid; SourcePath: Text; TargetPath: Text) + internal procedure MoveFile(SharePointAccount: Record "Ext. SharePoint Account"; SourcePath: Text; TargetPath: Text) var Response: Codeunit "SharePoint Graph Response"; FileName: Text; TargetFolderPath: Text; begin - InitPath(AccountId, SourcePath); - InitPath(AccountId, TargetPath); - InitializeGraphClient(AccountId); + InitPath(SharePointAccount, SourcePath); + InitPath(SharePointAccount, TargetPath); + InitializeGraphClient(SharePointAccount); // Split destination path into folder and filename SplitPath(TargetPath, TargetFolderPath, FileName); @@ -140,13 +140,13 @@ codeunit 4581 "Ext. SharePoint Graph Helper" ShowError(Response); end; - internal procedure FileExists(AccountId: Guid; Path: Text): Boolean + internal procedure FileExists(SharePointAccount: Record "Ext. SharePoint Account"; Path: Text): Boolean var Response: Codeunit "SharePoint Graph Response"; Exists: Boolean; begin - InitPath(AccountId, Path); - InitializeGraphClient(AccountId); + InitPath(SharePointAccount, Path); + InitializeGraphClient(SharePointAccount); Response := SharePointGraphClient.ItemExistsByPath(Path, Exists); @@ -156,12 +156,12 @@ codeunit 4581 "Ext. SharePoint Graph Helper" exit(Exists); end; - internal procedure DeleteFile(AccountId: Guid; Path: Text) + internal procedure DeleteFile(SharePointAccount: Record "Ext. SharePoint Account"; Path: Text) var Response: Codeunit "SharePoint Graph Response"; begin - InitPath(AccountId, Path); - InitializeGraphClient(AccountId); + InitPath(SharePointAccount, Path); + InitializeGraphClient(SharePointAccount); Response := SharePointGraphClient.DeleteItemByPath(Path); @@ -173,14 +173,14 @@ codeunit 4581 "Ext. SharePoint Graph Helper" #region Directory Operations - internal procedure ListDirectories(AccountId: Guid; Path: Text; FilePaginationData: Codeunit "File Pagination Data"; var TempFileAccountContent: Record "File Account Content" temporary) + internal procedure ListDirectories(SharePointAccount: Record "Ext. SharePoint Account"; Path: Text; FilePaginationData: Codeunit "File Pagination Data"; var TempFileAccountContent: Record "File Account Content" temporary) var TempGraphDriveItem: Record "SharePoint Graph Drive Item" temporary; OriginalPath: Text; begin OriginalPath := Path; - InitPath(AccountId, Path); - InitializeGraphClient(AccountId); + InitPath(SharePointAccount, Path); + InitializeGraphClient(SharePointAccount); // List children in the directory Path := Path.TrimEnd('/'); @@ -204,15 +204,15 @@ codeunit 4581 "Ext. SharePoint Graph Helper" FilePaginationData.SetEndOfListing(true); end; - internal procedure CreateDirectory(AccountId: Guid; Path: Text) + internal procedure CreateDirectory(SharePointAccount: Record "Ext. SharePoint Account"; Path: Text) var GraphDriveItem: Record "SharePoint Graph Drive Item"; Response: Codeunit "SharePoint Graph Response"; ParentPath: Text; FolderName: Text; begin - InitPath(AccountId, Path); - InitializeGraphClient(AccountId); + InitPath(SharePointAccount, Path); + InitializeGraphClient(SharePointAccount); // Split path into parent path and folder name SplitPath(Path, ParentPath, FolderName); @@ -223,13 +223,13 @@ codeunit 4581 "Ext. SharePoint Graph Helper" ShowError(Response); end; - internal procedure DirectoryExists(AccountId: Guid; Path: Text): Boolean + internal procedure DirectoryExists(SharePointAccount: Record "Ext. SharePoint Account"; Path: Text): Boolean var Response: Codeunit "SharePoint Graph Response"; Exists: Boolean; begin - InitPath(AccountId, Path); - InitializeGraphClient(AccountId); + InitPath(SharePointAccount, Path); + InitializeGraphClient(SharePointAccount); Response := SharePointGraphClient.ItemExistsByPath(Path, Exists); @@ -239,12 +239,12 @@ codeunit 4581 "Ext. SharePoint Graph Helper" exit(Exists); end; - internal procedure DeleteDirectory(AccountId: Guid; Path: Text) + internal procedure DeleteDirectory(SharePointAccount: Record "Ext. SharePoint Account"; Path: Text) var Response: Codeunit "SharePoint Graph Response"; begin - InitPath(AccountId, Path); - InitializeGraphClient(AccountId); + InitPath(SharePointAccount, Path); + InitializeGraphClient(SharePointAccount); Response := SharePointGraphClient.DeleteItemByPath(Path); @@ -256,9 +256,8 @@ codeunit 4581 "Ext. SharePoint Graph Helper" #region Helper Methods - local procedure InitializeGraphClient(AccountId: Guid) + local procedure InitializeGraphClient(SharePointAccount: Record "Ext. SharePoint Account") var - SharePointAccount: Record "Ext. SharePoint Account"; GraphAuthorization: Codeunit "Graph Authorization"; GraphAuthInterface: Interface "Graph Authorization"; ClientSecret: SecretText; @@ -267,7 +266,6 @@ codeunit 4581 "Ext. SharePoint Graph Helper" Scopes: List of [Text]; begin // Get and validate account - SharePointAccount.Get(AccountId); if SharePointAccount.Disabled then Error(AccountDisabledErr, SharePointAccount.Name); @@ -343,11 +341,8 @@ codeunit 4581 "Ext. SharePoint Graph Helper" exit('/'); end; - local procedure InitPath(AccountId: Guid; var Path: Text) - var - SharePointAccount: Record "Ext. SharePoint Account"; + local procedure InitPath(SharePointAccount: Record "Ext. SharePoint Account"; var Path: Text) begin - SharePointAccount.Get(AccountId); Path := CombinePath(SharePointAccount."Base Relative Folder Path", Path); end; diff --git a/src/Apps/W1/External File Storage - SharePoint Connector/App/src/ExtSharePointRestHelper.Codeunit.al b/src/Apps/W1/External File Storage - SharePoint Connector/App/src/ExtSharePointRestHelper.Codeunit.al index 6e8a2447e4..27f849ae29 100644 --- a/src/Apps/W1/External File Storage - SharePoint Connector/App/src/ExtSharePointRestHelper.Codeunit.al +++ b/src/Apps/W1/External File Storage - SharePoint Connector/App/src/ExtSharePointRestHelper.Codeunit.al @@ -22,15 +22,15 @@ codeunit 4582 "Ext. SharePoint REST Helper" #region File Operations - internal procedure ListFiles(AccountId: Guid; Path: Text; FilePaginationData: Codeunit "File Pagination Data"; var TempFileAccountContent: Record "File Account Content" temporary) + internal procedure ListFiles(SharePointAccount: Record "Ext. SharePoint Account"; Path: Text; FilePaginationData: Codeunit "File Pagination Data"; var TempFileAccountContent: Record "File Account Content" temporary) var SharePointFile: Record "SharePoint File"; SharePointClient: Codeunit "SharePoint Client"; OriginalPath: Text; begin OriginalPath := Path; - InitPath(AccountId, Path); - InitSharePointClient(AccountId, SharePointClient); + InitPath(SharePointAccount, Path); + InitSharePointClient(SharePointAccount, SharePointClient); if not SharePointClient.GetFolderFilesByServerRelativeUrl(Path, SharePointFile) then ShowError(SharePointClient); @@ -48,14 +48,14 @@ codeunit 4582 "Ext. SharePoint REST Helper" until SharePointFile.Next() = 0; end; - internal procedure GetFile(AccountId: Guid; Path: Text; Stream: InStream) + internal procedure GetFile(SharePointAccount: Record "Ext. SharePoint Account"; Path: Text; Stream: InStream) var SharePointClient: Codeunit "SharePoint Client"; Content: HttpContent; TempBlobStream: InStream; begin - InitPath(AccountId, Path); - InitSharePointClient(AccountId, SharePointClient); + InitPath(SharePointAccount, Path); + InitSharePointClient(SharePointAccount, SharePointClient); if not SharePointClient.DownloadFileContentByServerRelativeUrl(Path, TempBlobStream) then ShowError(SharePointClient); @@ -65,14 +65,14 @@ codeunit 4582 "Ext. SharePoint REST Helper" Content.ReadAs(Stream); end; - internal procedure CreateFile(AccountId: Guid; Path: Text; Stream: InStream) + internal procedure CreateFile(SharePointAccount: Record "Ext. SharePoint Account"; Path: Text; Stream: InStream) var SharePointFile: Record "SharePoint File"; SharePointClient: Codeunit "SharePoint Client"; ParentPath, FileName : Text; begin - InitPath(AccountId, Path); - InitSharePointClient(AccountId, SharePointClient); + InitPath(SharePointAccount, Path); + InitSharePointClient(SharePointAccount, SharePointClient); SplitPath(Path, ParentPath, FileName); if SharePointClient.AddFileToFolder(ParentPath, FileName, Stream, SharePointFile, false) then exit; @@ -80,33 +80,33 @@ codeunit 4582 "Ext. SharePoint REST Helper" ShowError(SharePointClient); end; - internal procedure CopyFile(AccountId: Guid; SourcePath: Text; TargetPath: Text) + internal procedure CopyFile(SharePointAccount: Record "Ext. SharePoint Account"; SourcePath: Text; TargetPath: Text) var TempBlob: Codeunit "Temp Blob"; Stream: InStream; begin TempBlob.CreateInStream(Stream); - GetFile(AccountId, SourcePath, Stream); - CreateFile(AccountId, TargetPath, Stream); + GetFile(SharePointAccount, SourcePath, Stream); + CreateFile(SharePointAccount, TargetPath, Stream); end; - internal procedure MoveFile(AccountId: Guid; SourcePath: Text; TargetPath: Text) + internal procedure MoveFile(SharePointAccount: Record "Ext. SharePoint Account"; SourcePath: Text; TargetPath: Text) var Stream: InStream; begin - GetFile(AccountId, SourcePath, Stream); - CreateFile(AccountId, TargetPath, Stream); - DeleteFile(AccountId, SourcePath); + GetFile(SharePointAccount, SourcePath, Stream); + CreateFile(SharePointAccount, TargetPath, Stream); + DeleteFile(SharePointAccount, SourcePath); end; - internal procedure FileExists(AccountId: Guid; Path: Text): Boolean + internal procedure FileExists(SharePointAccount: Record "Ext. SharePoint Account"; Path: Text): Boolean var SharePointFile: Record "SharePoint File"; SharePointClient: Codeunit "SharePoint Client"; begin - InitPath(AccountId, Path); - InitSharePointClient(AccountId, SharePointClient); + InitPath(SharePointAccount, Path); + InitSharePointClient(SharePointAccount, SharePointClient); if not SharePointClient.GetFolderFilesByServerRelativeUrl(GetParentPath(Path), SharePointFile) then ShowError(SharePointClient); @@ -114,12 +114,12 @@ codeunit 4582 "Ext. SharePoint REST Helper" exit(not SharePointFile.IsEmpty()); end; - internal procedure DeleteFile(AccountId: Guid; Path: Text) + internal procedure DeleteFile(SharePointAccount: Record "Ext. SharePoint Account"; Path: Text) var SharePointClient: Codeunit "SharePoint Client"; begin - InitPath(AccountId, Path); - InitSharePointClient(AccountId, SharePointClient); + InitPath(SharePointAccount, Path); + InitSharePointClient(SharePointAccount, SharePointClient); if SharePointClient.DeleteFileByServerRelativeUrl(Path) then exit; @@ -130,15 +130,15 @@ codeunit 4582 "Ext. SharePoint REST Helper" #region Directory Operations - internal procedure ListDirectories(AccountId: Guid; Path: Text; FilePaginationData: Codeunit "File Pagination Data"; var TempFileAccountContent: Record "File Account Content" temporary) + internal procedure ListDirectories(SharePointAccount: Record "Ext. SharePoint Account"; Path: Text; FilePaginationData: Codeunit "File Pagination Data"; var TempFileAccountContent: Record "File Account Content" temporary) var SharePointFolder: Record "SharePoint Folder"; SharePointClient: Codeunit "SharePoint Client"; OriginalPath: Text; begin OriginalPath := Path; - InitPath(AccountId, Path); - InitSharePointClient(AccountId, SharePointClient); + InitPath(SharePointAccount, Path); + InitSharePointClient(SharePointAccount, SharePointClient); if not SharePointClient.GetSubFoldersByServerRelativeUrl(Path, SharePointFolder) then ShowError(SharePointClient); @@ -156,25 +156,25 @@ codeunit 4582 "Ext. SharePoint REST Helper" until SharePointFolder.Next() = 0; end; - internal procedure CreateDirectory(AccountId: Guid; Path: Text) + internal procedure CreateDirectory(SharePointAccount: Record "Ext. SharePoint Account"; Path: Text) var SharePointFolder: Record "SharePoint Folder"; SharePointClient: Codeunit "SharePoint Client"; begin - InitPath(AccountId, Path); - InitSharePointClient(AccountId, SharePointClient); + InitPath(SharePointAccount, Path); + InitSharePointClient(SharePointAccount, SharePointClient); if SharePointClient.CreateFolder(Path, SharePointFolder) then exit; ShowError(SharePointClient); end; - internal procedure DirectoryExists(AccountId: Guid; Path: Text) Result: Boolean + internal procedure DirectoryExists(SharePointAccount: Record "Ext. SharePoint Account"; Path: Text) Result: Boolean var SharePointClient: Codeunit "SharePoint Client"; begin - InitPath(AccountId, Path); - InitSharePointClient(AccountId, SharePointClient); + InitPath(SharePointAccount, Path); + InitSharePointClient(SharePointAccount, SharePointClient); Result := SharePointClient.FolderExistsByServerRelativeUrl(Path); @@ -182,12 +182,12 @@ codeunit 4582 "Ext. SharePoint REST Helper" ShowError(SharePointClient); end; - internal procedure DeleteDirectory(AccountId: Guid; Path: Text) + internal procedure DeleteDirectory(SharePointAccount: Record "Ext. SharePoint Account"; Path: Text) var SharePointClient: Codeunit "SharePoint Client"; begin - InitPath(AccountId, Path); - InitSharePointClient(AccountId, SharePointClient); + InitPath(SharePointAccount, Path); + InitSharePointClient(SharePointAccount, SharePointClient); if SharePointClient.DeleteFolderByServerRelativeUrl(Path) then exit; @@ -198,15 +198,13 @@ codeunit 4582 "Ext. SharePoint REST Helper" #region Helper Methods - local procedure InitSharePointClient(var AccountId: Guid; var SharePointClient: Codeunit "SharePoint Client") + local procedure InitSharePointClient(SharePointAccount: Record "Ext. SharePoint Account"; 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); @@ -255,11 +253,8 @@ codeunit 4582 "Ext. SharePoint REST Helper" FileName := Path.TrimEnd(PathSeparator()).Substring(Path.LastIndexOf(PathSeparator()) + 1); end; - local procedure InitPath(AccountId: Guid; var Path: Text) - var - SharePointAccount: Record "Ext. SharePoint Account"; + local procedure InitPath(SharePointAccount: Record "Ext. SharePoint Account"; var Path: Text) begin - SharePointAccount.Get(AccountId); Path := CombinePath(SharePointAccount."Base Relative Folder Path", Path); end; From 4c5b2198bdeab375da515143ee3839c3d6c7eae0 Mon Sep 17 00:00:00 2001 From: Tine Staric Date: Mon, 29 Dec 2025 08:35:05 +0100 Subject: [PATCH 4/7] Implement file upload size handling in SharePoint Graph Helper --- .../App/src/ExtSharePointGraphHelper.Codeunit.al | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/Apps/W1/External File Storage - SharePoint Connector/App/src/ExtSharePointGraphHelper.Codeunit.al b/src/Apps/W1/External File Storage - SharePoint Connector/App/src/ExtSharePointGraphHelper.Codeunit.al index e33f09487c..943b7cf405 100644 --- a/src/Apps/W1/External File Storage - SharePoint Connector/App/src/ExtSharePointGraphHelper.Codeunit.al +++ b/src/Apps/W1/External File Storage - SharePoint Connector/App/src/ExtSharePointGraphHelper.Codeunit.al @@ -86,6 +86,7 @@ codeunit 4581 "Ext. SharePoint Graph Helper" Response: Codeunit "SharePoint Graph Response"; FileName: Text; FolderPath: Text; + MaxSimpleUploadSize: Integer; begin InitPath(SharePointAccount, Path); InitializeGraphClient(SharePointAccount); @@ -93,8 +94,14 @@ codeunit 4581 "Ext. SharePoint Graph Helper" // Split path into folder and filename SplitPath(Path, FolderPath, FileName); - // Use chunked upload (handles files of all sizes) - Response := SharePointGraphClient.UploadLargeFile(FolderPath, FileName, Stream, GraphDriveItem); + // Use simple upload for files under 3.9MB, chunked upload for larger files + // 3.9MB threshold provides safety margin below Graph API's 4MB limit + MaxSimpleUploadSize := 4088218; // 3.9 MB + + if Stream.Length <= MaxSimpleUploadSize then + Response := SharePointGraphClient.UploadFile(FolderPath, FileName, Stream, GraphDriveItem) + else + Response := SharePointGraphClient.UploadLargeFile(FolderPath, FileName, Stream, GraphDriveItem); if not Response.IsSuccessful() then ShowError(Response); From 24f05d8cb8b8decdb2818bf7e895406b25a8e13c Mon Sep 17 00:00:00 2001 From: Tine Staric Date: Mon, 29 Dec 2025 09:05:59 +0100 Subject: [PATCH 5/7] Update file upload size threshold to 4MB in SharePoint Graph Helper --- .../App/src/ExtSharePointGraphHelper.Codeunit.al | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Apps/W1/External File Storage - SharePoint Connector/App/src/ExtSharePointGraphHelper.Codeunit.al b/src/Apps/W1/External File Storage - SharePoint Connector/App/src/ExtSharePointGraphHelper.Codeunit.al index 943b7cf405..3d1b26eeda 100644 --- a/src/Apps/W1/External File Storage - SharePoint Connector/App/src/ExtSharePointGraphHelper.Codeunit.al +++ b/src/Apps/W1/External File Storage - SharePoint Connector/App/src/ExtSharePointGraphHelper.Codeunit.al @@ -94,9 +94,9 @@ codeunit 4581 "Ext. SharePoint Graph Helper" // Split path into folder and filename SplitPath(Path, FolderPath, FileName); - // Use simple upload for files under 3.9MB, chunked upload for larger files - // 3.9MB threshold provides safety margin below Graph API's 4MB limit - MaxSimpleUploadSize := 4088218; // 3.9 MB + // Use simple upload for files under 4MB, chunked upload for larger files + // Graph API supports simple upload up to 4MB + MaxSimpleUploadSize := 4 * 1024 * 1024; // 4 MB if Stream.Length <= MaxSimpleUploadSize then Response := SharePointGraphClient.UploadFile(FolderPath, FileName, Stream, GraphDriveItem) From dfbd7e599311711ba636230f516fdaae91086ad4 Mon Sep 17 00:00:00 2001 From: Tine Staric Date: Mon, 29 Dec 2025 09:09:42 +0100 Subject: [PATCH 6/7] Remove unused namespace --- .../App/src/ExtSharePointGraphHelper.Codeunit.al | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Apps/W1/External File Storage - SharePoint Connector/App/src/ExtSharePointGraphHelper.Codeunit.al b/src/Apps/W1/External File Storage - SharePoint Connector/App/src/ExtSharePointGraphHelper.Codeunit.al index 3d1b26eeda..c5215a7f87 100644 --- a/src/Apps/W1/External File Storage - SharePoint Connector/App/src/ExtSharePointGraphHelper.Codeunit.al +++ b/src/Apps/W1/External File Storage - SharePoint Connector/App/src/ExtSharePointGraphHelper.Codeunit.al @@ -8,7 +8,6 @@ namespace System.ExternalFileStorage; using System.Integration.Graph.Authorization; using System.Integration.Sharepoint; using System.Utilities; -using System.Integration.Graph; /// /// Helper implementation for SharePoint file operations using Microsoft Graph API. From 10cd320059ec33df587da49046368143f969518a Mon Sep 17 00:00:00 2001 From: Bert Verbeek Date: Fri, 9 Jan 2026 12:45:28 +0100 Subject: [PATCH 7/7] Add validation trigger for "Use Graph API" field in SharePoint Account pages --- .../App/src/ExtSharePointAccount.Page.al | 10 +++++++++- .../App/src/ExtSharePointAccountWizard.Page.al | 7 +++---- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/src/Apps/W1/External File Storage - SharePoint Connector/App/src/ExtSharePointAccount.Page.al b/src/Apps/W1/External File Storage - SharePoint Connector/App/src/ExtSharePointAccount.Page.al index 35ea3bc919..a2c76416d8 100644 --- a/src/Apps/W1/External File Storage - SharePoint Connector/App/src/ExtSharePointAccount.Page.al +++ b/src/Apps/W1/External File Storage - SharePoint Connector/App/src/ExtSharePointAccount.Page.al @@ -97,7 +97,15 @@ page 4580 "Ext. SharePoint Account" } } field(Disabled; Rec.Disabled) { } - field("Use Graph API"; Rec."Use Graph API") { } + field("Use Graph API"; Rec."Use Graph API") + { + trigger OnValidate() + var + CheckBasePathMsg: Label 'The API type has been changed. Please verify that the Base Relative Folder Path is still correct for the selected API type.'; + begin + Message(CheckBasePathMsg); + end; + } } } diff --git a/src/Apps/W1/External File Storage - SharePoint Connector/App/src/ExtSharePointAccountWizard.Page.al b/src/Apps/W1/External File Storage - SharePoint Connector/App/src/ExtSharePointAccountWizard.Page.al index baff31f23b..a802e8deb4 100644 --- a/src/Apps/W1/External File Storage - SharePoint Connector/App/src/ExtSharePointAccountWizard.Page.al +++ b/src/Apps/W1/External File Storage - SharePoint Connector/App/src/ExtSharePointAccountWizard.Page.al @@ -130,6 +130,9 @@ page 4581 "Ext. SharePoint Account Wizard" IsNextEnabled := SharePointConnectorImpl.IsAccountValid(Rec); end; } + field("Use Graph API"; Rec."Use Graph API") + { + } field("Base Relative Folder Path"; Rec."Base Relative Folder Path") { @@ -140,10 +143,6 @@ page 4581 "Ext. SharePoint Account Wizard" IsNextEnabled := SharePointConnectorImpl.IsAccountValid(Rec); end; } - - field("Use Graph API"; Rec."Use Graph API") - { - } } }