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 TypeTable
Object ID8951
Object NameAFS 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 TypeTable
Object ID8950
Object NameAFS 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