diff --git a/src/System Application/App/Azure File Services API/README.md b/src/System Application/App/Azure File Services API/README.md
new file mode 100644
index 0000000000..74f9d5a821
--- /dev/null
+++ b/src/System Application/App/Azure File Services API/README.md
@@ -0,0 +1,2499 @@
+This module provides functionality to work with files and directories from Azure File Share Services.
+
+# Public Objects
+
+## AFS Handle
+
+Holds information about file handles that are currently open.
+
+### Object Definition
+
+
+Object Type | Table |
+Object ID | 8951 |
+Object Name | AFS Handle |
+
+
+### Fields
+
+| Number | Name | Type |
+| ---- | ------- | ----------- |
+| 1 | Entry No. | Integer |
+| 10 | Handle ID | Text[50] |
+| 11 | Path | Text[2048] |
+| 12 | Client IP | Text[2048] |
+| 13 | Open Time | DateTime |
+| 14 | Last Reconnect Time | DateTime |
+| 15 | File ID | Text[50] |
+| 16 | Parent ID | Text[50] |
+| 17 | Session ID | Text[50] |
+| 20 | Next Marker | Text[2048] |
+
+## AFS Directory Content
+
+Holds information about directory content in a storage account.
+
+### Object Definition
+
+
+Object Type | Table |
+Object ID | 8950 |
+Object Name | AFS Directory Content |
+
+
+### Fields
+
+| Number | Name | Type |
+| ---- | ------- | ----------- |
+| 1 | Entry No. | Integer |
+| 2 | Parent Directory | Text[2048] |
+| 3 | Level | Integer |
+| 4 | Full Name | Text[2048] |
+| 10 | Name | Text[2048] |
+| 11 | Creation Time | DateTime |
+| 12 | Last Modified | DateTime |
+| 13 | Content Length | Integer |
+| 14 | Last Access Time | DateTime |
+| 15 | Change Time | DateTime |
+| 16 | Resource Type | Enum "AFS File Resource Type" |
+| 17 | Etag | Text[200] |
+| 18 | Archive | Boolean |
+| 19 | Hidden | Boolean |
+| 20 | Last Write Time | DateTime |
+| 21 | Read Only | Boolean |
+| 22 | Permission Key | Text[200] |
+| 100 | XML Value | Blob |
+| 110 | URI | Text[2048] |
+
+
+## AFS File Client
+
+
+Provides functionality to access the Azure File Storage.
+
+### Properties
+
+| Property | Value |
+| --- | --- |
+| Object Type | Codeunit |
+| Object Subtype | Normal |
+| Object ID | 8950 |
+| Accessibility Level | Public |
+
+### Procedures
+
+#### `Initialize()`
+
+Initializes the AFS Client.
+
+
+##### Syntax
+
+```al
+Initialize(StorageAccount: Text, FileShare: Text, Authorization: Interface "Storage Service Authorization")
+```
+
+##### Parameters
+
+*StorageAccount*
+ Type: Text
+
+The name of the storage account to use.
+
+*FileShare*
+ Type: Text
+
+The name of the file share to use.
+
+*Authorization*
+ Type: Interface "Storage Service Authorization"
+
+The authorization to use.
+
+
+#### `Initialize()`
+
+Initializes the AFS Client.
+
+
+##### Syntax
+
+```al
+Initialize(StorageAccount: Text, FileShare: Text, Authorization: Interface "Storage Service Authorization", APIVersion: Enum "Storage Service API Version")
+```
+
+##### Parameters
+
+*StorageAccount*
+ Type: Text
+
+The name of the storage account to use.
+
+*FileShare*
+ Type: Text
+
+The name of the file share to use.
+
+*Authorization*
+ Type: Interface "Storage Service Authorization"
+
+The authorization to use.
+
+*APIVersion*
+ Type: Enum "Storage Service API Version"
+
+The API Version to use.
+
+
+#### `CreateFile()`
+
+Creates a file in the file share.
+This does not fill in the file content, it only initializes the file.
+
+
+##### Syntax
+
+```al
+[Codeunit "AFS Operation Response"] := CreateFile(FilePath: Text, InStream: InStream)
+```
+
+##### Parameters
+
+*FilePath*
+ Type: Text
+
+The path where the file will be created.
+
+*InStream*
+ Type: InStream
+
+The file content, only used to check file size.
+
+
+##### Return
+
+*Codeunit "AFS Operation Response"*
+
+An operation response object.
+
+#### `CreateFile()`
+
+Creates a file in the file share.
+This does not fill in the file content, it only initializes the file.
+
+
+##### Syntax
+
+```al
+[Codeunit "AFS Operation Response"] := CreateFile(FilePath: Text, InStream: InStream, AFSOptionalParameters: Codeunit "AFS Optional Parameters")
+```
+
+##### Parameters
+
+*FilePath*
+ Type: Text
+
+The path where the file will be created.
+
+*InStream*
+ Type: InStream
+
+The file content, only used to check file size.
+
+*AFSOptionalParameters*
+ Type: Codeunit "AFS Optional Parameters"
+
+Optional parameters to pass with the request.
+
+
+##### Return
+
+*Codeunit "AFS Operation Response"*
+
+An operation response object.
+
+#### `CreateFile()`
+
+Creates a file in the file share.
+This does not fill in the file content, it only initializes the file.
+
+
+##### Syntax
+
+```al
+[Codeunit "AFS Operation Response"] := CreateFile(FilePath: Text, FileSize: Integer)
+```
+
+##### Parameters
+
+*FilePath*
+ Type: Text
+
+The path where the file will be created.
+
+*FileSize*
+ Type: Integer
+
+The size of the file to initialize, in bytes.
+
+
+##### Return
+
+*Codeunit "AFS Operation Response"*
+
+An operation response object.
+
+#### `CreateFile()`
+
+Creates a file in the file share.
+This does not fill in the file content, it only initializes the file.
+
+
+##### Syntax
+
+```al
+[Codeunit "AFS Operation Response"] := CreateFile(FilePath: Text, FileSize: Integer, AFSOptionalParameters: Codeunit "AFS Optional Parameters")
+```
+
+##### Parameters
+
+*FilePath*
+ Type: Text
+
+The path where the file will be created.
+
+*FileSize*
+ Type: Integer
+
+The size of the file to initialize, in bytes.
+
+*AFSOptionalParameters*
+ Type: Codeunit "AFS Optional Parameters"
+
+Optional parameters to pass with the request.
+
+
+##### Return
+
+*Codeunit "AFS Operation Response"*
+
+An operation response object.
+
+#### `GetFileAsFile()`
+
+Receives a file as a File from a file share.
+The file will be downloaded through the browser.
+
+
+##### Syntax
+
+```al
+[Codeunit "AFS Operation Response"] := GetFileAsFile(FilePath: Text)
+```
+
+##### Parameters
+
+*FilePath*
+ Type: Text
+
+The path to the file.
+
+
+##### Return
+
+*Codeunit "AFS Operation Response"*
+
+An operation response object
+
+#### `GetFileAsFile()`
+
+Receives a file as a File from a file share.
+The file will be downloaded through the browser.
+
+
+##### Syntax
+
+```al
+[Codeunit "AFS Operation Response"] := GetFileAsFile(FilePath: Text, AFSOptionalParameters: Codeunit "AFS Optional Parameters")
+```
+
+##### Parameters
+
+*FilePath*
+ Type: Text
+
+The path to the file.
+
+*AFSOptionalParameters*
+ Type: Codeunit "AFS Optional Parameters"
+
+Optional parameters to pass with the request.
+
+
+##### Return
+
+*Codeunit "AFS Operation Response"*
+
+An operation response object
+
+#### `GetFileAsStream()`
+
+Receives a file as a stream from a file share.
+
+
+##### Syntax
+
+```al
+[Codeunit "AFS Operation Response"] := GetFileAsStream(FilePath: Text, var TargetInStream: InStream)
+```
+
+##### Parameters
+
+*FilePath*
+ Type: Text
+
+The path to the file.
+
+*TargetInStream*
+ Type: InStream
+
+The result instream containing the content of the file.
+
+
+##### Return
+
+*Codeunit "AFS Operation Response"*
+
+An operation response object
+
+#### `GetFileAsStream()`
+
+Receives a file as a stream from a file share.
+
+
+##### Syntax
+
+```al
+[Codeunit "AFS Operation Response"] := GetFileAsStream(FilePath: Text, var TargetInStream: InStream, AFSOptionalParameters: Codeunit "AFS Optional Parameters")
+```
+
+##### Parameters
+
+*FilePath*
+ Type: Text
+
+The path to the file.
+
+*TargetInStream*
+ Type: InStream
+
+The result instream containing the content of the file.
+
+*AFSOptionalParameters*
+ Type: Codeunit "AFS Optional Parameters"
+
+Optional parameters to pass with the request.
+
+
+##### Return
+
+*Codeunit "AFS Operation Response"*
+
+An operation response object
+
+#### `GetFileAsText()`
+
+Receives a file as a text from a file share.
+
+
+##### Syntax
+
+```al
+[Codeunit "AFS Operation Response"] := GetFileAsText(FilePath: Text, var TargetText: Text)
+```
+
+##### Parameters
+
+*FilePath*
+ Type: Text
+
+The path to the file.
+
+*TargetText*
+ Type: Text
+
+The result text containing the content of the file.
+
+
+##### Return
+
+*Codeunit "AFS Operation Response"*
+
+An operation response object
+
+#### `GetFileAsText()`
+
+Receives a file as a text from a file share.
+
+
+##### Syntax
+
+```al
+[Codeunit "AFS Operation Response"] := GetFileAsText(FilePath: Text, var TargetText: Text, AFSOptionalParameters: Codeunit "AFS Optional Parameters")
+```
+
+##### Parameters
+
+*FilePath*
+ Type: Text
+
+The path to the file.
+
+*TargetText*
+ Type: Text
+
+The result text containing the content of the file.
+
+*AFSOptionalParameters*
+ Type: Codeunit "AFS Optional Parameters"
+
+Optional parameters to pass with the request.
+
+
+##### Return
+
+*Codeunit "AFS Operation Response"*
+
+An operation response object
+
+#### GetFileMetadata()
+
+Receives file metadata as dictionary from a file share.
+
+##### Syntax
+
+```al
+[Codeunit "AFS Operation Response"] := GetFileMetadata(FilePath: Text, var TargetMetadata: Dictionary of [Text, Text])
+```
+
+##### Parameters
+
+*FilePath*
+ Type: Text
+
+The path to the file.
+
+*TargetMetadata*
+ Type: Dictionary of [Text, Text]
+
+The result dictionary containing the metadata of the file in the form of metadata key and a value.
+
+
+##### Return
+
+*Codeunit "AFS Operation Response"*
+
+An operation response object
+
+#### GetFileMetadata()
+
+Receives file metadata as dictionary from a file share.
+
+##### Syntax
+
+```al
+[Codeunit "AFS Operation Response"] := GetFileMetadata(FilePath: Text, var TargetMetadata: Dictionary of [Text, Text], AFSOptionalParameters: Codeunit "AFS Optional Parameters")
+```
+
+##### Parameters
+
+*FilePath*
+ Type: Text
+
+The path to the file.
+
+*TargetMetadata*
+ Type: Dictionary of [Text, Text]
+
+The result dictionary containing the metadata of the file in the form of metadata key and a value.
+
+*AFSOptionalParameters*
+ Type: Codeunit "AFS Optional Parameters"
+
+Optional parameters to pass with the request.
+
+
+##### Return
+
+*Codeunit "AFS Operation Response"*
+
+An operation response object
+
+#### SetFileMetadata()
+
+Sets the file metadata.
+
+##### Syntax
+
+```al
+[Codeunit "AFS Operation Response"] := SetFileMetadata(FilePath: Text, Metadata: Dictionary of [Text, Text])
+```
+
+##### Parameters
+
+*FilePath*
+ Type: Text
+
+The path to the file.
+
+*Metadata*
+ Type: Dictionary of [Text, Text]
+
+The dictionary containing the metadata of the file in the form of metadata key and a value.
+
+
+##### Return
+
+*Codeunit "AFS Operation Response"*
+
+An operation response object
+
+#### SetFileMetadata()
+
+Sets the file metadata.
+
+##### Syntax
+
+```al
+[Codeunit "AFS Operation Response"] := SetFileMetadata(FilePath: Text, Metadata: Dictionary of [Text, Text], AFSOptionalParameters: Codeunit "AFS Optional Parameters")
+```
+
+##### Parameters
+
+*FilePath*
+ Type: Text
+
+The path to the file.
+
+*Metadata*
+ Type: Dictionary of [Text, Text]
+
+The dictionary containing the metadata of the file in the form of metadata key and a value.
+
+*AFSOptionalParameters*
+ Type: Codeunit "AFS Optional Parameters"
+
+Optional parameters to pass with the request.
+
+
+##### Return
+
+*Codeunit "AFS Operation Response"*
+
+An operation response object
+
+#### `PutFileUI()`
+
+Uploads a file to the file share.
+User will be prompted to specify the file to send.
+
+
+##### Syntax
+
+```al
+[Codeunit "AFS Operation Response"] := PutFileUI()
+```
+
+##### Return
+
+*Codeunit "AFS Operation Response"*
+
+An operation response object
+
+#### `PutFileUI()`
+
+Uploads a file to the file share.
+User will be prompted to specify the file to send.
+
+
+##### Syntax
+
+```al
+[Codeunit "AFS Operation Response"] := PutFileUI(AFSOptionalParameters: Codeunit "AFS Optional Parameters")
+```
+
+##### Parameters
+
+*AFSOptionalParameters*
+ Type: Codeunit "AFS Optional Parameters"
+
+Optional parameters to pass with the request.
+
+
+##### Return
+
+*Codeunit "AFS Operation Response"*
+
+An operation response object
+
+#### `PutFileStream()`
+
+Uploads a file to the file share from instream.
+
+
+##### Syntax
+
+```al
+[Codeunit "AFS Operation Response"] := PutFileStream(FilePath: Text, var SourceInStream: InStream)
+```
+
+##### Parameters
+
+*FilePath*
+ Type: Text
+
+The path to the file.
+
+*SourceInStream*
+ Type: InStream
+
+The source instream containing the content of the file.
+
+
+##### Return
+
+*Codeunit "AFS Operation Response"*
+
+An operation response object
+
+#### `PutFileStream()`
+
+Uploads a file to the file share from instream.
+
+
+##### Syntax
+
+```al
+[Codeunit "AFS Operation Response"] := PutFileStream(FilePath: Text, var SourceInStream: InStream, AFSOptionalParameters: Codeunit "AFS Optional Parameters")
+```
+
+##### Parameters
+
+*FilePath*
+ Type: Text
+
+The path to the file.
+
+*SourceInStream*
+ Type: InStream
+
+The source instream containing the content of the file.
+
+*AFSOptionalParameters*
+ Type: Codeunit "AFS Optional Parameters"
+
+Optional parameters to pass with the request.
+
+
+##### Return
+
+*Codeunit "AFS Operation Response"*
+
+An operation response object
+
+#### `PutFileText()`
+
+Uploads a file to the file share from text.
+
+
+##### Syntax
+
+```al
+[Codeunit "AFS Operation Response"] := PutFileText(FilePath: Text, var SourceText: Text)
+```
+
+##### Parameters
+
+*FilePath*
+ Type: Text
+
+The path to the file.
+
+*SourceText*
+ Type: Text
+
+The source text containing the content of the file.
+
+
+##### Return
+
+*Codeunit "AFS Operation Response"*
+
+An operation response object
+
+#### `PutFileText()`
+
+Uploads a file to the file share from text.
+
+
+##### Syntax
+
+```al
+[Codeunit "AFS Operation Response"] := PutFileText(FilePath: Text, var SourceText: Text, AFSOptionalParameters: Codeunit "AFS Optional Parameters")
+```
+
+##### Parameters
+
+*FilePath*
+ Type: Text
+
+The path to the file.
+
+*SourceText*
+ Type: Text
+
+The source text containing the content of the file.
+
+*AFSOptionalParameters*
+ Type: Codeunit "AFS Optional Parameters"
+
+Optional parameters to pass with the request.
+
+
+##### Return
+
+*Codeunit "AFS Operation Response"*
+
+An operation response object
+
+#### `DeleteFile()`
+
+Deletes a file from the file share.
+
+
+##### Syntax
+
+```al
+[Codeunit "AFS Operation Response"] := DeleteFile(FilePath: Text)
+```
+
+##### Parameters
+
+*FilePath*
+ Type: Text
+
+The path to the file.
+
+
+##### Return
+
+*Codeunit "AFS Operation Response"*
+
+An operation response object
+
+#### `DeleteFile()`
+
+Deletes a file from the file share.
+
+
+##### Syntax
+
+```al
+[Codeunit "AFS Operation Response"] := DeleteFile(FilePath: Text, AFSOptionalParameters: Codeunit "AFS Optional Parameters")
+```
+
+##### Parameters
+
+*FilePath*
+ Type: Text
+
+The path to the file.
+
+*AFSOptionalParameters*
+ Type: Codeunit "AFS Optional Parameters"
+
+Optional parameters to pass with the request.
+
+
+##### Return
+
+*Codeunit "AFS Operation Response"*
+
+An operation response object
+
+#### `ListDirectory()`
+
+Lists files and directories from the file share.
+
+
+##### Syntax
+
+```al
+[Codeunit "AFS Operation Response"] := ListDirectory(DirectoryPath: Text, var AFSDirectoryContent: Record "AFS Directory Content")
+```
+
+##### Parameters
+
+*DirectoryPath*
+ Type: Text
+
+The path of the directory to list.
+
+*AFSDirectoryContent*
+ Type: Record "AFS Directory Content"
+
+The result collection with contents of the directory (temporary)
+
+
+##### Return
+
+*Codeunit "AFS Operation Response"*
+
+An operation response object
+
+#### `ListDirectory()`
+
+Lists files and directories from the file share.
+
+
+##### Syntax
+
+```al
+[Codeunit "AFS Operation Response"] := ListDirectory(DirectoryPath: Text, var AFSDirectoryContent: Record "AFS Directory Content", AFSOptionalParameters: Codeunit "AFS Optional Parameters")
+```
+
+##### Parameters
+
+*DirectoryPath*
+ Type: Text
+
+The path of the directory to list.
+
+*AFSDirectoryContent*
+ Type: Record "AFS Directory Content"
+
+The result collection with contents of the directory (temporary)
+
+*AFSOptionalParameters*
+ Type: Codeunit "AFS Optional Parameters"
+
+Optional parameters to pass with the request.
+
+
+##### Return
+
+*Codeunit "AFS Operation Response"*
+
+An operation response object
+
+#### `ListDirectory()`
+
+Lists files and directories from the file share.
+
+
+##### Syntax
+
+```al
+[Codeunit "AFS Operation Response"] := ListDirectory(DirectoryPath: Text, PreserveDirectoryContent: Boolean, var AFSDirectoryContent: Record "AFS Directory Content")
+```
+
+##### Parameters
+
+*DirectoryPath*
+ Type: Text
+
+The path of the directory to list.
+
+*PreserveDirectoryContent*
+ Type: Boolean
+
+Specifies if the result collection should be cleared before filling it with the response data.
+
+*AFSDirectoryContent*
+ Type: Record "AFS Directory Content"
+
+The result collection with contents of the directory (temporary)
+
+
+##### Return
+
+*Codeunit "AFS Operation Response"*
+
+An operation response object
+
+#### `ListDirectory()`
+
+Lists files and directories from the file share.
+
+
+##### Syntax
+
+```al
+[Codeunit "AFS Operation Response"] := ListDirectory(DirectoryPath: Text, PreserveDirectoryContent: Boolean, var AFSDirectoryContent: Record "AFS Directory Content", AFSOptionalParameters: Codeunit "AFS Optional Parameters")
+```
+
+##### Parameters
+
+*DirectoryPath*
+ Type: Text
+
+The path of the directory to list.
+
+*PreserveDirectoryContent*
+ Type: Boolean
+
+Specifies if the result collection should be cleared before filling it with the response data.
+
+*AFSDirectoryContent*
+ Type: Record "AFS Directory Content"
+
+The result collection with contents of the directory (temporary)
+
+*AFSOptionalParameters*
+ Type: Codeunit "AFS Optional Parameters"
+
+Optional parameters to pass with the request.
+
+
+##### Return
+
+*Codeunit "AFS Operation Response"*
+
+An operation response object
+
+#### `CreateDirectory()`
+
+Creates directory on the file share.
+
+
+##### Syntax
+
+```al
+[Codeunit "AFS Operation Response"] := CreateDirectory(DirectoryPath: Text)
+```
+
+##### Parameters
+
+*DirectoryPath*
+ Type: Text
+
+The path of the directory to create.
+
+
+##### Return
+
+*Codeunit "AFS Operation Response"*
+
+An operation response object
+
+#### `CreateDirectory()`
+
+Creates directory on the file share.
+
+
+##### Syntax
+
+```al
+[Codeunit "AFS Operation Response"] := CreateDirectory(DirectoryPath: Text, AFSOptionalParameters: Codeunit "AFS Optional Parameters")
+```
+
+##### Parameters
+
+*DirectoryPath*
+ Type: Text
+
+The path of the directory to create.
+
+*AFSOptionalParameters*
+ Type: Codeunit "AFS Optional Parameters"
+
+Optional parameters to pass with the request.
+
+
+##### Return
+
+*Codeunit "AFS Operation Response"*
+
+An operation response object
+
+#### `DeleteDirectory()`
+
+Deletes an empty directory from the file share.
+
+
+##### Syntax
+
+```al
+[Codeunit "AFS Operation Response"] := DeleteDirectory(DirectoryPath: Text)
+```
+
+##### Parameters
+
+*DirectoryPath*
+ Type: Text
+
+The path of the directory to delete.
+
+
+##### Return
+
+*Codeunit "AFS Operation Response"*
+
+An operation response object
+
+#### `DeleteDirectory()`
+
+Deletes an empty directory from the file share.
+
+
+##### Syntax
+
+```al
+[Codeunit "AFS Operation Response"] := DeleteDirectory(DirectoryPath: Text, AFSOptionalParameters: Codeunit "AFS Optional Parameters")
+```
+
+##### Parameters
+
+*DirectoryPath*
+ Type: Text
+
+The path of the directory to delete.
+
+*AFSOptionalParameters*
+ Type: Codeunit "AFS Optional Parameters"
+
+Optional parameters to pass with the request.
+
+
+##### Return
+
+*Codeunit "AFS Operation Response"*
+
+An operation response object
+
+#### `CopyFile()`
+
+Copies a file on the file share.
+
+
+##### Syntax
+
+```al
+[Codeunit "AFS Operation Response"] := CopyFile(SourceFileURI: Text, DestinationFilePath: Text)
+```
+
+##### Parameters
+
+*SourceFileURI*
+ Type: Text
+
+The URI to the source file. If the source file is on a different share than the destination file, the URI needs to be authorized.
+
+*DestinationFilePath*
+ Type: Text
+
+The path where to destination file should be created.
+
+
+##### Return
+
+*Codeunit "AFS Operation Response"*
+
+An operation response object
+
+#### `CopyFile()`
+
+Copies a file on the file share.
+
+
+##### Syntax
+
+```al
+[Codeunit "AFS Operation Response"] := CopyFile(SourceFileURI: Text, DestinationFilePath: Text, AFSOptionalParameters: Codeunit "AFS Optional Parameters")
+```
+
+##### Parameters
+
+*SourceFileURI*
+ Type: Text
+
+The URI to the source file. If the source file is on a different share than the destination file, the URI needs to be authorized.
+
+*DestinationFilePath*
+ Type: Text
+
+The path where to destination file should be created.
+
+*AFSOptionalParameters*
+ Type: Codeunit "AFS Optional Parameters"
+
+Optional parameters to pass with the request.
+
+
+##### Return
+
+*Codeunit "AFS Operation Response"*
+
+An operation response object
+
+#### `AbortCopyFile()`
+
+Stops a file copy operation that is in progress.
+
+
+##### Syntax
+
+```al
+[Codeunit "AFS Operation Response"] := AbortCopyFile(DestinationFilePath: Text, CopyID: Text)
+```
+
+##### Parameters
+
+*DestinationFilePath*
+ Type: Text
+
+The path where to destination file should be created.
+
+*CopyID*
+ Type: Text
+
+The ID of the copy opeartion to abort.
+
+
+##### Return
+
+*Codeunit "AFS Operation Response"*
+
+An operation response object
+
+#### `AbortCopyFile()`
+
+Stops a file copy operation that is in progress.
+
+
+##### Syntax
+
+```al
+[Codeunit "AFS Operation Response"] := AbortCopyFile(DestinationFilePath: Text, CopyID: Text, AFSOptionalParameters: Codeunit "AFS Optional Parameters")
+```
+
+##### Parameters
+
+*DestinationFilePath*
+ Type: Text
+
+The path where to destination file should be created.
+
+*CopyID*
+ Type: Text
+
+The ID of the copy opeartion to abort.
+
+*AFSOptionalParameters*
+ Type: Codeunit "AFS Optional Parameters"
+
+Optional parameters to pass with the request.
+
+
+##### Return
+
+*Codeunit "AFS Operation Response"*
+
+An operation response object
+
+#### `ListHandles()`
+
+Lists all the open handles to the file.
+
+
+##### Syntax
+
+```al
+[Codeunit "AFS Operation Response"] := ListHandles(Path: Text, var AFSHandle: Record "AFS Handle")
+```
+
+##### Parameters
+
+*Path*
+ Type: Text
+
+The path to the file.
+
+*AFSHandle*
+ Type: Record "AFS Handle"
+
+The result collection containing all the handles to the file (temporary).
+
+
+##### Return
+
+*Codeunit "AFS Operation Response"*
+
+An operation response object
+
+#### `ListHandles()`
+
+Lists all the open handles to the file.
+
+
+##### Syntax
+
+```al
+[Codeunit "AFS Operation Response"] := ListHandles(Path: Text, var AFSHandle: Record "AFS Handle", AFSOptionalParameters: Codeunit "AFS Optional Parameters")
+```
+
+##### Parameters
+
+*Path*
+ Type: Text
+
+The path to the file.
+
+*AFSHandle*
+ Type: Record "AFS Handle"
+
+The result collection containing all the handles to the file (temporary).
+
+*AFSOptionalParameters*
+ Type: Codeunit "AFS Optional Parameters"
+
+Optional parameters to pass with the request.
+
+
+##### Return
+
+*Codeunit "AFS Operation Response"*
+
+An operation response object
+
+#### `RenameFile()`
+
+Renames a file on the file share.
+
+
+##### Syntax
+
+```al
+[Codeunit "AFS Operation Response"] := RenameFile(SourceFilePath: Text, DestinationFilePath: Text)
+```
+
+##### Parameters
+
+*SourceFilePath*
+ Type: Text
+
+The path to the source file.
+
+*DestinationFilePath*
+ Type: Text
+
+The path to which the file will be renamed.
+
+
+##### Return
+
+*Codeunit "AFS Operation Response"*
+
+An operation response object
+
+#### `RenameFile()`
+
+Renames a file on the file share.
+
+
+##### Syntax
+
+```al
+[Codeunit "AFS Operation Response"] := RenameFile(SourceFilePath: Text, DestinationFilePath: Text, AFSOptionalParameters: Codeunit "AFS Optional Parameters")
+```
+
+##### Parameters
+
+*SourceFilePath*
+ Type: Text
+
+The path to the source file.
+
+*DestinationFilePath*
+ Type: Text
+
+The path to which the file will be renamed.
+
+*AFSOptionalParameters*
+ Type: Codeunit "AFS Optional Parameters"
+
+Optional parameters to pass with the request.
+
+
+##### Return
+
+*Codeunit "AFS Operation Response"*
+
+An operation response object
+
+#### `AcquireLease()`
+
+Requests a new lease. If the file does not have an active lease, the file service creates a lease on the file.
+
+
+##### Syntax
+
+```al
+[Codeunit "AFS Operation Response"] := AcquireLease(FilePath: Text, ProposedLeaseId: Guid, var LeaseId: Guid)
+```
+
+##### Parameters
+
+*FilePath*
+ Type: Text
+
+The path to the file.
+
+*ProposedLeaseId*
+ Type: Guid
+
+The proposed id for the new lease.
+
+*LeaseId*
+ Type: Guid
+
+Guid containing the response value from x-ms-lease-id HttpHeader
+
+
+##### Return
+
+*Codeunit "AFS Operation Response"*
+
+An operation reponse object
+
+#### `AcquireLease()`
+
+Requests a new lease. If the file does not have an active lease, the file service creates a lease on the file.
+
+
+##### Syntax
+
+```al
+[Codeunit "AFS Operation Response"] := AcquireLease(FilePath: Text, ProposedLeaseId: Guid, AFSOptionalParameters: Codeunit "AFS Optional Parameters", var LeaseId: Guid)
+```
+
+##### Parameters
+
+*FilePath*
+ Type: Text
+
+The path to the file.
+
+*ProposedLeaseId*
+ Type: Guid
+
+The proposed id for the new lease.
+
+*AFSOptionalParameters*
+ Type: Codeunit "AFS Optional Parameters"
+
+Optional parameters to pass with the request.
+
+*LeaseId*
+ Type: Guid
+
+Guid containing the response value from x-ms-lease-id HttpHeader
+
+
+##### Return
+
+*Codeunit "AFS Operation Response"*
+
+An operation reponse object
+
+#### `ChangeLease()`
+
+Changes a lease id to a new lease id.
+
+
+##### Syntax
+
+```al
+[Codeunit "AFS Operation Response"] := ChangeLease(FilePath: Text, ProposedLeaseId: Guid, var LeaseId: Guid)
+```
+
+##### Parameters
+
+*FilePath*
+ Type: Text
+
+The path to the file.
+
+*ProposedLeaseId*
+ Type: Guid
+
+The proposed id for the new lease.
+
+*LeaseId*
+ Type: Guid
+
+Previous lease id. Will be replaced by a new lease id if the request is successful.
+
+
+##### Return
+
+*Codeunit "AFS Operation Response"*
+
+Return value of type Codeunit "AFS Operation Response".
+
+#### `ChangeLease()`
+
+Changes a lease id to a new lease id.
+
+
+##### Syntax
+
+```al
+[Codeunit "AFS Operation Response"] := ChangeLease(FilePath: Text, ProposedLeaseId: Guid, AFSOptionalParameters: Codeunit "AFS Optional Parameters", var LeaseId: Guid)
+```
+
+##### Parameters
+
+*FilePath*
+ Type: Text
+
+The path to the file.
+
+*ProposedLeaseId*
+ Type: Guid
+
+The proposed id for the new lease.
+
+*AFSOptionalParameters*
+ Type: Codeunit "AFS Optional Parameters"
+
+Optional parameters to pass with the request.
+
+*LeaseId*
+ Type: Guid
+
+Previous lease id. Will be replaced by a new lease id if the request is successful.
+
+
+##### Return
+
+*Codeunit "AFS Operation Response"*
+
+Return value of type Codeunit "AFS Operation Response".
+
+#### `ReleaseLease()`
+
+Releases a lease on a File if it is no longer needed so that another client may immediately acquire a lease against the file.
+
+
+##### Syntax
+
+```al
+[Codeunit "AFS Operation Response"] := ReleaseLease(FilePath: Text, LeaseId: Guid)
+```
+
+##### Parameters
+
+*FilePath*
+ Type: Text
+
+The path to the file.
+
+*LeaseId*
+ Type: Guid
+
+The Guid for the lease that should be released
+
+
+##### Return
+
+*Codeunit "AFS Operation Response"*
+
+An operation reponse object
+
+#### `ReleaseLease()`
+
+Releases a lease on a File if it is no longer needed so that another client may immediately acquire a lease against the file.
+
+
+##### Syntax
+
+```al
+[Codeunit "AFS Operation Response"] := ReleaseLease(FilePath: Text, LeaseId: Guid, AFSOptionalParameters: Codeunit "AFS Optional Parameters")
+```
+
+##### Parameters
+
+*FilePath*
+ Type: Text
+
+The path to the file.
+
+*LeaseId*
+ Type: Guid
+
+The Guid for the lease that should be released
+
+*AFSOptionalParameters*
+ Type: Codeunit "AFS Optional Parameters"
+
+Optional parameters to pass with the request.
+
+
+##### Return
+
+*Codeunit "AFS Operation Response"*
+
+An operation reponse object
+
+#### `BreakLease()`
+
+Breaks a lease on a file.
+
+
+##### Syntax
+
+```al
+[Codeunit "AFS Operation Response"] := BreakLease(FilePath: Text, LeaseId: Guid)
+```
+
+##### Parameters
+
+*FilePath*
+ Type: Text
+
+The path to the file.
+
+*LeaseId*
+ Type: Guid
+
+The Guid for the lease that should be broken
+
+
+##### Return
+
+*Codeunit "AFS Operation Response"*
+
+An operation reponse object
+
+#### `BreakLease()`
+
+Breaks a lease on a file.
+
+
+##### Syntax
+
+```al
+[Codeunit "AFS Operation Response"] := BreakLease(FilePath: Text, LeaseId: Guid, AFSOptionalParameters: Codeunit "AFS Optional Parameters")
+```
+
+##### Parameters
+
+*FilePath*
+ Type: Text
+
+The path to the file.
+
+*LeaseId*
+ Type: Guid
+
+The Guid for the lease that should be broken
+
+*AFSOptionalParameters*
+ Type: Codeunit "AFS Optional Parameters"
+
+Optional parameters to pass with the request.
+
+
+##### Return
+
+*Codeunit "AFS Operation Response"*
+
+An operation reponse object
+
+
+## AFS Operation Response
+
+
+Stores the response of an AFS client operation.
+
+### Properties
+
+| Property | Value |
+| --- | --- |
+| Object Type | Codeunit |
+| Object Subtype | Normal |
+| Object ID | 8959 |
+| Accessibility Level | Public |
+
+### Procedures
+
+#### `IsSuccessful()`
+
+Checks whether the operation was successful.
+
+
+##### Syntax
+
+```al
+[Boolean] := IsSuccessful()
+```
+
+##### Return
+
+*Boolean*
+
+True if the operation was successful; otherwise - false.
+
+#### `GetError()`
+
+Gets the error (if any) of the response.
+
+
+##### Syntax
+
+```al
+[Text] := GetError()
+```
+
+##### Return
+
+*Text*
+
+Text representation of the error that occurred during the operation.
+
+#### `GetHeaders()`
+
+Gets the HttpHeaders (if any) of the response.
+
+
+##### Syntax
+
+```al
+[HttpHeaders] := GetHeaders()
+```
+
+##### Return
+
+*HttpHeaders*
+
+HttpHeaders.
+
+#### `SetError()`
+
+
+##### Syntax
+
+```al
+SetError(Error: Text)
+```
+
+##### Parameters
+
+*Error*
+ Type: Text
+
+
+#### `GetResultAsText()`
+
+Gets the result of a AFS client operation as text,
+
+
+##### Syntax
+
+```al
+[Boolean] := GetResultAsText(var Result: Text)
+```
+
+##### Parameters
+
+*Result*
+ Type: Text
+
+
+##### Return
+
+*Boolean*
+
+False if an runtime error occurred. Otherwise true.
+
+#### `GetResultAsStream()`
+
+Gets the result of a AFS client operation as stream,
+
+
+##### Syntax
+
+```al
+[Boolean] := GetResultAsStream(var ResultInStream: InStream)
+```
+
+##### Parameters
+
+*ResultInStream*
+ Type: InStream
+
+
+##### Return
+
+*Boolean*
+
+False if an runtime error occurred. Otherwise true.
+
+#### `SetHttpResponse()`
+
+
+##### Syntax
+
+```al
+SetHttpResponse(NewHttpResponseMessage: HttpResponseMessage)
+```
+
+##### Parameters
+
+*NewHttpResponseMessage*
+ Type: HttpResponseMessage
+
+
+#### `GetHeaderValueFromResponseHeaders()`
+
+
+##### Syntax
+
+```al
+[Text] := GetHeaderValueFromResponseHeaders(HeaderName: Text)
+```
+
+##### Parameters
+
+*HeaderName*
+ Type: Text
+
+
+
+## AFS Optional Parameters
+
+
+Holds procedures to format headers and parameters to be used in requests.
+
+### Properties
+
+| Property | Value |
+| --- | --- |
+| Object Type | Codeunit |
+| Object Subtype | Normal |
+| Object ID | 8956 |
+| Accessibility Level | Public |
+
+### Procedures
+
+#### `Range()`
+
+Sets the value for 'x-ms-range' HttpHeader for a request.
+
+
+##### Syntax
+
+```al
+Range(BytesStartValue: Integer, BytesEndValue: Integer)
+```
+
+##### Parameters
+
+*BytesStartValue*
+ Type: Integer
+
+Integer value specifying the Bytes start range value
+
+*BytesEndValue*
+ Type: Integer
+
+Integer value specifying the Bytes end range value
+
+
+#### `Write()`
+
+Sets the value for 'x-ms-write' HttpHeader for a request.
+
+
+##### Syntax
+
+```al
+Write(Value: Enum "AFS Write")
+```
+
+##### Parameters
+
+*Value*
+ Type: Enum "AFS Write"
+
+Enum "AFS Write" value specifying the HttpHeader value
+
+
+#### `LeaseId()`
+
+Sets the value for 'x-ms-lease-id' HttpHeader for a request.
+
+
+##### Syntax
+
+```al
+LeaseId(Value: Guid)
+```
+
+##### Parameters
+
+*Value*
+ Type: Guid
+
+Guid value specifying the LeaseID
+
+
+#### `LeaseAction()`
+
+Sets the value for 'x-ms-lease-action' HttpHeader for a request.
+
+
+##### Syntax
+
+```al
+LeaseAction(Value: Enum "AFS Lease Action")
+```
+
+##### Parameters
+
+*Value*
+ Type: Enum "AFS Lease Action"
+
+Enum "AFS Lease Action" value specifying the LeaseAction
+
+
+#### `LeaseDuration()`
+
+Sets the value for 'x-ms-lease-duration' HttpHeader for a request.
+
+
+##### Syntax
+
+```al
+LeaseDuration(Value: Integer)
+```
+
+##### Parameters
+
+*Value*
+ Type: Integer
+
+Integer value specifying the LeaseDuration in seconds
+
+
+#### `ProposedLeaseId()`
+
+Sets the value for 'x-ms-proposed-lease-id' HttpHeader for a request.
+
+
+##### Syntax
+
+```al
+ProposedLeaseId(Value: Guid)
+```
+
+##### Parameters
+
+*Value*
+ Type: Guid
+
+Guid value specifying the ProposedLeaseId in seconds
+
+
+#### `ClientRequestId()`
+
+Sets the value for 'x-ms-client-request-id' HttpHeader for a request.
+
+
+##### Syntax
+
+```al
+ClientRequestId(Value: Text)
+```
+
+##### Parameters
+
+*Value*
+ Type: Text
+
+Text value specifying the HttpHeader value
+
+
+#### `FileLastWriteTime()`
+
+Sets the value for 'x-ms-file-last-write-time' HttpHeader for a request.
+
+
+##### Syntax
+
+```al
+FileLastWriteTime(Value: Enum "AFS File Last Write Time")
+```
+
+##### Parameters
+
+*Value*
+ Type: Enum "AFS File Last Write Time"
+
+Enum "AFS File Last Write Time" value specifying the HttpHeader value
+
+
+#### `FileRequestIntent()`
+
+Sets the value for 'x-ms-file-request-intent' HttpHeader for a request, 'backup' is an acceptable value.
+
+
+##### Syntax
+
+```al
+FileRequestIntent(Value: Text)
+```
+
+##### Parameters
+
+*Value*
+ Type: Text
+
+Text value specifying the HttpHeader value
+
+
+#### `FilePermission()`
+
+Sets the value for 'x-ms-file-permission' HttpHeader for a request.
+
+
+##### Syntax
+
+```al
+FilePermission(Value: Text)
+```
+
+##### Parameters
+
+*Value*
+ Type: Text
+
+Text value specifying the HttpHeader value
+
+
+#### `FilePermissionKey()`
+
+Sets the value for 'x-ms-file-permission-key' HttpHeader for a request.
+
+
+##### Syntax
+
+```al
+FilePermissionKey(Value: Text)
+```
+
+##### Parameters
+
+*Value*
+ Type: Text
+
+Text value specifying the HttpHeader value
+
+
+#### `FileAttributes()`
+
+Sets the value for 'x-ms-file-attributes' HttpHeader for a request.
+
+
+##### Syntax
+
+```al
+FileAttributes(Value: List of [Enum "AFS File Attribute"])
+```
+
+##### Parameters
+
+*Value*
+ Type: List of [Enum "AFS File Attribute"]
+
+Text value specifying the HttpHeader value
+
+
+#### `FileCreationTime()`
+
+Sets the value for 'x-ms-file-creation-time' HttpHeader for a request.
+
+
+##### Syntax
+
+```al
+FileCreationTime(Value: DateTime)
+```
+
+##### Parameters
+
+*Value*
+ Type: DateTime
+
+Datetime of the file creation
+
+
+#### `FileLastWriteTime()`
+
+Sets the value for 'x-ms-file-last-write-time' HttpHeader for a request.
+
+
+##### Syntax
+
+```al
+FileLastWriteTime(Value: DateTime)
+```
+
+##### Parameters
+
+*Value*
+ Type: DateTime
+
+Datetime of the file last write time
+
+
+#### `FileChangeTime()`
+
+Sets the value for 'x-ms-file-change-time' HttpHeader for a request.
+
+
+##### Syntax
+
+```al
+FileChangeTime(Value: DateTime)
+```
+
+##### Parameters
+
+*Value*
+ Type: DateTime
+
+Datetime of the file last change time
+
+
+#### `Meta()`
+
+Sets the value for 'x-ms-meta-name' HttpHeader for a request. name should adhere to C# identifiers naming convention.
+
+
+##### Syntax
+
+```al
+Meta(Key: Text, Value: Text)
+```
+
+##### Parameters
+
+*Key*
+ Type: Text
+
+Text value specifying the metadata name key
+
+*Value*
+ Type: Text
+
+Text value specifying the HttpHeader value
+
+
+#### `FilePermissionCopyMode()`
+
+Sets the value for 'x-ms-file-permission-copy-mode' HttpHeader for a request.
+
+
+##### Syntax
+
+```al
+FilePermissionCopyMode(Value: Enum "AFS File Permission Copy Mode")
+```
+
+##### Parameters
+
+*Value*
+ Type: Enum "AFS File Permission Copy Mode"
+
+Enum "AFS File Permission Copy Mode" value specifying the HttpHeader value
+
+
+#### `CopySource()`
+
+Sets the value for 'x-ms-copy-source' HttpHeader for a request.
+
+
+##### Syntax
+
+```al
+CopySource(Value: Text)
+```
+
+##### Parameters
+
+*Value*
+ Type: Text
+
+Text value specifying the HttpHeader value
+
+
+#### `AllowTrailingDot()`
+
+Sets the value for 'x-ms-allow-trailing-dot' HttpHeader for a request.
+
+
+##### Syntax
+
+```al
+AllowTrailingDot(Value: Boolean)
+```
+
+##### Parameters
+
+*Value*
+ Type: Boolean
+
+Boolean value specifying the HttpHeader value
+
+
+#### `FileRenameReplaceIfExists()`
+
+Sets the value for 'x-ms-file-rename-replace-if-exists' HttpHeader for a request.
+
+
+##### Syntax
+
+```al
+FileRenameReplaceIfExists(Value: Boolean)
+```
+
+##### Parameters
+
+*Value*
+ Type: Boolean
+
+Boolean value specifying the HttpHeader value
+
+
+#### `FileRenameIgnoreReadOnly()`
+
+Sets the value for 'x-ms-file-rename-ignore-readonly' HttpHeader for a request.
+
+
+##### Syntax
+
+```al
+FileRenameIgnoreReadOnly(Value: Boolean)
+```
+
+##### Parameters
+
+*Value*
+ Type: Boolean
+
+Boolean value specifying the HttpHeader value
+
+
+#### `SourceLeaseId()`
+
+Sets the value for 'x-ms-source-lease-id' HttpHeader for a request.
+
+
+##### Syntax
+
+```al
+SourceLeaseId(Value: Guid)
+```
+
+##### Parameters
+
+*Value*
+ Type: Guid
+
+Guid value specifying the SourceLeaseID
+
+
+#### `DestinationLeaseId()`
+
+Sets the value for 'x-ms-destination-lease-id' HttpHeader for a request.
+
+
+##### Syntax
+
+```al
+DestinationLeaseId(Value: Guid)
+```
+
+##### Parameters
+
+*Value*
+ Type: Guid
+
+Guid value specifying the DestinationLeaseID
+
+
+#### `FileCopyIgnoreReadOnly()`
+
+Sets the value for 'x-ms-file-copy-ignore-readonly' HttpHeader for a request.
+
+
+##### Syntax
+
+```al
+FileCopyIgnoreReadOnly(Value: Boolean)
+```
+
+##### Parameters
+
+*Value*
+ Type: Boolean
+
+Boolean value specifying the HttpHeader value
+
+
+#### `FileCopySetArchive()`
+
+Sets the value for 'x-ms-file-copy-set-archive' HttpHeader for a request.
+
+
+##### Syntax
+
+```al
+FileCopySetArchive(Value: Boolean)
+```
+
+##### Parameters
+
+*Value*
+ Type: Boolean
+
+Boolean value specifying the HttpHeader value
+
+
+#### `FileExtendedInfo()`
+
+Sets the value for 'x-ms-file-extended-info' HttpHeader for a request.
+
+
+##### Syntax
+
+```al
+FileExtendedInfo(Value: Boolean)
+```
+
+##### Parameters
+
+*Value*
+ Type: Boolean
+
+Boolean value specifying the HttpHeader value
+
+
+#### `RangeGetContentMD5()`
+
+Sets the value for 'x-ms-range-get-content-md5' HttpHeader for a request.
+
+
+##### Syntax
+
+```al
+RangeGetContentMD5(Value: Boolean)
+```
+
+##### Parameters
+
+*Value*
+ Type: Boolean
+
+Boolean value specifying the HttpHeader value
+
+
+#### `Recursive()`
+
+Sets the value for 'x-ms-recursive' HttpHeader for a request.
+
+
+##### Syntax
+
+```al
+Recursive(Value: Boolean)
+```
+
+##### Parameters
+
+*Value*
+ Type: Boolean
+
+Boolean value specifying the HttpHeader value
+
+
+#### `Timeout()`
+
+Sets the optional timeout value for the request.
+
+
+##### Syntax
+
+```al
+Timeout(Value: Integer)
+```
+
+##### Parameters
+
+*Value*
+ Type: Integer
+
+Timeout in seconds. Most operations have a max. limit of 30 seconds. For more Information see: https://go.microsoft.com/fwlink/?linkid=2210591
+
+
+#### `Prefix()`
+
+Filters the results to return only blobs whose names begin with the specified prefix.
+
+
+##### Syntax
+
+```al
+Prefix(Value: Text)
+```
+
+##### Parameters
+
+*Value*
+ Type: Text
+
+Prefix to search for
+
+
+#### `ShareSnapshot()`
+
+Specifies the share snapshot to query for the list of files and directories.
+
+
+##### Syntax
+
+```al
+ShareSnapshot(Value: DateTime)
+```
+
+##### Parameters
+
+*Value*
+ Type: DateTime
+
+Datetime of the snapshot to query
+
+
+#### `Marker()`
+
+A string value that identifies the portion of the list to be returned with the next list operation.
+
+
+##### Syntax
+
+```al
+Marker(Value: Text)
+```
+
+##### Parameters
+
+*Value*
+ Type: Text
+
+Text marker that was returned in previous operation
+
+
+#### `MaxResults()`
+
+Specifies the maximum number of files or directories to return
+
+
+##### Syntax
+
+```al
+MaxResults(Value: Integer)
+```
+
+##### Parameters
+
+*Value*
+ Type: Integer
+
+Max. number of results to return. Must be positive, must not be greater than 5000
+
+
+#### `Include()`
+
+Specifies one or more properties to include in the response.
+
+
+##### Syntax
+
+```al
+Include(Value: List of [Enum "AFS Properties"])
+```
+
+##### Parameters
+
+*Value*
+ Type: List of [Enum "AFS Properties"]
+
+List of properties to include.
+
+
+#### `SetRequestHeader()`
+
+
+##### Syntax
+
+```al
+SetRequestHeader(Header: Text, HeaderValue: Text)
+```
+
+##### Parameters
+
+*Header*
+ Type: Text
+
+*HeaderValue*
+ Type: Text
+
+
+#### `GetRequestHeaders()`
+
+
+##### Syntax
+
+```al
+[Dictionary of [Text, Text]] := GetRequestHeaders()
+```
+
+#### `SetParameter()`
+
+
+##### Syntax
+
+```al
+SetParameter(Header: Text, HeaderValue: Text)
+```
+
+##### Parameters
+
+*Header*
+ Type: Text
+
+*HeaderValue*
+ Type: Text
+
+
+#### `GetParameters()`
+
+
+##### Syntax
+
+```al
+[Dictionary of [Text, Text]] := GetParameters()
+```
+
diff --git a/src/System Application/App/Azure File Services API/app.json b/src/System Application/App/Azure File Services API/app.json
new file mode 100644
index 0000000000..3eb02e4bb0
--- /dev/null
+++ b/src/System Application/App/Azure File Services API/app.json
@@ -0,0 +1,68 @@
+{
+ "id": "a6660ad9-7675-4f68-a2f9-a938c21de68a",
+ "name": "Azure File Services API",
+ "publisher": "Microsoft",
+ "version": "24.0.0.0",
+ "brief": "Reproduces the Azure File Share service REST API",
+ "description": "Provides a set of AL functionality and Helper libraries to make use of Azure File Share Storage in MSDyn365BC",
+ "privacyStatement": "https://go.microsoft.com/fwlink/?linkid=724009",
+ "EULA": "https://go.microsoft.com/fwlink/?linkid=2009120",
+ "help": "https://go.microsoft.com/fwlink/?linkid=2103698",
+ "url": "https://go.microsoft.com/fwlink/?linkid=724011",
+ "logo": "",
+ "dependencies": [
+ {
+ "id": "e409d343-14fa-42a4-a1be-fec499383e59",
+ "name": "Azure Storage Services Authorization",
+ "publisher": "Microsoft",
+ "version": "24.0.0.0"
+ },
+ {
+ "id": "7e3b999e-1182-45d2-8b82-d5127ddba9b2",
+ "name": "DotNet Aliases",
+ "publisher": "Microsoft",
+ "version": "24.0.0.0"
+ },
+ {
+ "id": "1b2efb4b-8c44-4d74-a56f-60646645bb21",
+ "name": "URI",
+ "publisher": "Microsoft",
+ "version": "24.0.0.0"
+ },
+ {
+ "id": "e31ad830-3d46-472e-afeb-1d3d35247943",
+ "name": "BLOB Storage",
+ "publisher": "Microsoft",
+ "version": "24.0.0.0"
+ },
+ {
+ "id": "3f95f4c2-037f-44d4-b089-b56ffc01693f",
+ "name": "OAuth",
+ "publisher": "Microsoft",
+ "version": "24.0.0.0"
+ },
+ {
+ "id": "de35f591-7216-4e60-8be1-1911d71a7fc2",
+ "name": "Telemetry",
+ "publisher": "Microsoft",
+ "version": "24.0.0.0"
+ }
+ ],
+ "screenshots": [],
+ "platform": "24.0.0.0",
+ "idRanges": [
+ {
+ "from": 8950,
+ "to": 8963
+ }
+ ],
+ "resourceExposurePolicy": {
+ "allowDebugging": true,
+ "allowDownloadingSource": true,
+ "includeSourceInSymbolFile": true
+ },
+ "features": [
+ "NoImplicitWith"
+ ],
+ "target": "OnPrem"
+}
\ No newline at end of file
diff --git a/src/System Application/App/Azure File Services API/src/AFSDirectoryContent.Table.al b/src/System Application/App/Azure File Services API/src/AFSDirectoryContent.Table.al
new file mode 100644
index 0000000000..b4dd88cc43
--- /dev/null
+++ b/src/System Application/App/Azure File Services API/src/AFSDirectoryContent.Table.al
@@ -0,0 +1,126 @@
+// ------------------------------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+// ------------------------------------------------------------------------------------------------
+
+namespace System.Azure.Storage.Files;
+
+///
+/// Holds information about directory content in a storage account.
+///
+table 8950 "AFS Directory Content"
+{
+ Access = Public;
+ InherentEntitlements = X;
+ InherentPermissions = X;
+ Extensible = false;
+ DataClassification = SystemMetadata;
+ TableType = Temporary;
+
+ fields
+ {
+ field(1; "Entry No."; Integer)
+ {
+ DataClassification = SystemMetadata;
+ Caption = 'Entry No.', Locked = true;
+ }
+ field(2; "Parent Directory"; Text[2048])
+ {
+ DataClassification = SystemMetadata;
+ Caption = 'Parent Directory', Locked = true;
+ }
+ field(3; Level; Integer)
+ {
+ DataClassification = SystemMetadata;
+ Caption = 'Level', Locked = true;
+ }
+ field(4; "Full Name"; Text[2048])
+ {
+ DataClassification = CustomerContent;
+ Caption = 'Full Name', Locked = true;
+ }
+ field(10; Name; Text[2048])
+ {
+ DataClassification = CustomerContent;
+ Caption = 'Name', Locked = true;
+ }
+ field(11; "Creation Time"; DateTime)
+ {
+ DataClassification = SystemMetadata;
+ Caption = 'CreationTime', Locked = true;
+ }
+ field(12; "Last Modified"; DateTime)
+ {
+ DataClassification = SystemMetadata;
+ Caption = 'Last-Modified', Locked = true;
+ }
+ field(13; "Content Length"; Integer)
+ {
+ DataClassification = SystemMetadata;
+ Caption = 'Content-Length', Locked = true;
+ }
+ field(14; "Last Access Time"; DateTime)
+ {
+ DataClassification = SystemMetadata;
+ Caption = 'LastAccessTime', Locked = true;
+ }
+ field(15; "Change Time"; DateTime)
+ {
+ DataClassification = SystemMetadata;
+ Caption = 'ChangeTime', Locked = true;
+ }
+ field(16; "Resource Type"; Enum "AFS File Resource Type")
+ {
+ DataClassification = SystemMetadata;
+ Caption = 'ResourceType', Locked = true;
+ }
+ field(17; Etag; Text[200])
+ {
+ DataClassification = SystemMetadata;
+ Caption = 'Etag', Locked = true;
+ }
+ field(18; Archive; Boolean)
+ {
+ DataClassification = SystemMetadata;
+ Caption = 'Archive', Locked = true;
+ }
+ field(19; Hidden; Boolean)
+ {
+ DataClassification = SystemMetadata;
+ Caption = 'Hidden', Locked = true;
+ }
+ field(20; "Last Write Time"; DateTime)
+ {
+ DataClassification = SystemMetadata;
+ Caption = 'LastWriteTime', Locked = true;
+ }
+ field(21; "Read Only"; Boolean)
+ {
+ DataClassification = SystemMetadata;
+ Caption = 'ReadOnly', Locked = true;
+ }
+ field(22; "Permission Key"; Text[200])
+ {
+ DataClassification = SystemMetadata;
+ Caption = 'PermissionKey', Locked = true;
+ }
+ field(100; "XML Value"; Blob)
+ {
+ DataClassification = CustomerContent;
+ Caption = 'XML Value', Locked = true;
+ }
+ field(110; URI; Text[2048])
+ {
+ DataClassification = SystemMetadata;
+ Caption = 'URI', Locked = true;
+ }
+ }
+
+ keys
+ {
+ key(PK; "Entry No.")
+ {
+ Clustered = true;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/System Application/App/Azure File Services API/src/AFSFileClient.Codeunit.al b/src/System Application/App/Azure File Services API/src/AFSFileClient.Codeunit.al
new file mode 100644
index 0000000000..a012aa10a4
--- /dev/null
+++ b/src/System Application/App/Azure File Services API/src/AFSFileClient.Codeunit.al
@@ -0,0 +1,623 @@
+// ------------------------------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+// ------------------------------------------------------------------------------------------------
+
+namespace System.Azure.Storage.Files;
+
+using System.Azure.Storage;
+
+///
+/// Provides functionality to access the Azure File Storage.
+///
+codeunit 8950 "AFS File Client"
+{
+ Access = Public;
+ InherentEntitlements = X;
+ InherentPermissions = X;
+
+ var
+ AFSFileClientImpl: Codeunit "AFS File Client Impl.";
+
+ ///
+ /// Initializes the AFS Client.
+ ///
+ /// The name of the storage account to use.
+ /// The name of the file share to use.
+ /// The authorization to use.
+ procedure Initialize(StorageAccount: Text; FileShare: Text; Authorization: Interface "Storage Service Authorization")
+ var
+ StorageServiceAuthorization: Codeunit "Storage Service Authorization";
+ begin
+ AFSFileClientImpl.Initialize(StorageAccount, FileShare, '', Authorization, StorageServiceAuthorization.GetDefaultAPIVersion());
+ end;
+
+ ///
+ /// Initializes the AFS Client.
+ ///
+ /// The name of the storage account to use.
+ /// The name of the file share to use.
+ /// The authorization to use.
+ /// The API Version to use.
+ procedure Initialize(StorageAccount: Text; FileShare: Text; Authorization: Interface "Storage Service Authorization"; APIVersion: Enum "Storage Service API Version")
+ begin
+ AFSFileClientImpl.Initialize(StorageAccount, FileShare, '', Authorization, APIVersion);
+ end;
+
+ ///
+ /// Creates a file in the file share.
+ /// This does not fill in the file content, it only initializes the file.
+ ///
+ /// The path where the file will be created.
+ /// The file content, only used to check file size.
+ /// An operation response object.
+ procedure CreateFile(FilePath: Text; InStream: InStream): Codeunit "AFS Operation Response"
+ var
+ AFSOptionalParameters: Codeunit "AFS Optional Parameters";
+ begin
+ exit(AFSFileClientImpl.CreateFile(FilePath, InStream, AFSOptionalParameters));
+ end;
+
+ ///
+ /// Creates a file in the file share.
+ /// This does not fill in the file content, it only initializes the file.
+ ///
+ /// The path where the file will be created.
+ /// The file content, only used to check file size.
+ /// Optional parameters to pass with the request.
+ /// An operation response object.
+ procedure CreateFile(FilePath: Text; InStream: InStream; AFSOptionalParameters: Codeunit "AFS Optional Parameters"): Codeunit "AFS Operation Response"
+ begin
+ exit(AFSFileClientImpl.CreateFile(FilePath, InStream, AFSOptionalParameters));
+ end;
+
+ ///
+ /// Creates a file in the file share.
+ /// This does not fill in the file content, it only initializes the file.
+ ///
+ /// The path where the file will be created.
+ /// The size of the file to initialize, in bytes.
+ /// An operation response object.
+ procedure CreateFile(FilePath: Text; FileSize: Integer): Codeunit "AFS Operation Response"
+ var
+ AFSOptionalParameters: Codeunit "AFS Optional Parameters";
+ begin
+ exit(AFSFileClientImpl.CreateFile(FilePath, FileSize, AFSOptionalParameters));
+ end;
+
+ ///
+ /// Creates a file in the file share.
+ /// This does not fill in the file content, it only initializes the file.
+ ///
+ /// The path where the file will be created.
+ /// The size of the file to initialize, in bytes.
+ /// Optional parameters to pass with the request.
+ /// An operation response object.
+ procedure CreateFile(FilePath: Text; FileSize: Integer; AFSOptionalParameters: Codeunit "AFS Optional Parameters"): Codeunit "AFS Operation Response"
+ begin
+ exit(AFSFileClientImpl.CreateFile(FilePath, FileSize, AFSOptionalParameters));
+ end;
+
+ ///
+ /// Receives a file as a File from a file share.
+ /// The file will be downloaded through the browser.
+ ///
+ /// The path to the file.
+ /// An operation response object
+ procedure GetFileAsFile(FilePath: Text): Codeunit "AFS Operation Response"
+ var
+ AFSOptionalParameters: Codeunit "AFS Optional Parameters";
+ begin
+ exit(AFSFileClientImpl.GetFileAsFile(FilePath, AFSOptionalParameters));
+ end;
+
+ ///
+ /// Receives a file as a File from a file share.
+ /// The file will be downloaded through the browser.
+ ///
+ /// The path to the file.
+ /// Optional parameters to pass with the request.
+ /// An operation response object
+ procedure GetFileAsFile(FilePath: Text; AFSOptionalParameters: Codeunit "AFS Optional Parameters"): Codeunit "AFS Operation Response"
+ begin
+ exit(AFSFileClientImpl.GetFileAsFile(FilePath, AFSOptionalParameters));
+ end;
+
+ ///
+ /// Receives a file as a stream from a file share.
+ ///
+ /// The path to the file.
+ /// The result instream containing the content of the file.
+ /// An operation response object
+ procedure GetFileAsStream(FilePath: Text; var TargetInStream: InStream): Codeunit "AFS Operation Response"
+ var
+ AFSOptionalParameters: Codeunit "AFS Optional Parameters";
+ begin
+ exit(AFSFileClientImpl.GetFileAsStream(FilePath, TargetInStream, AFSOptionalParameters));
+ end;
+
+ ///
+ /// Receives a file as a stream from a file share.
+ ///
+ /// The path to the file.
+ /// The result instream containing the content of the file.
+ /// Optional parameters to pass with the request.
+ /// An operation response object
+ procedure GetFileAsStream(FilePath: Text; var TargetInStream: InStream; AFSOptionalParameters: Codeunit "AFS Optional Parameters"): Codeunit "AFS Operation Response"
+ begin
+ exit(AFSFileClientImpl.GetFileAsStream(FilePath, TargetInStream, AFSOptionalParameters));
+ end;
+
+ ///
+ /// Receives a file as a text from a file share.
+ ///
+ /// The path to the file.
+ /// The result text containing the content of the file.
+ /// An operation response object
+ procedure GetFileAsText(FilePath: Text; var TargetText: Text): Codeunit "AFS Operation Response"
+ var
+ AFSOptionalParameters: Codeunit "AFS Optional Parameters";
+ begin
+ exit(AFSFileClientImpl.GetFileAsText(FilePath, TargetText, AFSOptionalParameters));
+ end;
+
+ ///
+ /// Receives a file as a text from a file share.
+ ///
+ /// The path to the file.
+ /// The result text containing the content of the file.
+ /// Optional parameters to pass with the request.
+ /// An operation response object
+ procedure GetFileAsText(FilePath: Text; var TargetText: Text; AFSOptionalParameters: Codeunit "AFS Optional Parameters"): Codeunit "AFS Operation Response"
+ begin
+ exit(AFSFileClientImpl.GetFileAsText(FilePath, TargetText, AFSOptionalParameters));
+ end;
+
+ ///
+ /// Receives file metadata as dictionary from a file share.
+ ///
+ /// The path to the file.
+ /// The result dictionary containing the metadata of the file in the form of metadata key and a value.
+ /// An operation response object
+ procedure GetFileMetadata(FilePath: Text; var TargetMetadata: Dictionary of [Text, Text]): Codeunit "AFS Operation Response"
+ var
+ AFSOptionalParameters: Codeunit "AFS Optional Parameters";
+ begin
+ exit(AFSFileClientImpl.GetFileMetadata(FilePath, TargetMetadata, AFSOptionalParameters));
+ end;
+
+ ///
+ /// Receives file metadata as dictionary from a file share.
+ ///
+ /// The path to the file.
+ /// The result dictionary containing the metadata of the file in the form of metadata key and a value.
+ /// Optional parameters to pass with the request.
+ /// An operation response object
+ procedure GetFileMetadata(FilePath: Text; var TargetMetadata: Dictionary of [Text, Text]; AFSOptionalParameters: Codeunit "AFS Optional Parameters"): Codeunit "AFS Operation Response"
+ begin
+ exit(AFSFileClientImpl.GetFileMetadata(FilePath, TargetMetadata, AFSOptionalParameters));
+ end;
+
+ ///
+ /// Sets the file metadata.
+ ///
+ /// The path to the file.
+ /// The dictionary containing the metadata of the file in the form of metadata key and a value.
+ /// An operation response object
+ procedure SetFileMetadata(FilePath: Text; Metadata: Dictionary of [Text, Text]): Codeunit "AFS Operation Response"
+ var
+ AFSOptionalParameters: Codeunit "AFS Optional Parameters";
+ begin
+ exit(AFSFileClientImpl.SetFileMetadata(FilePath, Metadata, AFSOptionalParameters));
+ end;
+
+ ///
+ /// Sets the file metadata.
+ ///
+ /// The path to the file.
+ /// The dictionary containing the metadata of the file in the form of metadata key and a value.
+ /// Optional parameters to pass with the request.
+ /// An operation response object
+ procedure SetFileMetadata(FilePath: Text; var Metadata: Dictionary of [Text, Text]; AFSOptionalParameters: Codeunit "AFS Optional Parameters"): Codeunit "AFS Operation Response"
+ begin
+ exit(AFSFileClientImpl.SetFileMetadata(FilePath, Metadata, AFSOptionalParameters));
+ end;
+
+ ///
+ /// Uploads a file to the file share.
+ /// User will be prompted to specify the file to send.
+ ///
+ /// An operation response object
+ procedure PutFileUI(): Codeunit "AFS Operation Response"
+ var
+ AFSOptionalParameters: Codeunit "AFS Optional Parameters";
+ begin
+ exit(AFSFileClientImpl.PutFileUI(AFSOptionalParameters));
+ end;
+
+ ///
+ /// Uploads a file to the file share.
+ /// User will be prompted to specify the file to send.
+ ///
+ /// Optional parameters to pass with the request.
+ /// An operation response object
+ procedure PutFileUI(AFSOptionalParameters: Codeunit "AFS Optional Parameters"): Codeunit "AFS Operation Response"
+ begin
+ exit(AFSFileClientImpl.PutFileUI(AFSOptionalParameters));
+ end;
+
+ ///
+ /// Uploads a file to the file share from instream.
+ ///
+ /// The path to the file.
+ /// The source instream containing the content of the file.
+ /// An operation response object
+ procedure PutFileStream(FilePath: Text; var SourceInStream: InStream): Codeunit "AFS Operation Response"
+ var
+ AFSOptionalParameters: Codeunit "AFS Optional Parameters";
+ begin
+ exit(AFSFileClientImpl.PutFileStream(FilePath, SourceInStream, AFSOptionalParameters));
+ end;
+
+ ///
+ /// Uploads a file to the file share from instream.
+ ///
+ /// The path to the file.
+ /// The source instream containing the content of the file.
+ /// Optional parameters to pass with the request.
+ /// An operation response object
+ procedure PutFileStream(FilePath: Text; var SourceInStream: InStream; AFSOptionalParameters: Codeunit "AFS Optional Parameters"): Codeunit "AFS Operation Response"
+ begin
+ exit(AFSFileClientImpl.PutFileStream(FilePath, SourceInStream, AFSOptionalParameters));
+ end;
+
+ ///
+ /// Uploads a file to the file share from text.
+ ///
+ /// The path to the file.
+ /// The source text containing the content of the file.
+ /// An operation response object
+ procedure PutFileText(FilePath: Text; SourceText: Text): Codeunit "AFS Operation Response"
+ var
+ AFSOptionalParameters: Codeunit "AFS Optional Parameters";
+ begin
+ exit(AFSFileClientImpl.PutFileText(FilePath, SourceText, AFSOptionalParameters));
+ end;
+
+ ///
+ /// Uploads a file to the file share from text.
+ ///
+ /// The path to the file.
+ /// The source text containing the content of the file.
+ /// Optional parameters to pass with the request.
+ /// An operation response object
+ procedure PutFileText(FilePath: Text; SourceText: Text; AFSOptionalParameters: Codeunit "AFS Optional Parameters"): Codeunit "AFS Operation Response"
+ begin
+ exit(AFSFileClientImpl.PutFileText(FilePath, SourceText, AFSOptionalParameters));
+ end;
+
+ ///
+ /// Deletes a file from the file share.
+ ///
+ /// The path to the file.
+ /// An operation response object
+ procedure DeleteFile(FilePath: Text): Codeunit "AFS Operation Response"
+ var
+ AFSOptionalParameters: Codeunit "AFS Optional Parameters";
+ begin
+ exit(AFSFileClientImpl.DeleteFile(FilePath, AFSOptionalParameters));
+ end;
+
+ ///
+ /// Deletes a file from the file share.
+ ///
+ /// The path to the file.
+ /// Optional parameters to pass with the request.
+ /// An operation response object
+ procedure DeleteFile(FilePath: Text; AFSOptionalParameters: Codeunit "AFS Optional Parameters"): Codeunit "AFS Operation Response"
+ begin
+ exit(AFSFileClientImpl.DeleteFile(FilePath, AFSOptionalParameters));
+ end;
+
+ ///
+ /// Lists files and directories from the file share.
+ ///
+ /// The path of the directory to list.
+ /// The result collection with contents of the directory (temporary)
+ /// An operation response object
+ procedure ListDirectory(DirectoryPath: Text[2048]; var AFSDirectoryContent: Record "AFS Directory Content"): Codeunit "AFS Operation Response"
+ var
+ AFSOptionalParameters: Codeunit "AFS Optional Parameters";
+ begin
+ exit(AFSFileClientImpl.ListDirectory(DirectoryPath, AFSDirectoryContent, false, AFSOptionalParameters));
+ end;
+
+ ///
+ /// Lists files and directories from the file share.
+ ///
+ /// The path of the directory to list.
+ /// The result collection with contents of the directory (temporary)
+ /// Optional parameters to pass with the request.
+ /// An operation response object
+ procedure ListDirectory(DirectoryPath: Text[2048]; var AFSDirectoryContent: Record "AFS Directory Content"; AFSOptionalParameters: Codeunit "AFS Optional Parameters"): Codeunit "AFS Operation Response"
+ begin
+ exit(AFSFileClientImpl.ListDirectory(DirectoryPath, AFSDirectoryContent, false, AFSOptionalParameters));
+ end;
+
+ ///
+ /// Lists files and directories from the file share.
+ ///
+ /// The path of the directory to list.
+ /// Specifies if the result collection should be cleared before filling it with the response data.
+ /// The result collection with contents of the directory (temporary)
+ /// An operation response object
+ procedure ListDirectory(DirectoryPath: Text[2048]; PreserveDirectoryContent: Boolean; var AFSDirectoryContent: Record "AFS Directory Content"): Codeunit "AFS Operation Response"
+ var
+ AFSOptionalParameters: Codeunit "AFS Optional Parameters";
+ begin
+ exit(AFSFileClientImpl.ListDirectory(DirectoryPath, AFSDirectoryContent, PreserveDirectoryContent, AFSOptionalParameters));
+ end;
+
+ ///
+ /// Lists files and directories from the file share.
+ ///
+ /// The path of the directory to list.
+ /// Specifies if the result collection should be cleared before filling it with the response data.
+ /// The result collection with contents of the directory (temporary)
+ /// Optional parameters to pass with the request.
+ /// An operation response object
+ procedure ListDirectory(DirectoryPath: Text[2048]; PreserveDirectoryContent: Boolean; var AFSDirectoryContent: Record "AFS Directory Content"; AFSOptionalParameters: Codeunit "AFS Optional Parameters"): Codeunit "AFS Operation Response"
+ begin
+ exit(AFSFileClientImpl.ListDirectory(DirectoryPath, AFSDirectoryContent, PreserveDirectoryContent, AFSOptionalParameters));
+ end;
+
+ ///
+ /// Creates directory on the file share.
+ ///
+ /// The path of the directory to create.
+ /// An operation response object
+ procedure CreateDirectory(DirectoryPath: Text): Codeunit "AFS Operation Response"
+ var
+ AFSOptionalParameters: Codeunit "AFS Optional Parameters";
+ begin
+ exit(AFSFileClientImpl.CreateDirectory(DirectoryPath, AFSOptionalParameters));
+ end;
+
+ ///
+ /// Creates directory on the file share.
+ ///
+ /// The path of the directory to create.
+ /// Optional parameters to pass with the request.
+ /// An operation response object
+ procedure CreateDirectory(DirectoryPath: Text; AFSOptionalParameters: Codeunit "AFS Optional Parameters"): Codeunit "AFS Operation Response"
+ begin
+ exit(AFSFileClientImpl.CreateDirectory(DirectoryPath, AFSOptionalParameters));
+ end;
+
+ ///
+ /// Deletes an empty directory from the file share.
+ ///
+ /// The path of the directory to delete.
+ /// An operation response object
+ procedure DeleteDirectory(DirectoryPath: Text): Codeunit "AFS Operation Response"
+ var
+ AFSOptionalParameters: Codeunit "AFS Optional Parameters";
+ begin
+ exit(AFSFileClientImpl.DeleteDirectory(DirectoryPath, AFSOptionalParameters));
+ end;
+
+ ///
+ /// Deletes an empty directory from the file share.
+ ///
+ /// The path of the directory to delete.
+ /// Optional parameters to pass with the request.
+ /// An operation response object
+ procedure DeleteDirectory(DirectoryPath: Text; AFSOptionalParameters: Codeunit "AFS Optional Parameters"): Codeunit "AFS Operation Response"
+ begin
+ exit(AFSFileClientImpl.DeleteDirectory(DirectoryPath, AFSOptionalParameters));
+ end;
+
+ ///
+ /// Copies a file on the file share.
+ ///
+ /// The URI to the source file. If the source file is on a different share than the destination file, the URI needs to be authorized.
+ /// The path where to destination file should be created.
+ /// An operation response object
+ procedure CopyFile(SourceFileURI: Text; DestinationFilePath: Text): Codeunit "AFS Operation Response"
+ var
+ AFSOptionalParameters: Codeunit "AFS Optional Parameters";
+ begin
+ exit(AFSFileClientImpl.CopyFile(SourceFileURI, DestinationFilePath, AFSOptionalParameters));
+ end;
+
+ ///
+ /// Copies a file on the file share.
+ ///
+ /// The URI to the source file. If the source file is on a different share than the destination file, the URI needs to be authorized.
+ /// The path where to destination file should be created.
+ /// Optional parameters to pass with the request.
+ /// An operation response object
+ procedure CopyFile(SourceFileURI: Text; DestinationFilePath: Text; AFSOptionalParameters: Codeunit "AFS Optional Parameters"): Codeunit "AFS Operation Response"
+ begin
+ exit(AFSFileClientImpl.CopyFile(SourceFileURI, DestinationFilePath, AFSOptionalParameters));
+ end;
+
+ ///
+ /// Stops a file copy operation that is in progress.
+ ///
+ /// The path where to destination file should be created.
+ /// The ID of the copy opeartion to abort.
+ /// An operation response object
+ procedure AbortCopyFile(DestinationFilePath: Text; CopyID: Text): Codeunit "AFS Operation Response"
+ var
+ AFSOptionalParameters: Codeunit "AFS Optional Parameters";
+ begin
+ exit(AFSFileClientImpl.AbortCopyFile(DestinationFilePath, CopyID, AFSOptionalParameters));
+ end;
+
+ ///
+ /// Stops a file copy operation that is in progress.
+ ///
+ /// The path where to destination file should be created.
+ /// The ID of the copy opeartion to abort.
+ /// Optional parameters to pass with the request.
+ /// An operation response object
+ procedure AbortCopyFile(DestinationFilePath: Text; CopyID: Text; AFSOptionalParameters: Codeunit "AFS Optional Parameters"): Codeunit "AFS Operation Response"
+ begin
+ exit(AFSFileClientImpl.AbortCopyFile(DestinationFilePath, CopyID, AFSOptionalParameters));
+ end;
+
+ ///
+ /// Lists all the open handles to the file.
+ ///
+ /// The path to the file.
+ /// The result collection containing all the handles to the file (temporary).
+ /// An operation response object
+ procedure ListHandles(Path: Text; var AFSHandle: Record "AFS Handle"): Codeunit "AFS Operation Response"
+ var
+ AFSOptionalParameters: Codeunit "AFS Optional Parameters";
+ begin
+ exit(AFSFileClientImpl.ListFileHandles(Path, AFSHandle, AFSOptionalParameters));
+ end;
+
+ ///
+ /// Lists all the open handles to the file.
+ ///
+ /// The path to the file.
+ /// The result collection containing all the handles to the file (temporary).
+ /// Optional parameters to pass with the request.
+ /// An operation response object
+ procedure ListHandles(Path: Text; var AFSHandle: Record "AFS Handle"; AFSOptionalParameters: Codeunit "AFS Optional Parameters"): Codeunit "AFS Operation Response"
+ begin
+ exit(AFSFileClientImpl.ListFileHandles(Path, AFSHandle, AFSOptionalParameters));
+ end;
+
+ ///
+ /// Renames a file on the file share.
+ ///
+ /// The path to the source file.
+ /// The path to which the file will be renamed.
+ /// An operation response object
+ procedure RenameFile(SourceFilePath: Text; DestinationFilePath: Text): Codeunit "AFS Operation Response"
+ var
+ AFSOptionalParameters: Codeunit "AFS Optional Parameters";
+ begin
+ exit(AFSFileClientImpl.RenameFile(SourceFilePath, DestinationFilePath, AFSOptionalParameters))
+ end;
+
+ ///
+ /// Renames a file on the file share.
+ ///
+ /// The path to the source file.
+ /// The path to which the file will be renamed.
+ /// Optional parameters to pass with the request.
+ /// An operation response object
+ procedure RenameFile(SourceFilePath: Text; DestinationFilePath: Text; AFSOptionalParameters: Codeunit "AFS Optional Parameters"): Codeunit "AFS Operation Response"
+ begin
+ exit(AFSFileClientImpl.RenameFile(SourceFilePath, DestinationFilePath, AFSOptionalParameters))
+ end;
+
+ ///
+ /// Requests a new lease. If the file does not have an active lease, the file service creates a lease on the file.
+ ///
+ /// The path to the file.
+ /// The proposed id for the new lease.
+ /// Guid containing the response value from x-ms-lease-id HttpHeader
+ /// An operation reponse object
+ procedure AcquireLease(FilePath: Text; ProposedLeaseId: Guid; var LeaseId: Guid): Codeunit "AFS Operation Response"
+ var
+ AFSOptionalParameters: Codeunit "AFS Optional Parameters";
+ begin
+ exit(AFSFileClientImpl.FileAcquireLease(FilePath, AFSOptionalParameters, ProposedLeaseId, LeaseId));
+ end;
+
+ ///
+ /// Requests a new lease. If the file does not have an active lease, the file service creates a lease on the file.
+ ///
+ /// The path to the file.
+ /// The proposed id for the new lease.
+ /// Optional parameters to pass with the request.
+ /// Guid containing the response value from x-ms-lease-id HttpHeader
+ /// An operation reponse object
+ procedure AcquireLease(FilePath: Text; ProposedLeaseId: Guid; AFSOptionalParameters: Codeunit "AFS Optional Parameters"; var LeaseId: Guid): Codeunit "AFS Operation Response"
+ begin
+ exit(AFSFileClientImpl.FileAcquireLease(FilePath, AFSOptionalParameters, ProposedLeaseId, LeaseId));
+ end;
+
+ ///
+ /// Changes a lease id to a new lease id.
+ ///
+ /// The path to the file.
+ /// The proposed id for the new lease.
+ /// Previous lease id. Will be replaced by a new lease id if the request is successful.
+ /// Return value of type Codeunit "AFS Operation Response".
+ procedure ChangeLease(FilePath: Text; ProposedLeaseId: Guid; var LeaseId: Guid): Codeunit "AFS Operation Response"
+ var
+ AFSOptionalParameters: Codeunit "AFS Optional Parameters";
+ begin
+ exit(AFSFileClientImpl.FileChangeLease(FilePath, AFSOptionalParameters, LeaseId, ProposedLeaseId));
+ end;
+
+ ///
+ /// Changes a lease id to a new lease id.
+ ///
+ /// The path to the file.
+ /// The proposed id for the new lease.
+ /// Optional parameters to pass with the request.
+ /// Previous lease id. Will be replaced by a new lease id if the request is successful.
+ /// Return value of type Codeunit "AFS Operation Response".
+ procedure ChangeLease(FilePath: Text; ProposedLeaseId: Guid; AFSOptionalParameters: Codeunit "AFS Optional Parameters"; var LeaseId: Guid): Codeunit "AFS Operation Response"
+ begin
+ exit(AFSFileClientImpl.FileChangeLease(FilePath, AFSOptionalParameters, LeaseId, ProposedLeaseId));
+ end;
+
+ ///
+ /// Releases a lease on a File if it is no longer needed so that another client may immediately acquire a lease against the file.
+ ///
+ /// The path to the file.
+ /// The Guid for the lease that should be released
+ /// An operation reponse object
+ procedure ReleaseLease(FilePath: Text; LeaseId: Guid): Codeunit "AFS Operation Response"
+ var
+ AFSOptionalParameters: Codeunit "AFS Optional Parameters";
+ begin
+ exit(AFSFileClientImpl.FileReleaseLease(FilePath, AFSOptionalParameters, LeaseId));
+ end;
+
+ ///
+ /// Releases a lease on a File if it is no longer needed so that another client may immediately acquire a lease against the file.
+ ///
+ /// The path to the file.
+ /// The Guid for the lease that should be released
+ /// Optional parameters to pass with the request.
+ /// An operation reponse object
+ procedure ReleaseLease(FilePath: Text; LeaseId: Guid; AFSOptionalParameters: Codeunit "AFS Optional Parameters"): Codeunit "AFS Operation Response"
+ begin
+ exit(AFSFileClientImpl.FileReleaseLease(FilePath, AFSOptionalParameters, LeaseId));
+ end;
+
+ ///
+ /// Breaks a lease on a file.
+ ///
+ /// The path to the file.
+ /// The Guid for the lease that should be broken
+ /// An operation reponse object
+ procedure BreakLease(FilePath: Text; LeaseId: Guid): Codeunit "AFS Operation Response"
+ var
+ AFSOptionalParameters: Codeunit "AFS Optional Parameters";
+ begin
+ exit(AFSFileClientImpl.FileBreakLease(FilePath, AFSOptionalParameters, LeaseId));
+ end;
+
+ ///
+ /// Breaks a lease on a file.
+ ///
+ /// The path to the file.
+ /// The Guid for the lease that should be broken
+ /// Optional parameters to pass with the request.
+ /// An operation reponse object
+ procedure BreakLease(FilePath: Text; LeaseId: Guid; AFSOptionalParameters: Codeunit "AFS Optional Parameters"): Codeunit "AFS Operation Response"
+ begin
+ exit(AFSFileClientImpl.FileBreakLease(FilePath, AFSOptionalParameters, LeaseId));
+ end;
+}
\ No newline at end of file
diff --git a/src/System Application/App/Azure File Services API/src/AFSFileClientImpl.Codeunit.al b/src/System Application/App/Azure File Services API/src/AFSFileClientImpl.Codeunit.al
new file mode 100644
index 0000000000..69aad4e50a
--- /dev/null
+++ b/src/System Application/App/Azure File Services API/src/AFSFileClientImpl.Codeunit.al
@@ -0,0 +1,711 @@
+// ------------------------------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+// ------------------------------------------------------------------------------------------------
+
+namespace System.Azure.Storage.Files;
+
+using System.Azure.Storage;
+using System.Utilities;
+using System.Telemetry;
+
+codeunit 8951 "AFS File Client Impl."
+{
+ Access = Internal;
+ InherentEntitlements = X;
+ InherentPermissions = X;
+
+ var
+ AFSOperationPayload: Codeunit "AFS Operation Payload";
+ AFSHttpContentHelper: Codeunit "AFS HttpContent Helper";
+ AFSWebRequestHelper: Codeunit "AFS Web Request Helper";
+ AFSFormatHelper: Codeunit "AFS Format Helper";
+ Telemetry: Codeunit Telemetry;
+ CreateFileOperationNotSuccessfulErr: Label 'Could not create file %1 in %2.', Comment = '%1 = File Name; %2 = File Share Name';
+ PutFileOperationNotSuccessfulErr: Label 'Could not put file %1 ranges in %2.', Comment = '%1 = File Name; %2 = File Share Name';
+ CreateDirectoryOperationNotSuccessfulErr: Label 'Could not create directory %1 in %2.', Comment = '%1 = Directory Name; %2 = File Share Name';
+ GetFileOperationNotSuccessfulErr: Label 'Could not get File %1.', Comment = '%1 = File Path';
+ GetFileMetadataOperationNotSuccessfulErr: Label 'Could not get File %1 metadata.', Comment = '%1 = File Path';
+ SetFileMetadataOperationNotSuccessfulErr: Label 'Could not set File %1 metadata.', Comment = '%1 = File Path';
+ CopyFileOperationNotSuccessfulErr: Label 'Could not copy File %1.', Comment = '%1 = File Path';
+ DeleteFileOperationNotSuccessfulErr: Label 'Could not %3 File %1 in file share %2.', Comment = '%1 = File Name; %2 = File Share Name, %3 = Delete/Undelete';
+ DeleteDirectoryOperationNotSuccessfulErr: Label 'Could not delete directory %1 in file share %2.', Comment = '%1 = File Name; %2 = File Share Name';
+ AbortCopyFileOperationNotSuccessfulErr: Label 'Could not abort copy of File %1.', Comment = '%1 = File Path';
+ LeaseOperationNotSuccessfulErr: Label 'Could not %1 lease for %2 %3.', Comment = '%1 = Lease Action, %2 = Type (File or Share), %3 = Name';
+ ListDirectoryOperationNotSuccessfulErr: Label 'Could not list directory %1 in file share %2.', Comment = '%1 = Directory Name; %2 = File Share Name';
+ ListHandlesOperationNotSuccessfulErr: Label 'Could not list handles of %1 in file share %2.', Comment = '%1 = Path; %2 = File Share Name';
+ RenameFileOperationNotSuccessfulErr: Label 'Could not rename file %1 to %2 on file share %3.', Comment = '%1 = Source Path; %2 = Destination Path; %3 = File Share Name';
+ ParameterMissingErr: Label 'You need to specify %1 (%2)', Comment = '%1 = Parameter Name, %2 = Header Identifer';
+ LeaseAcquireLbl: Label 'acquire';
+ LeaseBreakLbl: Label 'break';
+ LeaseChangeLbl: Label 'change';
+ LeaseReleaseLbl: Label 'release';
+ FileLbl: Label 'File';
+
+ CreatingFileTxt: Label 'Creating a new file.', Locked = true;
+ FileCreatedTxt: Label 'File was created.', Locked = true;
+ FileCreationFailedTxt: Label 'File was not created. Operation returned error %1.', Locked = true;
+ CreatingDirectoryTxt: Label 'Creating a new directory.', Locked = true;
+ DirectoryCreatedTxt: Label 'Directory was created.', Locked = true;
+ DirectoryCreationFailedTxt: Label 'Directory was not created. Operation returned error %1', Locked = true;
+ DeletingDirectoryTxt: Label 'Deleting a directory.', Locked = true;
+ DirectoryDeletedTxt: Label 'Directory was deleted.', Locked = true;
+ DirectoryDeletionFailedTxt: Label 'Directory was not deleted. Operation returned error %1.', Locked = true;
+ ListingDirectoryTxt: Label 'Listing contents of directory.', Locked = true;
+ DirectoryListedTxt: Label 'The contents of directory were listed.', Locked = true;
+ DirectoryListingFailedTxt: Label 'The contents of directory could not be listed. Operation returned error %1.', Locked = true;
+ ListingFileHandlesTxt: Label 'Listing open file handles.', Locked = true;
+ FileHandlesListedTxt: Label 'The handles were listed.', Locked = true;
+ ListingFileHandlesFailedTxt: Label 'Handles could not be listed. Operation returned error %1.', Locked = true;
+ RenamingFileTxt: Label 'Renaming file.', Locked = true;
+ FileRenamedTxt: Label 'File was renamed.', Locked = true;
+ FileRenamingFailedTxt: Label 'File was not renamed. Operation returned error %1.', Locked = true;
+ GettingFileAsFileTxt: Label 'Getting file as a directly downloaded file.', Locked = true;
+ FileRetrievedAsFileTxt: Label 'File was retrieved and downloaded.', Locked = true;
+ GettingFileAsFileFailedTxt: Label 'File was not downloaded. Operation returned error %1.', Locked = true;
+ GettingFileAsStreamTxt: Label 'Getting file as a stream.', Locked = true;
+ FileRetrievedAsStreamTxt: Label 'File was retrieved as a stream.', Locked = true;
+ GettingFileAsStreamFailedTxt: Label 'File was not retrieved as a stream. Operation returned error %1.', Locked = true;
+ GettingFileAsTextTxt: Label 'Getting file as text.', Locked = true;
+ FileRetrievedAsTextTxt: Label 'File was retrieved as text.', Locked = true;
+ GettingFileAsTextFailedTxt: Label 'File was not retrieved as text. Operation returned error %1.', Locked = true;
+ GettingFileMetadataTxt: Label 'Getting file metadata.', Locked = true;
+ FileMetadataRetrievedTxt: Label 'File metadata was retrieved.', Locked = true;
+ GettingFileMetadataFailedTxt: Label 'File metadata was not retrieved. Operation returned error %1.', Locked = true;
+ SettingFileMetadataTxt: Label 'Setting file metadata.', Locked = true;
+ FileMetadataSetTxt: Label 'File metadata was set.', Locked = true;
+ SettingFileMetadataFailedTxt: Label 'File metadata was not set. Operation returned error %1.', Locked = true;
+ PuttingFileUITxt: Label 'Putting file through UI.', Locked = true;
+ FileSentUITxt: Label 'File was sent through UI.', Locked = true;
+ PuttingFileUIFailedTxt: Label 'File was not sent through UI. Operation returned error %1.', Locked = true;
+ PuttingFileUIAbortedTxt: Label 'Putting file was aborted by the user.', Locked = true;
+ PuttingFileStreamTxt: Label 'Putting file as stream.', Locked = true;
+ FileSentStreamTxt: Label 'File was sent as stream.', Locked = true;
+ PuttingFileStreamFailedTxt: Label 'File was not sent as stream. Operation returned error %1.', Locked = true;
+ PuttingFileTextTxt: Label 'Putting file as text.', Locked = true;
+ FileSentTextTxt: Label 'File was sent as text.', Locked = true;
+ PuttingFileTextFailedTxt: Label 'File was not sent as text. Operation returned error %1.', Locked = true;
+ DeletingFileTxt: Label 'Deleting file.', Locked = true;
+ FileDeletedTxt: Label 'File was deleted.', Locked = true;
+ DeletingFileFailedTxt: Label 'File was not deleted. Operation returned error %1.', Locked = true;
+ FileCopyingTxt: Label 'Copying file.', Locked = true;
+ FileCopiedTxt: Label 'File was copied.', Locked = true;
+ FileCopyingFailedTxt: Label 'File was not copied. Operation returned error %1.', Locked = true;
+ AbortingCopyTxt: Label 'Aborting copying operation with copy id %1.', Locked = true;
+ CopyingAbortedTxt: Label 'Copying operation with copy id %1 was aborted.', Locked = true;
+ AbortingCopyFailedTxt: Label 'Copying operation with copy id %1 was not aborted. Operation returned error %2.', Locked = true;
+ AcquiringLeaseTxt: Label 'Acquiring lease for file.', Locked = true;
+ LeaseAcquiredTxt: Label 'Lease %1 for file was acquired.', Locked = true;
+ AcquiringLeaseFailedTxt: Label 'Lease for file was not acquired. Operation returned error %1.', Locked = true;
+ ReleasingLeaseTxt: Label 'Releasing lease %1 for file.', Locked = true;
+ LeaseReleasedTxt: Label 'Lease %1 for file was released.', Locked = true;
+ ReleasingLeaseFailedTxt: Label 'Lease %1 for file was not released. Operation returned error %2.', Locked = true;
+ BreakingLeaseTxt: Label 'Breaking lease %1 for file.', Locked = true;
+ LeaseBrokenTxt: Label 'Lease %1 for file was broken.', Locked = true;
+ BreakingLeaseFailedTxt: Label 'Lease %1 for file was not broken. Operation returned error %2.', Locked = true;
+ ChangingLeaseTxt: Label 'Changing lease %1 to %2 for file.', Locked = true;
+ LeaseChangedTxt: Label 'Lease was changed to %1 for file.', Locked = true;
+ ChangingLeaseFailedTxt: Label 'Lease was not changed to %1 for file. Operation returned error %2.', Locked = true;
+ PuttingFileRangesTxt: Label 'Putting file ranges.', Locked = true;
+ FileRangeSentTxt: Label 'File range: %1-%2 was sent.', Locked = true;
+ PuttingFileRangesFailedTxt: Label 'File range %1-%2 was not sent. Operation returned error %3', Locked = true;
+
+ [NonDebuggable]
+ procedure Initialize(StorageAccountName: Text; FileShare: Text; Path: Text; Authorization: Interface "Storage Service Authorization"; ApiVersion: Enum "Storage Service API Version")
+ begin
+ AFSOperationPayload.Initialize(StorageAccountName, FileShare, Path, Authorization, ApiVersion);
+ end;
+
+ procedure SetBaseUrl(BaseUrl: Text)
+ begin
+ AFSOperationPayload.SetBaseUrl(BaseUrl);
+ end;
+
+ procedure CreateDirectory(DirectoryPath: Text; AFSOptionalParameters: Codeunit "AFS Optional Parameters"): Codeunit "AFS Operation Response"
+ var
+ AFSOperationResponse: Codeunit "AFS Operation Response";
+ AFSOperation: Enum "AFS Operation";
+ begin
+ Telemetry.LogMessage('0000AFS', CreatingDirectoryTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher);
+ AFSOperationPayload.SetOperation(AFSOperation::CreateDirectory);
+ AFSOperationPayload.SetPath(DirectoryPath);
+ AFSOperationPayload.AddRequestHeader('x-ms-file-attributes', 'Directory');
+ AFSOperationPayload.AddRequestHeader('x-ms-file-creation-time', 'now');
+ AFSOperationPayload.AddRequestHeader('x-ms-file-last-write-time', 'now');
+ AFSOperationPayload.AddRequestHeader('x-ms-file-permission', 'inherit');
+ AFSOperationPayload.SetOptionalParameters(AFSOptionalParameters);
+
+ AFSOperationResponse := AFSWebRequestHelper.PutOperation(AFSOperationPayload, StrSubstNo(CreateDirectoryOperationNotSuccessfulErr, AFSOperationPayload.GetPath(), AFSOperationPayload.GetFileShareName()));
+ if AFSOperationResponse.IsSuccessful() then
+ Telemetry.LogMessage('0000AFS', DirectoryCreatedTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher)
+ else
+ Telemetry.LogMessage('0000AFS', StrSubstNo(DirectoryCreationFailedTxt, AFSOperationResponse.GetError()), Verbosity::Warning, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher);
+ exit(AFSOperationResponse);
+ end;
+
+ procedure DeleteDirectory(DirectoryPath: Text; AFSOptionalParameters: Codeunit "AFS Optional Parameters"): Codeunit "AFS Operation Response"
+ var
+ AFSOperationResponse: Codeunit "AFS Operation Response";
+ AFSOperation: Enum "AFS Operation";
+ begin
+ Telemetry.LogMessage('0000AFS', DeletingDirectoryTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher);
+ AFSOperationPayload.SetOperation(AFSOperation::DeleteDirectory);
+ AFSOperationPayload.SetOptionalParameters(AFSOptionalParameters);
+ AFSOperationPayload.SetPath(DirectoryPath);
+
+ AFSOperationResponse := AFSWebRequestHelper.DeleteOperation(AFSOperationPayload, StrSubstNo(DeleteDirectoryOperationNotSuccessfulErr, AFSOperationPayload.GetPath(), AFSOperationPayload.GetFileShareName()));
+ if AFSOperationResponse.IsSuccessful() then
+ Telemetry.LogMessage('0000AFS', DirectoryDeletedTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher)
+ else
+ Telemetry.LogMessage('0000AFS', StrSubstNo(DirectoryDeletionFailedTxt, AFSOperationResponse.GetError()), Verbosity::Warning, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher);
+
+ exit(AFSOperationResponse);
+ end;
+
+ procedure ListDirectory(DirectoryPath: Text[2048]; var AFSDirectoryContent: Record "AFS Directory Content"; PreserveDirectoryContent: Boolean; AFSOptionalParameters: Codeunit "AFS Optional Parameters"): Codeunit "AFS Operation Response"
+ var
+ AFSOperationResponse: Codeunit "AFS Operation Response";
+ AFSHelperLibrary: Codeunit "AFS Helper Library";
+ AFSOperation: Enum "AFS Operation";
+ ResponseText: Text;
+ NodeList: XmlNodeList;
+ DirectoryURI: Text;
+ begin
+ Telemetry.LogMessage('0000AFS', ListingDirectoryTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher);
+ AFSOperationPayload.SetOperation(AFSOperation::ListDirectory);
+ AFSOperationPayload.SetOptionalParameters(AFSOptionalParameters);
+ AFSOperationPayload.SetPath(DirectoryPath);
+
+ AFSOperationResponse := AFSWebRequestHelper.GetOperationAsText(AFSOperationPayload, ResponseText, StrSubstNo(ListDirectoryOperationNotSuccessfulErr, AFSOperationPayload.GetPath(), AFSOperationPayload.GetFileShareName()));
+ if AFSOperationResponse.IsSuccessful() then
+ Telemetry.LogMessage('0000AFS', DirectoryListedTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher)
+ else
+ Telemetry.LogMessage('0000AFS', StrSubstNo(DirectoryListingFailedTxt, AFSOperationResponse.GetError()), Verbosity::Warning, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher);
+
+ NodeList := AFSHelperLibrary.CreateDirectoryContentNodeListFromResponse(ResponseText);
+ DirectoryURI := AFSHelperLibrary.GetDirectoryPathFromResponse(ResponseText);
+
+ AFSHelperLibrary.DirectoryContentNodeListToTempRecord(DirectoryURI, DirectoryPath, NodeList, PreserveDirectoryContent, AFSDirectoryContent);
+
+ exit(AFSOperationResponse);
+ end;
+
+ procedure ListFileHandles(Path: Text; var AFSHandle: Record "AFS Handle"; AFSOptionalParameters: Codeunit "AFS Optional Parameters"): Codeunit "AFS Operation Response"
+ var
+ AFSOperationResponse: Codeunit "AFS Operation Response";
+ AFSHelperLibrary: Codeunit "AFS Helper Library";
+ AFSOperation: Enum "AFS Operation";
+ ResponseText: Text;
+ NodeList: XmlNodeList;
+ begin
+ Telemetry.LogMessage('0000AFS', ListingFileHandlesTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher);
+ AFSOperationPayload.SetOperation(AFSOperation::ListFileHandles);
+ AFSOperationPayload.SetOptionalParameters(AFSOptionalParameters);
+ AFSOperationPayload.SetPath(Path);
+
+ AFSOperationResponse := AFSWebRequestHelper.GetOperationAsText(AFSOperationPayload, ResponseText, StrSubstNo(ListHandlesOperationNotSuccessfulErr, AFSOperationPayload.GetPath(), AFSOperationPayload.GetFileShareName()));
+ if AFSOperationResponse.IsSuccessful() then
+ Telemetry.LogMessage('0000AFS', FileHandlesListedTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher)
+ else
+ Telemetry.LogMessage('0000AFS', StrSubstNo(ListingFileHandlesFailedTxt, AFSOperationResponse.GetError()), Verbosity::Warning, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher);
+
+ NodeList := AFSHelperLibrary.CreateHandleNodeListFromResponse(ResponseText);
+ AFSHelperLibrary.HandleNodeListToTempRecord(NodeList, AFSHandle);
+ AFSHandle."Next Marker" := CopyStr(AFSHelperLibrary.GetNextMarkerFromResponse(ResponseText), 1, MaxStrLen(AFSHandle."Next Marker"));
+ AFSHandle.Modify();
+
+ exit(AFSOperationResponse);
+ end;
+
+ procedure RenameFile(SourceFilePath: Text; DestinationFilePath: Text; AFSOptionalParameters: Codeunit "AFS Optional Parameters"): Codeunit "AFS Operation Response"
+ var
+ AFSOperationResponse: Codeunit "AFS Operation Response";
+ AFSOperation: Enum "AFS Operation";
+ begin
+ Telemetry.LogMessage('0000AFS', RenamingFileTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher);
+ AFSOperationPayload.SetOperation(AFSOperation::RenameFile);
+ AFSOperationPayload.AddRequestHeader('x-ms-file-rename-source', SourceFilePath);
+ AFSOperationPayload.SetOptionalParameters(AFSOptionalParameters);
+ AFSOperationPayload.SetPath(DestinationFilePath);
+
+ AFSOperationResponse := AFSWebRequestHelper.PutOperation(AFSOperationPayload, StrSubstNo(RenameFileOperationNotSuccessfulErr, SourceFilePath, AFSOperationPayload.GetPath(), AFSOperationPayload.GetFileShareName()));
+ if AFSOperationResponse.IsSuccessful() then
+ Telemetry.LogMessage('0000AFS', FileRenamedTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher)
+ else
+ Telemetry.LogMessage('0000AFS', StrSubstNo(FileRenamingFailedTxt, AFSOperationResponse.GetError()), Verbosity::Warning, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher);
+ exit(AFSOperationResponse);
+ end;
+
+ procedure CreateFile(FilePath: Text; InStream: InStream; AFSOptionalParameters: Codeunit "AFS Optional Parameters"): Codeunit "AFS Operation Response"
+ begin
+ exit(CreateFile(FilePath, AFSHttpContentHelper.GetContentLength(InStream), AFSOptionalParameters));
+ end;
+
+ procedure CreateFile(FilePath: Text; FileSize: Integer; AFSOptionalParameters: Codeunit "AFS Optional Parameters"): Codeunit "AFS Operation Response"
+ var
+ AFSOperationResponse: Codeunit "AFS Operation Response";
+ AFSOperation: Enum "AFS Operation";
+ begin
+ Telemetry.LogMessage('0000AFS', CreatingFileTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher);
+ AFSOperationPayload.SetOperation(AFSOperation::CreateFile);
+ AFSOperationPayload.SetPath(FilePath);
+ AFSOperationPayload.AddRequestHeader('x-ms-type', 'file');
+ AFSOperationPayload.AddRequestHeader('x-ms-file-attributes', 'None');
+ AFSOperationPayload.AddRequestHeader('x-ms-file-creation-time', 'now');
+ AFSOperationPayload.AddRequestHeader('x-ms-file-last-write-time', 'now');
+ AFSOperationPayload.AddRequestHeader('x-ms-file-permission', 'inherit');
+ AFSOperationPayload.SetOptionalParameters(AFSOptionalParameters);
+
+ AFSHttpContentHelper.AddFilePutContentHeaders(AFSOperationPayload, FileSize, '', 0, 0);
+
+ AFSOperationResponse := AFSWebRequestHelper.PutOperation(AFSOperationPayload, StrSubstNo(CreateFileOperationNotSuccessfulErr, AFSOperationPayload.GetPath(), AFSOperationPayload.GetFileShareName()));
+ if AFSOperationResponse.IsSuccessful() then
+ Telemetry.LogMessage('0000AFS', FileCreatedTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher)
+ else
+ Telemetry.LogMessage('0000AFS', StrSubstNo(FileCreationFailedTxt, AFSOperationResponse.GetError()), Verbosity::Warning, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher);
+
+ exit(AFSOperationResponse);
+ end;
+
+ procedure GetFileAsFile(FilePath: Text; AFSOptionalParameters: Codeunit "AFS Optional Parameters"): Codeunit "AFS Operation Response"
+ var
+ AFSOperationResponse: Codeunit "AFS Operation Response";
+ TargetInStream: InStream;
+ begin
+ Telemetry.LogMessage('0000AFS', GettingFileAsFileTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher);
+ AFSOperationResponse := GetFileAsStream(FilePath, TargetInStream, AFSOptionalParameters);
+
+ if AFSOperationResponse.IsSuccessful() then begin
+ FilePath := AFSOperationPayload.GetPath();
+ DownloadFromStream(TargetInStream, '', '', '', FilePath);
+ Telemetry.LogMessage('0000AFS', FileRetrievedAsFileTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher);
+ end else
+ Telemetry.LogMessage('0000AFS', StrSubstNo(GettingFileAsFileFailedTxt, AFSOperationResponse.GetError()), Verbosity::Warning, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher);
+ exit(AFSOperationResponse);
+ end;
+
+ procedure GetFileAsStream(FilePath: Text; var TargetInStream: InStream; AFSOptionalParameters: Codeunit "AFS Optional Parameters"): Codeunit "AFS Operation Response"
+ var
+ AFSOperationResponse: Codeunit "AFS Operation Response";
+ AFSOperation: Enum "AFS Operation";
+ begin
+ Telemetry.LogMessage('0000AFS', GettingFileAsStreamTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher);
+
+ AFSOperationPayload.SetOperation(AFSOperation::GetFile);
+ AFSOperationPayload.SetPath(FilePath);
+ AFSOperationPayload.SetOptionalParameters(AFSOptionalParameters);
+
+ AFSOperationResponse := AFSWebRequestHelper.GetOperationAsStream(AFSOperationPayload, TargetInStream, StrSubstNo(GetFileOperationNotSuccessfulErr, AFSOperationPayload.GetPath()));
+ if AFSOperationResponse.IsSuccessful() then
+ Telemetry.LogMessage('0000AFS', FileRetrievedAsStreamTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher)
+ else
+ Telemetry.LogMessage('0000AFS', StrSubstNo(GettingFileAsStreamFailedTxt, AFSOperationResponse.GetError()), Verbosity::Warning, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher);
+ exit(AFSOperationResponse);
+ end;
+
+ procedure GetFileAsText(FilePath: Text; var TargetText: Text; AFSOptionalParameters: Codeunit "AFS Optional Parameters"): Codeunit "AFS Operation Response"
+ var
+ AFSOperationResponse: Codeunit "AFS Operation Response";
+ AFSOperation: Enum "AFS Operation";
+ begin
+ Telemetry.LogMessage('0000AFS', GettingFileAsTextTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher);
+
+ AFSOperationPayload.SetOperation(AFSOperation::GetFile);
+ AFSOperationPayload.SetOptionalParameters(AFSOptionalParameters);
+ AFSOperationPayload.SetPath(FilePath);
+
+ AFSOperationResponse := AFSWebRequestHelper.GetOperationAsText(AFSOperationPayload, TargetText, StrSubstNo(GetFileOperationNotSuccessfulErr, AFSOperationPayload.GetPath()));
+ if AFSOperationResponse.IsSuccessful() then
+ Telemetry.LogMessage('0000AFS', FileRetrievedAsTextTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher)
+ else
+ Telemetry.LogMessage('0000AFS', StrSubstNo(GettingFileAsTextFailedTxt, AFSOperationResponse.GetError()), Verbosity::Warning, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher);
+ exit(AFSOperationResponse);
+ end;
+
+ procedure GetFileMetadata(FilePath: Text; var TargetMetadata: Dictionary of [Text, Text]; AFSOptionalParameters: Codeunit "AFS Optional Parameters"): Codeunit "AFS Operation Response"
+ var
+ AFSOperationResponse: Codeunit "AFS Operation Response";
+ AFSHttpHeaderHelper: Codeunit "AFS HttpHeader Helper";
+ AFSOperation: Enum "AFS Operation";
+ TargetText: Text;
+ begin
+ Telemetry.LogMessage('0000AFS', GettingFileMetadataTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher);
+ AFSOperationPayload.SetOperation(AFSOperation::GetFileMetadata);
+ AFSOperationPayload.SetOptionalParameters(AFSOptionalParameters);
+ AFSOperationPayload.SetPath(FilePath);
+
+ AFSOperationResponse := AFSWebRequestHelper.GetOperationAsText(AFSOperationPayload, TargetText, StrSubstNo(GetFileMetadataOperationNotSuccessfulErr, AFSOperationPayload.GetPath()));
+ if AFSOperationResponse.IsSuccessful() then
+ Telemetry.LogMessage('0000AFS', FileMetadataRetrievedTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher)
+ else
+ Telemetry.LogMessage('0000AFS', StrSubstNo(GettingFileMetadataFailedTxt, AFSOperationResponse.GetError()), Verbosity::Warning, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher);
+ TargetMetadata := AFSHttpHeaderHelper.GetMetadataHeaders(AFSOperationResponse.GetHeaders());
+ exit(AFSOperationResponse);
+ end;
+
+ procedure SetFileMetadata(FilePath: Text; Metadata: Dictionary of [Text, Text]; AFSOptionalParameters: Codeunit "AFS Optional Parameters"): Codeunit "AFS Operation Response"
+ var
+ AFSOperationResponse: Codeunit "AFS Operation Response";
+ AFSOperation: Enum "AFS Operation";
+ MetadataKey: Text;
+ MetadataValue: Text;
+ begin
+ Telemetry.LogMessage('0000AFS', SettingFileMetadataTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher);
+ AFSOperationPayload.SetOperation(AFSOperation::SetFileMetadata);
+ AFSOperationPayload.SetOptionalParameters(AFSOptionalParameters);
+ AFSOperationPayload.SetPath(FilePath);
+
+ foreach MetadataKey in Metadata.Keys() do begin
+ MetadataValue := Metadata.Get(MetadataKey);
+ if not MetadataKey.StartsWith('x-ms-meta-') then
+ MetadataKey := 'x-ms-meta-' + MetadataKey;
+ AFSOperationPayload.AddRequestHeader(MetadataKey, MetadataValue);
+ end;
+
+ AFSOperationResponse := AFSWebRequestHelper.PutOperation(AFSOperationPayload, StrSubstNo(SetFileMetadataOperationNotSuccessfulErr, AFSOperationPayload.GetPath()));
+ if AFSOperationResponse.IsSuccessful() then
+ Telemetry.LogMessage('0000AFS', FileMetadataSetTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher)
+ else
+ Telemetry.LogMessage('0000AFS', StrSubstNo(SettingFileMetadataFailedTxt, AFSOperationResponse.GetError()), Verbosity::Warning, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher);
+ exit(AFSOperationResponse);
+ end;
+
+ procedure PutFileUI(AFSOptionalParameters: Codeunit "AFS Optional Parameters"): Codeunit "AFS Operation Response"
+ var
+ AFSOperationResponse: Codeunit "AFS Operation Response";
+ Filename: Text;
+ SourceInStream: InStream;
+ begin
+ Telemetry.LogMessage('0000AFS', PuttingFileUITxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher);
+ if UploadIntoStream('', '', '', FileName, SourceInStream) then begin
+ AFSOperationResponse := PutFileStream(Filename, SourceInStream, AFSOptionalParameters);
+ if AFSOperationResponse.IsSuccessful() then
+ Telemetry.LogMessage('0000AFS', FileSentUITxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher)
+ else
+ Telemetry.LogMessage('0000AFS', StrSubstNo(PuttingFileUIFailedTxt, AFSOperationResponse.GetError()), Verbosity::Warning, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher);
+ end else
+ Telemetry.LogMessage('0000AFS', PuttingFileUIAbortedTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher);
+ exit(AFSOperationResponse);
+ end;
+
+ procedure PutFileStream(FilePath: Text; var SourceInStream: InStream; AFSOptionalParameters: Codeunit "AFS Optional Parameters"): Codeunit "AFS Operation Response"
+ var
+ AFSOperationResponse: Codeunit "AFS Operation Response";
+ SourceContentVariant: Variant;
+ begin
+ Telemetry.LogMessage('0000AFS', PuttingFileStreamTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher);
+ SourceContentVariant := SourceInStream;
+ AFSOperationResponse := PutFile(FilePath, AFSOptionalParameters, SourceContentVariant);
+ if AFSOperationResponse.IsSuccessful() then
+ Telemetry.LogMessage('0000AFS', FileSentStreamTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher)
+ else
+ Telemetry.LogMessage('0000AFS', StrSubstNo(PuttingFileStreamFailedTxt, AFSOperationResponse.GetError()), Verbosity::Warning, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher);
+ exit(AFSOperationResponse);
+ end;
+
+ procedure PutFileText(FilePath: Text; SourceText: Text; AFSOptionalParameters: Codeunit "AFS Optional Parameters"): Codeunit "AFS Operation Response"
+ var
+ AFSOperationResponse: Codeunit "AFS Operation Response";
+ SourceContentVariant: Variant;
+ begin
+ Telemetry.LogMessage('0000AFS', PuttingFileTextTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher);
+ SourceContentVariant := SourceText;
+ AFSOperationResponse := PutFile(FilePath, AFSOptionalParameters, SourceContentVariant);
+ if AFSOperationResponse.IsSuccessful() then
+ Telemetry.LogMessage('0000AFS', FileSentTextTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher)
+ else
+ Telemetry.LogMessage('0000AFS', StrSubstNo(PuttingFileTextFailedTxt, AFSOperationResponse.GetError()), Verbosity::Warning, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher);
+ exit(AFSOperationResponse);
+ end;
+
+ local procedure PutFile(FilePath: Text; AFSOptionalParameters: Codeunit "AFS Optional Parameters"; var SourceContentVariant: Variant): Codeunit "AFS Operation Response"
+ var
+ AFSOperationResponse: Codeunit "AFS Operation Response";
+ TextTempBlob: Codeunit "Temp Blob";
+ AFSOperation: Enum "AFS Operation";
+ HttpContent: HttpContent;
+ SourceInStream: InStream;
+ SourceText: Text;
+ SourceTextStream: InStream;
+ SourceTextOutStream: OutStream;
+ begin
+ AFSOperationPayload.SetOperation(AFSOperation::PutRange);
+ AFSOperationPayload.SetPath(FilePath);
+ AFSOperationPayload.SetOptionalParameters(AFSOptionalParameters);
+
+ case true of
+ SourceContentVariant.IsInStream():
+ begin
+ SourceInStream := SourceContentVariant;
+
+ PutFileRanges(AFSOperationResponse, HttpContent, SourceInStream);
+ end;
+ SourceContentVariant.IsText():
+ begin
+ SourceText := SourceContentVariant;
+ TextTempBlob.CreateOutStream(SourceTextOutStream);
+ SourceTextOutStream.WriteText(SourceText);
+ TextTempBlob.CreateInStream(SourceTextStream);
+
+ PutFileRanges(AFSOperationResponse, HttpContent, SourceTextStream);
+ end;
+ end;
+
+ exit(AFSOperationResponse);
+ end;
+
+ local procedure PutFileRanges(var AFSOperationResponse: Codeunit "AFS Operation Response"; var HttpContent: HttpContent; var SourceInStream: InStream)
+ var
+ TempBlob: Codeunit "Temp Blob";
+ MaxAllowedRange: Integer;
+ CurrentPostion: Integer;
+ BytesToWrite: Integer;
+ BytesLeftToWrite: Integer;
+ SmallerStream: InStream;
+ SmallerOutStream: OutStream;
+ ResponseIndex: Integer;
+ begin
+ Telemetry.LogMessage('0000AFS', PuttingFileRangesTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher);
+ MaxAllowedRange := AFSHttpContentHelper.GetMaxRange();
+ BytesLeftToWrite := AFSHttpContentHelper.GetContentLength(SourceInStream);
+ CurrentPostion := 0;
+ while BytesLeftToWrite > 0 do begin
+ ResponseIndex += 1;
+ if BytesLeftToWrite > MaxAllowedRange then
+ BytesToWrite := MaxAllowedRange
+ else
+ BytesToWrite := BytesLeftToWrite;
+
+ Clear(TempBlob);
+ Clear(SmallerStream);
+ Clear(SmallerOutStream);
+ TempBlob.CreateOutStream(SmallerOutStream);
+ CopyStream(SmallerOutStream, SourceInStream, BytesToWrite);
+ TempBlob.CreateInStream(SmallerStream);
+ AFSHttpContentHelper.AddFilePutContentHeaders(HttpContent, AFSOperationPayload, SmallerStream, CurrentPostion, CurrentPostion + BytesToWrite - 1);
+ AFSOperationResponse := AFSWebRequestHelper.PutOperation(AFSOperationPayload, HttpContent, StrSubstNo(PutFileOperationNotSuccessfulErr, AFSOperationPayload.GetPath(), AFSOperationPayload.GetFileShareName()));
+ if AFSOperationResponse.IsSuccessful() then
+ Telemetry.LogMessage('0000AFS', StrSubstNo(FileRangeSentTxt, CurrentPostion, CurrentPostion + BytesToWrite - 1), Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher)
+ else
+ Telemetry.LogMessage('0000AFS', StrSubstNo(PuttingFileRangesFailedTxt, CurrentPostion, CurrentPostion + BytesToWrite - 1, AFSOperationResponse.GetError()), Verbosity::Warning, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher);
+ CurrentPostion += BytesToWrite;
+ BytesLeftToWrite -= BytesToWrite;
+
+ // A way to handle multiple responses
+ OnPutFileRangesAfterPutOperation(ResponseIndex, AFSOperationResponse);
+ end;
+ end;
+
+ procedure DeleteFile(FilePath: Text; AFSOptionalParameters: Codeunit "AFS Optional Parameters"): Codeunit "AFS Operation Response"
+ var
+ AFSOperationResponse: Codeunit "AFS Operation Response";
+ AFSOperation: Enum "AFS Operation";
+ begin
+ Telemetry.LogMessage('0000AFS', DeletingFileTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher);
+ AFSOperationPayload.SetOperation(AFSOperation::DeleteFile);
+ AFSOperationPayload.SetOptionalParameters(AFSOptionalParameters);
+ AFSOperationPayload.SetPath(FilePath);
+
+ AFSOperationResponse := AFSWebRequestHelper.DeleteOperation(AFSOperationPayload, StrSubstNo(DeleteFileOperationNotSuccessfulErr, AFSOperationPayload.GetPath(), AFSOperationPayload.GetFileShareName(), 'Delete'));
+ if AFSOperationResponse.IsSuccessful() then
+ Telemetry.LogMessage('0000AFS', FileDeletedTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher)
+ else
+ Telemetry.LogMessage('0000AFS', StrSubstNo(DeletingFileFailedTxt, AFSOperationResponse.GetError()), Verbosity::Warning, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher);
+
+ exit(AFSOperationResponse);
+ end;
+
+ procedure CopyFile(SourceFileURI: Text; DestinationFilePath: Text; AFSOptionalParameters: Codeunit "AFS Optional Parameters"): Codeunit "AFS Operation Response"
+ var
+ AFSOperationResponse: Codeunit "AFS Operation Response";
+ AFSOperation: Enum "AFS Operation";
+ begin
+ Telemetry.LogMessage('0000AFS', FileCopyingTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher);
+ AFSOperationPayload.SetOperation(AFSOperation::CopyFile);
+ AFSOperationPayload.AddRequestHeader('x-ms-copy-source', SourceFileURI);
+ AFSOperationPayload.SetOptionalParameters(AFSOptionalParameters);
+ AFSOperationPayload.SetPath(DestinationFilePath);
+
+ AFSOperationResponse := AFSWebRequestHelper.PutOperation(AFSOperationPayload, StrSubstNo(CopyFileOperationNotSuccessfulErr, AFSOperationPayload.GetPath()));
+ if AFSOperationResponse.IsSuccessful() then
+ Telemetry.LogMessage('0000AFS', FileCopiedTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher)
+ else
+ Telemetry.LogMessage('0000AFS', StrSubstNo(FileCopyingFailedTxt, AFSOperationResponse.GetError()), Verbosity::Warning, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher);
+ exit(AFSOperationResponse);
+ end;
+
+ procedure AbortCopyFile(DestinationFilePath: Text; CopyID: Text; AFSOptionalParameters: Codeunit "AFS Optional Parameters"): Codeunit "AFS Operation Response"
+ var
+ AFSOperationResponse: Codeunit "AFS Operation Response";
+ AFSOperation: Enum "AFS Operation";
+ begin
+ Telemetry.LogMessage('0000AFS', StrSubstNo(AbortingCopyTxt, CopyID), Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher);
+ AFSOperationPayload.SetOperation(AFSOperation::AbortCopyFile);
+ AFSOperationPayload.AddRequestHeader('x-ms-copy-action', 'abort');
+ AFSOperationPayload.AddUriParameter('copyid', CopyID);
+ AFSOperationPayload.SetOptionalParameters(AFSOptionalParameters);
+ AFSOperationPayload.SetPath(DestinationFilePath);
+
+ AFSOperationResponse := AFSWebRequestHelper.PutOperation(AFSOperationPayload, StrSubstNo(AbortCopyFileOperationNotSuccessfulErr, AFSOperationPayload.GetPath()));
+ if AFSOperationResponse.IsSuccessful() then
+ Telemetry.LogMessage('0000AFS', StrSubstNo(CopyingAbortedTxt, CopyID), Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher)
+ else
+ Telemetry.LogMessage('0000AFS', StrSubstNo(AbortingCopyFailedTxt, CopyID, AFSOperationResponse.GetError()), Verbosity::Warning, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher);
+ exit(AFSOperationResponse);
+ end;
+
+ procedure FileAcquireLease(FilePath: Text; AFSOptionalParameters: Codeunit "AFS Optional Parameters"; ProposedLeaseId: Guid; var LeaseId: Guid): Codeunit "AFS Operation Response"
+ var
+ AFSOperationResponse: Codeunit "AFS Operation Response";
+ AFSOperation: Enum "AFS Operation";
+ begin
+ Telemetry.LogMessage('0000AFS', AcquiringLeaseTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher);
+ AFSOperationPayload.SetOperation(AFSOperation::LeaseFile);
+ AFSOperationPayload.SetPath(FilePath);
+
+ AFSOperationResponse := AcquireLease(AFSOptionalParameters, ProposedLeaseId, LeaseId, StrSubstNo(LeaseOperationNotSuccessfulErr, LeaseAcquireLbl, FileLbl, AFSOperationPayload.GetPath()));
+ if AFSOperationResponse.IsSuccessful() then
+ Telemetry.LogMessage('0000AFS', StrSubstNo(LeaseAcquiredTxt, LeaseId), Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher)
+ else
+ Telemetry.LogMessage('0000AFS', StrSubstNo(AcquiringLeaseFailedTxt, AFSOperationResponse.GetError()), Verbosity::Warning, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher);
+ exit(AFSOperationResponse);
+ end;
+
+ procedure FileReleaseLease(FilePath: Text; AFSOptionalParameters: Codeunit "AFS Optional Parameters"; LeaseId: Guid): Codeunit "AFS Operation Response"
+ var
+ AFSOperationResponse: Codeunit "AFS Operation Response";
+ AFSOperation: Enum "AFS Operation";
+ begin
+ Telemetry.LogMessage('0000AFS', StrSubstNo(ReleasingLeaseTxt, LeaseId), Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher);
+ AFSOperationPayload.SetOperation(AFSOperation::LeaseFile);
+ AFSOperationPayload.SetPath(FilePath);
+
+ AFSOperationResponse := ReleaseLease(AFSOptionalParameters, LeaseId, StrSubstNo(LeaseOperationNotSuccessfulErr, LeaseReleaseLbl, FileLbl, AFSOperationPayload.GetPath()));
+ if AFSOperationResponse.IsSuccessful() then
+ Telemetry.LogMessage('0000AFS', StrSubstNo(LeaseReleasedTxt, LeaseId), Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher)
+ else
+ Telemetry.LogMessage('0000AFS', StrSubstNo(ReleasingLeaseFailedTxt, LeaseId, AFSOperationResponse.GetError()), Verbosity::Warning, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher);
+ exit(AFSOperationResponse);
+ end;
+
+ procedure FileBreakLease(FilePath: Text; AFSOptionalParameters: Codeunit "AFS Optional Parameters"; LeaseId: Guid): Codeunit "AFS Operation Response"
+ var
+ AFSOperationResponse: Codeunit "AFS Operation Response";
+ AFSOperation: Enum "AFS Operation";
+ begin
+ Telemetry.LogMessage('0000AFS', StrSubstNo(BreakingLeaseTxt, LeaseId), Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher);
+ AFSOperationPayload.SetOperation(AFSOperation::LeaseFile);
+ AFSOperationPayload.SetPath(FilePath);
+
+ AFSOperationResponse := BreakLease(AFSOptionalParameters, LeaseId, StrSubstNo(LeaseOperationNotSuccessfulErr, LeaseBreakLbl, FileLbl, AFSOperationPayload.GetPath()));
+ if AFSOperationResponse.IsSuccessful() then
+ Telemetry.LogMessage('0000AFS', StrSubstNo(LeaseBrokenTxt, LeaseId), Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher)
+ else
+ Telemetry.LogMessage('0000AFS', StrSubstNo(BreakingLeaseFailedTxt, LeaseId, AFSOperationResponse.GetError()), Verbosity::Warning, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher);
+ exit(AFSOperationResponse);
+ end;
+
+ procedure FileChangeLease(FilePath: Text; AFSOptionalParameters: Codeunit "AFS Optional Parameters"; var LeaseId: Guid; ProposedLeaseId: Guid): Codeunit "AFS Operation Response"
+ var
+ AFSOperationResponse: Codeunit "AFS Operation Response";
+ AFSOperation: Enum "AFS Operation";
+ begin
+ Telemetry.LogMessage('0000AFS', StrSubstNo(ChangingLeaseTxt, LeaseId, ProposedLeaseId), Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher);
+ AFSOperationPayload.SetOperation(AFSOperation::LeaseFile);
+ AFSOperationPayload.SetPath(FilePath);
+
+ AFSOperationResponse := ChangeLease(AFSOptionalParameters, LeaseId, ProposedLeaseId, StrSubstNo(LeaseOperationNotSuccessfulErr, LeaseChangeLbl, FileLbl, AFSOperationPayload.GetPath()));
+ if AFSOperationResponse.IsSuccessful() then
+ Telemetry.LogMessage('0000AFS', StrSubstNo(LeaseChangedTxt, LeaseId), Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher)
+ else
+ Telemetry.LogMessage('0000AFS', StrSubstNo(ChangingLeaseFailedTxt, ProposedLeaseId, AFSOperationResponse.GetError()), Verbosity::Warning, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher);
+ exit(AFSOperationResponse);
+ end;
+
+ #region Private Lease-functions
+ local procedure AcquireLease(AFSOptionalParameters: Codeunit "AFS Optional Parameters"; ProposedLeaseId: Guid; var LeaseId: Guid; OperationNotSuccessfulErr: Text): Codeunit "AFS Operation Response"
+ var
+ AFSOperationResponse: Codeunit "AFS Operation Response";
+ LeaseAction: Enum "AFS Lease Action";
+ DurationSeconds: Integer;
+ begin
+ DurationSeconds := -1;
+
+ AFSOptionalParameters.LeaseAction(LeaseAction::Acquire);
+ AFSOptionalParameters.LeaseDuration(DurationSeconds);
+ if not IsNullGuid(ProposedLeaseId) then
+ AFSOptionalParameters.ProposedLeaseId(ProposedLeaseId);
+
+ AFSOperationPayload.SetOptionalParameters(AFSOptionalParameters);
+
+ AFSOperationResponse := AFSWebRequestHelper.PutOperation(AFSOperationPayload, OperationNotSuccessfulErr);
+ if AFSOperationResponse.IsSuccessful() then
+ LeaseId := AFSFormatHelper.RemoveCurlyBracketsFromString(AFSOperationResponse.GetHeaderValueFromResponseHeaders('x-ms-lease-id'));
+ exit(AFSOperationResponse);
+ end;
+
+ local procedure ReleaseLease(AFSOptionalParameters: Codeunit "AFS Optional Parameters"; LeaseId: Guid; OperationNotSuccessfulErr: Text): Codeunit "AFS Operation Response"
+ var
+ AFSOperationResponse: Codeunit "AFS Operation Response";
+ LeaseAction: Enum "AFS Lease Action";
+ begin
+ AFSOptionalParameters.LeaseAction(LeaseAction::Release);
+
+ CheckGuidNotNull(LeaseId, 'LeaseId', 'x-ms-lease-id');
+
+ AFSOptionalParameters.LeaseId(LeaseId);
+
+ AFSOperationPayload.SetOptionalParameters(AFSOptionalParameters);
+
+ AFSOperationResponse := AFSWebRequestHelper.PutOperation(AFSOperationPayload, OperationNotSuccessfulErr);
+ exit(AFSOperationResponse);
+ end;
+
+ local procedure BreakLease(AFSOptionalParameters: Codeunit "AFS Optional Parameters"; LeaseId: Guid; OperationNotSuccessfulErr: Text): Codeunit "AFS Operation Response"
+ var
+ AFSOperationResponse: Codeunit "AFS Operation Response";
+ LeaseAction: Enum "AFS Lease Action";
+ begin
+ AFSOptionalParameters.LeaseAction(LeaseAction::Break);
+
+ if not IsNullGuid(LeaseId) then
+ AFSOptionalParameters.LeaseId(LeaseId);
+ AFSOperationPayload.SetOptionalParameters(AFSOptionalParameters);
+
+ AFSOperationResponse := AFSWebRequestHelper.PutOperation(AFSOperationPayload, OperationNotSuccessfulErr);
+ exit(AFSOperationResponse);
+ end;
+
+ local procedure ChangeLease(AFSOptionalParameters: Codeunit "AFS Optional Parameters"; var LeaseId: Guid; ProposedLeaseId: Guid; OperationNotSuccessfulErr: Text): Codeunit "AFS Operation Response"
+ var
+ AFSOperationResponse: Codeunit "AFS Operation Response";
+ LeaseAction: Enum "AFS Lease Action";
+ begin
+ AFSOptionalParameters.LeaseAction(LeaseAction::Change);
+
+ CheckGuidNotNull(LeaseId, 'LeaseId', 'x-ms-lease-id');
+ CheckGuidNotNull(ProposedLeaseId, 'ProposedLeaseId', 'x-ms-proposed-lease-id');
+
+ AFSOptionalParameters.LeaseId(LeaseId);
+ AFSOptionalParameters.ProposedLeaseId(ProposedLeaseId);
+
+ AFSOperationPayload.SetOptionalParameters(AFSOptionalParameters);
+
+ AFSOperationResponse := AFSWebRequestHelper.PutOperation(AFSOperationPayload, OperationNotSuccessfulErr);
+ LeaseId := AFSFormatHelper.RemoveCurlyBracketsFromString(AFSOperationResponse.GetHeaderValueFromResponseHeaders('x-ms-lease-id'));
+ exit(AFSOperationResponse);
+ end;
+
+ local procedure CheckGuidNotNull(ValueVariant: Variant; ParameterName: Text; HeaderIdentifer: Text)
+ begin
+ if ValueVariant.IsGuid() then
+ if IsNullGuid(ValueVariant) then
+ Error(ParameterMissingErr, ParameterName, HeaderIdentifer);
+ end;
+ #endregion
+
+ [IntegrationEvent(false, false)]
+ local procedure OnPutFileRangesAfterPutOperation(ResponseIndex: Integer; var AFSOperationResponse: Codeunit "AFS Operation Response")
+ begin
+ end;
+}
\ No newline at end of file
diff --git a/src/System Application/App/Azure File Services API/src/AFSHandle.Table.al b/src/System Application/App/Azure File Services API/src/AFSHandle.Table.al
new file mode 100644
index 0000000000..4cbc21ee2f
--- /dev/null
+++ b/src/System Application/App/Azure File Services API/src/AFSHandle.Table.al
@@ -0,0 +1,72 @@
+// ------------------------------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+// ------------------------------------------------------------------------------------------------
+
+namespace System.Azure.Storage.Files;
+
+///
+/// Holds information about file handles that are currently open.
+///
+table 8951 "AFS Handle"
+{
+ Access = Public;
+ InherentEntitlements = X;
+ InherentPermissions = X;
+ Extensible = false;
+ DataClassification = SystemMetadata;
+ TableType = Temporary;
+
+ fields
+ {
+ field(1; "Entry No."; Integer)
+ {
+ Caption = 'Entry No.';
+ }
+ field(10; "Handle ID"; Text[50])
+ {
+ Caption = 'HandleId';
+ }
+ field(11; Path; Text[2048])
+ {
+ Caption = 'Path';
+ }
+ field(12; "Client IP"; Text[2048])
+ {
+ Caption = 'ClientIp';
+ DataClassification = EndUserPseudonymousIdentifiers;
+ }
+ field(13; "Open Time"; DateTime)
+ {
+ Caption = 'OpenTime';
+ }
+ field(14; "Last Reconnect Time"; DateTime)
+ {
+ Caption = 'LastReconnectTime';
+ }
+ field(15; "File ID"; Text[50])
+ {
+ Caption = 'FileId';
+ }
+ field(16; "Parent ID"; Text[50])
+ {
+ Caption = 'ParentId';
+ }
+ field(17; "Session ID"; Text[50])
+ {
+ Caption = 'SessionId';
+ }
+ field(20; "Next Marker"; Text[2048])
+ {
+ Caption = 'NextMarker';
+ }
+ }
+
+ keys
+ {
+ key(PK; "Entry No.")
+ {
+ Clustered = true;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/System Application/App/Azure File Services API/src/AFSOperationPayload.Codeunit.al b/src/System Application/App/Azure File Services API/src/AFSOperationPayload.Codeunit.al
new file mode 100644
index 0000000000..d17bdcfa64
--- /dev/null
+++ b/src/System Application/App/Azure File Services API/src/AFSOperationPayload.Codeunit.al
@@ -0,0 +1,193 @@
+// ------------------------------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+// ------------------------------------------------------------------------------------------------
+
+namespace System.Azure.Storage.Files;
+
+using System.Azure.Storage;
+
+codeunit 8952 "AFS Operation Payload"
+{
+ Access = Internal;
+ InherentEntitlements = X;
+ InherentPermissions = X;
+
+ var
+ [NonDebuggable]
+ ContentHeaders: Dictionary of [Text, Text];
+ [NonDebuggable]
+ RequestHeaders: Dictionary of [Text, Text];
+ [NonDebuggable]
+ UriParameters: Dictionary of [Text, Text];
+
+ Authorization: Interface "Storage Service Authorization";
+ ApiVersion: Enum "Storage Service API Version";
+ [NonDebuggable]
+ StorageBaseUrl: Text;
+ [NonDebuggable]
+ StorageAccountName: Text;
+ [NonDebuggable]
+ FileShareName: Text;
+ [NonDebuggable]
+ Path: Text;
+
+ AFSOperation: Enum "AFS Operation";
+
+ procedure GetAuthorization(): Interface "Storage Service Authorization"
+ begin
+ exit(Authorization);
+ end;
+
+ procedure SetAuthorization(StorageServiceAuthorization: Interface "Storage Service Authorization")
+ begin
+ Authorization := StorageServiceAuthorization;
+ end;
+
+ [NonDebuggable]
+ procedure SetOperation(NewOperation: Enum "AFS Operation")
+ begin
+ AFSOperation := NewOperation;
+
+ // Clear state
+ Clear(ContentHeaders);
+ Clear(RequestHeaders);
+ Clear(UriParameters);
+ end;
+
+ procedure GetApiVersion(): Enum "Storage Service API Version"
+ begin
+ exit(ApiVersion);
+ end;
+
+ procedure SetApiVersion(StorageServiceApiVersion: Enum "Storage Service API Version");
+ begin
+ ApiVersion := StorageServiceApiVersion;
+ end;
+
+ [NonDebuggable]
+ procedure GetStorageAccountName(): Text
+ begin
+ exit(StorageAccountName);
+ end;
+
+ [NonDebuggable]
+ procedure SetStorageAccountName(StorageAccount: Text);
+ begin
+ StorageAccountName := StorageAccount;
+ end;
+
+ [NonDebuggable]
+ procedure GetFileShareName(): Text
+ begin
+ exit(FileShareName);
+ end;
+
+ [NonDebuggable]
+ procedure SetFileShareName(FileShare: Text);
+ begin
+ FileShareName := FileShare;
+ end;
+
+ [NonDebuggable]
+ procedure GetPath(): Text
+ begin
+ exit(Path);
+ end;
+
+ [NonDebuggable]
+ procedure SetPath(NewPath: Text);
+ begin
+ Path := NewPath;
+ end;
+
+ [NonDebuggable]
+ procedure SetBaseUrl(BaseUrl: Text)
+ begin
+ StorageBaseUrl := BaseUrl;
+ end;
+
+ procedure GetOperation(): Enum "AFS Operation"
+ begin
+ exit(AFSOperation);
+ end;
+
+ [NonDebuggable]
+ procedure GetRequestHeaders(): Dictionary of [Text, Text]
+ begin
+ exit(RequestHeaders);
+ end;
+
+ [NonDebuggable]
+ procedure GetContentHeaders(): Dictionary of [Text, Text]
+ begin
+ exit(ContentHeaders);
+ end;
+
+ [NonDebuggable]
+ procedure Initialize(StorageAccount: Text; FileShare: Text; PathText: Text; StorageServiceAuthorization: Interface "Storage Service Authorization"; StorageServiceAPIVersion: Enum "Storage Service API Version")
+ begin
+ StorageAccountName := StorageAccount;
+ ApiVersion := StorageServiceAPIVersion;
+ Authorization := StorageServiceAuthorization;
+ FileShareName := FileShare;
+ Path := PathText;
+
+ Clear(StorageBaseUrl);
+ Clear(UriParameters);
+ Clear(RequestHeaders);
+ Clear(ContentHeaders);
+ Clear(AFSOperation);
+ end;
+
+ ///
+ /// Creates the Uri for this object, based on given values
+ ///
+ /// An Uri (as Text) for this API Operation
+ [NonDebuggable]
+ internal procedure ConstructUri(): Text
+ var
+ AFSURIHelper: Codeunit "AFS URI Helper";
+ begin
+ AFSURIHelper.SetOptionalUriParameter(UriParameters);
+ exit(AFSURIHelper.ConstructUri(StorageBaseUrl, StorageAccountName, FileShareName, Path, AFSOperation));
+ end;
+
+ [NonDebuggable]
+ procedure AddRequestHeader(HeaderKey: Text; HeaderValue: Text)
+ begin
+ if RequestHeaders.Remove(HeaderKey) then;
+ RequestHeaders.Add(HeaderKey, HeaderValue);
+ end;
+
+ [NonDebuggable]
+ procedure AddContentHeader(HeaderKey: Text; HeaderValue: Text)
+ begin
+ if ContentHeaders.Remove(HeaderKey) then;
+ ContentHeaders.Add(HeaderKey, HeaderValue);
+ end;
+
+ [NonDebuggable]
+ procedure AddUriParameter(ParameterKey: Text; ParameterValue: Text)
+ begin
+ UriParameters.Remove(ParameterKey);
+ UriParameters.Add(ParameterKey, ParameterValue);
+ end;
+
+ [NonDebuggable]
+ procedure SetOptionalParameters(AFSOptionalParameters: Codeunit "AFS Optional Parameters")
+ var
+ Optionals: Dictionary of [Text, Text];
+ OptionalParameterKey: Text;
+ begin
+ // Add request headers
+ Optionals := AFSOptionalParameters.GetRequestHeaders();
+ foreach OptionalParameterKey in Optionals.Keys() do
+ AddRequestHeader(OptionalParameterKey, Optionals.Get(OptionalParameterKey));
+
+ // Add URI parameters
+ Optionals := AFSOptionalParameters.GetParameters();
+ foreach OptionalParameterKey in Optionals.Keys() do
+ AddUriParameter(OptionalParameterKey, Optionals.Get(OptionalParameterKey));
+ end;
+}
\ No newline at end of file
diff --git a/src/System Application/App/Azure File Services API/src/AFSOperationResponse.Codeunit.al b/src/System Application/App/Azure File Services API/src/AFSOperationResponse.Codeunit.al
new file mode 100644
index 0000000000..11c1b024b7
--- /dev/null
+++ b/src/System Application/App/Azure File Services API/src/AFSOperationResponse.Codeunit.al
@@ -0,0 +1,93 @@
+// ------------------------------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+// ------------------------------------------------------------------------------------------------
+
+namespace System.Azure.Storage.Files;
+
+///
+/// Stores the response of an AFS client operation.
+///
+codeunit 8959 "AFS Operation Response"
+{
+ Access = Public;
+ InherentEntitlements = X;
+ InherentPermissions = X;
+
+ ///
+ /// Checks whether the operation was successful.
+ ///
+ /// True if the operation was successful; otherwise - false.
+ procedure IsSuccessful(): Boolean
+ begin
+ exit(HttpResponseMessage.IsSuccessStatusCode);
+ end;
+
+ ///
+ /// Gets the error (if any) of the response.
+ ///
+ /// Text representation of the error that occurred during the operation.
+ procedure GetError(): Text
+ begin
+ exit(ResponseError);
+ end;
+
+ ///
+ /// Gets the HttpHeaders (if any) of the response.
+ ///
+ /// HttpHeaders of the response.
+ procedure GetHeaders(): HttpHeaders
+ begin
+ exit(HttpResponseMessage.Headers());
+ end;
+
+ internal procedure SetError(Error: Text)
+ begin
+ ResponseError := Error;
+ end;
+
+ ///
+ /// Gets the result of a AFS client operation as text,
+ ///
+ /// The content of the response.
+ [NonDebuggable]
+ [TryFunction]
+ internal procedure GetResultAsText(var Result: Text);
+ begin
+ HttpResponseMessage.Content.ReadAs(Result);
+ end;
+
+ ///
+ /// Gets the result of a AFS client operation as stream,
+ ///
+ /// The content of the response.
+ [NonDebuggable]
+ [TryFunction]
+ internal procedure GetResultAsStream(var ResultInStream: InStream)
+ begin
+ HttpResponseMessage.Content.ReadAs(ResultInStream);
+ end;
+
+ [NonDebuggable]
+ internal procedure SetHttpResponse(NewHttpResponseMessage: HttpResponseMessage)
+ begin
+ HttpResponseMessage := NewHttpResponseMessage;
+ end;
+
+ [NonDebuggable]
+ internal procedure GetHeaderValueFromResponseHeaders(HeaderName: Text): Text
+ var
+ Headers: HttpHeaders;
+ Values: array[100] of Text;
+ begin
+ Headers := HttpResponseMessage.Headers;
+ if not Headers.GetValues(HeaderName, Values) then
+ exit('');
+ exit(Values[1]);
+ end;
+
+ var
+ [NonDebuggable]
+ HttpResponseMessage: HttpResponseMessage;
+ ResponseError: Text;
+}
\ No newline at end of file
diff --git a/src/System Application/App/Azure File Services API/src/AFSOptionalParameters.Codeunit.al b/src/System Application/App/Azure File Services API/src/AFSOptionalParameters.Codeunit.al
new file mode 100644
index 0000000000..09214c9850
--- /dev/null
+++ b/src/System Application/App/Azure File Services API/src/AFSOptionalParameters.Codeunit.al
@@ -0,0 +1,415 @@
+// ------------------------------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+// ------------------------------------------------------------------------------------------------
+
+namespace System.Azure.Storage.Files;
+
+///
+/// Holds procedures to format headers and parameters to be used in requests.
+///
+codeunit 8956 "AFS Optional Parameters"
+{
+ Access = Public;
+ InherentEntitlements = X;
+ InherentPermissions = X;
+
+ var
+ AFSFormatHelper: Codeunit "AFS Format Helper";
+ RequestHeaders: Dictionary of [Text, Text];
+ Parameters: Dictionary of [Text, Text];
+
+ ///
+ /// Sets the value for 'x-ms-range' HttpHeader for a request.
+ ///
+ /// Integer value specifying the Bytes start range value
+ /// Integer value specifying the Bytes end range value
+ procedure Range(BytesStartValue: Integer; BytesEndValue: Integer)
+ var
+ RangeBytesLbl: Label 'bytes=%1-%2', Comment = '%1 = Start Range; %2 = End Range', Locked = true;
+ begin
+ SetRequestHeader('x-ms-range', StrSubstNo(RangeBytesLbl, BytesStartValue, BytesEndValue));
+ end;
+
+ ///
+ /// Sets the value for 'x-ms-write' HttpHeader for a request.
+ ///
+ /// Enum "AFS Write" value specifying the HttpHeader value
+ procedure Write("Value": Enum "AFS Write")
+ begin
+ SetRequestHeader('x-ms-write', Format("Value"));
+ end;
+
+ ///
+ /// Sets the value for 'x-ms-lease-id' HttpHeader for a request.
+ ///
+ /// Guid value specifying the LeaseID
+ procedure LeaseId("Value": Guid)
+ begin
+ SetRequestHeader('x-ms-lease-id', AFSFormatHelper.RemoveCurlyBracketsFromString(Format("Value").ToLower()));
+ end;
+
+ ///
+ /// Sets the value for 'x-ms-lease-action' HttpHeader for a request.
+ ///
+ /// Enum "AFS Lease Action" value specifying the LeaseAction
+ internal procedure LeaseAction("Value": Enum "AFS Lease Action")
+ begin
+ SetRequestHeader('x-ms-lease-action', Format("Value"));
+ end;
+
+ ///
+ /// Sets the value for 'x-ms-lease-duration' HttpHeader for a request.
+ ///
+ /// Integer value specifying the LeaseDuration in seconds
+ procedure LeaseDuration("Value": Integer)
+ begin
+ SetRequestHeader('x-ms-lease-duration', Format("Value"));
+ end;
+
+ ///
+ /// Sets the value for 'x-ms-proposed-lease-id' HttpHeader for a request.
+ ///
+ /// Guid value specifying the ProposedLeaseId in seconds
+ procedure ProposedLeaseId("Value": Guid)
+ begin
+ SetRequestHeader('x-ms-proposed-lease-id', AFSFormatHelper.RemoveCurlyBracketsFromString(Format("Value").ToLower()));
+ end;
+
+ ///
+ /// Sets the value for 'x-ms-client-request-id' HttpHeader for a request.
+ ///
+ /// Text value specifying the HttpHeader value
+ procedure ClientRequestId("Value": Text)
+ begin
+ SetRequestHeader('x-ms-client-request-id', "Value");
+ end;
+
+ ///
+ /// Sets the value for 'x-ms-file-last-write-time' HttpHeader for a request.
+ ///
+ /// Enum "AFS File Last Write Time" value specifying the HttpHeader value
+ procedure FileLastWriteTime("Value": Enum "AFS File Last Write Time")
+ begin
+ SetRequestHeader('x-ms-file-last-write-time', Format("Value"));
+ end;
+
+ ///
+ /// Sets the value for 'x-ms-file-request-intent' HttpHeader for a request, 'backup' is an acceptable value.
+ ///
+ /// Text value specifying the HttpHeader value
+ procedure FileRequestIntent("Value": Text)
+ begin
+ SetRequestHeader('x-ms-file-request-intent', "Value");
+ end;
+
+ ///
+ /// Sets the value for 'x-ms-file-permission' HttpHeader for a request.
+ ///
+ /// Text value specifying the HttpHeader value
+ procedure FilePermission("Value": Text)
+ begin
+ SetRequestHeader('x-ms-file-permission', "Value");
+ end;
+
+ ///
+ /// Sets the value for 'x-ms-file-permission-key' HttpHeader for a request.
+ ///
+ /// Text value specifying the HttpHeader value
+ procedure FilePermissionKey("Value": Text)
+ begin
+ SetRequestHeader('x-ms-file-permission-key', "Value");
+ end;
+
+ ///
+ /// Sets the value for 'x-ms-file-attributes' HttpHeader for a request.
+ ///
+ /// Text value specifying the HttpHeader value
+ procedure FileAttributes("Value": List of [Enum "AFS File Attribute"])
+ var
+ FileAttribute: Enum "AFS File Attribute";
+ ValueText: Text;
+ begin
+ foreach FileAttribute in "Value" do
+ ValueText += Format(FileAttribute) + ',';
+ ValueText := ValueText.TrimEnd(',');
+
+ SetRequestHeader('x-ms-file-attributes', ValueText);
+ end;
+
+ ///
+ /// Sets the value for 'x-ms-file-creation-time' HttpHeader for a request.
+ ///
+ /// Datetime of the file creation
+ procedure FileCreationTime("Value": DateTime)
+ begin
+ SetParameter('x-ms-file-creation-time', AFSFormatHelper.GetRfc1123DateTime("Value"));
+ end;
+
+ ///
+ /// Sets the value for 'x-ms-file-last-write-time' HttpHeader for a request.
+ ///
+ /// Datetime of the file last write time
+ procedure FileLastWriteTime("Value": DateTime)
+ begin
+ SetParameter('x-ms-file-last-write-time', AFSFormatHelper.GetRfc1123DateTime("Value"));
+ end;
+
+ ///
+ /// Sets the value for 'x-ms-file-change-time' HttpHeader for a request.
+ ///
+ /// Datetime of the file last change time
+ procedure FileChangeTime("Value": DateTime)
+ begin
+ SetParameter('x-ms-file-change-time', AFSFormatHelper.GetRfc1123DateTime("Value"));
+ end;
+
+ ///
+ /// Sets the value for 'x-ms-meta-name' HttpHeader for a request. name should adhere to C# identifiers naming convention.
+ ///
+ /// Text value specifying the metadata name key
+ /// Text value specifying the HttpHeader value
+ procedure Meta("Key": Text; "Value": Text)
+ begin
+ SetRequestHeader('x-ms-meta-' + "Key", "Value");
+ end;
+
+ ///
+ /// Sets the value for 'x-ms-file-permission-copy-mode' HttpHeader for a request.
+ ///
+ /// Enum "AFS File Permission Copy Mode" value specifying the HttpHeader value
+ procedure FilePermissionCopyMode("Value": Enum "AFS File Permission Copy Mode")
+ begin
+ SetRequestHeader('x-ms-file-permission-copy-mode', Format("Value"));
+ end;
+
+ ///
+ /// Sets the value for 'x-ms-copy-source' HttpHeader for a request.
+ ///
+ /// Text value specifying the HttpHeader value
+ procedure CopySource("Value": Text)
+ begin
+ SetRequestHeader('x-ms-copy-source', "Value");
+ end;
+
+ ///
+ /// Sets the value for 'x-ms-allow-trailing-dot' HttpHeader for a request.
+ ///
+ /// Boolean value specifying the HttpHeader value
+ procedure AllowTrailingDot("Value": Boolean)
+ var
+ ValueText: Text;
+ begin
+ // Set as text, because otherwise it might give different formatted values based on language locale
+ ValueText := ConvertBooleanToText("Value");
+
+ SetRequestHeader('x-ms-allow-trailing-dot', ValueText);
+ end;
+
+ ///
+ /// Sets the value for 'x-ms-file-rename-replace-if-exists' HttpHeader for a request.
+ ///
+ /// Boolean value specifying the HttpHeader value
+ procedure FileRenameReplaceIfExists("Value": Boolean)
+ var
+ ValueText: Text;
+ begin
+ // Set as text, because otherwise it might give different formatted values based on language locale
+ ValueText := ConvertBooleanToText("Value");
+
+ SetRequestHeader('x-ms-file-rename-replace-if-exists', ValueText);
+ end;
+
+ ///
+ /// Sets the value for 'x-ms-file-rename-ignore-readonly' HttpHeader for a request.
+ ///
+ /// Boolean value specifying the HttpHeader value
+ procedure FileRenameIgnoreReadOnly("Value": Boolean)
+ var
+ ValueText: Text;
+ begin
+ // Set as text, because otherwise it might give different formatted values based on language locale
+ ValueText := ConvertBooleanToText("Value");
+
+ SetRequestHeader('x-ms-file-rename-ignore-readonly', ValueText);
+ end;
+
+ ///
+ /// Sets the value for 'x-ms-source-lease-id' HttpHeader for a request.
+ ///
+ /// Guid value specifying the SourceLeaseID
+ procedure SourceLeaseId("Value": Guid)
+ begin
+ SetRequestHeader('x-ms-source-lease-id', AFSFormatHelper.RemoveCurlyBracketsFromString(Format("Value").ToLower()));
+ end;
+
+ ///
+ /// Sets the value for 'x-ms-destination-lease-id' HttpHeader for a request.
+ ///
+ /// Guid value specifying the DestinationLeaseID
+ procedure DestinationLeaseId("Value": Guid)
+ begin
+ SetRequestHeader('x-ms-destination-lease-id', AFSFormatHelper.RemoveCurlyBracketsFromString(Format("Value").ToLower()));
+ end;
+
+ ///
+ /// Sets the value for 'x-ms-file-copy-ignore-readonly' HttpHeader for a request.
+ ///
+ /// Boolean value specifying the HttpHeader value
+ procedure FileCopyIgnoreReadOnly("Value": Boolean)
+ var
+ ValueText: Text;
+ begin
+ // Set as text, because otherwise it might give different formatted values based on language locale
+ ValueText := ConvertBooleanToText("Value");
+
+ SetRequestHeader('x-ms-file-copy-ignore-readonly', ValueText);
+ end;
+
+ ///
+ /// Sets the value for 'x-ms-file-copy-set-archive' HttpHeader for a request.
+ ///
+ /// Boolean value specifying the HttpHeader value
+ procedure FileCopySetArchive("Value": Boolean)
+ var
+ ValueText: Text;
+ begin
+ // Set as text, because otherwise it might give different formatted values based on language locale
+ ValueText := ConvertBooleanToText("Value");
+
+ SetRequestHeader('x-ms-file-copy-set-archive', ValueText);
+ end;
+
+ ///
+ /// Sets the value for 'x-ms-file-extended-info' HttpHeader for a request.
+ ///
+ /// Boolean value specifying the HttpHeader value
+ procedure FileExtendedInfo("Value": Boolean)
+ var
+ ValueText: Text;
+ begin
+ // Set as text, because otherwise it might give different formatted values based on language locale
+ ValueText := ConvertBooleanToText("Value");
+
+ SetRequestHeader('x-ms-file-extended-info', ValueText);
+ end;
+
+ ///
+ /// Sets the value for 'x-ms-range-get-content-md5' HttpHeader for a request.
+ ///
+ /// Boolean value specifying the HttpHeader value
+ procedure RangeGetContentMD5("Value": Boolean)
+ var
+ ValueText: Text;
+ begin
+ // Set as text, because otherwise it might give different formatted values based on language locale
+ ValueText := ConvertBooleanToText("Value");
+
+ SetRequestHeader('x-ms-range-get-content-md5', ValueText);
+ end;
+
+ ///
+ /// Sets the value for 'x-ms-recursive' HttpHeader for a request.
+ ///
+ /// Boolean value specifying the HttpHeader value
+ procedure Recursive("Value": Boolean)
+ var
+ ValueText: Text;
+ begin
+ // Set as text, because otherwise it might give different formatted values based on language locale
+ ValueText := ConvertBooleanToText("Value");
+
+ SetRequestHeader('x-ms-recursive', ValueText);
+ end;
+
+ ///
+ /// Sets the optional timeout value for the request.
+ ///
+ /// Timeout in seconds. Most operations have a max. limit of 30 seconds. For more Information see: https://go.microsoft.com/fwlink/?linkid=2210591
+ procedure Timeout("Value": Integer)
+ begin
+ SetParameter('timeout', Format("Value"));
+ end;
+
+ ///
+ /// Filters the results to return only blobs whose names begin with the specified prefix.
+ ///
+ /// Prefix to search for
+ procedure Prefix("Value": Text)
+ begin
+ SetParameter('prefix', "Value");
+ end;
+
+ ///
+ /// Specifies the share snapshot to query for the list of files and directories.
+ ///
+ /// Datetime of the snapshot to query
+ procedure ShareSnapshot("Value": DateTime)
+ begin
+ SetParameter('sharesnapshot', AFSFormatHelper.GetRfc1123DateTime("Value"));
+ end;
+
+ ///
+ /// A string value that identifies the portion of the list to be returned with the next list operation.
+ ///
+ /// Text marker that was returned in previous operation
+ procedure Marker("Value": Text)
+ begin
+ SetParameter('marker', "Value");
+ end;
+
+ ///
+ /// Specifies the maximum number of files or directories to return
+ ///
+ /// Max. number of results to return. Must be positive, must not be greater than 5000
+ procedure MaxResults("Value": Integer)
+ begin
+ SetParameter('maxresults', Format("Value"));
+ end;
+
+ ///
+ /// Specifies one or more properties to include in the response.
+ ///
+ /// List of properties to include.
+ procedure Include("Value": List of [Enum "AFS Properties"])
+ var
+ Property: Enum "AFS Properties";
+ ValueText: Text;
+ begin
+ foreach Property in "Value" do
+ ValueText += Format(Property) + ',';
+ ValueText := ValueText.TrimEnd(',');
+
+ SetParameter('include', ValueText);
+ end;
+
+ local procedure SetRequestHeader(Header: Text; HeaderValue: Text)
+ begin
+ RequestHeaders.Remove(Header);
+ RequestHeaders.Add(Header, HeaderValue);
+ end;
+
+ internal procedure GetRequestHeaders(): Dictionary of [Text, Text]
+ begin
+ exit(RequestHeaders);
+ end;
+
+ local procedure SetParameter(Header: Text; HeaderValue: Text)
+ begin
+ Parameters.Remove(Header);
+ Parameters.Add(Header, HeaderValue);
+ end;
+
+ local procedure ConvertBooleanToText("Value": Boolean) ValueText: Text
+ begin
+ if "Value" then
+ ValueText := 'true'
+ else
+ ValueText := 'false';
+ end;
+
+ internal procedure GetParameters(): Dictionary of [Text, Text]
+ begin
+ exit(Parameters);
+ end;
+}
\ No newline at end of file
diff --git a/src/System Application/App/Azure File Services API/src/API Enums/AFSFileAttribute.Enum.al b/src/System Application/App/Azure File Services API/src/API Enums/AFSFileAttribute.Enum.al
new file mode 100644
index 0000000000..c82ec398ed
--- /dev/null
+++ b/src/System Application/App/Azure File Services API/src/API Enums/AFSFileAttribute.Enum.al
@@ -0,0 +1,80 @@
+// ------------------------------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+// ------------------------------------------------------------------------------------------------
+
+namespace System.Azure.Storage.Files;
+
+///
+/// Describes possible values for file attributes.
+/// For the list of possible values see: https://learn.microsoft.com/en-us/rest/api/storageservices/create-file#file-system-attributes
+///
+enum 8955 "AFS File Attribute"
+{
+ Access = Public;
+ Extensible = false;
+
+ ///
+ /// Indicates that the file is read-only.
+ ///
+ value(0; "Read Only")
+ {
+ Caption = 'ReadOnly', Locked = true;
+ }
+ ///
+ /// Indicates that the file is hidden, and thus is not included in an ordinary directory listing.
+ ///
+ value(1; Hidden)
+ {
+ Caption = 'Hidden', Locked = true;
+ }
+ ///
+ /// Indicates that the file is used by the operating system.
+ ///
+ value(2; System)
+ {
+ Caption = 'System', Locked = true;
+ }
+ ///
+ /// Indicates that the there are no attributes.
+ ///
+ value(3; "None")
+ {
+ Caption = 'None', Locked = true;
+ }
+ ///
+ /// Indicates that the file is an archive file. Applications use this attribute to mark files for backup or removal.
+ ///
+ value(4; Archive)
+ {
+ Caption = 'Archive', Locked = true;
+ }
+ ///
+ /// Indicates that the file is temporary. File systems attempt to keep all of the data in memory for quicker access rather than flushing the data back to mass storage. A temporary file should be deleted by the application as soon as it is no longer needed.
+ ///
+ value(5; "Temporary")
+ {
+ Caption = 'Temporary', Locked = true;
+ }
+ ///
+ /// Indicates that the file is offline. The data of the file is not immediately available.
+ ///
+ value(6; Offline)
+ {
+ Caption = 'Offline', Locked = true;
+ }
+ ///
+ /// Indicates that the file will not be indexed by the content indexing service.
+ ///
+ value(7; "Not Content Indexed")
+ {
+ Caption = 'NotContentIndexed', Locked = true;
+ }
+ ///
+ /// Indicates that the file should not be read by background data integrity scanner.
+ ///
+ value(8; "No Scrub Data")
+ {
+ Caption = 'NoScrubData', Locked = true;
+ }
+}
\ No newline at end of file
diff --git a/src/System Application/App/Azure File Services API/src/API Enums/AFSFileLastWriteTime.Enum.al b/src/System Application/App/Azure File Services API/src/API Enums/AFSFileLastWriteTime.Enum.al
new file mode 100644
index 0000000000..afbe5b8a10
--- /dev/null
+++ b/src/System Application/App/Azure File Services API/src/API Enums/AFSFileLastWriteTime.Enum.al
@@ -0,0 +1,30 @@
+// ------------------------------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+// ------------------------------------------------------------------------------------------------
+
+namespace System.Azure.Storage.Files;
+
+///
+/// Describes possible values for File Last Write Time header.
+///
+enum 8952 "AFS File Last Write Time"
+{
+ Access = Public;
+ Extensible = false;
+
+ ///
+ /// The file last write time is set to the current time.
+ ///
+ value(0; Now)
+ {
+ Caption = 'now', Locked = true;
+ }
+ ///
+ /// The file last write time is preserved.
+ ///
+ value(1; Preserve)
+ {
+ Caption = 'preserve', Locked = true;
+ }
+}
\ No newline at end of file
diff --git a/src/System Application/App/Azure File Services API/src/API Enums/AFSFilePermissionCopyMode.Enum.al b/src/System Application/App/Azure File Services API/src/API Enums/AFSFilePermissionCopyMode.Enum.al
new file mode 100644
index 0000000000..6393aff47a
--- /dev/null
+++ b/src/System Application/App/Azure File Services API/src/API Enums/AFSFilePermissionCopyMode.Enum.al
@@ -0,0 +1,30 @@
+// ------------------------------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+// ------------------------------------------------------------------------------------------------
+
+namespace System.Azure.Storage.Files;
+
+///
+/// Describes possible values for File Permission Copy Mode header.
+///
+enum 8956 "AFS File Permission Copy Mode"
+{
+ Access = Public;
+ Extensible = false;
+
+ ///
+ /// Copy file permissions from source to destination.
+ ///
+ value(0; Source)
+ {
+ Caption = 'source', Locked = true;
+ }
+ ///
+ /// Override file permissions on destination with permissions from the request.
+ ///
+ value(1; Override)
+ {
+ Caption = 'override', Locked = true;
+ }
+}
\ No newline at end of file
diff --git a/src/System Application/App/Azure File Services API/src/API Enums/AFSFileResourceType.Enum.al b/src/System Application/App/Azure File Services API/src/API Enums/AFSFileResourceType.Enum.al
new file mode 100644
index 0000000000..d8744418ad
--- /dev/null
+++ b/src/System Application/App/Azure File Services API/src/API Enums/AFSFileResourceType.Enum.al
@@ -0,0 +1,30 @@
+// ------------------------------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+// ------------------------------------------------------------------------------------------------
+
+namespace System.Azure.Storage.Files;
+
+///
+/// Indicator of what type the resource is.
+///
+enum 8954 "AFS File Resource Type"
+{
+ Access = Public;
+ Extensible = false;
+
+ ///
+ /// Indicates entry is of file type.
+ ///
+ value(0; File)
+ {
+ Caption = 'File', Locked = true;
+ }
+ ///
+ /// Indicates entry is of directory type.
+ ///
+ value(1; Directory)
+ {
+ Caption = 'Directory', Locked = true;
+ }
+}
\ No newline at end of file
diff --git a/src/System Application/App/Azure File Services API/src/API Enums/AFSLeaseAction.Enum.al b/src/System Application/App/Azure File Services API/src/API Enums/AFSLeaseAction.Enum.al
new file mode 100644
index 0000000000..046a6d9f71
--- /dev/null
+++ b/src/System Application/App/Azure File Services API/src/API Enums/AFSLeaseAction.Enum.al
@@ -0,0 +1,56 @@
+// ------------------------------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+// ------------------------------------------------------------------------------------------------
+
+namespace System.Azure.Storage.Files;
+
+///
+/// Defines possible actions for opearations relating to leases.
+/// See more: https://learn.microsoft.com/en-us/rest/api/storageservices/lease-file
+///
+enum 8957 "AFS Lease Action"
+{
+ Access = Internal;
+ Extensible = false;
+
+ ///
+ /// Requests a new lease.
+ ///
+ value(0; Acquire)
+ {
+ Caption = 'acquire', Locked = true;
+ }
+
+ ///
+ /// Renews the lease.
+ ///
+ value(1; Renew)
+ {
+ Caption = 'renew', Locked = true;
+ }
+
+ ///
+ /// Changes the lease ID of an active lease.
+ ///
+ value(2; Change)
+ {
+ Caption = 'change', Locked = true;
+ }
+
+ ///
+ /// Releases the lease
+ ///
+ value(3; Release)
+ {
+ Caption = 'release', Locked = true;
+ }
+
+ ///
+ /// Breaks the lease, if the file has an active lease
+ ///
+ value(4; Break)
+ {
+ Caption = 'break', Locked = true;
+ }
+}
\ No newline at end of file
diff --git a/src/System Application/App/Azure File Services API/src/API Enums/AFSOperation.Enum.al b/src/System Application/App/Azure File Services API/src/API Enums/AFSOperation.Enum.al
new file mode 100644
index 0000000000..0da3aac5e6
--- /dev/null
+++ b/src/System Application/App/Azure File Services API/src/API Enums/AFSOperation.Enum.al
@@ -0,0 +1,197 @@
+// ------------------------------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+// ------------------------------------------------------------------------------------------------
+
+namespace System.Azure.Storage.Files;
+
+///
+/// Defines possible operations to the azure file share API.
+/// Operations are divided into operations on:
+/// - file (https://learn.microsoft.com/en-us/rest/api/storageservices/operations-on-files)
+/// - directory (https://learn.microsoft.com/en-us/rest/api/storageservices/operations-on-directories)
+/// - file share (https://learn.microsoft.com/en-us/rest/api/storageservices/operations-on-shares--file-service-)
+/// - file service (https://learn.microsoft.com/en-us/rest/api/storageservices/operations-on-the-account--file-service-)
+///
+enum 8950 "AFS Operation"
+{
+ Access = Internal;
+ Extensible = false;
+
+ value(0; GetFileServiceProperties)
+ {
+ Caption = 'Get File Service Properties', Locked = true;
+ }
+ value(1; SetFileServiceProperties)
+ {
+ Caption = 'Set File Service Properties', Locked = true;
+ }
+ value(2; PreflightFileRequest)
+ {
+ Caption = 'Preflight File Request', Locked = true;
+ }
+ value(20; ListShares)
+ {
+ Caption = 'List Shares', Locked = true;
+ }
+ value(21; CreateShare)
+ {
+ Caption = 'Create Share', Locked = true;
+ }
+ value(22; SnapshotShare)
+ {
+ Caption = 'Snapshot Share', Locked = true;
+ }
+ value(23; GetShareProperties)
+ {
+ Caption = 'Get Share Properties', Locked = true;
+ }
+ value(24; SetShareProperties)
+ {
+ Caption = 'Set Share Properties', Locked = true;
+ }
+ value(25; GetShareMetadata)
+ {
+ Caption = 'Get Share Metadata', Locked = true;
+ }
+ value(26; SetShareMetadata)
+ {
+ Caption = 'Set Share Metadata', Locked = true;
+ }
+ value(27; DeleteShare)
+ {
+ Caption = 'Delete Share', Locked = true;
+ }
+ value(28; RestoreShare)
+ {
+ Caption = 'Restore Share', Locked = true;
+ }
+ value(29; GetShareACL)
+ {
+ Caption = 'Get Share ACL', Locked = true;
+ }
+ value(30; SetShareACL)
+ {
+ Caption = 'Set Share ACL', Locked = true;
+ }
+ value(31; GetShareStats)
+ {
+ Caption = 'Get Share Stats', Locked = true;
+ }
+ value(32; CreatePermission)
+ {
+ Caption = 'Create Permission', Locked = true;
+ }
+ value(33; GetPermission)
+ {
+ Caption = 'Get Permission', Locked = true;
+ }
+ value(34; LeaseShare)
+ {
+ Caption = 'Lease Share', Locked = true;
+ }
+ value(40; ListDirectory)
+ {
+ Caption = 'List Directory', Locked = true;
+ }
+ value(41; CreateDirectory)
+ {
+ Caption = 'Create Directory', Locked = true;
+ }
+ value(42; GetDirectoryProperties)
+ {
+ Caption = 'Get Directory Properties', Locked = true;
+ }
+ value(43; SetDirectoryProperties)
+ {
+ Caption = 'Set Directory Properties', Locked = true;
+ }
+ value(44; DeleteDirectory)
+ {
+ Caption = 'Delete Directory', Locked = true;
+ }
+ value(45; GetDirectoryMetadata)
+ {
+ Caption = 'Get Directory Metadata', Locked = true;
+ }
+ value(46; SetDirectoryMetadata)
+ {
+ Caption = 'Set Directory Metadata', Locked = true;
+ }
+ value(47; ListDirectoryHandles)
+ {
+ Caption = 'List Directory Handles', Locked = true;
+ }
+ value(48; ForceCloseDirectoryHandles)
+ {
+ Caption = 'Force Close Directory Handles', Locked = true;
+ }
+ value(49; RenameDirectory)
+ {
+ Caption = 'Rename Directory', Locked = true;
+ }
+ value(60; CreateFile)
+ {
+ Caption = 'Create File', Locked = true;
+ }
+ value(61; GetFile)
+ {
+ Caption = 'Get File', Locked = true;
+ }
+ value(62; GetFileProperties)
+ {
+ Caption = 'Get File Properties', Locked = true;
+ }
+ value(63; SetFileProperties)
+ {
+ Caption = 'Set File Properties', Locked = true;
+ }
+ value(64; PutRange)
+ {
+ Caption = 'Put Range', Locked = true;
+ }
+ value(65; PutRangefromURL)
+ {
+ Caption = 'Put Rangefrom URL', Locked = true;
+ }
+ value(66; ListRanges)
+ {
+ Caption = 'List Ranges', Locked = true;
+ }
+ value(67; GetFileMetadata)
+ {
+ Caption = 'Get File Metadata', Locked = true;
+ }
+ value(68; SetFileMetadata)
+ {
+ Caption = 'Set File Metadata', Locked = true;
+ }
+ value(69; DeleteFile)
+ {
+ Caption = 'Delete File', Locked = true;
+ }
+ value(71; CopyFile)
+ {
+ Caption = 'Copy File', Locked = true;
+ }
+ value(72; AbortCopyFile)
+ {
+ Caption = 'Abort Copy File', Locked = true;
+ }
+ value(73; ListFileHandles)
+ {
+ Caption = 'List File Handles', Locked = true;
+ }
+ value(74; ForceCloseFileHandles)
+ {
+ Caption = 'Force Close File Handles', Locked = true;
+ }
+ value(75; LeaseFile)
+ {
+ Caption = 'Lease File', Locked = true;
+ }
+ value(76; RenameFile)
+ {
+ Caption = 'Rename File', Locked = true;
+ }
+}
\ No newline at end of file
diff --git a/src/System Application/App/Azure File Services API/src/API Enums/AFSProperties.Enum.al b/src/System Application/App/Azure File Services API/src/API Enums/AFSProperties.Enum.al
new file mode 100644
index 0000000000..cf0263e7a4
--- /dev/null
+++ b/src/System Application/App/Azure File Services API/src/API Enums/AFSProperties.Enum.al
@@ -0,0 +1,44 @@
+// ------------------------------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+// ------------------------------------------------------------------------------------------------
+
+namespace System.Azure.Storage.Files;
+
+///
+/// Describes possible values of the additional AFS properties.
+///
+enum 8953 "AFS Properties"
+{
+ Access = Public;
+ Extensible = false;
+
+ ///
+ /// Indicates the the Timestamps should be included in the response.
+ ///
+ value(0; Timestamps)
+ {
+ Caption = 'Timestamps', Locked = true;
+ }
+ ///
+ /// Indicates the the ETag should be included in the response.
+ ///
+ value(1; ETag)
+ {
+ Caption = 'ETag', Locked = true;
+ }
+ ///
+ /// Indicates the the Attributes should be included in the response.
+ ///
+ value(2; Attributes)
+ {
+ Caption = 'Attributes', Locked = true;
+ }
+ ///
+ /// Indicates the the PermissionKey should be included in the response.
+ ///
+ value(3; PermissionKey)
+ {
+ Caption = 'PermissionKey', Locked = true;
+ }
+}
\ No newline at end of file
diff --git a/src/System Application/App/Azure File Services API/src/API Enums/AFSWrite.Enum.al b/src/System Application/App/Azure File Services API/src/API Enums/AFSWrite.Enum.al
new file mode 100644
index 0000000000..6786a8ae0a
--- /dev/null
+++ b/src/System Application/App/Azure File Services API/src/API Enums/AFSWrite.Enum.al
@@ -0,0 +1,30 @@
+// ------------------------------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+// ------------------------------------------------------------------------------------------------
+
+namespace System.Azure.Storage.Files;
+
+///
+/// Describes possible values for the Write header.
+///
+enum 8951 "AFS Write"
+{
+ Access = Public;
+ Extensible = false;
+
+ ///
+ /// Indicates that the data should be updated.
+ ///
+ value(0; Update)
+ {
+ Caption = 'update', Locked = true;
+ }
+ ///
+ /// Indicates that the data should be cleared.
+ ///
+ value(1; Clear)
+ {
+ Caption = 'clear', Locked = true;
+ }
+}
\ No newline at end of file
diff --git a/src/System Application/App/Azure File Services API/src/Helper/AFSDirectoryContentHelper.Codeunit.al b/src/System Application/App/Azure File Services API/src/Helper/AFSDirectoryContentHelper.Codeunit.al
new file mode 100644
index 0000000000..77dc798869
--- /dev/null
+++ b/src/System Application/App/Azure File Services API/src/Helper/AFSDirectoryContentHelper.Codeunit.al
@@ -0,0 +1,220 @@
+// ------------------------------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+// ------------------------------------------------------------------------------------------------
+
+namespace System.Azure.Storage.Files;
+
+codeunit 8961 "AFS Directory Content Helper"
+{
+ Access = Internal;
+ InherentEntitlements = X;
+ InherentPermissions = X;
+
+ [NonDebuggable]
+ procedure AddNewEntryFromNode(DirectoryURI: Text; DirectoryPath: Text[2048]; var AFSDirectoryContent: Record "AFS Directory Content"; var Node: XmlNode; XPathName: Text)
+ var
+ AFSHelperLibrary: Codeunit "AFS Helper Library";
+ NameFromXml: Text;
+ OuterXml: Text;
+ ChildNodes: XmlNodeList;
+ PropertiesNode: XmlNode;
+ AttributesNode: XmlNode;
+ PermissionKeyNode: XmlNode;
+ ResourceType: Enum "AFS File Resource Type";
+ begin
+ NameFromXml := AFSHelperLibrary.GetValueFromNode(Node, XPathName);
+ Node.WriteTo(OuterXml);
+ Node.SelectSingleNode('.//Properties', PropertiesNode);
+ ChildNodes := PropertiesNode.AsXmlElement().GetChildNodes();
+ if Node.SelectSingleNode('.//Attributes', AttributesNode) then;
+ if Node.SelectSingleNode('.//PermissionKey', PermissionKeyNode) then;
+ Evaluate(ResourceType, Node.AsXmlElement().Name);
+
+ AddNewEntry(DirectoryURI, DirectoryPath, ResourceType, AFSDirectoryContent, NameFromXml, OuterXml, ChildNodes, AttributesNode, PermissionKeyNode);
+ end;
+
+ [NonDebuggable]
+ procedure AddNewEntry(DirectoryURI: Text; DirectoryPath: Text[2048]; ResourceType: Enum "AFS File Resource Type"; var AFSDirectoryContent: Record "AFS Directory Content"; NameFromXml: Text; OuterXml: Text; ChildNodes: XmlNodeList; AttributesNode: XmlNode; PermissionKeyNode: XmlNode)
+ var
+ OutStream: OutStream;
+ EntryNo: Integer;
+ begin
+ AddParentEntries(DirectoryPath, AFSDirectoryContent);
+
+ EntryNo := GetNextEntryNo(AFSDirectoryContent);
+
+ AFSDirectoryContent.Init();
+ AFSDirectoryContent."Parent Directory" := DirectoryPath;
+ AFSDirectoryContent."Full Name" := AFSDirectoryContent."Parent Directory";
+ if DirectoryPath.EndsWith('/') or (DirectoryPath = '') then
+ AFSDirectoryContent."Full Name" += NameFromXml
+ else
+ AFSDirectoryContent."Full Name" += '/' + NameFromXml;
+ AFSDirectoryContent.Level := GetLevel(AFSDirectoryContent."Full Name");
+ AFSDirectoryContent."Resource Type" := ResourceType;
+ AFSDirectoryContent.Name := GetName(NameFromXml);
+ if DirectoryURI.EndsWith('/') then
+ AFSDirectoryContent.URI := DirectoryURI + GetName(NameFromXml)
+ else
+ AFSDirectoryContent.URI := DirectoryURI + '/' + GetName(NameFromXml);
+
+ SetPropertyFields(AFSDirectoryContent, ChildNodes);
+ SetAttributesFields(AFSDirectoryContent, AttributesNode);
+ SetPermissionKeyField(AFSDirectoryContent, PermissionKeyNode);
+
+ AFSDirectoryContent."XML Value".CreateOutStream(OutStream);
+ OutStream.Write(OuterXml);
+
+ AFSDirectoryContent."Entry No." := EntryNo;
+ AFSDirectoryContent.Insert(true);
+ end;
+
+ [NonDebuggable]
+ local procedure AddParentEntries(DirectoryPath: Text; var AFSDirectoryContent: Record "AFS Directory Content")
+ var
+ FullNameTooLongErr: Label 'The full name (%1) of the directory is too long (over %2 characters).', Comment = '%1 - full name, %2 - max length';
+ ParentEntries: List of [Text];
+ CurrentParent, ParentEntryFullName, ParentEntryName : Text[2048];
+ Level, EntryNo : Integer;
+ begin
+ // Check if the entry has parents: the DirectoryPath will be something like folder1/folder2.
+ // For every parent folder, add a parent entry.
+ // The list of node that comes from sorted by full name, so there is no need for extra re-arrangement.
+
+ if DirectoryPath = '' then
+ exit;
+
+ CurrentParent := ''; // used to accumulate the full names of the entries
+
+ ParentEntries := DirectoryPath.Split('/');
+
+ for Level := 1 to ParentEntries.Count() do begin
+ ParentEntryName := CopyStr(ParentEntries.Get(Level), 1, MaxStrLen(AFSDirectoryContent.Name));
+ ParentEntryFullName := CopyStr(CurrentParent + ParentEntryName, 1, MaxStrLen(AFSDirectoryContent.Name));
+
+ // Only create the parent entry if it doesn't exist already.
+ // The full name should be unique.
+ AFSDirectoryContent.SetRange("Full Name", ParentEntryFullName);
+ if not AFSDirectoryContent.FindLast() and (ParentEntryName <> '') then begin
+ EntryNo := GetNextEntryNo(AFSDirectoryContent);
+
+ AFSDirectoryContent.Init();
+ AFSDirectoryContent.Level := Level - 1; // Levels start from 0 to be used for indentation
+ AFSDirectoryContent.Name := ParentEntryName;
+ AFSDirectoryContent."Full Name" := ParentEntryFullName;
+ AFSDirectoryContent."Parent Directory" := CurrentParent;
+ AFSDirectoryContent."Resource Type" := AFSDirectoryContent."Resource Type"::Directory;
+
+ AFSDirectoryContent."Entry No." := EntryNo;
+ AFSDirectoryContent.Insert(true);
+ end;
+
+ if StrLen(AFSDirectoryContent."Full Name" + '/') > 2048 then
+ Error(FullNameTooLongErr, AFSDirectoryContent."Full Name", MaxStrLen(AFSDirectoryContent."Full Name"));
+ CurrentParent := CopyStr(AFSDirectoryContent."Full Name" + '/', 1, MaxStrLen(AFSDirectoryContent.Name));
+ end;
+ end;
+
+ [NonDebuggable]
+ local procedure SetPropertyFields(var AFSDirectoryContent: Record "AFS Directory Content"; ChildNodes: XmlNodeList)
+ var
+ AFSFormatHelper: Codeunit "AFS Format Helper";
+ AFSHelperLibrary: Codeunit "AFS Helper Library";
+ RecordRef: RecordRef;
+ FieldRef: FieldRef;
+ ChildNode: XmlNode;
+ PropertyName: Text;
+ PropertyValue: Text;
+ FldNo: Integer;
+ begin
+ if ChildNodes.Count = 0 then
+ exit;
+
+ foreach ChildNode in ChildNodes do begin
+ PropertyName := ChildNode.AsXmlElement().Name;
+ PropertyValue := ChildNode.AsXmlElement().InnerText;
+ if PropertyValue <> '' then begin
+ RecordRef.GetTable(AFSDirectoryContent);
+ if AFSHelperLibrary.GetFieldByCaption(Database::"AFS Directory Content", PropertyName, FldNo) then begin
+ FieldRef := RecordRef.Field(FldNo);
+ case FieldRef.Type of
+ FieldRef.Type::DateTime:
+ FieldRef.Value := AFSFormatHelper.ConvertToDateTime(PropertyValue);
+ FieldRef.Type::Integer:
+ FieldRef.Value := AFSFormatHelper.ConvertToInteger(PropertyValue);
+ FieldRef.Type::Option:
+ FieldRef.Value := AFSFormatHelper.ConvertToEnum(FieldRef.Name, PropertyValue);
+ else
+ FieldRef.Value := PropertyValue;
+ end;
+ end;
+ end;
+ RecordRef.SetTable(AFSDirectoryContent);
+ end;
+ end;
+
+ local procedure SetAttributesFields(var AFSDirectoryContent: Record "AFS Directory Content" temporary; AttributesNode: XmlNode)
+ var
+ AFSHelperLibrary: Codeunit "AFS Helper Library";
+ RecordRef: RecordRef;
+ FieldRef: FieldRef;
+ AttributesList: List of [Text];
+ Attribute: Text;
+ FldNo: Integer;
+ begin
+ if not AttributesNode.IsXmlElement() then
+ exit;
+
+ AttributesList := AttributesNode.AsXmlElement().InnerText.Replace(' ', '').Split('|');
+ foreach Attribute in AttributesList do begin
+ RecordRef.GetTable(AFSDirectoryContent);
+ if AFSHelperLibrary.GetFieldByCaption(Database::"AFS Directory Content", Attribute, FldNo) then begin
+ FieldRef := RecordRef.Field(FldNo);
+ if FieldRef.Type = FieldType::Boolean then
+ FieldRef.Value := true;
+ end;
+ RecordRef.SetTable(AFSDirectoryContent);
+ end;
+ end;
+
+ local procedure SetPermissionKeyField(var AFSDirectoryContent: Record "AFS Directory Content" temporary; PermissionKeyNode: XmlNode)
+ begin
+ if not PermissionKeyNode.IsXmlElement() then
+ exit;
+ AFSDirectoryContent."Permission Key" := CopyStr(PermissionKeyNode.AsXmlElement().InnerText, 1, MaxStrLen(AFSDirectoryContent."Permission Key"));
+ end;
+
+ [NonDebuggable]
+ local procedure GetNextEntryNo(var AFSDirectoryContent: Record "AFS Directory Content"): Integer
+ begin
+ AFSDirectoryContent.Reset();
+
+ if AFSDirectoryContent.FindLast() then
+ exit(AFSDirectoryContent."Entry No." + 1)
+ else
+ exit(1);
+ end;
+
+ [NonDebuggable]
+ local procedure GetLevel(Name: Text): Integer
+ var
+ StringSplit: List of [Text];
+ begin
+ if not Name.Contains('/') then
+ exit(0);
+ StringSplit := Name.Split('/');
+ exit(StringSplit.Count() - 1);
+ end;
+
+ [NonDebuggable]
+ local procedure GetName(Name: Text): Text[250]
+ var
+ StringSplit: List of [Text];
+ begin
+ if not Name.Contains('/') then
+ exit(CopyStr(Name, 1, 250));
+ StringSplit := Name.Split('/');
+ exit(CopyStr(StringSplit.Get(StringSplit.Count()), 1, 250));
+ end;
+}
\ No newline at end of file
diff --git a/src/System Application/App/Azure File Services API/src/Helper/AFSFormatHelper.Codeunit.al b/src/System Application/App/Azure File Services API/src/Helper/AFSFormatHelper.Codeunit.al
new file mode 100644
index 0000000000..f114a9ee78
--- /dev/null
+++ b/src/System Application/App/Azure File Services API/src/Helper/AFSFormatHelper.Codeunit.al
@@ -0,0 +1,80 @@
+// ------------------------------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+// ------------------------------------------------------------------------------------------------
+
+namespace System.Azure.Storage.Files;
+
+using System.Utilities;
+
+codeunit 8955 "AFS Format Helper"
+{
+ Access = Internal;
+ InherentEntitlements = X;
+ InherentPermissions = X;
+
+ [NonDebuggable]
+ procedure AppendToUri(var UriText: Text; ParameterIdentifier: Text; ParameterValue: Text)
+ var
+ UriBuilder: Codeunit "Uri Builder";
+ Uri: Codeunit Uri;
+ begin
+ UriBuilder.Init(UriText);
+ if ParameterIdentifier <> '' then
+ UriBuilder.AddQueryParameter(ParameterIdentifier, ParameterValue)
+ else
+ UriBuilder.AddQueryFlag(ParameterValue);
+ UriBuilder.GetUri(Uri);
+ UriText := Uri.GetAbsoluteUri();
+ end;
+
+ [NonDebuggable]
+ procedure RemoveCurlyBracketsFromString("Value": Text): Text
+ begin
+ exit(DelChr("Value", '=', '{}'));
+ end;
+
+ procedure ConvertToDateTime(PropertyValue: Text): DateTime
+ var
+ NewDateTime: DateTime;
+ begin
+ NewDateTime := 0DT;
+ // PropertyValue is something like the following: 'Mon, 24 May 2021 12:25:27 GMT'
+ // 'Evaluate' converts these correctly
+ Evaluate(NewDateTime, PropertyValue);
+ exit(NewDateTime);
+ end;
+
+ procedure ConvertToInteger(PropertyValue: Text): Integer
+ var
+ NewInteger: Integer;
+ begin
+ if Evaluate(NewInteger, PropertyValue) then
+ exit(NewInteger);
+ end;
+
+ procedure ConvertToEnum(FieldName: Text; PropertyValue: Text): Variant
+ begin
+ if FieldName = 'Resource Type' then
+ case LowerCase(PropertyValue) of
+ LowerCase(Format(Enum::"AFS File Resource Type"::File)):
+ exit(Enum::"AFS File Resource Type"::File);
+ LowerCase(Format(Enum::"AFS File Resource Type"::Directory)):
+ exit(Enum::"AFS File Resource Type"::Directory);
+ end;
+ end;
+
+ procedure GetRfc1123DateTime(MyDateTime: DateTime): Text
+ begin
+ exit(FormatDateTime(MyDateTime, 'R')); // https://go.microsoft.com/fwlink/?linkid=2210384
+ end;
+
+ local procedure FormatDateTime(MyDateTime: DateTime; FormatSpecifier: Text): Text
+ var
+ DateTimeAsXmlString: Text;
+ DateTimeDotNet: DotNet System.DateTime;
+ begin
+ DateTimeAsXmlString := Format(MyDateTime, 0, 9); // Format as XML, e.g.: 2020-11-11T08:50:07.553Z
+ exit(DateTimeDotNet.Parse(DateTimeAsXmlString).ToUniversalTime().ToString(FormatSpecifier));
+ end;
+}
\ No newline at end of file
diff --git a/src/System Application/App/Azure File Services API/src/Helper/AFSHandleHelper.Codeunit.al b/src/System Application/App/Azure File Services API/src/Helper/AFSHandleHelper.Codeunit.al
new file mode 100644
index 0000000000..0b60a0886a
--- /dev/null
+++ b/src/System Application/App/Azure File Services API/src/Helper/AFSHandleHelper.Codeunit.al
@@ -0,0 +1,42 @@
+// ------------------------------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+// ------------------------------------------------------------------------------------------------
+
+namespace System.Azure.Storage.Files;
+
+codeunit 8962 "AFS Handle Helper"
+{
+ Access = Internal;
+ InherentEntitlements = X;
+ InherentPermissions = X;
+
+ internal procedure AddNewEntryFromNode(var AFSHandle: Record "AFS Handle" temporary; Node: XmlNode)
+ var
+ AFSHelperLibrary: Codeunit "AFS Helper Library";
+ AFSFormatHelper: Codeunit "AFS Format Helper";
+ HandleRef: RecordRef;
+ HandleFieldRef: FieldRef;
+ ChildNode: XmlNode;
+ FieldNo: Integer;
+ EntryNo: Integer;
+ begin
+ if AFSHandle.FindLast() then
+ EntryNo := AFSHandle."Entry No." + 1
+ else
+ EntryNo := 1;
+ AFSHandle.Init();
+ AFSHandle."Entry No." := EntryNo;
+ HandleRef.GetTable(AFSHandle);
+ foreach ChildNode in Node.AsXmlElement().GetChildNodes() do
+ if AFSHelperLibrary.GetFieldByCaption(Database::"AFS Handle", ChildNode.AsXmlElement().Name, FieldNo) then begin
+ HandleFieldRef := HandleRef.Field(FieldNo);
+ if HandleFieldRef.Type = HandleFieldRef.Type::DateTime then
+ HandleFieldRef.Value(AFSFormatHelper.ConvertToDateTime(ChildNode.AsXmlElement().InnerText))
+ else
+ HandleFieldRef.Value(ChildNode.AsXmlElement().InnerText);
+ end;
+ HandleRef.SetTable(AFSHandle);
+ AFSHandle.Insert();
+ end;
+}
diff --git a/src/System Application/App/Azure File Services API/src/Helper/AFSHelperLibrary.Codeunit.al b/src/System Application/App/Azure File Services API/src/Helper/AFSHelperLibrary.Codeunit.al
new file mode 100644
index 0000000000..499fea9b7c
--- /dev/null
+++ b/src/System Application/App/Azure File Services API/src/Helper/AFSHelperLibrary.Codeunit.al
@@ -0,0 +1,150 @@
+// ------------------------------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+// ------------------------------------------------------------------------------------------------
+
+namespace System.Azure.Storage.Files;
+
+using System.Reflection;
+
+codeunit 8960 "AFS Helper Library"
+{
+ Access = Internal;
+ InherentEntitlements = X;
+ InherentPermissions = X;
+ Permissions = tabledata Field = r;
+
+ [NonDebuggable]
+ procedure CreateHandleNodeListFromResponse(ResponseAsText: Text): XmlNodeList
+ begin
+ exit(CreateXPathNodeListFromResponse(ResponseAsText, '/*/Entries/Handle'));
+ end;
+
+ [NonDebuggable]
+ procedure CreateDirectoryContentNodeListFromResponse(ResponseAsText: Text): XmlNodeList
+ begin
+ exit(CreateXPathNodeListFromResponse(ResponseAsText, '/*/Entries/File|/*/Entries/Directory'));
+ end;
+
+ [NonDebuggable]
+ procedure GetDirectoryPathFromResponse(ResponseAsText: Text): Text
+ var
+ Document: XmlDocument;
+ Root: XmlElement;
+ begin
+ GetXmlDocumentFromResponse(Document, ResponseAsText);
+ Document.GetRoot(Root);
+ exit(GetAttributeValueFromElement(Root, 'DirectoryPath'))
+ end;
+
+ [NonDebuggable]
+ procedure GetNextMarkerFromResponse(ResponseAsText: Text): Text
+ var
+ Document: XmlDocument;
+ Root: XmlElement;
+ begin
+ GetXmlDocumentFromResponse(Document, ResponseAsText);
+ Document.GetRoot(Root);
+ exit(GetValueFromNode(Root.AsXmlNode(), '/*/NextMarker'))
+ end;
+
+ [NonDebuggable]
+ procedure DirectoryContentNodeListToTempRecord(DirectoryURI: Text; DirectoryPath: Text[2048]; NodeList: XmlNodeList; PreserveDirectoryContent: Boolean; var AFSDirectoryContent: Record "AFS Directory Content")
+ begin
+ NodeListToTempRecord(DirectoryURI, DirectoryPath, NodeList, './/Name', PreserveDirectoryContent, AFSDirectoryContent);
+ end;
+
+ [NonDebuggable]
+ internal procedure HandleNodeListToTempRecord(NodeList: XmlNodeList; var AFSHandle: Record "AFS Handle" temporary)
+ begin
+ NodeListToTempRecord(NodeList, AFSHandle);
+ end;
+
+ [NonDebuggable]
+ local procedure GetXmlDocumentFromResponse(var Document: XmlDocument; ResponseAsText: Text)
+ var
+ ReadingAsXmlErr: Label 'Error reading Response as XML.';
+ begin
+ if not XmlDocument.ReadFrom(ResponseAsText, Document) then
+ Error(ReadingAsXmlErr);
+ end;
+
+ [NonDebuggable]
+ local procedure CreateXPathNodeListFromResponse(ResponseAsText: Text; XPath: Text): XmlNodeList
+ var
+ Document: XmlDocument;
+ RootNode: XmlElement;
+ NodeList: XmlNodeList;
+ begin
+ GetXmlDocumentFromResponse(Document, ResponseAsText);
+ Document.GetRoot(RootNode);
+ RootNode.SelectNodes(XPath, NodeList);
+ exit(NodeList);
+ end;
+
+ [NonDebuggable]
+ procedure GetValueFromNode(Node: XmlNode; XPath: Text): Text
+ var
+ Node2: XmlNode;
+ Value: Text;
+ begin
+ Node.SelectSingleNode(XPath, Node2);
+ Value := Node2.AsXmlElement().InnerText();
+ exit(Value);
+ end;
+
+ [NonDebuggable]
+ procedure GetAttributeValueFromElement(Element: XmlElement; AttributeName: Text): Text
+ var
+ Attribute: XmlAttribute;
+ begin
+ if not Element.HasAttributes then
+ exit;
+ foreach Attribute in Element.Attributes() do
+ if Attribute.Name = AttributeName then
+ exit(Attribute.Value);
+ end;
+
+ [NonDebuggable]
+ local procedure NodeListToTempRecord(DirectoryURI: Text; DirectoryPath: Text[2048]; NodeList: XmlNodeList; XPathName: Text; PreserveDirectoryContent: Boolean; var AFSDirectoryContent: Record "AFS Directory Content")
+ var
+ AFSDirectoryContentHelper: Codeunit "AFS Directory Content Helper";
+ Node: XmlNode;
+ begin
+ if not PreserveDirectoryContent then
+ AFSDirectoryContent.DeleteAll();
+
+ if NodeList.Count = 0 then
+ exit;
+
+ foreach Node in NodeList do
+ AFSDirectoryContentHelper.AddNewEntryFromNode(DirectoryURI, DirectoryPath, AFSDirectoryContent, Node, XPathName);
+ end;
+
+ [NonDebuggable]
+ local procedure NodeListToTempRecord(NodeList: XmlNodeList; var AFSHandle: Record "AFS Handle")
+ var
+ AFSHandleHelper: Codeunit "AFS Handle Helper";
+ Node: XmlNode;
+ begin
+ if NodeList.Count = 0 then
+ exit;
+
+ foreach Node in NodeList do
+ AFSHandleHelper.AddNewEntryFromNode(AFSHandle, Node);
+ end;
+
+ [NonDebuggable]
+ procedure GetFieldByCaption(TableNo: Integer; FieldCaption: Text; var FieldNo: Integer): Boolean
+ var
+ Field: Record Field;
+ begin
+ Clear(FieldNo);
+ Field.Reset();
+ Field.SetRange(TableNo, TableNo);
+ Field.SetRange("Field Caption", CopyStr(FieldCaption, 1, MaxStrLen(Field.FieldName)));
+ if Field.FindFirst() then
+ FieldNo := Field."No.";
+ exit(FieldNo <> 0);
+ end;
+}
\ No newline at end of file
diff --git a/src/System Application/App/Azure File Services API/src/Helper/AFSHttpContentHelper.Codeunit.al b/src/System Application/App/Azure File Services API/src/Helper/AFSHttpContentHelper.Codeunit.al
new file mode 100644
index 0000000000..5a49205506
--- /dev/null
+++ b/src/System Application/App/Azure File Services API/src/Helper/AFSHttpContentHelper.Codeunit.al
@@ -0,0 +1,116 @@
+// ------------------------------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+// ------------------------------------------------------------------------------------------------
+
+namespace System.Azure.Storage.Files;
+
+codeunit 8953 "AFS HttpContent Helper"
+{
+ Access = Internal;
+ InherentEntitlements = X;
+ InherentPermissions = X;
+
+ var
+ ContentLengthLbl: Label '%1', Comment = '%1 = Length', Locked = true;
+ RangeLbl: Label 'bytes=%1-%2', Comment = '%1 = Range Start, %2 = Range End', Locked = true;
+
+ ///
+ /// Checks if the HttpContent is empty
+ ///
+ /// The HttpContent to check.
+ /// true if the content is not empty, false otherwise.
+ [NonDebuggable]
+ procedure ContentSet(HttpContent: HttpContent): Boolean
+ var
+ VarContent: Text;
+ begin
+ HttpContent.ReadAs(VarContent);
+ if StrLen(VarContent) > 0 then
+ exit(true);
+
+ exit(VarContent <> '');
+ end;
+
+ ///
+ /// Retrieves the length of the given stream (used for "HttpContent-Length" header in PUT-operations)
+ ///
+ /// The InStream for Request Body.
+ /// The length of the current stream
+ [NonDebuggable]
+ procedure GetContentLength(var SourceInStream: InStream): Integer
+ var
+ Length: Integer;
+ begin
+ Evaluate(Length, Format(SourceInStream.Length()));
+ exit(Length);
+ end;
+
+ ///
+ /// Retrieves the length of the given stream (used for "HttpContent-Length" header in PUT-operations)
+ ///
+ /// The Text for Request Body.
+ /// The length of the current stream
+ [NonDebuggable]
+ procedure GetContentLength(SourceText: Text): Integer
+ var
+ Length: Integer;
+ begin
+ Length := StrLen(SourceText);
+ exit(Length);
+ end;
+
+ [NonDebuggable]
+ procedure AddFilePutContentHeaders(AFSOperationPayload: Codeunit "AFS Operation Payload"; ContentLength: Integer; ContentType: Text; RangeStart: Integer; RangeEnd: Integer)
+ var
+ HttpContent: HttpContent;
+ begin
+ AddFilePutContentHeaders(HttpContent, AFSOperationPayload, ContentLength, ContentType, RangeStart, RangeEnd);
+ end;
+
+ [NonDebuggable]
+ procedure AddFilePutContentHeaders(var HttpContent: HttpContent; AFSOperationPayload: Codeunit "AFS Operation Payload"; var SourceInStream: InStream; RangeStart: Integer; RangeEnd: Integer)
+ var
+ Length: Integer;
+ begin
+ // Do this before calling "GetContentLength", because for some reason the system errors out with "Cannot access a closed Stream."
+ HttpContent.WriteFrom(SourceInStream);
+
+ Length := GetContentLength(SourceInStream);
+
+ AddFilePutContentHeaders(HttpContent, AFSOperationPayload, Length, 'application/octet-stream', RangeStart, RangeEnd);
+ end;
+
+ [NonDebuggable]
+ local procedure AddFilePutContentHeaders(var HttpContent: HttpContent; AFSOperationPayload: Codeunit "AFS Operation Payload"; ContentLength: Integer; ContentType: Text; RangeStart: Integer; RangeEnd: Integer)
+ var
+ Headers: HttpHeaders;
+ FileServiceAPIOperation: Enum "AFS Operation";
+ begin
+ if ContentType = '' then
+ ContentType := 'application/octet-stream';
+
+ HttpContent.GetHeaders(Headers);
+
+ AFSOperationPayload.AddContentHeader('Content-Type', ContentType);
+ if AFSOperationPayload.GetOperation() in [FileServiceAPIOperation::CreateFile] then
+ AFSOperationPayload.AddContentHeader('x-ms-content-length', StrSubstNo(ContentLengthLbl, ContentLength))
+ else
+ AFSOperationPayload.AddContentHeader('Content-Length', StrSubstNo(ContentLengthLbl, ContentLength));
+
+ if AFSOperationPayload.GetOperation() in [FileServiceAPIOperation::PutRange, FileServiceAPIOperation::PutRangefromURL] then begin
+ AFSOperationPayload.AddRequestHeader('x-ms-write', 'update');
+ AFSOperationPayload.AddRequestHeader('x-ms-range', StrSubstNo(RangeLbl, RangeStart, RangeEnd));
+ end;
+ end;
+
+ ///
+ /// Retrieves the max range avaialble to update with PutRange request.
+ ///
+ /// The max range available to update.
+ [NonDebuggable]
+ procedure GetMaxRange(): Integer
+ begin
+ exit(4 * Power(2, 20));
+ end;
+}
\ No newline at end of file
diff --git a/src/System Application/App/Azure File Services API/src/Helper/AFSHttpHeaderHelper.Codeunit.al b/src/System Application/App/Azure File Services API/src/Helper/AFSHttpHeaderHelper.Codeunit.al
new file mode 100644
index 0000000000..0b8ed04085
--- /dev/null
+++ b/src/System Application/App/Azure File Services API/src/Helper/AFSHttpHeaderHelper.Codeunit.al
@@ -0,0 +1,107 @@
+// ------------------------------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+// ------------------------------------------------------------------------------------------------
+
+namespace System.Azure.Storage.Files;
+
+using System.Security.Authentication;
+
+codeunit 8958 "AFS HttpHeader Helper"
+{
+ Access = Internal;
+ InherentEntitlements = X;
+ InherentPermissions = X;
+
+ [NonDebuggable]
+ procedure GetHttpResponseHeaders(HttpResponseMessage: HttpResponseMessage): Dictionary of [Text, List of [Text]]
+ begin
+ exit(GetHttpResponseHeaders(HttpResponseMessage.Headers));
+ end;
+
+ [NonDebuggable]
+ procedure GetHttpResponseHeaders(ResponseHeaders: HttpHeaders): Dictionary of [Text, List of [Text]]
+ var
+ HeaderKey: Text;
+ ResponseHeadersDict: Dictionary of [Text, List of [Text]];
+ HeaderValues: List of [Text];
+ begin
+ foreach HeaderKey in ResponseHeaders.Keys() do
+ if not ResponseHeadersDict.ContainsKey(HeaderKey) then begin
+ ResponseHeaders.GetValues(HeaderKey, HeaderValues);
+ ResponseHeadersDict.Add(HeaderKey, HeaderValues);
+ end;
+ exit(ResponseHeadersDict);
+ end;
+
+ [NonDebuggable]
+ procedure GetMetadataHeaders(HttpResponseMessage: HttpResponseMessage): Dictionary of [Text, Text]
+ begin
+ exit(GetMetadataHeaders(HttpResponseMessage.Headers));
+ end;
+
+ [NonDebuggable]
+ procedure GetMetadataHeaders(ResponseHeaders: HttpHeaders): Dictionary of [Text, Text]
+ var
+ HeaderKey: Text;
+ TrimmedHeaderKey: Text;
+ MetadataHeaders: Dictionary of [Text, Text];
+ HeaderValues: List of [Text];
+ HeaderValue: Text;
+ begin
+ foreach HeaderKey in ResponseHeaders.Keys() do
+ if HeaderKey.StartsWith('x-ms-meta-') then begin
+ TrimmedHeaderKey := HeaderKey.Remove(1, StrLen('x-ms-meta-'));
+ if not MetadataHeaders.ContainsKey(TrimmedHeaderKey) then begin
+ Clear(HeaderValues);
+ ResponseHeaders.GetValues(HeaderKey, HeaderValues);
+ if HeaderValues.Count > 0 then begin
+ HeaderValue := HeaderValues.Get(1);
+ MetadataHeaders.Add(TrimmedHeaderKey, HeaderValue);
+ end;
+ end;
+ end;
+ exit(MetadataHeaders);
+ end;
+
+ [NonDebuggable]
+ procedure HandleRequestHeaders(HttpRequestType: Enum "Http Request Type"; var HttpRequestMessage: HttpRequestMessage; var AFSOperationPayload: Codeunit "AFS Operation Payload")
+ var
+ AFSFormatHelper: Codeunit "AFS Format Helper";
+ UsedDateTimeText: Text;
+ Headers: HttpHeaders;
+ RequestHeaders: Dictionary of [Text, Text];
+ HeaderKey: Text;
+ begin
+ // Add to the following headers to all requests
+ UsedDateTimeText := AFSFormatHelper.GetRfc1123DateTime(CurrentDateTime());
+ AFSOperationPayload.AddRequestHeader('Date', UsedDateTimeText);
+ AFSOperationPayload.AddRequestHeader('x-ms-version', Format(AFSOperationPayload.GetApiVersion()));
+
+ RequestHeaders := AFSOperationPayload.GetRequestHeaders();
+ HttpRequestMessage.GetHeaders(Headers);
+
+ foreach HeaderKey in RequestHeaders.Keys() do begin
+ if Headers.Remove(HeaderKey) then;
+ Headers.Add(HeaderKey, RequestHeaders.Get(HeaderKey));
+ end;
+ end;
+
+ [NonDebuggable]
+ procedure HandleContentHeaders(var HttpContent: HttpContent; var AFSOperationPayload: Codeunit "AFS Operation Payload"): Boolean
+ var
+ Headers: HttpHeaders;
+ ContentHeaders: Dictionary of [Text, Text];
+ HeaderKey: Text;
+ begin
+ HttpContent.GetHeaders(Headers);
+
+ ContentHeaders := AFSOperationPayload.GetContentHeaders();
+
+ foreach HeaderKey in ContentHeaders.Keys() do begin
+ if Headers.Remove(HeaderKey) then;
+ Headers.Add(HeaderKey, ContentHeaders.Get(HeaderKey));
+ end;
+ exit(ContentHeaders.Count > 0);
+ end;
+}
\ No newline at end of file
diff --git a/src/System Application/App/Azure File Services API/src/Helper/AFSURIHelper.Codeunit.al b/src/System Application/App/Azure File Services API/src/Helper/AFSURIHelper.Codeunit.al
new file mode 100644
index 0000000000..8e3a7a8d22
--- /dev/null
+++ b/src/System Application/App/Azure File Services API/src/Helper/AFSURIHelper.Codeunit.al
@@ -0,0 +1,164 @@
+// ------------------------------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+// ------------------------------------------------------------------------------------------------
+
+namespace System.Azure.Storage.Files;
+
+codeunit 8957 "AFS URI Helper"
+{
+ Access = Internal;
+ InherentEntitlements = X;
+ InherentPermissions = X;
+
+ var
+ [NonDebuggable]
+ OptionalUriParameters: Dictionary of [Text, Text];
+ FileShareBaseUrlLbl: Label 'https://%1.file.core.windows.net', Comment = '%1 = Storage Account Name', Locked = true;
+
+ [NonDebuggable]
+ procedure SetOptionalUriParameter(NewOptionalUriParameters: Dictionary of [Text, Text])
+ begin
+ OptionalUriParameters := NewOptionalUriParameters;
+ end;
+
+ [NonDebuggable]
+ procedure ConstructUri(StorageBaseUrl: Text; StorageAccountName: Text; FileShareName: Text; FilePath: Text; Operation: Enum "AFS Operation"): Text
+ var
+ ConstructedUrl: Text;
+ begin
+ TestConstructUrlParameter(StorageAccountName, FileShareName, FilePath, Operation);
+
+ if StorageBaseUrl = '' then
+ StorageBaseUrl := FileShareBaseUrlLbl;
+
+ ConstructedUrl := StrSubstNo(StorageBaseUrl, StorageAccountName);
+
+ AppendFileShareIfNecessary(ConstructedUrl, FileShareName, Operation);
+ AppendPathIfNecessary(ConstructedUrl, FilePath, Operation);
+ AppendRestTypeIfNecessary(ConstructedUrl, Operation);
+ AppendCompValueIfNecessary(ConstructedUrl, Operation);
+
+ AddOptionalUriParameters(ConstructedUrl);
+
+ exit(ConstructedUrl);
+ end;
+
+ [NonDebuggable]
+ local procedure AppendFileShareIfNecessary(var ConstructedUrl: Text; FileShare: Text; Operation: Enum "AFS Operation")
+ begin
+ // e.g. https://.blob.core.windows.net/?restype=container
+ if not (Operation in [Operation::CreateFile, Operation::PutRange, Operation::DeleteFile, Operation::GetFile, Operation::ListDirectory, Operation::CreateDirectory, Operation::DeleteDirectory, Operation::CopyFile, Operation::AbortCopyFile, Operation::ListFileHandles, Operation::ListDirectoryHandles, Operation::RenameFile, Operation::LeaseFile, Operation::GetFileMetadata, Operation::SetFileMetadata]) then
+ exit;
+ if not ConstructedUrl.EndsWith('/') then
+ ConstructedUrl += '/';
+ ConstructedUrl += FileShare;
+ end;
+
+ [NonDebuggable]
+ local procedure AppendPathIfNecessary(var ConstructedUrl: Text; Path: Text; Operation: Enum "AFS Operation")
+ begin
+ // e.g. https://.blob.core.windows.net//
+ if not (Operation in [Operation::CreateFile, Operation::PutRange, Operation::DeleteFile, Operation::GetFile, Operation::ListDirectory, Operation::CreateDirectory, Operation::DeleteDirectory, Operation::CopyFile, Operation::AbortCopyFile, Operation::ListFileHandles, Operation::ListDirectoryHandles, Operation::RenameFile, Operation::LeaseFile, Operation::GetFileMetadata, Operation::SetFileMetadata]) then
+ exit;
+ if not ConstructedUrl.EndsWith('/') then
+ ConstructedUrl += '/';
+ ConstructedUrl += Path;
+ end;
+
+ [NonDebuggable]
+ local procedure AppendRestTypeIfNecessary(var ConstructedUrl: Text; Operation: Enum "AFS Operation")
+ var
+ AFSFormatHelper: Codeunit "AFS Format Helper";
+ RestType: Text;
+ RestTypeLbl: Label 'restype', Locked = true;
+ DirectoryRestTypeLbl: Label 'directory', Locked = true;
+ begin
+ // e.g. https://.blob.core.windows.net/?restype=account&comp=properties
+ case Operation of
+ Operation::CreateDirectory, Operation::ListDirectory, Operation::DeleteDirectory:
+ RestType := DirectoryRestTypeLbl;
+ end;
+ if RestType = '' then
+ exit;
+ AFSFormatHelper.AppendToUri(ConstructedUrl, RestTypeLbl, RestType);
+ end;
+
+ [NonDebuggable]
+ local procedure AppendCompValueIfNecessary(var ConstructedUrl: Text; Operation: Enum "AFS Operation")
+ var
+ AFSFormatHelper: Codeunit "AFS Format Helper";
+ CompValue: Text;
+ CompIdentifierLbl: Label 'comp', Locked = true;
+ ListExtensionLbl: Label 'list', Locked = true;
+ ListHandlesExtensionLbl: Label 'listhandles', Locked = true;
+ RenameExtensionLbl: Label 'rename', Locked = true;
+ LeaseExtensionLbl: Label 'lease', Locked = true;
+ CopyExtensionLbl: Label 'copy', Locked = true;
+ RangeExtensionLbl: Label 'range', Locked = true;
+ MetadataExtensionLbl: Label 'metadata', Locked = true;
+ begin
+ // e.g. https://.blob.core.windows.net/?restype=account&comp=properties
+ case Operation of
+ Operation::PutRange, Operation::PutRangefromURL:
+ CompValue := RangeExtensionLbl;
+ Operation::ListDirectory:
+ CompValue := ListExtensionLbl;
+ Operation::AbortCopyFile:
+ CompValue := CopyExtensionLbl;
+ Operation::ListFileHandles, Operation::ListDirectoryHandles:
+ CompValue := ListHandlesExtensionLbl;
+ Operation::RenameFile:
+ CompValue := RenameExtensionLbl;
+ Operation::LeaseFile:
+ CompValue := LeaseExtensionLbl;
+ Operation::GetFileMetadata, Operation::SetFileMetadata:
+ CompValue := MetadataExtensionLbl;
+ end;
+ if CompValue = '' then
+ exit;
+ AFSFormatHelper.AppendToUri(ConstructedUrl, CompIdentifierLbl, CompValue);
+ end;
+
+ [NonDebuggable]
+ local procedure AddOptionalUriParameters(var Uri: Text)
+ var
+ AFSFormatHelper: Codeunit "AFS Format Helper";
+ ParameterIdentifier: Text;
+ ParameterValue: Text;
+ begin
+ if OptionalUriParameters.Count = 0 then
+ exit;
+
+ foreach ParameterIdentifier in OptionalUriParameters.Keys do
+ if not (ParameterIdentifier in ['copyid', 'blockid']) then begin
+ OptionalUriParameters.Get(ParameterIdentifier, ParameterValue);
+ AFSFormatHelper.AppendToUri(Uri, ParameterIdentifier, ParameterValue);
+ end;
+ end;
+
+ [NonDebuggable]
+ local procedure TestConstructUrlParameter(StorageAccountName: Text; FileShareName: Text; Path: Text; Operation: Enum "AFS Operation")
+ var
+ ValueCanNotBeEmptyErr: Label '%1 can not be empty', Comment = '%1 = Variable Name';
+ StorageAccountNameLbl: Label 'Storage Account Name';
+ FileShareLbl: Label 'File Share Name';
+ PathLbl: Label 'Path';
+ begin
+ if StorageAccountName = '' then
+ Error(ValueCanNotBeEmptyErr, StorageAccountNameLbl);
+
+ case true of
+ Operation in [Operation::CreateFile, Operation::PutRange, Operation::DeleteFile, Operation::CreateDirectory, Operation::CopyFile, Operation::RenameFile, Operation::ListFileHandles, Operation::ListDirectoryHandles, Operation::LeaseFile, Operation::GetFileMetadata, Operation::SetFileMetadata]:
+ begin
+ if FileShareName = '' then
+ Error(ValueCanNotBeEmptyErr, FileShareLbl);
+ if Path = '' then
+ Error(ValueCanNotBeEmptyErr, PathLbl);
+ end;
+ Operation in [Operation::ListDirectory]:
+ if FileShareName = '' then
+ Error(ValueCanNotBeEmptyErr, FileShareLbl);
+ end;
+ end;
+}
\ No newline at end of file
diff --git a/src/System Application/App/Azure File Services API/src/Helper/AFSWebRequestHelper.Codeunit.al b/src/System Application/App/Azure File Services API/src/Helper/AFSWebRequestHelper.Codeunit.al
new file mode 100644
index 0000000000..0ed144bdb9
--- /dev/null
+++ b/src/System Application/App/Azure File Services API/src/Helper/AFSWebRequestHelper.Codeunit.al
@@ -0,0 +1,187 @@
+// ------------------------------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+// ------------------------------------------------------------------------------------------------
+
+namespace System.Azure.Storage.Files;
+
+using System.Security.Authentication;
+using System.Azure.Storage;
+
+codeunit 8954 "AFS Web Request Helper"
+{
+ Access = Internal;
+ InherentEntitlements = X;
+ InherentPermissions = X;
+
+ var
+ ReadResponseFailedErr: Label 'Could not read response.';
+ HttpResponseInfoErr: Label '%1.\\Response Code: %2 %3', Comment = '%1 = Default Error Message ; %2 = Status Code; %3 = Reason Phrase';
+
+ [NonDebuggable]
+ procedure GetOperationAsText(var AFSOperationPayload: Codeunit "AFS Operation Payload"; var ResponseText: Text; OperationNotSuccessfulErr: Text): Codeunit "AFS Operation Response"
+ var
+ AFSOperationResponse: Codeunit "AFS Operation Response";
+ begin
+ AFSOperationResponse := GetOperation(AFSOperationPayload, OperationNotSuccessfulErr);
+
+
+ if not AFSOperationResponse.GetResultAsText(ResponseText) then
+ Error(ReadResponseFailedErr);
+
+ exit(AFSOperationResponse);
+ end;
+
+ [NonDebuggable]
+ procedure GetOperationAsStream(var AFSOperationPayload: Codeunit "AFS Operation Payload"; var InStream: InStream; OperationNotSuccessfulErr: Text): Codeunit "AFS Operation Response"
+ var
+ AFSOperationResponse: Codeunit "AFS Operation Response";
+ begin
+ AFSOperationResponse := GetOperation(AFSOperationPayload, OperationNotSuccessfulErr);
+
+ if not AFSOperationResponse.GetResultAsStream(InStream) then
+ Error(ReadResponseFailedErr);
+
+ exit(AFSOperationResponse);
+ end;
+
+ [NonDebuggable]
+ local procedure GetOperation(var AFSOperationPayload: Codeunit "AFS Operation Payload"; OperationNotSuccessfulErr: Text): Codeunit "AFS Operation Response"
+ var
+ AFSOperationResponse: Codeunit "AFS Operation Response";
+ HttpClient: HttpClient;
+ HttpRequestMessage: HttpRequestMessage;
+ begin
+ PrepareRequestMsg(HttpRequestMessage, AFSOperationPayload, Enum::"Http Request Type"::GET);
+
+ AFSOperationResponse := SendRequest(HttpClient, HttpRequestMessage, OperationNotSuccessfulErr);
+ exit(AFSOperationResponse);
+ end;
+
+ [NonDebuggable]
+ procedure HeadOperation(var AFSOperationPayload: Codeunit "AFS Operation Payload"; OperationNotSuccessfulErr: Text): Codeunit "AFS Operation Response"
+ var
+ AFSOperationResponse: Codeunit "AFS Operation Response";
+ HttpClient: HttpClient;
+ HttpRequestMessage: HttpRequestMessage;
+ begin
+ PrepareRequestMsg(HttpRequestMessage, AFSOperationPayload, Enum::"Http Request Type"::HEAD);
+
+ AFSOperationResponse := SendRequest(HttpClient, HttpRequestMessage, OperationNotSuccessfulErr);
+ exit(AFSOperationResponse);
+ end;
+
+ [NonDebuggable]
+ procedure PutOperation(var AFSOperationPayload: Codeunit "AFS Operation Payload"; OperationNotSuccessfulErr: Text): Codeunit "AFS Operation Response"
+ var
+ AFSOperationResponse: Codeunit "AFS Operation Response";
+ HttpContent: HttpContent;
+ begin
+ AFSOperationResponse := PutOperation(AFSOperationPayload, HttpContent, OperationNotSuccessfulErr);
+ exit(AFSOperationResponse);
+ end;
+
+ [NonDebuggable]
+ procedure PutOperation(var AFSOperationPayload: Codeunit "AFS Operation Payload"; HttpContent: HttpContent; OperationNotSuccessfulErr: Text): Codeunit "AFS Operation Response"
+ var
+ AFSOperationResponse: Codeunit "AFS Operation Response";
+ HttpClient: HttpClient;
+ HttpRequestMessage: HttpRequestMessage;
+ begin
+ PrepareRequestMsg(HttpRequestMessage, AFSOperationPayload, Enum::"Http Request Type"::PUT, HttpContent);
+
+ AFSOperationResponse := SendRequest(HttpClient, HttpRequestMessage, OperationNotSuccessfulErr);
+ exit(AFSOperationResponse);
+ end;
+
+ [NonDebuggable]
+ procedure DeleteOperation(var AFSOperationPayload: Codeunit "AFS Operation Payload"; OperationNotSuccessfulErr: Text): Codeunit "AFS Operation Response"
+ var
+ AFSOperationResponse: Codeunit "AFS Operation Response";
+ HttpClient: HttpClient;
+ HttpRequestMessage: HttpRequestMessage;
+ begin
+ PrepareRequestMsg(HttpRequestMessage, AFSOperationPayload, Enum::"Http Request Type"::DELETE);
+
+ AFSOperationResponse := SendRequest(HttpClient, HttpRequestMessage, OperationNotSuccessfulErr);
+ exit(AFSOperationResponse);
+ end;
+
+ [NonDebuggable]
+ procedure PostOperation(var AFSOperationPayload: Codeunit "AFS Operation Payload"; OperationNotSuccessfulErr: Text): Codeunit "AFS Operation Response"
+ var
+ AFSOperationResponse: Codeunit "AFS Operation Response";
+ HttpContent: HttpContent;
+ begin
+ AFSOperationResponse := PostOperation(AFSOperationPayload, HttpContent, OperationNotSuccessfulErr);
+ exit(AFSOperationResponse);
+ end;
+
+ [NonDebuggable]
+ procedure PostOperation(var AFSOperationPayload: Codeunit "AFS Operation Payload"; HttpContent: HttpContent; OperationNotSuccessfulErr: Text): Codeunit "AFS Operation Response"
+ var
+ AFSOperationResponse: Codeunit "AFS Operation Response";
+ HttpClient: HttpClient;
+ HttpRequestMessage: HttpRequestMessage;
+ begin
+ PrepareRequestMsg(HttpRequestMessage, AFSOperationPayload, Enum::"Http Request Type"::POST, HttpContent);
+
+ AFSOperationResponse := SendRequest(HttpClient, HttpRequestMessage, OperationNotSuccessfulErr);
+ exit(AFSOperationResponse);
+ end;
+
+ [NonDebuggable]
+ procedure OptionsOperation(var AFSOperationPayload: Codeunit "AFS Operation Payload"; OperationNotSuccessfulErr: Text): Codeunit "AFS Operation Response"
+ var
+ AFSOperationResponse: Codeunit "AFS Operation Response";
+ HttpClient: HttpClient;
+ HttpRequestMessage: HttpRequestMessage;
+ begin
+ PrepareRequestMsg(HttpRequestMessage, AFSOperationPayload, Enum::"Http Request Type"::OPTIONS);
+
+ AFSOperationResponse := SendRequest(HttpClient, HttpRequestMessage, OperationNotSuccessfulErr);
+ exit(AFSOperationResponse);
+ end;
+
+ [NonDebuggable]
+ local procedure PrepareRequestMsg(var HttpRequestMessage: HttpRequestMessage; var AFSOperationPayload: Codeunit "AFS Operation Payload"; HttpRequestType: Enum "Http Request Type")
+ var
+ AFSHttpHeaderHelper: Codeunit "AFS HttpHeader Helper";
+ Authorization: Interface "Storage Service Authorization";
+ begin
+ HttpRequestMessage.Method(Format(HttpRequestType));
+ HttpRequestMessage.SetRequestUri(AFSOperationPayload.ConstructUri());
+ AFSHttpHeaderHelper.HandleRequestHeaders(HttpRequestType, HttpRequestMessage, AFSOperationPayload);
+
+ Authorization := AFSOperationPayload.GetAuthorization();
+ Authorization.Authorize(HttpRequestMessage, AFSOperationPayload.GetStorageAccountName());
+ end;
+
+ [NonDebuggable]
+ local procedure PrepareRequestMsg(var HttpRequestMessage: HttpRequestMessage; var AFSOperationPayload: Codeunit "AFS Operation Payload"; HttpRequestType: Enum "Http Request Type"; HttpContent: HttpContent)
+ var
+ AFSHttpContentHelper: Codeunit "AFS HttpContent Helper";
+ AFSHttpHeaderHelper: Codeunit "AFS HttpHeader Helper";
+ begin
+ if AFSHttpContentHelper.ContentSet(HttpContent) or AFSHttpHeaderHelper.HandleContentHeaders(HttpContent, AFSOperationPayload) then
+ HttpRequestMessage.Content := HttpContent;
+
+ PrepareRequestMsg(HttpRequestMessage, AFSOperationPayload, HttpRequestType);
+ end;
+
+ [NonDebuggable]
+ local procedure SendRequest(var HttpClient: HttpClient; HttpRequestMessage: HttpRequestMessage; OperationNotSuccessfulErr: Text): Codeunit "AFS Operation Response"
+ var
+ AFSOperationResponse: Codeunit "AFS Operation Response";
+ HttpResponseMessage: HttpResponseMessage;
+ begin
+ if not HttpClient.Send(HttpRequestMessage, HttpResponseMessage) then
+ Error(OperationNotSuccessfulErr);
+
+ if not HttpResponseMessage.IsSuccessStatusCode() then
+ AFSOperationResponse.SetError(StrSubstNo(HttpResponseInfoErr, OperationNotSuccessfulErr, HttpResponseMessage.HttpStatusCode, HttpResponseMessage.ReasonPhrase));
+
+ AFSOperationResponse.SetHttpResponse(HttpResponseMessage);
+ exit(AFSOperationResponse);
+ end;
+}
\ No newline at end of file
diff --git a/src/System Application/Test/Azure File Services API/app.json b/src/System Application/Test/Azure File Services API/app.json
new file mode 100644
index 0000000000..7cafc3c05c
--- /dev/null
+++ b/src/System Application/Test/Azure File Services API/app.json
@@ -0,0 +1,59 @@
+{
+ "id": "70b7015c-a198-40d7-af21-660ba444f36f",
+ "name": "Azure File Services API Test",
+ "publisher": "Microsoft",
+ "version": "24.0.0.0",
+ "brief": "Reproduces the Azure File service REST API",
+ "description": "Provides a set of AL functionality and Helper libraries to make use of Azure File Storage in MSDyn365BC",
+ "privacyStatement": "https://go.microsoft.com/fwlink/?linkid=724009",
+ "EULA": "https://go.microsoft.com/fwlink/?linkid=2009120",
+ "help": "https://go.microsoft.com/fwlink/?linkid=2103698",
+ "url": "https://go.microsoft.com/fwlink/?linkid=724011",
+ "logo": "",
+ "dependencies": [
+ {
+ "id": "a6660ad9-7675-4f68-a2f9-a938c21de68a",
+ "name": "Azure File Services API",
+ "publisher": "Microsoft",
+ "version": "24.0.0.0"
+ },
+ {
+ "id": "e409d343-14fa-42a4-a1be-fec499383e59",
+ "name": "Azure Storage Services Authorization",
+ "publisher": "Microsoft",
+ "version": "24.0.0.0"
+ },
+ {
+ "id": "dd0be2ea-f733-4d65-bb34-a28f4624fb14",
+ "publisher": "Microsoft",
+ "name": "Library Assert",
+ "version": "24.0.0.0"
+ },
+ {
+ "id": "e7320ebb-08b3-4406-b1ec-b4927d3e280b",
+ "publisher": "Microsoft",
+ "name": "Any",
+ "version": "24.0.0.0"
+ },
+ {
+ "id": "e31ad830-3d46-472e-afeb-1d3d35247943",
+ "name": "BLOB Storage",
+ "publisher": "Microsoft",
+ "version": "24.0.0.0"
+ }
+ ],
+ "screenshots": [],
+ "platform": "24.0.0.0",
+ "idRanges": [
+ {
+ "from": 132514,
+ "to": 132520
+ }
+ ],
+ "contextSensitiveHelpUrl": "https://learn.microsoft.com/dynamics365/business-central/dev-itpro/terms/legal/",
+ "resourceExposurePolicy": {
+ "allowDebugging": true,
+ "allowDownloadingSource": true,
+ "includeSourceInSymbolFile": true
+ }
+}
\ No newline at end of file
diff --git a/src/System Application/Test/Azure File Services API/src/AFSFileClientTest.Codeunit.al b/src/System Application/Test/Azure File Services API/src/AFSFileClientTest.Codeunit.al
new file mode 100644
index 0000000000..17a2460ca9
--- /dev/null
+++ b/src/System Application/Test/Azure File Services API/src/AFSFileClientTest.Codeunit.al
@@ -0,0 +1,310 @@
+// ------------------------------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+// ------------------------------------------------------------------------------------------------
+
+namespace System.Test.Azure.Storage.Files;
+
+using System.Azure.Storage;
+using System.Azure.Storage.Files;
+using System.TestLibraries.Utilities;
+using System.Utilities;
+
+codeunit 132517 "AFS File Client Test"
+{
+ Subtype = Test;
+
+ [Test]
+ procedure CreateTextFileTest()
+ var
+ AFSFileClient: Codeunit "AFS File Client";
+ AFSOperationResponse: Codeunit "AFS Operation Response";
+ FileContentLbl: Label 'Hello World!', Locked = true;
+ FilePathLbl: Label 'test.txt', Locked = true;
+ FileContentReturn: Text;
+ begin
+ // [SCENARIO] User wants to send a text file to azure file share.
+
+ // [GIVEN] A storage account
+ AFSInitTestStorage.ClearFileShare();
+ SharedKeyAuthorization := AFSGetTestStorageAuth.GetDefaultAccountSAS(AFSInitTestStorage.GetAccessKey());
+
+ // [WHEN] The programmer creates a text file in the file share and puts the content in it
+ AFSFileClient.Initialize(AFSInitTestStorage.GetStorageAccountName(), AFSInitTestStorage.GetFileShareName(), SharedKeyAuthorization);
+
+ AFSOperationResponse := AFSFileClient.CreateFile(FilePathLbl, StrLen(FileContentLbl));
+ LibraryAssert.AreEqual(true, AFSOperationResponse.IsSuccessful(), AFSOperationResponse.GetError());
+ AFSOperationResponse := AFSFileClient.PutFileText(FilePathLbl, FileContentLbl);
+ LibraryAssert.AreEqual(true, AFSOperationResponse.IsSuccessful(), AFSOperationResponse.GetError());
+
+ // [THEN] The file is created and the content is correct
+ AFSOperationResponse := AFSFileClient.GetFileAsText(FilePathLbl, FileContentReturn);
+ LibraryAssert.AreEqual(true, AFSOperationResponse.IsSuccessful(), AFSOperationResponse.GetError());
+ LibraryAssert.AreEqual(FileContentLbl, FileContentReturn, 'File content mismatch.');
+ end;
+
+ [Test]
+ procedure CreateTextFileNoParentDirectoryTest()
+ var
+ AFSFileClient: Codeunit "AFS File Client";
+ AFSOperationResponse: Codeunit "AFS Operation Response";
+ FileContentLbl: Label 'Hello World!', Locked = true;
+ FilePathLbl: Label 'parentdir/test.txt', Locked = true;
+ ExpectedErrorLbl: Label 'Could not create file %1 in %2..\\Response Code: 404 The specified parent path does not exist.', Comment = '%1 - file name, %2 - file share name';
+ begin
+ // [SCENARIO] User wants to send a text file to azure file share into the folder that doesn't exist.
+
+ // [GIVEN] A storage account
+ AFSInitTestStorage.ClearFileShare();
+ SharedKeyAuthorization := AFSGetTestStorageAuth.GetDefaultAccountSAS(AFSInitTestStorage.GetAccessKey());
+
+ // [WHEN] The programmer creates a text file in the file share in the path that doesn't exist
+ AFSFileClient.Initialize(AFSInitTestStorage.GetStorageAccountName(), AFSInitTestStorage.GetFileShareName(), SharedKeyAuthorization);
+ AFSOperationResponse := AFSFileClient.CreateFile(FilePathLbl, StrLen(FileContentLbl));
+ LibraryAssert.AreEqual(false, AFSOperationResponse.IsSuccessful(), 'The CreateFile operation should return an error.');
+
+ // [THEN] An error is returned
+ LibraryAssert.AreEqual(StrSubstNo(ExpectedErrorLbl, FilePathLbl, AFSInitTestStorage.GetFileShareName()), AFSOperationResponse.GetError(), 'Error message mismatch.');
+ end;
+
+ [Test]
+ procedure ListDirectoryTest()
+ var
+ AFSDirectoryContent: Record "AFS Directory Content";
+ AFSFileClient: Codeunit "AFS File Client";
+ AFSOperationResponse: Codeunit "AFS Operation Response";
+ begin
+ // [SCENARIO] User wants to see files in the directory.
+
+ // [GIVEN] A storage account with a file share and a preset file structure
+ // -- parentdir
+ // -- test.txt
+ // -- test2.txt
+ // -- deeperdir
+ // -- test3.txt
+ // -- test4.txt
+ // -- anotherdir
+ // -- image.jpg
+ // -- document.pdf
+ // -- spreadsheet.xlsx
+ // -- emptydir
+ AFSInitTestStorage.ClearFileShare();
+ SharedKeyAuthorization := AFSGetTestStorageAuth.GetDefaultAccountSAS(AFSInitTestStorage.GetAccessKey());
+ InitializeFileShareStructure();
+
+ // [WHEN] The programmer runs a list operation on the parent directory
+ AFSFileClient.Initialize(AFSInitTestStorage.GetStorageAccountName(), AFSInitTestStorage.GetFileShareName(), SharedKeyAuthorization);
+ AFSOperationResponse := AFSFileClient.ListDirectory('', AFSDirectoryContent);
+ LibraryAssert.AreEqual(true, AFSOperationResponse.IsSuccessful(), AFSOperationResponse.GetError());
+
+ // [THEN] A correct list of files and/or directories is returned
+ LibraryAssert.AreEqual(2, AFSDirectoryContent.Count(), 'Wrong number of files and/or directories returned.');
+ AFSDirectoryContent.SetRange("Full Name", 'parentdir');
+ LibraryAssert.RecordIsNotEmpty(AFSDirectoryContent);
+ AFSDirectoryContent.SetRange("Full Name", 'anotherdir');
+ LibraryAssert.RecordIsNotEmpty(AFSDirectoryContent);
+ end;
+
+ [Test]
+ procedure CreateAndGetFileInDirectoryTest()
+ var
+ AFSFileClient: Codeunit "AFS File Client";
+ AFSOperationResponse: Codeunit "AFS Operation Response";
+ TempBlob: Codeunit "Temp Blob";
+ WriteOutStream: OutStream;
+ FileInStream: InStream;
+ ReturnInStream: InStream;
+ ReturnContent: Text;
+ FileContentLbl: Label 'Hello World!', Locked = true;
+ begin
+ // [SCENARIO] User wants to create a file in a new directory and then download it as stream.
+
+ // [GIVEN] A storage account
+ AFSInitTestStorage.ClearFileShare();
+ SharedKeyAuthorization := AFSGetTestStorageAuth.GetDefaultAccountSAS(AFSInitTestStorage.GetAccessKey());
+
+ AFSFileClient.Initialize(AFSInitTestStorage.GetStorageAccountName(), AFSInitTestStorage.GetFileShareName(), SharedKeyAuthorization);
+
+ // [WHEN] The programmer creates a directory and a file in it
+ AFSFileClient.Initialize(AFSInitTestStorage.GetStorageAccountName(), AFSInitTestStorage.GetFileShareName(), SharedKeyAuthorization);
+ AFSOperationResponse := AFSFileClient.CreateDirectory('parentdir');
+ LibraryAssert.AreEqual(true, AFSOperationResponse.IsSuccessful(), AFSOperationResponse.GetError());
+ AFSOperationResponse := AFSFileClient.CreateFile('parentdir/test.txt', StrLen(FileContentLbl));
+ LibraryAssert.AreEqual(true, AFSOperationResponse.IsSuccessful(), AFSOperationResponse.GetError());
+ TempBlob.CreateOutStream(WriteOutStream, TextEncoding::UTF8);
+ WriteOutStream.WriteText(FileContentLbl);
+ TempBlob.CreateInStream(FileInStream);
+ AFSOperationResponse := AFSFileClient.PutFileStream('parentdir/test.txt', FileInStream);
+ LibraryAssert.AreEqual(true, AFSOperationResponse.IsSuccessful(), AFSOperationResponse.GetError());
+
+ // [WHEN] The programmer downloads the file as stream
+ AFSFileClient.GetFileAsStream('parentdir/test.txt', ReturnInStream);
+ LibraryAssert.AreEqual(true, AFSOperationResponse.IsSuccessful(), AFSOperationResponse.GetError());
+
+ // [THEN] The content of the file is correct
+ ReturnInStream.ReadText(ReturnContent);
+ LibraryAssert.AreEqual(FileContentLbl, ReturnContent, 'File content mismatch.');
+ end;
+
+ [Test]
+ procedure DeleteFileTest()
+ var
+ AFSFileClient: Codeunit "AFS File Client";
+ AFSOperationResponse: Codeunit "AFS Operation Response";
+ ReturnContent: Text;
+ begin
+ // [SCENARIO] User wants to delete an existing file.
+
+ // [GIVEN] A storage account with a file share and a preset file structure
+ // -- parentdir
+ // -- test.txt
+ // -- test2.txt
+ // -- deeperdir
+ // -- test3.txt
+ // -- test4.txt
+ // -- anotherdir
+ // -- image.jpg
+ // -- document.pdf
+ // -- spreadsheet.xlsx
+ // -- emptydir
+ AFSInitTestStorage.ClearFileShare();
+ SharedKeyAuthorization := AFSGetTestStorageAuth.GetDefaultAccountSAS(AFSInitTestStorage.GetAccessKey());
+ InitializeFileShareStructure();
+
+ // [WHEN] The programmer deletes a file
+ AFSFileClient.Initialize(AFSInitTestStorage.GetStorageAccountName(), AFSInitTestStorage.GetFileShareName(), SharedKeyAuthorization);
+ AFSOperationResponse := AFSFileClient.DeleteFile('parentdir/test.txt');
+ LibraryAssert.AreEqual(true, AFSOperationResponse.IsSuccessful(), AFSOperationResponse.GetError());
+
+ // [THEN] The operation is succesful and the file is deleted
+ AFSOperationResponse := AFSFileClient.GetFileAsText('parentdir/test.txt', ReturnContent);
+ LibraryAssert.AreEqual(false, AFSOperationResponse.IsSuccessful(), 'The file should not exist.');
+ end;
+
+ [Test]
+ procedure DeleteDirectoryTest()
+ var
+ AFSDirectoryContent: Record "AFS Directory Content";
+ AFSFileClient: Codeunit "AFS File Client";
+ AFSOperationResponse: Codeunit "AFS Operation Response";
+ begin
+ // [SCENARIO] User wants to delete an existing empty directory.
+
+ // [GIVEN] A storage account with a file share and a preset file structure
+ // -- parentdir
+ // -- test.txt
+ // -- test2.txt
+ // -- deeperdir
+ // -- test3.txt
+ // -- test4.txt
+ // -- anotherdir
+ // -- image.jpg
+ // -- document.pdf
+ // -- spreadsheet.xlsx
+ // -- emptydir
+ AFSInitTestStorage.ClearFileShare();
+ SharedKeyAuthorization := AFSGetTestStorageAuth.GetDefaultAccountSAS(AFSInitTestStorage.GetAccessKey());
+ InitializeFileShareStructure();
+
+ // [WHEN] The programmer deletes a directory
+ AFSFileClient.Initialize(AFSInitTestStorage.GetStorageAccountName(), AFSInitTestStorage.GetFileShareName(), SharedKeyAuthorization);
+ AFSOperationResponse := AFSFileClient.DeleteDirectory('anotherdir/emptydir');
+ LibraryAssert.AreEqual(true, AFSOperationResponse.IsSuccessful(), AFSOperationResponse.GetError());
+
+ // [THEN] The operation is succesful and the directory is deleted
+ AFSOperationResponse := AFSFileClient.ListDirectory('anotherdir/emptydir', AFSDirectoryContent);
+ LibraryAssert.AreEqual(false, AFSOperationResponse.IsSuccessful(), 'The directory should not exist.');
+ end;
+
+ [Test]
+ procedure CopyFileTest()
+ var
+ AFSFileClient: Codeunit "AFS File Client";
+ AFSOperationResponse: Codeunit "AFS Operation Response";
+ HttpRequestMessage: HttpRequestMessage;
+ ReturnContent: Text;
+ FileContentLbl: Label 'Hello World!', Locked = true;
+ SourceFileURI: Text;
+ begin
+ // [SCENARIO] User wants to copy an existing file.
+
+ // [GIVEN] A storage account with a file share and an existing file
+ AFSInitTestStorage.ClearFileShare();
+ SharedKeyAuthorization := AFSGetTestStorageAuth.GetDefaultAccountSAS(AFSInitTestStorage.GetAccessKey());
+
+ AFSFileClient.Initialize(AFSInitTestStorage.GetStorageAccountName(), AFSInitTestStorage.GetFileShareName(), SharedKeyAuthorization);
+ AFSFileClient.CreateFile('sourcefile.txt', StrLen(FileContentLbl));
+ AFSFileClient.PutFileText('sourcefile.txt', FileContentLbl);
+
+ // [WHEN] The programmer copies a file
+ // NOTE: When copying a file using shared access signature you need to authorize the source file with the same shared access signature
+ SourceFileURI := 'https://' + AFSInitTestStorage.GetStorageAccountName() + '.file.core.windows.net/' + AFSInitTestStorage.GetFileShareName() + '/sourcefile.txt';
+ HttpRequestMessage.SetRequestUri(SourceFileURI);
+ SharedKeyAuthorization.Authorize(HttpRequestMessage, AFSInitTestStorage.GetStorageAccountName());
+ SourceFileURI := HttpRequestMessage.GetRequestUri();
+ AFSOperationResponse := AFSFileClient.CopyFile(SourceFileURI, 'targetfile.txt');
+ LibraryAssert.AreEqual(true, AFSOperationResponse.IsSuccessful(), AFSOperationResponse.GetError());
+
+ // [THEN] The operation is succesful and the new file is created with the same content
+ AFSOperationResponse := AFSFileClient.GetFileAsText('targetfile.txt', ReturnContent);
+ LibraryAssert.AreEqual(true, AFSOperationResponse.IsSuccessful(), AFSOperationResponse.GetError());
+ LibraryAssert.AreEqual(FileContentLbl, ReturnContent, 'File content mismatch.');
+ end;
+
+ [Test]
+ procedure FileMetadataTest()
+ var
+ AFSFileClient: Codeunit "AFS File Client";
+ AFSOperationResponse: Codeunit "AFS Operation Response";
+ Metadata: Dictionary of [Text, Text];
+ ReturnMetadata: Dictionary of [Text, Text];
+ KeyText: Text;
+ begin
+ // [SCENARIO] User wants to set metadata for an existing file.
+
+ // [GIVEN] A storage account with a file share and an existing file
+ AFSInitTestStorage.ClearFileShare();
+ SharedKeyAuthorization := AFSGetTestStorageAuth.GetDefaultAccountSAS(AFSInitTestStorage.GetAccessKey());
+
+ AFSFileClient.Initialize(AFSInitTestStorage.GetStorageAccountName(), AFSInitTestStorage.GetFileShareName(), SharedKeyAuthorization);
+ AFSFileClient.CreateFile('sourcefile.txt', 0);
+
+ // [WHEN] The programmer sets some metadata for a file
+ Metadata.Add('author', 'John Doe');
+ Metadata.Add('scope', 'Public');
+ Metadata.Add('importance', 'High');
+ AFSOperationResponse := AFSFileClient.SetFileMetadata('sourcefile.txt', Metadata);
+ LibraryAssert.AreEqual(true, AFSOperationResponse.IsSuccessful(), AFSOperationResponse.GetError());
+
+ // [THEN] The operation is succesful and the file has correct metadata
+ AFSOperationResponse := AFSFileClient.GetFileMetadata('sourcefile.txt', ReturnMetadata);
+ LibraryAssert.AreEqual(true, AFSOperationResponse.IsSuccessful(), AFSOperationResponse.GetError());
+ LibraryAssert.AreEqual(Metadata.Count(), ReturnMetadata.Count(), 'Metadata count mismatch.');
+ foreach KeyText in Metadata.Keys() do
+ LibraryAssert.AreEqual(Metadata.Get(KeyText), ReturnMetadata.Get(KeyText), 'Metadata value mismatch.');
+ end;
+
+ local procedure InitializeFileShareStructure()
+ var
+ AFSFileClient: Codeunit "AFS File Client";
+ begin
+ AFSFileClient.Initialize(AFSInitTestStorage.GetStorageAccountName(), AFSInitTestStorage.GetFileShareName(), SharedKeyAuthorization);
+ AFSFileClient.CreateDirectory('parentdir');
+ AFSFileClient.CreateFile('parentdir/test.txt', 0);
+ AFSFileClient.CreateFile('parentdir/test2.txt', 0);
+ AFSFileClient.CreateDirectory('parentdir/deeperdir');
+ AFSFileClient.CreateFile('parentdir/deeperdir/test3.txt', 0);
+ AFSFileClient.CreateFile('parentdir/deeperdir/test4.txt', 0);
+ AFSFileClient.CreateDirectory('anotherdir');
+ AFSFileClient.CreateFile('anotherdir/image.jpg', 0);
+ AFSFileClient.CreateFile('anotherdir/document.pdf', 0);
+ AFSFileClient.CreateFile('anotherdir/spreadsheet.xlsx', 0);
+ AFSFileClient.CreateDirectory('anotherdir/emptydir');
+ end;
+
+ var
+ LibraryAssert: Codeunit "Library Assert";
+ AFSGetTestStorageAuth: Codeunit "AFS Get Test Storage Auth.";
+ AFSInitTestStorage: Codeunit "AFS Init. Test Storage";
+ SharedKeyAuthorization: Interface "Storage Service Authorization";
+}
\ No newline at end of file
diff --git a/src/System Application/Test/Azure File Services API/src/AFSGetTestStorageAuth.Codeunit.al b/src/System Application/Test/Azure File Services API/src/AFSGetTestStorageAuth.Codeunit.al
new file mode 100644
index 0000000000..4525d0bdbb
--- /dev/null
+++ b/src/System Application/Test/Azure File Services API/src/AFSGetTestStorageAuth.Codeunit.al
@@ -0,0 +1,59 @@
+// ------------------------------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+// ------------------------------------------------------------------------------------------------
+
+namespace System.Test.Azure.Storage.Files;
+
+using System.Azure.Storage;
+
+///
+/// Provides common authorization functionality for using Azure storage accounts in tests for Azure Storage Services.
+///
+codeunit 132515 "AFS Get Test Storage Auth."
+{
+ Access = Internal;
+
+ var
+ AFSInitTestStorage: Codeunit "AFS Init. Test Storage";
+
+ procedure GetDefaultAccountSAS(): Interface "Storage Service Authorization"
+ begin
+ exit(GetDefaultAccountSAS(AFSInitTestStorage.GetAccessKey()));
+ end;
+
+ procedure GetDefaultAccountSAS(AccountKey: Text): Interface "Storage Service Authorization"
+ var
+ StorageServiceAuthorization: Codeunit "Storage Service Authorization";
+ begin
+ exit(GetDefaultAccountSAS(AccountKey, StorageServiceAuthorization.GetDefaultAPIVersion()));
+ end;
+
+ procedure GetDefaultAccountSAS(AccountKey: Text; APIVersion: Enum "Storage Service API Version"): Interface "Storage Service Authorization"
+ var
+ StorageServiceAuthorization: Codeunit "Storage Service Authorization";
+ SignedServices: List of [Enum "SAS Service Type"];
+ SignedPermissions: List of [Enum "SAS Permission"];
+ SignedResources: List of [Enum "SAS Resource Type"];
+ SignedExpiry: DateTime;
+ Second: Integer;
+ begin
+ Second := 1000;
+ SignedExpiry := CurrentDateTime() + (60 * Second);
+
+ SignedServices.Add(Enum::"SAS Service Type"::File);
+ SignedPermissions.AddRange(Enum::"SAS Permission"::Create, Enum::"SAS Permission"::Write,
+ Enum::"SAS Permission"::Delete, Enum::"SAS Permission"::List, Enum::"SAS Permission"::Read
+ );
+ SignedResources.AddRange(Enum::"SAS Resource Type"::Object, Enum::"SAS Resource Type"::Container);
+
+ exit(StorageServiceAuthorization.CreateAccountSAS(
+ AccountKey,
+ APIVersion,
+ SignedServices,
+ SignedResources,
+ SignedPermissions,
+ SignedExpiry
+ ));
+ end;
+}
\ No newline at end of file
diff --git a/src/System Application/Test/Azure File Services API/src/AFSInitTestStorage.Codeunit.al b/src/System Application/Test/Azure File Services API/src/AFSInitTestStorage.Codeunit.al
new file mode 100644
index 0000000000..3ea81b375b
--- /dev/null
+++ b/src/System Application/Test/Azure File Services API/src/AFSInitTestStorage.Codeunit.al
@@ -0,0 +1,82 @@
+// ------------------------------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+// ------------------------------------------------------------------------------------------------
+
+namespace System.Test.Azure.Storage.Files;
+
+using System.Azure.Storage.Files;
+
+///
+/// Provides functionality for initializing or resetting azure storage accounts file shares.
+///
+codeunit 132516 "AFS Init. Test Storage"
+{
+ Access = Internal;
+
+ var
+ AFSGetTestStorageAuth: Codeunit "AFS Get Test Storage Auth.";
+ FileShareNameTxt: Label 'filesharename', Locked = true;
+ StorageAccountNameTxt: Label 'storageaccountname', Locked = true;
+ AccessKeyTxt: Label 'base64accountkey', Locked = true;
+
+ procedure ClearFileShare(): Text
+ var
+ AFSDirectoryContent: Record "AFS Directory Content";
+ AFSFileClient: Codeunit "AFS File Client";
+ Visited: List of [Text];
+ begin
+ AFSFileClient.Initialize(GetStorageAccountName(), GetFileShareName(), AFSGetTestStorageAuth.GetDefaultAccountSAS());
+ AFSFileClient.ListDirectory('', AFSDirectoryContent);
+ if not AFSDirectoryContent.FindSet() then
+ exit;
+ DeleteDirectoryRecursive(AFSFileClient, AFSDirectoryContent, Visited);
+ end;
+
+ local procedure DeleteDirectoryRecursive(var AFSFileClient: Codeunit "AFS File Client"; var AFSDirectoryContent: Record "AFS Directory Content"; var Visited: List of [Text])
+ var
+ AFSDirectoryContentLocal: Record "AFS Directory Content";
+ begin
+ if not AFSDirectoryContent.FindSet() then
+ exit;
+ repeat
+ if not Visited.Contains(AFSDirectoryContent."Full Name") then
+ if AFSDirectoryContent."Resource Type" = AFSDirectoryContent."Resource Type"::File then
+ AFSFileClient.DeleteFile(AFSDirectoryContent."Full Name")
+ else begin
+ AFSFileClient.ListDirectory(AFSDirectoryContent."Full Name", AFSDirectoryContentLocal);
+ Visited.Add(AFSDirectoryContent."Full Name");
+ DeleteDirectoryRecursive(AFSFileClient, AFSDirectoryContentLocal, Visited);
+ AFSFileClient.DeleteDirectory(AFSDirectoryContent."Full Name");
+ Visited.Remove(AFSDirectoryContent."Full Name");
+ end;
+ until AFSDirectoryContent.Next() = 0;
+ end;
+
+ ///
+ /// Gets storage account name.
+ ///
+ /// Storage account name
+ procedure GetStorageAccountName(): Text
+ begin
+ exit(StorageAccountNameTxt);
+ end;
+
+ ///
+ /// Gets file share name.
+ ///
+ /// File share name
+ procedure GetFileShareName(): Text
+ begin
+ exit(FileShareNameTxt);
+ end;
+
+ ///
+ /// Gets storage account key.
+ ///
+ /// Storage account key
+ procedure GetAccessKey(): Text;
+ begin
+ exit(AccessKeyTxt);
+ end;
+}
\ No newline at end of file
diff --git a/src/System Application/Test/DisabledTests/AFSFileClient.json b/src/System Application/Test/DisabledTests/AFSFileClient.json
new file mode 100644
index 0000000000..d7e72ed7f5
--- /dev/null
+++ b/src/System Application/Test/DisabledTests/AFSFileClient.json
@@ -0,0 +1,8 @@
+[
+ {
+ "bug": "",
+ "codeunitId": 132517,
+ "codeunitName": "AFS File Client Test",
+ "method": "*"
+ }
+]
\ No newline at end of file