src/Client/Get-XrmRecordFileDownload.ps1

<#
    .SYNOPSIS
    Download a file from a file or image column.

    .DESCRIPTION
    Download a file stored in a Dataverse file/image column using the InitializeFileBlocksDownload and DownloadBlock SDK messages.

    .PARAMETER XrmClient
    Xrm connector initialized to target instance. Use latest one by default. (Dataverse ServiceClient)

    .PARAMETER RecordReference
    EntityReference of the record containing the file column.

    .PARAMETER FileAttributeName
    Logical name of the file or image column.

    .PARAMETER OutputPath
    Full file path where the downloaded file will be saved. Optional. If not provided, saves to temp folder with original filename.

    .OUTPUTS
    System.String. The full path of the downloaded file.

    .EXAMPLE
    $filePath = Get-XrmRecordFileDownload -RecordReference $recordRef -FileAttributeName "new_document";
    $filePath = Get-XrmRecordFileDownload -RecordReference $recordRef -FileAttributeName "entityimage" -OutputPath "C:\Temp\photo.png";

    .LINK
    https://learn.microsoft.com/en-us/power-apps/developer/data-platform/file-attributes
#>

function Get-XrmRecordFileDownload {
    [CmdletBinding()]
    [OutputType([System.String])]
    param
    (
        [Parameter(Mandatory = $false, ValueFromPipeline)]
        [Microsoft.PowerPlatform.Dataverse.Client.ServiceClient]
        $XrmClient = $Global:XrmClient,

        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [Microsoft.Xrm.Sdk.EntityReference]
        $RecordReference,

        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string]
        $FileAttributeName,

        [Parameter(Mandatory = $false)]
        [string]
        $OutputPath
    )
    begin {
        $StopWatch = [System.Diagnostics.Stopwatch]::StartNew();
        Trace-XrmFunction -Name $MyInvocation.MyCommand.Name -Stage Start -Parameters ($MyInvocation.MyCommand.Parameters);
    }
    process {
        # Initialize download
        $initRequest = New-XrmRequest -Name "InitializeFileBlocksDownload";
        $target = New-XrmEntityReference -LogicalName $RecordReference.LogicalName -Id $RecordReference.Id;
        $initRequest | Add-XrmRequestParameter -Name "Target" -Value $target | Out-Null;
        $initRequest | Add-XrmRequestParameter -Name "FileAttributeName" -Value $FileAttributeName | Out-Null;

        $initResponse = $XrmClient | Invoke-XrmRequest -Request $initRequest;
        $fileContinuationToken = $initResponse.Results["FileContinuationToken"];
        $fileName = $initResponse.Results["FileName"];
        $fileSizeInBytes = $initResponse.Results["FileSizeInBytes"];

        if (-not $PSBoundParameters.ContainsKey('OutputPath')) {
            $OutputPath = Join-Path $env:TEMP $fileName;
        }

        # Download blocks
        $fileStream = [System.IO.File]::Create($OutputPath);
        try {
            $offset = 0;
            $blockSize = 4 * 1024 * 1024; # 4 MB blocks

            while ($offset -lt $fileSizeInBytes) {
                $downloadRequest = New-XrmRequest -Name "DownloadBlock";
                $downloadRequest | Add-XrmRequestParameter -Name "FileContinuationToken" -Value $fileContinuationToken | Out-Null;
                $downloadRequest | Add-XrmRequestParameter -Name "BlockLength" -Value $blockSize | Out-Null;
                $downloadRequest | Add-XrmRequestParameter -Name "Offset" -Value $offset | Out-Null;

                $downloadResponse = $XrmClient | Invoke-XrmRequest -Request $downloadRequest;
                $data = $downloadResponse.Results["Data"];

                $fileStream.Write($data, 0, $data.Length);
                $offset += $data.Length;
            }
        }
        finally {
            $fileStream.Close();
            $fileStream.Dispose();
        }

        $OutputPath;
    }
    end {
        $StopWatch.Stop();
        Trace-XrmFunction -Name $MyInvocation.MyCommand.Name -Stage Stop -StopWatch $StopWatch;
    }
}

Export-ModuleMember -Function Get-XrmRecordFileDownload -Alias *;