Modules/businessdev.ALbuild.Containers/Private/Save-BcRemoteFile.ps1

function Save-BcRemoteFile {
    <#
    .SYNOPSIS
        Downloads a file to disk via a streaming HttpClient.
 
    .DESCRIPTION
        Internal helper. Streams the response straight to disk (ResponseHeadersRead + CopyTo) so large
        BC artifact ZIPs (the application and platform packages are hundreds of MB to several GB)
        download fast and with low memory. Invoke-WebRequest buffers the whole response and is very slow
        on Windows PowerShell 5.1; a streaming HttpClient is fast, follows redirects, honours the system
        proxy and needs nothing installed. Works on Windows PowerShell 5.1 and PowerShell 7+.
 
    .PARAMETER Url
        Source URL.
 
    .PARAMETER OutFile
        Destination file path (overwritten if present).
 
    .PARAMETER TimeoutSec
        Time to wait for the response headers. The body transfer afterwards is NOT bound by this (a
        large but still-progressing download must not be killed), matching ResponseHeadersRead.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string] $Url,
        [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string] $OutFile,
        [int] $TimeoutSec = 600
    )

    # Windows PowerShell 5.1 does not load System.Net.Http by default; PowerShell 7 already has it.
    if ($PSVersionTable.PSVersion.Major -lt 6) { Add-Type -AssemblyName System.Net.Http -ErrorAction SilentlyContinue }

    $handler = [System.Net.Http.HttpClientHandler]::new()
    $handler.AllowAutoRedirect = $true   # follow the artifact CDN redirects
    $handler.UseProxy = $true            # honour the agent's configured system proxy
    $client = [System.Net.Http.HttpClient]::new($handler)
    # Timeout covers waiting for the response headers; with ResponseHeadersRead the body stream that
    # follows is not subject to it, so a large download over a slow link is not aborted mid-transfer.
    $client.Timeout = [TimeSpan]::FromSeconds($TimeoutSec)
    try {
        $response = $client.GetAsync($Url, [System.Net.Http.HttpCompletionOption]::ResponseHeadersRead).GetAwaiter().GetResult()
        try {
            if (-not $response.IsSuccessStatusCode) {
                throw "Download of '$Url' failed: HTTP $([int]$response.StatusCode) $($response.ReasonPhrase)."
            }
            $source = $response.Content.ReadAsStreamAsync().GetAwaiter().GetResult()
            $target = [System.IO.File]::Open($OutFile, [System.IO.FileMode]::Create, [System.IO.FileAccess]::Write, [System.IO.FileShare]::None)
            try { $source.CopyTo($target, 1MB) }   # stream in 1 MB chunks; no full-response buffering
            finally { $target.Dispose(); $source.Dispose() }
        }
        finally { $response.Dispose() }
    }
    finally { $client.Dispose() }
}