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() } } |