Dynamics 365 Business Central: How to upload files from Business Central to OneDrive via AL (Graph API)

Dynamics 365 Business Central

Hi, Readers.
We discussed Dynamics 365 Business Central: How to import/read files from OneDrive to Business Central via AL (Graph API) last week. Today I would like to briefly introduce another situation, how to upload files from Business Central to OneDrive via AL (Graph API).

OneDrive for work or school (formerly known as OneDrive for Business) is a cloud storage service that is included in Microsoft 365. Business Central makes it easy to store, manage, and share files with other people through OneDrive. We have discussed these standard features before.

However, the standard has not been able to achieve automation, such as automatically reading files from OneDrive into Business Central or uploading files from Business Central to OneDrive. While this can be done with Power Automate, it is not very convenient as standard APIs are not scalable and developers need to maintain multiple systems.

So, is it possible to do this through AL Code? Yes, we can use Microsoft Graph API.
Microsoft Graph API: Microsoft Graph is a RESTful web API that enables you to access Microsoft Cloud service resources. After you register your app and get authentication tokens for a user or service, you can make requests to the Microsoft Graph API.

Registering your application establishes a trust relationship between your app and the Microsoft identity platform. This part of the setup is the same as in Using OAuth to connect Business Central APIs and Web Service in Postman. Just the API permissions are different.

To configure application permissions for the app in the app registrations experience on the Microsoft Entra admin center, follow these steps:

  • On the application’s API permissions page, choose Add a permission.
  • Select Microsoft Graph > select Application permissions.
  • In the Select Permissions dialog, choose the permissions to configure to the app.

This time I only added Microsoft Graph -> Application -> Files.ReadWrite.All.

PS: Permissions for OneDrive API

Delegated permissions: Also called scopes, allow the application to act on behalf of the signed-in user. (access on behalf of a user)

PermissionDisplay StringDescriptionAdmin Consent Required
Files.ReadRead user filesAllows the app to read the signed-in user’s files.No
Files.Read.AllRead all files that user can accessAllows the app to read all files the signed-in user can access.No
Files.ReadWriteHave full access to user filesAllows the app to read, create, update, and delete the signed-in user’s files.No
Files.ReadWrite.AllHave full access to all files user can accessAllows the app to read, create, update, and delete all files the signed-in user can access.No
Files.ReadWrite.AppFolderHave full access to the application’s folder (preview)(Preview) Allows the app to read, create, update, and delete files in the application’s folder.No
Files.Read.SelectedRead files that the user selectsLimited support in Microsoft Graph – see Remarks
(Preview) Allows the app to read files that the user selects. The app has access for several hours after the user selects a file.
No
Files.ReadWrite.SelectedRead and write files that the user selectsLimited support in Microsoft Graph — see Remarks
(Preview) Allows the app to read and write files that the user selects. The app has access for several hours after the user selects a file.
No

Application permissions: Also called app roles, allow the app to access data on its own, without a signed-in user. (access without a user)

PermissionDisplay StringDescriptionAdmin Consent Required
Files.Read.AllRead files in all site collectionsAllows the app to read all files in all site collections without a signed in user.Yes
Files.ReadWrite.AllRead and write files in all site collectionsAllows the app to read, create, update, and delete all files in all site collections without a signed in user.Yes

Then we can test in Postman to see if it is accessible.

Enter the following info in Postman. (You can use variables instead to keep sensitive data secure)

Access Token URLhttps://login.microsoftonline.com/d8f36038-1f93-4543-affc-5dc92b6ee871/oauth2/v2.0/token (Change it to your tenant ID)

Client ID: a80c03cf-6ffa-4b6e-b2c8-6005310d3d87

Client Secret: fME7Q~cAaSBhXMGZoHY3ei64nn1fxGpqF42mh

Scope: https://graph.microsoft.com/.default (Note that Scope is different from BC)

Cilent Authentication: Send client credentials in body

Test Endpoint:

https://graph.microsoft.com/v1.0/users/{UserName}/drive/root/

For example,

https://graph.microsoft.com/v1.0/users/Admin@2qcj3x.onmicrosoft.com/drive/root

Add children to access the contents of My files in OneDrive.

https://graph.microsoft.com/v1.0/users/Admin@2qcj3x.onmicrosoft.com/drive/root/children

For example,

Then we can add the folder name to read the folder information. For example, OneDriveAPITest

https://graph.microsoft.com/v1.0/users/Admin@2qcj3x.onmicrosoft.com/drive/root/children/OneDriveAPITest

Or

https://graph.microsoft.com/v1.0/users/Admin@2qcj3x.onmicrosoft.com//drive/root:/OneDriveAPITest

As above, add children to read the contents of the folder.

https://graph.microsoft.com/v1.0/users/Admin@2qcj3x.onmicrosoft.com/drive/root/children/OneDriveAPITest/children

Or

https://graph.microsoft.com/v1.0/users/Admin@2qcj3x.onmicrosoft.com/drive/root:/OneDriveAPITest:/children

For example,

Let’s read the contents of the file, Items.csv.

https://graph.microsoft.com/v1.0/users/Admin@2qcj3x.onmicrosoft.com/drive/root:/OneDriveAPITest/{FileName}:/content

For example,

https://graph.microsoft.com/v1.0/users/Admin@2qcj3x.onmicrosoft.com/drive/root:/OneDriveAPITest/Items.csv:/content

Finally, let’s try uploading a file to OneDrive from Postman. First, the endpoint is the same as reading a file above (Including content).

https://graph.microsoft.com/v1.0/users/Admin@2qcj3x.onmicrosoft.com/drive/root:/OneDriveAPITest/{UploadFileName}:/content

For example, I want to upload the OneDriveUploadTest.csv file

Endpoint:

https://graph.microsoft.com/v1.0/users/Admin@2qcj3x.onmicrosoft.com/drive/root:/OneDriveAPITest/OneDriveUploadTest.csv:/content

Then the http action is not Get, but Put.
Headers -> Content-Type -> text/csv:

Body -> binary -> upload file:

Choose Send.
201 Created: A new resource was created successfully.

Test video:

PS:
1. If you get the following error, please check the API permissions in App Registration.

        “code”: “AccessDenied”,
        “message”: “Either scp or roles claim need to be present in the token.”,

2. If you encounter a 401 Unauthorized error, please check whether the Scope is set to the graph API. (https://graph.microsoft.com/.default)

        “code”: “InvalidAuthenticationToken”,
        “message”: “Access token validation failure. Invalid audience.”,

3. If you use delegated permissions, use the following endpoint:

https://graph.microsoft.com/v1.0/me/drive/root/

4. When the file cannot be found, the following error message will be displayed:

        “code”: “itemNotFound”,
        “message”: “The resource could not be found.”

5. When opening the file in Postman, because it can only be opened in XML, HTML, Txt or Json format, the following situation may occur, but the test is successful.

OK, next let’s see if we can upload files from AL. Here are a few important points.
Get OAuth Token:

Using OAuth Token to upload file to OneDrive: Http action – Put in AL (If a file with the same name already exists, it will be overwritten)

This time I did two simple tests in total, I hope it can be of some help to everyone.

1. Upload a fixed file to OneDrive (Fixed name and type)

For example, upload the following Attachment file

In the Document Attachment (1173) table:

Test video: As a test I displayed the ResponseText

Source Code: GitHub (Please note that the source code is for reference only, you can improve it according to your own needs)

pageextension 50100 CustomerListExt extends "Customer List"
{
    actions
    {
        addafter("Sent Emails")
        {
            action(UploadFileToOneDrive)
            {
                Caption = 'Upload File to OneDrive';
                ApplicationArea = All;
                Promoted = true;
                PromotedCategory = Process;
                PromotedIsBig = true;
                Image = GetActionMessages;

                trigger OnAction()
                var
                    OneDriveHandler: Codeunit OneDriveHandler;
                begin
                    OneDriveHandler.UploadFilesToOneDrive();
                end;
            }
        }
    }
}

codeunit 50120 OneDriveHandler
{
    procedure UploadFilesToOneDrive()
    var
        HttpClient: HttpClient;
        HttpRequestMessage: HttpRequestMessage;
        HttpResponseMessage: HttpResponseMessage;
        Headers: HttpHeaders;
        ContentHeader: HttpHeaders;
        RequestContent: HttpContent;
        JsonResponse: JsonObject;
        AuthToken: SecretText;
        OneDriveFileUrl: Text;
        ResponseText: Text;
        OutStream: OutStream;
        FileContent: InStream;
        DocAttach: Record "Document Attachment";
        TempBlob: Codeunit "Temp Blob";
    begin
        // Get OAuth token
        AuthToken := GetOAuthToken();

        if AuthToken.IsEmpty() then
            Error('Failed to obtain access token.');

        if DocAttach.Get(18, 10000, Enum::"Attachment Document Type"::"Quote", 0, 1) then
            if DocAttach."Document Reference ID".HasValue then begin
                TempBlob.CreateOutStream(OutStream);
                DocAttach."Document Reference ID".ExportStream(OutStream);
                TempBlob.CreateInStream(FileContent);
            end;

        // Define the OneDrive folder URL

        // delegated permissions
        //OneDriveFileUrl := 'https://graph.microsoft.com/v1.0/me/drive/root/children';

        // application permissions (replace with the actual user principal name)
        OneDriveFileUrl := 'https://graph.microsoft.com/v1.0/users/Admin@2qcj3x.onmicrosoft.com/drive/root:/OneDriveAPITest/OneDriveUploadTest.csv:/content';
        // Initialize the HTTP request
        HttpRequestMessage.SetRequestUri(OneDriveFileUrl);
        HttpRequestMessage.Method := 'PUT';
        HttpRequestMessage.GetHeaders(Headers);
        Headers.Add('Authorization', SecretStrSubstNo('Bearer %1', AuthToken));
        RequestContent.GetHeaders(ContentHeader);
        ContentHeader.Clear();
        ContentHeader.Add('Content-Type', 'text/csv');
        HttpRequestMessage.Content.WriteFrom(FileContent);

        // Send the HTTP request
        if HttpClient.Send(HttpRequestMessage, HttpResponseMessage) then begin
            // Log the status code for debugging
            //Message('HTTP Status Code: %1', HttpResponseMessage.HttpStatusCode());

            if HttpResponseMessage.IsSuccessStatusCode() then begin
                HttpResponseMessage.Content.ReadAs(ResponseText);
                JsonResponse.ReadFrom(ResponseText);
                Message(ResponseText);
            end else begin
                //Report errors!
                HttpResponseMessage.Content.ReadAs(ResponseText);
                Error('Failed to upload files to OneDrive: %1 %2', HttpResponseMessage.HttpStatusCode(), ResponseText);
            end;
        end else
            Error('Failed to send HTTP request to OneDrive');
    end;

    procedure GetOAuthToken() AuthToken: SecretText
    var
        ClientID: Text;
        ClientSecret: Text;
        TenantID: Text;
        AccessTokenURL: Text;
        OAuth2: Codeunit OAuth2;
        Scopes: List of [Text];
    begin
        ClientID := 'b4fe1687-f1ab-4bfa-b494-0e2236ed50bd';
        ClientSecret := 'huL8Q~edsQZ4pwyxka3f7.WUkoKNcPuqlOXv0bww';
        TenantID := '7e47da45-7f7d-448a-bd3d-1f4aa2ec8f62';
        AccessTokenURL := 'https://login.microsoftonline.com/' + TenantID + '/oauth2/v2.0/token';
        Scopes.Add('https://graph.microsoft.com/.default');
        if not OAuth2.AcquireTokenWithClientCredentials(ClientID, ClientSecret, AccessTokenURL, '', Scopes, AuthToken) then
            Error('Failed to get access token from response\%1', GetLastErrorText());
    end;
}

2. Upload the selected file from Attachments to OneDrive

To do this, we need to get the File Name and Mime Type first.
File Name in Document Attachment table:

Mime Type in Tenant Media table:

For example,

Simple test:

Test video: You can delete or modify the message (ResponseText) after uploading the file

Source Code: GitHub (Please note that the source code is for reference only, you can improve it according to your own needs)

pageextension 50100 DocumentAttachmentDetailsExt extends "Document Attachment Details"
{
    actions
    {
        addafter(UploadFile)
        {
            action(UploadFileToOneDrive)
            {
                Caption = 'Upload File to OneDrive';
                ApplicationArea = All;
                Promoted = true;
                PromotedCategory = Process;
                PromotedIsBig = true;
                Image = GetActionMessages;

                trigger OnAction()
                var
                    OneDriveHandler: Codeunit OneDriveHandler;
                    DocAttach: Record "Document Attachment";
                begin
                    DocAttach.Reset();
                    CurrPage.SetSelectionFilter(DocAttach);
                    if DocAttach.FindFirst() then
                        OneDriveHandler.UploadFilesToOneDrive(DocAttach);
                end;
            }
        }
    }
}

codeunit 50120 OneDriveHandler
{
    procedure UploadFilesToOneDrive(DocAttach: Record "Document Attachment")
    var
        HttpClient: HttpClient;
        HttpRequestMessage: HttpRequestMessage;
        HttpResponseMessage: HttpResponseMessage;
        Headers: HttpHeaders;
        ContentHeader: HttpHeaders;
        RequestContent: HttpContent;
        JsonResponse: JsonObject;
        AuthToken: SecretText;
        OneDriveFileUrl: Text;
        ResponseText: Text;
        OutStream: OutStream;
        FileContent: InStream;
        TempBlob: Codeunit "Temp Blob";
        FileName: Text;
        TenantMedia: Record "Tenant Media";
        MimeType: Text;
    begin
        // Get OAuth token
        AuthToken := GetOAuthToken();

        if AuthToken.IsEmpty() then
            Error('Failed to obtain access token.');

        if DocAttach."Document Reference ID".HasValue then begin
            TempBlob.CreateOutStream(OutStream);
            DocAttach."Document Reference ID".ExportStream(OutStream);
            TempBlob.CreateInStream(FileContent);
            FileName := DocAttach."File Name" + '.' + DocAttach."File Extension";
            if TenantMedia.Get(DocAttach."Document Reference ID".MediaId) then
                MimeType := TenantMedia."Mime Type";
        end;

        // Define the OneDrive folder URL

        // delegated permissions
        //OneDriveFileUrl := 'https://graph.microsoft.com/v1.0/me/drive/root/children';

        // application permissions (replace with the actual user principal name)
        OneDriveFileUrl := 'https://graph.microsoft.com/v1.0/users/Admin@2qcj3x.onmicrosoft.com/drive/root:/OneDriveAPITest/' + FileName + ':/content';
        // Initialize the HTTP request
        HttpRequestMessage.SetRequestUri(OneDriveFileUrl);
        HttpRequestMessage.Method := 'PUT';
        HttpRequestMessage.GetHeaders(Headers);
        Headers.Add('Authorization', SecretStrSubstNo('Bearer %1', AuthToken));
        RequestContent.GetHeaders(ContentHeader);
        ContentHeader.Clear();
        ContentHeader.Add('Content-Type', MimeType);
        HttpRequestMessage.Content.WriteFrom(FileContent);

        // Send the HTTP request
        if HttpClient.Send(HttpRequestMessage, HttpResponseMessage) then begin
            // Log the status code for debugging
            //Message('HTTP Status Code: %1', HttpResponseMessage.HttpStatusCode());

            if HttpResponseMessage.IsSuccessStatusCode() then begin
                HttpResponseMessage.Content.ReadAs(ResponseText);
                JsonResponse.ReadFrom(ResponseText);
                Message(ResponseText);
            end else begin
                //Report errors!
                HttpResponseMessage.Content.ReadAs(ResponseText);
                Error('Failed to upload files to OneDrive: %1 %2', HttpResponseMessage.HttpStatusCode(), ResponseText);
            end;
        end else
            Error('Failed to send HTTP request to OneDrive');
    end;

    procedure GetOAuthToken() AuthToken: SecretText
    var
        ClientID: Text;
        ClientSecret: Text;
        TenantID: Text;
        AccessTokenURL: Text;
        OAuth2: Codeunit OAuth2;
        Scopes: List of [Text];
    begin
        ClientID := 'b4fe1687-f1ab-4bfa-b494-0e2236ed50bd';
        ClientSecret := 'huL8Q~edsQZ4pwyxka3f7.WUkoKNcPuqlOXv0bww';
        TenantID := '7e47da45-7f7d-448a-bd3d-1f4aa2ec8f62';
        AccessTokenURL := 'https://login.microsoftonline.com/' + TenantID + '/oauth2/v2.0/token';
        Scopes.Add('https://graph.microsoft.com/.default');
        if not OAuth2.AcquireTokenWithClientCredentials(ClientID, ClientSecret, AccessTokenURL, '', Scopes, AuthToken) then
            Error('Failed to get access token from response\%1', GetLastErrorText());
    end;
}

At this point, I believe you should be able to understand how to upload file from Business Central to OneDrive. You can also set these process in the Job Queue so that the system can run automatically. Give it a try!!!😁

END

Hope this will help.

Thanks.

ZHU

コメント

Copied title and URL