Private/Save-OSDStageDownload.ps1

function Save-OSDStageDownload {
    [CmdletBinding(DefaultParameterSetName='Exact')]
    param(
        [Parameter(Mandatory)][uri]$Uri,
        [Parameter(Mandatory,ParameterSetName='Exact')][string]$DestinationPath,
        [Parameter(Mandatory,ParameterSetName='Vendor')][string]$DestinationDirectory,
        [Parameter(Mandatory,ParameterSetName='Vendor')][string]$ExpectedExtension,
        [Parameter(Mandatory)][string]$Activity,
        [int]$ProgressId = 20,
        [int]$ParentProgressId = -1
    )
    Add-Type -AssemblyName System.Net.Http -ErrorAction Stop
    $directory = if ($PSCmdlet.ParameterSetName -eq 'Vendor') { $DestinationDirectory } else { Split-Path -Path $DestinationPath -Parent }
    $extension = if ($PSCmdlet.ParameterSetName -eq 'Vendor') { $ExpectedExtension } else { [IO.Path]::GetExtension($DestinationPath) }
    New-Item -ItemType Directory -Path $directory -Force -ErrorAction Stop | Out-Null
    $tempPath = Join-Path $directory ('.osdstage-download-' + [guid]::NewGuid().ToString('N'))
    $handler = [Net.Http.HttpClientHandler]::new()
    $handler.AllowAutoRedirect = $true
    $client = [Net.Http.HttpClient]::new($handler)
    $client.Timeout = [TimeSpan]::FromHours(1)
    $response = $null
    $source = $null
    $target = $null
    $watch = [Diagnostics.Stopwatch]::StartNew()
    try {
        $response = $client.GetAsync($Uri, [Net.Http.HttpCompletionOption]::ResponseHeadersRead).GetAwaiter().GetResult()
        $response.EnsureSuccessStatusCode() | Out-Null
        $name = Get-OSDStageDownloadFileName -Response $response -ExpectedExtension $extension
        if ($PSCmdlet.ParameterSetName -eq 'Exact' -and $name -cne (Split-Path -Path $DestinationPath -Leaf)) {
            throw [IO.InvalidDataException]::new('The vendor response filename differs from the requested exact filename.')
        }
        $DestinationPath = Join-Path $directory $name
        $source = $response.Content.ReadAsStreamAsync().GetAwaiter().GetResult()
        $target = [IO.FileStream]::new($tempPath, [IO.FileMode]::CreateNew, [IO.FileAccess]::Write, [IO.FileShare]::None)
        $buffer = New-Object byte[] (1024 * 1024)
        [long]$downloaded = 0
        $totalBytes = $response.Content.Headers.ContentLength
        $lastSecond = -1
        while (($read = $source.Read($buffer,0,$buffer.Length)) -gt 0) {
            $target.Write($buffer,0,$read)
            $downloaded += $read
            $second = [int]$watch.Elapsed.TotalSeconds
            if ($second -ne $lastSecond) {
                $speed = [math]::Round(($downloaded / 1MB) / [math]::Max(0.001,$watch.Elapsed.TotalSeconds),1)
                $percent = if ($totalBytes -and $totalBytes -gt 0) { [math]::Min(100,[int](100 * $downloaded / $totalBytes)) } else { -1 }
                Write-Progress -Id $ProgressId -ParentId $ParentProgressId -Activity $Activity -Status ("{0}: {1:N1} MB at {2:N1} MB/s" -f $name,($downloaded/1MB),$speed) -PercentComplete $percent
                $lastSecond = $second
            }
        }
        $target.Dispose(); $target = $null
        if ($downloaded -le 0 -or ($null -ne $totalBytes -and $downloaded -ne $totalBytes)) {
            throw [IO.InvalidDataException]::new('Downloaded payload is empty or does not match Content-Length.')
        }
        Move-Item -LiteralPath $tempPath -Destination $DestinationPath -Force -ErrorAction Stop
        Get-Item -LiteralPath $DestinationPath -ErrorAction Stop
    }
    finally {
        if ($target) { $target.Dispose() }
        if ($source) { $source.Dispose() }
        if ($response) { $response.Dispose() }
        $client.Dispose(); $handler.Dispose(); $watch.Stop()
        Write-Progress -Id $ProgressId -Activity $Activity -Completed
        if (Test-Path -LiteralPath $tempPath) { Remove-Item -LiteralPath $tempPath -Force -ErrorAction SilentlyContinue }
    }
}