SharePoint File Management Implementation

Guidelines for Partners

When migrating a Business Central solution to the Cloud environment, issues often arise related to file management since the client’s local and server’s file systems are unavailable for the application whose target is set to Cloud. One possible way to solve the issues is to use the functionality of a standard SharePoint module, which would provide an alternative file system and replace the usage of a file management code unit. The downside is that SharePoint’s standard functionality is straightforward and limited, so this guideline provides an example of custom SharePoint file management which expands the standard SharePoint module’s functionality.

This guideline will not provide steps to set up SharePoint connector and Azure App since this information is covered in the colleague’s guideline.

Created custom objects for SharePoint File Management realization

 

This section describes all created custom objects and their purpose in implementing a SharePoint file management functionality.

SharePoint Setup Table and Setup Page – Table consists of fields that store information collected from the Azure application. The page intends for users to fill in values from the Azure application.

 

table 55039 "Sharepoint Connector Setup"
{
    DataClassification = ToBeClassified;
    fields
    {
        field(1; "Primary Key"; Code[10])
        {
            DataClassification = ToBeClassified;
            Caption = 'Primary Key';
        }
        field(2; "Client ID"; Text[250])
        {
            DataClassification = EndUserIdentifiableInformation;
            Caption = 'Client ID';
        }
        field(3; "Client Secret"; Text[250])
        {
            DataClassification = EndUserIdentifiableInformation;
            Caption = 'Client Secret';
        }
        field(4; "Sharepoint URL"; Text[250])
        {
            DataClassification = ToBeClassified;
            Caption = 'Sharepoint URL';
        }
    }
    keys
    {
        key(PK; "Primary Key")
        {
            Clustered = true;
        }
    }
}

page 55081 "Sharepoint Connector Setup"
{
    Caption = 'SharePoint Connector Setup';
    PageType = Card;
    ApplicationArea = All;
    UsageCategory = Administration;
    SourceTable = "Sharepoint Connector Setup";
    ModifyAllowed = true;
    InsertAllowed = false;
    DeleteAllowed = false;

    layout
    {
        area(Content)
        {
            group(Setup)
            {
                Caption = 'Setup';
                field("Client ID"; Rec."Client ID")
                {
                    ApplicationArea = All;
                    ToolTip = 'Specifies the Client ID. Obtained from Application''s
                    "SharePointConnector" Overview in Azure Portal.';
                }
                field("Client Secret"; Rec."Client Secret")
                {
                    ApplicationArea = All;
                    ExtendedDatatype = Masked;
                    ToolTip = 'Specifies the Client Secret. Obtained from "Value" field after
                    generating client secret in Azure Portal.';
                }
                field("Sharepoint URL"; Rec."Sharepoint URL")
                {
                    ApplicationArea = All;
                }
            }
        }
    }
}

SharePoint Management codeunit – Contains all SharePoint file management functions used to refactor client/server-side file system usage cases (usually related to codeunit’s 419 „File Management“ functionality).

SharePoint Request Management codeunit – Contains functions to build and send REST API requests. This codeunit is used to send only two requests: to get information about the SharePoint file to retrieve its InStream (currently, it is not possible to get the file’s InStream with standard functions) and to delete the SharePoint file (the standard module does not include a function which deletes SharePoint file).

SharePoint Operations codeunit – Contains functions that return a response to REST API request, which was sent from a function stored in the SharePoint Request Management codeunit.

SharePoint URL Builder codeunit – Contains functions used to build REST API request uri defined in the request message’s header.

SharePoint Folders List table and page – Table consists of fields that store information about the SharePoint folder. Page is used as a SharePoint file explorer. It is impossible to open the SharePoint file browser as in the local client, so the page displays all SharePoint folders with their paths to choose from.

table 55040 "Sharepoint Folder List"
{
    DataClassification = ToBeClassified;

    fields
    {
        field(1; Id; Guid)
        {
            Caption = 'Id';
        }


        field(2; Title; Text[250])
        {
            Caption = 'Title';
        }


        field(3; Created; DateTime)
        {
            Caption = 'Created';
        }
        field(5; "Server Relative Url"; Text[2048])
        {
            Caption = 'Server Relative Url';
        }
        field(6; OdataId; Text[2048])
        {
            Caption = 'Odata.Id';
        }
    }


    keys
    {
        key(Key1; Id)
        {
            Clustered = true;
        }
    }
}

page 55082 "Sharepoint Folders List" 
{ 
    Caption = 'SharePoint Folders List'; 
    PageType = List; 
    ApplicationArea = All; 
    SourceTable = "Sharepoint Folder List"; 

    layout 
    { 
        area(Content) 
        { 
            repeater(List) 
            { 
                Caption = 'SharePoint Folders List'; 
                field(Name; Rec.Title) 
                { 
                    ApplicationArea = All; 
                    Width = 30; 
                } 
                field("Server Relative Url"; Rec."Server Relative Url") 
                { 
                    ApplicationArea = All; 
                    Width = 150; 
                } 
            } 
        } 
    } 
    trigger OnAfterGetCurrRecord() 
    var 
        Singleinstance: Codeunit "Single Instance Codeunit"; 
    begin 
        Singleinstance.Set_FolderPath_P55082(Rec."Server Relative Url"); 
    end; 

Standard SharePoint module’s functions that are used in SharePoint File Management

 

Some SharePoint File Management functions check if provided file path begins with „/sites“ to determine if the root folder’s url is included since it is a necessary component of all SharePoint file paths. GetLists retrieves all SharePoint site’s available lists, and we constantly filter out the „Documents“ list to access the stored files. GetDocumentLibraryRootFolder function retrieves the list’s root folder. GetFolderFilesByServerRelativeUrl recovers the SharePoint site’s files by providing a folder path. GetSubFoldersByServerRelativeUrl retrieves the SharePoint site’s folders by giving a folder path.

 

1.    Initialization

Initialization functions are stored in the SharePoint Management codeunit. These functions are already described in the colleague’s guideline (section Sharepoint connector Initialization). The only difference is that mine InitializeConnection function initializes  SharePoint URL Builder to build REST API request uri, which is defined in the request message’s header, and GetSharePointAuthorization function saves created authorization code in the global variable Authorization, which is later used to authorize my two codeunit SharePoint Request Management REST API requests mentioned in the previous section. Initialization functions:

    local procedure InitializeConnection()
    var
        SharepointSetup: Record "Sharepoint Connector Setup";
        AadTenantId: Text;
        Diag: Interface "HTTP Diagnostics";
        SharePointList: Record "SharePoint List" temporary;
    begin
        if Connected then
            exit;
        SharepointSetup.Get();
        AadTenantId := GetAadTenantNameFromBaseUrl(SharepointSetup."Sharepoint URL");
        SharePointClient.Initialize(SharepointSetup."Sharepoint URL",
        GetSharePointAuthorization(AadTenantId));
        SharePointUriBuilder.Initialize(SharepointSetup."Sharepoint URL", 'Web');
        SharePointUriBuilder.ResetPath();
        SharePointClient.GetLists(SharePointList);
        Diag := SharePointClient.GetDiagnostics();
        if (not Diag.IsSuccessStatusCode()) then
            Error(DiagError, Diag.GetErrorMessage());
        Connected := true;
    end;

    local procedure GetSharePointAuthorization(AadTenantId: Text): Interface "SharePoint Authorization"
    var
        SharepointSetup: Record "Sharepoint Connector Setup";
        SharePointAuth: Codeunit "SharePoint Auth.";
        Scopes: List of [Text];
    begin
        SharepointSetup.Get();
        Scopes.Add('00000003-0000-0ff1-ce00-000000000000/.default');
        Authorization := SharePointAuth.CreateAuthorizationCode(AadTenantId,
        SharepointSetup."Client ID", SharepointSetup."Client Secret", Scopes);
        exit(Authorization);
    end;

    local procedure GetAadTenantNameFromBaseUrl(BaseUrl: Text): Text
    var
        Uri: Codeunit Uri;
        MySiteHostSuffixTxt: Label '-my.sharepoint.com', Locked = true;
        SharePointHostSuffixTxt: Label '.sharepoint.com', Locked = true;
        OnMicrosoftTxt: Label '.onmicrosoft.com', Locked = true;
        UrlInvalidErr: Label 'The Base Url %1 does not seem to be a valid SharePoint Online Url.',
        Comment = '%1=BaseUrl';
        Host: Text;
    begin
        Uri.Init(BaseUrl);
        Host := Uri.GetHost();
        if not Host.EndsWith(SharePointHostSuffixTxt) then
            Error(UrlInvalidErr, BaseUrl);
        if Host.EndsWith(MySiteHostSuffixTxt) then
            exit(CopyStr(Host, 1, StrPos(Host, MySiteHostSuffixTxt) - 1) + OnMicrosoftTxt);
        exit(CopyStr(Host, 1, StrPos(Host, SharePointHostSuffixTxt) - 1) + OnMicrosoftTxt);
    end;

codeunit 55046 "Sharepoint URL Builder"
{
    var
        ServerName, Namespace : Text;
        Uri: Text;
    procedure Initialize(NewServerName: Text; NewNamespace: Text)
    begin
        ServerName := NewServerName;
        Namespace := NewNamespace;

        if ServerName.StartsWith('https://') then
            ServerName := ServerName.Substring(9);
        if ServerName.StartsWith('http://') then
            ServerName := ServerName.Substring(8);
    end;

    procedure ResetPath()
    begin
        Uri := '';
    end;

2.    Checking if the SharePoint file exists

Function SharePointFileExists is stored in the SharePoint Management code, checking if the file exists on the SharePoint site. As the function’s parameters, it must pass a file path and a file name. In the end, SetRange filters out files by provided file name, and if the file is found function’s return parameter is set to true.

    procedure SharePointFileExists(FileName: Text; FilePath: text): Boolean
    var
        SharePointList: Record "SharePoint List" temporary;
        SharepointFolder: Record "SharePoint Folder" temporary;
        SharepointFolder2: Record "SharePoint Folder" temporary;
        SharepointFile: Record "SharePoint File";
    begin
        InitializeConnection();
        if not FilePath.StartsWith('/sites') then
            if SharePointClient.GetLists(SharePointList) then begin
                SharePointList.SetRange(Title, 'Documents');
                if SharePointList.FindFirst() then
                    if SharePointClient.GetDocumentLibraryRootFolder(SharePointList.OdataId,
                       SharePointFolder) then
                        if StrPos(FilePath, '/') = 1 then
                            FilePath := SharepointFolder."Server Relative Url" + FilePath
                        else
                            FilePath := SharepointFolder."Server Relative Url" + '/' + FilePath;
            end;
        SharePointClient.GetFolderFilesByServerRelativeUrl(FilePath, SharePointFile);
        SharepointFile.SetRange(Name, FileName);
        if SharepointFile.FindFirst() then
            exit(true);
    end;

The example below shows how to refactor codeunit’s 419 “File Management” standard function ServerFileExists by using the SharePointFileExists function:

 Var
    _NewServerFile: Text;
    _ErrFileAlreadyExist: Label 'A file with the same name already exists, please rename the file  
    before attaching';
    SharePointMgt: Codeunit "Sharepoint Management";
    _FileMgt: Codeunit "File Management";

   //if _FileMgt.ServerFileExists(_NewServerFile) then
   FileUploaded := UploadIntoStream(_TextPickupFile, '', Filter, _TempServerFileName, FileInStream);
   if SharePointMgt.SharePointFileExists(_TempServerFileName, _NewServerFile) then
       Error(_ErrFileAlreadyExist);

 

3.    Saving a file

Function SaveFile is stored in the SharePoint Management codeunit, and it’s used to save a file in the SharePoint site. As the function’s parameters, it is required to pass a file name, path, and an InStream. If the file is saved, the function returns the file path of the newly created file.

    procedure SaveFile(FileName: Text; FilePath: text; FileInStream: InStream) NewFilePath: Text;
    var
        SharePointFile: Record "SharePoint File" temporary;
        Diag: Interface "HTTP Diagnostics";
        FileDirectory: Text;
        SharePointList: Record "SharePoint List" temporary;
        SharepointFolder: Record "SharePoint Folder" temporary;
    begin
        InitializeConnection();
        if not FilePath.StartsWith('/sites') then
            if SharePointClient.GetLists(SharePointList) then begin
                SharePointList.SetRange(Title, 'Documents');
                if SharePointList.FindFirst() then
                    if SharePointClient.GetDocumentLibraryRootFolder(SharePointList.OdataId,
                       SharePointFolder) then
                        if StrPos(FilePath, '/') = 1 then
                            FilePath := SharepointFolder."Server Relative Url" + FilePath
                        else
                            FilePath := SharepointFolder."Server Relative Url" + '/' + FilePath;
            end;

        if SharePointClient.AddFileToFolder(FilePath, FileName, FileInStream, SharePointFile) then
        begin
            NewFilePath := SharePointFile."Server Relative Url";
        end else begin
            Diag := SharePointClient.GetDiagnostics();
            if (not Diag.IsSuccessStatusCode()) then
                Error(DiagError, Diag.GetErrorMessage());
        end;
    end

4.    Creating a directory

Function CreateSharePointDirectory is stored in the SharePoint Management codeunit and used to create a new directory in the SharePoint site. Per the function’s parameters, passing a folder path and an empty SharePoint Folder variable is required. After the function’s completion, the passed SharePoint Folder variable will return the newly created folder, which contains the path of the newly created directory. The Standard CreateFolder function creates a new sub-folder in the SharePoint site by providing a folder path. New created folder is returned as the second function’s CreateFolder parameter.

    procedure CreateSharePointDirectory(FolderPath: Text; var NewSharepointFolder: Record "SharePoint   
    Folder" temporary)
    var
        SharePointList: Record "SharePoint List" temporary;
        SharepointFolder: Record "SharePoint Folder" temporary;
        SharepointFolder2: Record "SharePoint Folder" temporary;
        SharepointFolderEmpty: Record "SharePoint Folder" temporary;
        RelativeUrl: Text;
        FolderName: Text;
        Filter: Text;
        SharePointFile: Record "SharePoint File" temporary;
        FInStream: InStream;

    begin
        InitializeConnection();
        if SharePointClient.GetLists(SharePointList) then begin
            SharePointList.SetRange(Title, 'Documents');
            if SharePointList.FindFirst() then
                if SharePointClient.GetDocumentLibraryRootFolder(SharePointList.OdataId,
                   SharePointFolder) then
                    RelativeUrl := SharepointFolder."Server Relative Url";
        end;

        if FolderPath.StartsWith('/sites') then
            FolderPath := DelStr(FolderPath, 1, StrLen(RelativeUrl));

        if FolderPath <> '' then
            if StrPos(FolderPath, '/') = 1 then
                FolderPath := DelStr(FolderPath, 1, 1);

        repeat
            if FolderPath <> '' then begin
                if StrPos(FolderPath, '/') = 0 then begin
                    FolderName := FolderPath;
                    if SharePointClient.GetSubFoldersByServerRelativeUrl(RelativeUrl,
                       SharepointFolder2) then begin
                        if SharepointFolder2.Exists then begin
                            Filter := '@' + RelativeUrl + '/' + FolderName;
                            SharepointFolder2.SetFilter("Server Relative Url", Filter);
                            if not SharepointFolder2.FindFirst() then
                                SharePointClient.CreateFolder(RelativeUrl + '/' + FolderName,
                                NewSharepointFolder)
                            else
                                NewSharepointFolder := SharepointFolder2;
                        end else
                            SharePointClient.CreateFolder(RelativeUrl + '/' + FolderName,
                            NewSharepointFolder);
                    end;
                    RelativeUrl := NewSharepointFolder."Server Relative Url";
                    FolderPath := '';
                end else begin
                    FolderName := CopyStr(FolderPath, 1, StrPos(FolderPath, '/') - 1);
                    if SharePointClient.GetSubFoldersByServerRelativeUrl(RelativeUrl,
                       SharepointFolder2) then begin
                        if SharepointFolder2.Exists then begin
                            Filter := '@' + RelativeUrl + '/' + FolderName;
                            SharepointFolder2.SetFilter("Server Relative Url", Filter);
                            if not SharepointFolder2.FindFirst() then
                                SharePointClient.CreateFolder(RelativeUrl + '/' + FolderName,
                                NewSharepointFolder)
                            else
                                NewSharepointFolder := SharepointFolder2;
                        end else
                            SharePointClient.CreateFolder(RelativeUrl + '/' + FolderName,
                            NewSharepointFolder);
                    end;
                    RelativeUrl := NewSharepointFolder."Server Relative Url";
                    FolderPath := CopyStr(FolderPath, StrPos(FolderPath, '/') + 1, StrLen(FolderPath)-
                    StrLen(CopyStr(FolderPath, 1, StrPos(FolderPath, '/'))));
                end;
            end;
            SharepointFolder2 := SharepointFolderEmpty;
        until FolderPath = '';
    end;

 

The example below shows how to refactor codeunit’s 419 “File Management” standard functions ServerDirectoryExists and ServerCreateDirectory using the function CreateSharePointDirectory. CreateSharePointDirectory creates a directory only if the same directory does not exist, so using a separate function to check the folder path is unnecessary.

var
   SharePointMgt: Codeunit "Sharepoint Management";
   NewSharepointFolder: Record "SharePoint Folder" temporary;
   AttachmentFolder: Text;

 // _NewServerDirectory := StrSubstNo('%1%2\%3', AttachmentFolder,  
 // _CompanyCodes.GetCurrentCompanyIntercoCode, Date2DMY(Today, 3));
 // if not _FileMgt.ServerDirectoryExists(_NewServerDirectory) then
 //     _FileMgt.ServerCreateDirectory(_NewServerDirectory);

 SharePointMgt.CreateSharePointDirectory(AttachmentFolder + '/' +
 _CompanyCodes.GetCurrentCompanyIntercoCode + '/' + Format(Date2DMY(Today, 3)), NewSharepointFolder);
 _NewServerDirectory := NewSharepointFolder."Server Relative Url";

5.    Checking if the SharePoint directory exists

Function SharePointDirectoryExists is stored in the SharePoint Management codeunit until it checks if a directory exists in the SharePoint site. Per the function’s parameters, passing a folder path and an empty text variable is required. After the function’s completion, if the

directory is found, the passed empty text variable will return the path of the found directory, and the function’s return parameter will be set to true. The second parameter of this function is helpful in cases where a provided directory for checking does not contain the SharePoint root folder’s path – for example, after running the function SharePointDirectoryExists, returned second parameter’s value could be used for saving files.

    procedure SharePointDirectoryExists(DirectoryPath: Text; var SharepointDirectory: Text): Boolean
    var
        SharePointList: Record "SharePoint List" temporary;
        SharepointFolder: Record "SharePoint Folder" temporary;
        RelativeUrl: Text;
        Filter: Text;
        FolderName: Text;
        NewSharepointFolder: Record "SharePoint Folder" temporary;
    begin
        InitializeConnection();
        if SharePointClient.GetLists(SharePointList) then begin
            SharePointList.SetRange(Title, 'Documents');
            if SharePointList.FindFirst() then
                if SharePointClient.GetDocumentLibraryRootFolder(SharePointList.OdataId,
                   SharePointFolder) then
                    RelativeUrl := SharepointFolder."Server Relative Url";
        end;

        if DirectoryPath.StartsWith('/sites') then
            DirectoryPath := DelStr(DirectoryPath, 1, StrLen(RelativeUrl));

        if DirectoryPath <> '' then
            if StrPos(DirectoryPath, '/') = 1 then
                DirectoryPath := DelStr(DirectoryPath, 1, 1);

        repeat
            if DirectoryPath <> '' then begin
                if StrPos(DirectoryPath, '/') = 0 then begin
                    FolderName := DirectoryPath;
                    if SharePointClient.GetSubFoldersByServerRelativeUrl(RelativeUrl,
                       NewSharepointFolder) then begin
                        if NewSharepointFolder.Exists then begin
                            Filter := '@' + RelativeUrl + '/' + FolderName;
                            NewSharepointFolder.SetFilter("Server Relative Url", Filter);
                            if not NewSharepointFolder.FindFirst() then
                                exit(false);
                        end;
                    end;
                    RelativeUrl := NewSharepointFolder."Server Relative Url";
                    DirectoryPath := '';
                end else begin
                    FolderName := CopyStr(DirectoryPath, 1, StrPos(DirectoryPath, '/') - 1);
                    if SharePointClient.GetSubFoldersByServerRelativeUrl(RelativeUrl,
                       NewSharepointFolder) then begin
                        if NewSharepointFolder.Exists then begin
                            Filter := '@' + RelativeUrl + '/' + FolderName;
                            NewSharepointFolder.SetFilter("Server Relative Url", Filter);
                            if not NewSharepointFolder.FindFirst() then
                                exit(false);
                        end;
                    end;
                    RelativeUrl := NewSharepointFolder."Server Relative Url";
                    DirectoryPath := CopyStr(DirectoryPath, StrPos(DirectoryPath, '/') + 1,
                    StrLen(DirectoryPath) - StrLen(CopyStr(DirectoryPath, 1, StrPos(DirectoryPath,
                    '/'))));
                end;
            end;
        until DirectoryPath = '';
        SharepointDirectory := RelativeUrl;
        exit(true);
    end;

 

The example below shows how to refactor codeunit’s 419 “File Management” standard function ServerDirectoryExists by using the function SharePointDirectoryExists:

 //if not FileMgt.ServerDirectoryExists(ImportFolder) then
 //    Error(_ErrFolderNotExist, ImportFolder ");
 if not SharePointMgt.SharePointDirectoryExists(ImportFolder, SharepointDirectory) then
     Error(_ErrFolderNotExist, ImportFolder);

6.    Getting files from a specified folder path

Function GetFilesFromPath is stored in SharePoint Management codeunit and retrieves files from a specified directory. As per the function’s parameters, passing a folder path and an empty SharePoint File variable is required. After the function’s completion, the passed empty SharePoint variable will return the files list from a specified directory.

    procedure GetFilesFromPath(DirectoryPath: Text; var SharePointFile: Record "SharePoint File"
    temporary)
    var
        SharePointList: Record "SharePoint List" temporary;
        SharepointFolder: Record "SharePoint Folder" temporary;
        RelativeUrl: Text;
    begin
        InitializeConnection();
        if not DirectoryPath.StartsWith('/sites') then begin
            if SharePointClient.GetLists(SharePointList) then begin
                SharePointList.SetRange(Title, 'Documents');
                if SharePointList.FindFirst() then
                    if SharePointClient.GetDocumentLibraryRootFolder(SharePointList.OdataId,
                       SharePointFolder) then begin
                        if StrPos(DirectoryPath, '/') = 1 then
                            RelativeUrl := SharepointFolder."Server Relative Url" + DirectoryPath
                        else
                            RelativeUrl := SharepointFolder."Server Relative Url" + '/' +
                            DirectoryPath;
                    end;
            end;
        end else
            RelativeUrl := DirectoryPath;

        SharePointClient.GetFolderFilesByServerRelativeUrl(RelativeUrl, SharePointFile);
    end;

 

The example below shows how to refactor DotNet files retrieving functions functionality by using the function GetFilesFromPath:

    var
        //SPLN1.00 - Start
        // [RunOnClient]
        // ClientDirectoryInfo: DotNet DirectoryInfo;
        // [RunOnClient]
        // ClientEnumerator: DotNet IEnumerator;
        // [RunOnClient]
        // ClientFileInfo: DotNet FileInfo;
        // [RunOnClient]
        // ClientList: DotNet List_Of_T;
        ImportFolder: Text;
        SharePointMgt: Codeunit "Sharepoint Management";
        SharePointFile: Record "SharePoint File" temporary;
    begin
        _Window.Open(Text001);

        //read the local folder to retrieve pdf files list
        //SPLN1.00 - Start
        // ClientDirectoryInfo := ClientDirectoryInfo.DirectoryInfo(ImportFolder);
        // ClientList := ClientDirectoryInfo.GetFiles('*.pdf');
        // ClientEnumerator := ClientList.GetEnumerator();
        SharePointMgt.GetFilesFromPath(ImportFolder, SharePointFile)
        SharePointFile.SetFilter(Name, '@*.pdf');
        // while ClientEnumerator.MoveNext do begin
        if SharePointFile.FindSet() then
            repeat

 

The second example shows a workaround with the function GetFilesFromPath when the OnPrem table „File“ is being filtered by a folder path:

var
   //SPLN1.00 - Start
   //_FileList: Record File;
   ImportFolder: Text;
   SharePointMgt: Codeunit "Sharepoint Management";
   SharePointFile: Record "SharePoint File" temporary;

 //_FileList.SetRange(Path, ImportFolder);
 //_FileList.SetRange("Is a file", true);
 //_FileList.SetFilter(Name, '*.csv*');
 //_FileList.SetFilter(Size, '>0');
 SharePointMgt.GetFilesFromPath(ImportFolder, SharePointFile);
 SharePointFile.SetRange(Exists, true);
 SharePointFile.SetFilter(Name, '*.csv*');
 SharePointFile.SetFilter(Length, '>0');
 //if _FileList.FindSet then
 if SharePointFile.FindSet then

7.    Getting a file

Function GetFile is stored in SharePoint Management codeunit and returns a specified file. As the function’s parameters, it is required to pass a directory path, a file name, and an empty SharePoint File variable that will return the retrieved file after the function’s completion. GetFile function is mostly used in combination with other SharePoint Management functions (This will be shown in the examples later).

    procedure GetFile(DirectoryPath: Text; FileName: Text; var ResultSharePointFile: Record
    "SharePoint File" temporary)
    var
        SharePointList: Record "SharePoint List" temporary;
        SharepointFolder: Record "SharePoint Folder" temporary;
        SharePointFile: Record "SharePoint File" temporary;
        RelativeUrl: Text;
    begin
        InitializeConnection();
        if not DirectoryPath.StartsWith('/sites') then begin
            if SharePointClient.GetLists(SharePointList) then begin
                SharePointList.SetRange(Title, 'Documents');
                if SharePointList.FindFirst() then
                    if SharePointClient.GetDocumentLibraryRootFolder(SharePointList.OdataId,
                       SharePointFolder) then begin
                        if StrPos(DirectoryPath, '/') = 1 then
                            RelativeUrl := SharepointFolder."Server Relative Url" + DirectoryPath
                        else
                            RelativeUrl := SharepointFolder."Server Relative Url" + '/' +
                            DirectoryPath;
                    end;
            end;
        end else
            RelativeUrl := DirectoryPath;

        SharePointClient.GetFolderFilesByServerRelativeUrl(RelativeUrl, SharePointFile);
        SharePointFile.SetRange(Name, FileName);
        if SharePointFile.FindFirst() then
            ResultSharePointFile := SharePointFile;
    end;

8.    Getting an InStream of a file

Function GetFileContentInStream is stored in SharePoint Management codeunit and returns an InStream of a specified file. Function GetFileContentInStream is stored in SharePoint Management codeunit and returns an InStream of a specified file.

    procedure GetFileContentInStream(OdataId: Text; var FileInStream: InStream): Boolean
    begin
        SharePointUriBuilder.ResetPath(OdataId);
        SharePointUriBuilder.SetObject('$value');

        SharePointRequestHelper.SetAuthorization(Authorization);
        SharePointOperationResponse := SharePointRequestHelper.Get(SharePointUriBuilder);
        if not SharePointOperationResponse.GetDiagnostics() then
            exit(false);

        SharePointOperationResponse.GetResultAsStream(FileInStream);

        exit(true);
    end;

 

GetFileContentInStream uses:

Procedures ResetPath, SetObject, and GetUri of SharePoint URL Builder codeunit to build REST API request uri, which has to be defined in the request message’s header:

    var
        Uri: Text;
        UriAppendTxt: Label '/%1', Comment = '%1 - URI part to append', Locked = true;
    procedure SetObject(Object: Text)
    begin
        Uri += StrSubstNo(UriAppendTxt, EscapeDataString(Object));
    end;

    local procedure EscapeDataString(TextToEscape: Text): Text
  
  var
       LocalUri: Codeunit Uri;
    begin
       exit(LocalUri.EscapeDataString(TextToEscape));
    end;

var
   ServerName, Namespace : Text;
   Uri: Text;
   UriLbl: Label 'https://{server_name}/_api/{namespace}', Locked = true;

    procedure GetUri(): Text
    var
        FullUri: Text;
    begin
        FullUri := UriLbl + Uri + '/';
        FullUri := FullUri.Replace('{server_name}',ServerName.TrimStart('/').TrimEnd('/')).Replace('{namespace}',
                   Namespace.TrimStart('/').TrimEnd('/'));
        exit(FullUri);
    end;

    procedure ResetPath(Id: Text)
    begin
        Uri := UriLbl;
        Uri := Uri.Replace('{server_name}',
               ServerName.TrimStart('/').TrimEnd('/')).Replace('{namespace}',Namespace.TrimStart('/').TrimEnd('/'));
        Uri := Id.Replace(Uri, '');
    end;

 

Procedures SetAuthorization, Get, PrepareRequestMsg, SendRequest of SharePoint Request Management codeunit to authorize, build and send REST API request:

var
   HttpClient: HttpClient;
   Authorization: Interface "SharePoint Authorization";
   OperationNotSuccessfulErr: Label 'An error has occurred';
   UserAgentLbl: Label 'NONISV|%1|Dynamics 365 Business Central - %2/%3', Locked = true, Comment = '%1 = App
   Publisher; %2 = App Name; %3 = App Version';

procedure SetAuthorization(Auth: Interface "SharePoint Authorization")
begin
    Authorization := Auth;
end;

procedure Get(SharePointUriBuilder: Codeunit "SharePoint URL Builder") OperationResponse: Codeunit "Sharepoint Operations"
begin
    OperationResponse := SendRequest(PrepareRequestMsg("Http Request Type"::GET, SharePointUriBuilder));
end;

local procedure PrepareRequestMsg(HttpRequestType: Enum "Http Request Type"; SharePointUriBuilder: Codeunit "SharePoint URL Builder") RequestMessage: HttpRequestMessage
var
   Headers: HttpHeaders;
Begin
    RequestMessage.Method(Format(HttpRequestType));
    RequestMessage.SetRequestUri(SharePointUriBuilder.GetUri());
    RequestMessage.GetHeaders(Headers);
    Headers.Add('Accept', 'application/json');
    Headers.Add('User-Agent', GetUserAgentString());
end;

local procedure SendRequest(HttpRequestMessage: HttpRequestMessage) OperationResponse: Codeunit "Sharepoint Operations"
var
   HttpResponseMessage: HttpResponseMessage;
   Content: Text;
Begin
    Authorization.Authorize(HttpRequestMessage);
    if not HttpClient.Send(HttpRequestMessage, HttpResponseMessage) then
      Error(OperationNotSuccessfulErr);

    HttpResponseMessage.Content.ReadAs(Content);
    OperationResponse.SetHttpResponse(HttpResponseMessage);
end;

Procedures GetResultAsStream, GetDiagnostics of SharePoint Operations codeunit to get diagnostics, resulting in InStream of sent REST API requests. The remaining functions read InStream and header values from the response message.

    procedure GetResultAsStream(var ResultInStream: InStream)
    begin
        TempBlobContent.CreateInStream(ResultInStream);
    end;

    procedure SetHttpResponse(HttpResponseMessage: HttpResponseMessage)
    var
        ContentOutStream: OutStream;
        ContentInStream: InStream;
    begin
        TempBlobContent.CreateOutStream(ContentOutStream);
        HttpResponseMessage.Content().ReadAs(ContentInStream);
        CopyStream(ContentOutStream, ContentInStream);
        HttpHeaders := HttpResponseMessage.Headers();
        SetParameters(HttpResponseMessage.IsSuccessStatusCode, HttpResponseMessage.HttpStatusCode,
        HttpResponseMessage.ReasonPhrase, GetRetryAfterHeaderValue(), GetErrorDescription());
    end;

    procedure GetHeaderValueFromResponseHeaders(HeaderName: Text): Text
    var
        Values: array[100] of Text;
    begin
        if not HttpHeaders.GetValues(HeaderName, Values) then
            exit('');
        exit(Values[1]);
    end;

    procedure GetRetryAfterHeaderValue() RetryAfter: Integer;
    var
        HeaderValue: Text;
    begin
        HeaderValue := GetHeaderValueFromResponseHeaders('Retry-After');
        if HeaderValue = '' then
            exit(0);
        if not Evaluate(RetryAfter, HeaderValue) then
            exit(0);
    end;

    local procedure GetErrorDescription(): Text
    var
        Result: Text;
        JObject: JsonObject;
        JToken: JsonToken;
    begin
        GetResultAsText(Result);
        if Result <> '' then
            if JObject.ReadFrom(Result) then
                if JObject.Get('error_description', JToken) then
                    exit(JToken.AsValue().AsText());
    end;

    procedure GetDiagnostics(): Boolean
    begin
        exit(IsSuccessStatusCode);
    end;

    procedure SetParameters(NewIsSuccesss: Boolean; NewHttpStatusCode: Integer; NewResponseReasonPhrase: Text;
    NewRetryAfter: Integer; NewErrorMessage: Text)
    begin
        SuccessStatusCode := NewIsSuccesss;
        HttpStatusCode := NewHttpStatusCode;
        ResponseReasonPhrase := NewResponseReasonPhrase;
        RetryAfter := NewRetryAfter;
        ErrorMessage := NewErrorMessage;
    end;

    procedure IsSuccessStatusCode(): Boolean
    begin
        exit(SuccessStatusCode);
    end;

    var
        TempBlobContent: Codeunit "Temp Blob";
        HttpHeaders: HttpHeaders;
        ErrorMessage: Text;
        ResponseReasonPhrase: Text;
        HttpStatusCode: Integer;
        RetryAfter: Integer;
        SuccessStatusCode: Boolean;


GetFileContentInStream function is mostly used in combination with other SharePoint Management functions, for example, to copy a file from one directory to another (Usage of this function will be shown in the examples later). As the function’s parameters, it is required to pass an Odata id, a parameter of the filing entity, and an InStream that will be populated with the file content. I created this function since the standard BC SharePoint module did not have a procedure to get the InStream of a file until the latest „Business Central“release, 22.2. In BC 22.2 function to get the file’s InStream is called DownloadFileContent.

   procedure DownloadFileContent(OdataId: Text; var FileInStream: InStream): Boolean
   begin
        exit(SharePointClientImpl.DownloadFileContent(OdataId, FileInStream));
   end;

9.    Delete a file

Function DeleteFile is stored in the SharePoint Management codeunit and deletes a specified file from the SharePoint site. As the function’s parameters, it is required to pass Odata id, which is a parameter of the file entity. The Standard SharePoint module does not have a function that deletes a file from the SharePoint site; therefore, as with the GetFileContentInStream procedure, it was necessary to build and send a custom REST API request.

    procedure DeleteFile(OdataId: Text): Boolean
    begin
        SharePointUriBuilder.ResetPath(OdataId);

        SharePointRequestHelper.SetAuthorization(Authorization);
        SharePointOperationResponse := SharePointRequestHelper.Delete(SharePointUriBuilder);
        if not SharePointOperationResponse.GetDiagnostics() then
            exit(false);

        exit(true);
    end;

DeleteFile uses:

  • Procedure ResetPath of SharePoint URL Builder codeunit to build REST API request uri, which must be defined in the request message’s header (image displayed in a section of GetFileContentInStream function).
  • Procedures SetAuthorization, Delete, PrepareRequestMsg, SendRequestDelete of SharePoint Request Management codeunit to authorize, build and send REST API request (image of procedure SetAuthorization is displayed in a section of GetFileContentInStream function):
    procedure Delete(SharePointUriBuilder: Codeunit "SharePoint URL Builder") OperationResponse: Codeunit
    "Sharepoint Operations"
    begin
        OperationResponse := SendRequestDelete(PrepareRequestMsg("Http Request Type"::DELETE,
        SharePointUriBuilder));
    end;

    local procedure SendRequestDelete(HttpRequestMessage: HttpRequestMessage) OperationResponse: Codeunit
    "Sharepoint Operations"
    var
        HttpResponseMessage: HttpResponseMessage;
        Content: Text;
    begin
        Authorization.Authorize(HttpRequestMessage);
        if not HttpClient.Send(HttpRequestMessage, HttpResponseMessage) then
            Error(OperationNotSuccessfulErr);

        OperationResponse.SetHttpResponse(HttpResponseMessage);
    end;

    local procedure PrepareRequestMsg(HttpRequestType: Enum "Http Request Type"; SharePointUriBuilder: Codeunit
    "SharePoint URL Builder") RequestMessage: HttpRequestMessage
    Var
       Headers: HttpHeaders;
    Begin
        RequestMessage.Method(Format(HttpRequestType));
        RequestMessage.SetRequestUri(SharePointUriBuilder.GetUri());
        RequestMessage.GetHeaders(Headers);
        Headers.Add('Accept', 'application/json');
        Headers.Add('User-Agent', GetUserAgentString());
    end;

  • Procedures GetDiagnostics and SetHttpResponse of SharePoint Operations codeunit to get diagnostics and read response message header values (images of these procedures are displayed in a section of the GetFileContentInStream function).

 

10.  Selecting a SharePoint folder (SharePoint file explorer)

Function SelectSharePointFolder is stored in the SharePoint Management codeunit, allowing the user to select a SharePoint site folder from the displayed folders list on a page (SharePoint Folders List page). As the function’s parameter, passing the Text variable that will be populated with the selected folder’s path value is required. SelectSharePointFolder calls the function BuildFoldersList, which finds every SharePoint site’s folder and returns a compiled list of folders.

    procedure SelectSharePointFolder(var SelectedFolder: Text): Boolean
    var
        SharePointList: Record "SharePoint List" temporary;
        SharepointFolder: Record "SharePoint Folder" temporary;
        SharePointListItem: Record "SharePoint List Item" temporary;
        NewSharepointFolder: Record "SharePoint Folder" temporary;
        SharePointFolderListPage: Page "Sharepoint Folders List";
        Singleinstance: Codeunit "Single Instance Codeunit";
    begin
        InitializeConnection();
        if SharePointClient.GetLists(SharePointList) then begin
            SharePointList.SetRange(Title, 'Documents');
            if SharePointList.FindFirst() then
                SharePointClient.GetDocumentLibraryRootFolder(SharePointList.OdataId, SharePointFolder);
        end;

        SharePointFolderList.DeleteAll();
        BuildFoldersList(SharePointFolder);

        if Page.RunModal(Page::"Sharepoint Folders List", SharePointFolderList) = Action::LookupOK then begin
            SelectedFolder := Singleinstance.Get_FolderPath_P55082();
            exit(true);
        end;

    end;

    procedure BuildFoldersList(var NewSharepointFolder: Record "SharePoint Folder" temporary)
    var
        SharepointFolder2: Record "SharePoint Folder" temporary;
    begin
        SharePointFolderList.Init();
        SharePointFolderList.Id := NewSharepointFolder."Unique Id";
        SharePointFolderList.Title := NewSharepointFolder.Name;
        SharePointFolderList.OdataId := NewSharepointFolder.OdataId;
        SharePointFolderList."Server Relative Url" := NewSharepointFolder."Server Relative Url";
        SharePointFolderList.Created := NewSharepointFolder.Created;
        SharePointFolderList.Insert();
        SharePointClient.GetSubFoldersByServerRelativeUrl(NewSharepointFolder."Server Relative Url",
        SharepointFolder2);
        if SharepointFolder2.Exists then begin
            if SharepointFolder2.FindSet() then
                repeat
                    NewSharepointFolder := SharepointFolder2;
                    BuildFoldersList(NewSharepointFolder);
                until SharepointFolder2.Next() = 0;
        end else
            exit;
    end;

    var
        SharePointFolderList: record "Sharepoint Folder List" temporary;

 

Examples below show how to refactor codeunit’s 419 “File Management” standard functions BrowseForFolderDialog and SelectFolderDialog by using the function SelectSharePointFolder:

//SPLN1.00 - Start
//PickedFolder := FileMgt.BrowseForFolderDialog(TextChooseFolder, PickedFolder, true);
SharePointMgt.SelectSharePointFolder(PickedFolder);
//SPLN1.00 - End

//SPLN1.00 - Start
//if _FileMgt.SelectFolderDialog(TextSelectFolder, _TempFolderName) then
if SharePointMgt.SelectSharePointFolder(_TempFolderName) then
    //SPLN1.00 - End

11.  Examples of SharePoint File Management usage

11.1. Copying file to a different directory

Since it is not possible to copy the file to a different directory with standard SharePoint functionality, firstly, we get the InStream of a file we want to copy with the procedure GetFileContentInStream and then use retrieved InStream to create a new file in the desired directory.

_PreviousFileName := _FileMgt.GetFileNameWithoutExtension(_FileName);
_PreviousFileExtension := _FileMgt.GetExtension(_FileName);
_NewFileName := _ArchiveFolder + '/' + _PreviousFileName + '_' + Format(CurrentDateTime, 0,  
                '<Year4><Month,2><Day,2><Hours24><Minutes,2>') + '.' + _PreviousFileExtension;
SharePointMgt.GetFileContentInStream(OdataId, FileInStream);
SharePointMgt.SaveFile(_PreviousFileName + '_' + Format(CurrentDateTime, 0,
     '<Year4><Month,2><Day,2><Hours24><Minutes,2>') + '.' + _PreviousFileExtension, _ArchiveFolder, FileInStream);
//Copy(_FileName, _NewFileName);

11.2. Copying file to a different directory and deleting the old one

First, with the function GetFile, we acquire the file we want to copy to get its InStream. Then with the function, GetFileContentInStream, retrieved InStream is used to create a new file in the desired directory. After checking if the new file was created with the function SharePointFileExists, the file in the old directory is deleted by the function DeleteFile.

 // FileSystemObject.CopyFile(_FromFile, _ToFile, false);
 // if FileSystemObject.FileExists(_ToFile) then
 //     FileSystemObject.DeleteFile(_FromFile);
 SharePointMgt.GetFile(SharePointMgt.FixPathForSharePoint(FileMgmt.GetDirectoryName(_FromFile)),
                       FileMgmt.GetFileName(_FromFile), OldSharePointFile);   
 SharePointMgt.GetFileContentInStream(OldSharePointFile.OdataId, FileInStream);
 SharePointMgt.SaveFile(FileMgmt.GetFileName(_ToFile),
               SharePointMgt.FixPathForSharePoint(FileMgmt.GetDirectoryName(_ToFile)), FileInStream);
 if SharePointMgt.SharePointFileExists(FileMgmt.GetFileName(_ToFile),
                  SharePointMgt.FixPathForSharePoint(FileMgmt.GetDirectoryName(_ToFile))) then
     SharePointMgt.DeleteFile(OldSharePointFile.OdataId);
//SPLN1.00 - End

 

Function FixPathForSharePoint fixes the path for SharePoint use since standard file management functions by default turn slashes “/” to a different side than used in SharePoint (local client’s path’s slash “\”).

    procedure FixPathForSharePoint(FolderPath: Text): Text
    var
        SPFolderPath: Text;
    begin
        SPFolderPath := FolderPath.Replace('\', '/');
        SPFolderPath := SPFolderPath.Replace('//', '/');
        if StrPos(SPFolderPath, ':') > 0 then
            SPFolderPath := DelStr(SPFolderPath, 1, StrPos(SPFolderPath, ':'));
        if SPFolderPath.EndsWith('/') then
            SPFolderPath := DelStr(SPFolderPath, StrLen(SPFolderPath), 1);
        exit(SPFolderPath);
    end;

Conclusion

 

One solution for solving issues related to the local file system’s usage is using a standard SharePoint module. SharePoint module provides an alternative file system but has a significant drawback – the standard module’s functionality is simple and very limited (e.g., common file copying or removing functions are missing). This guideline gives an example of SharePoint file management implementation, which replicates various “OnPrem” file management functionalities to expand the standard SharePoint module’s features. Looking on the bright side, the newly added function (file’s InStream download) to the standard SharePoint module in the latest „Business Central“ release shows that standard SharePoint functionality might be broadened in the future.