Save-HttpItem.psm1

function Save-HTTPItem
{
    [CmdletBinding()]
    param(
       [Parameter(Mandatory=$true)]
       $Uri,
       [Parameter(Mandatory=$true)]
       $Destination
    )

    begin
    {
        $fullUri = [Uri]$Uri
        if (($fullUri.Scheme -ne 'http') -and ($fullUri.Scheme -ne 'https'))
        {
            throw "Uri: $uri is not supported. Only http or https schema are supported."
        }
    }

    end
    {
        $headers = @{'x-ms-client-request-id'=$(hostname);'x-ms-version'='2015-02-21'}
        $prop = Invoke-HttpClient -FullUri $fullUri `
                                    -Verbose `
                                    -Headers $headers `
                                    -Method Head `
                                    -ea SilentlyContinue `
                                    -ev ev
        if ($ev)
        {
            throw $ev
        }
        
        $cLength = $prop.Headers.ContentLength
        $sizePerCall = 4mb
        # TODO: This is not accurate but close
        $totalJobs = ($cLength/$sizePerCall) + 1
        $completedJobs = 0

        $mmpFile = [System.IO.MemoryMappedFiles.MemoryMappedFile]::CreateFromFile($destination, [System.IO.FileMode]::Create,
                (Split-Path -Leaf $destination), $cLength, [System.IO.MemoryMappedFiles.MemoryMappedFileAccess]::ReadWrite );
        try
        {
            $iss = [initialsessionstate]::CreateDefault()
            $iss.Commands.Add([System.Management.Automation.Runspaces.SessionStateCmdletEntry]::new('Write-Verbose',[Microsoft.PowerShell.Commands.WriteVerboseCommand], $null))
            $iss.Commands.Add([System.Management.Automation.Runspaces.SessionStateCmdletEntry]::new('Wait-Debugger',[Microsoft.PowerShell.Commands.WaitDebuggerCommand], $null))
            $iss.Commands.Add([System.Management.Automation.Runspaces.SessionStateFunctionEntry]::new('Save-RSTBlobItem', (get-command Save-RSTBlobItem).Definition))
            $iss.Commands.Add([System.Management.Automation.Runspaces.SessionStateFunctionEntry]::new('Invoke-HTTPClient', (get-command Invoke-HTTPClient).Definition))
            #$iss.Commands.Add([System.Management.Automation.Runspaces.SessionStateFunctionEntry]::new('Write-Log', (get-command Write-Log).Definition))
            $iss.LanguageMode = 'NoLanguage'
            $rsp = [System.Management.Automation.Runspaces.RunspaceFactory]::CreateRunspacePool($iss)
            $null = $rsp.SetMinRunspaces(1)
            $null = $rsp.SetMaxRunspaces(10)
            $rsp.Open()

            Write-Progress -Activity "Downloading from $fulluri" -status 'Status' -PercentComplete 0

            $pipelines = [System.Collections.ObjectModel.Collection[powershell]]::new()
            $asyncResults = [System.Collections.ObjectModel.Collection[System.IAsyncResult]]::new()
            $waitHandles = @()
            $start = 0;
            $end = 0;
            do
            {
                # [System.Threading.WaitHandle]::WaitAny can accept only 64 wait handles
                for( ;($start -lt $cLength) -and ($asyncResults.Count -lt 63); $start = $end + 1)
                {
                    $end = $start + $sizePerCall - 1;
                    if ($end -ge $cLength) 
                    { 
                        $end = $cLength - 1
                    }
                    $ps = [powershell]::Create().AddCommand('Save-RSTBlobItem').AddParameter('fullUri',$fullUri).AddParameter('mmpFile',$mmpFile).AddParameter('byteStart',$start).AddParameter('byteEnd',$end)
                    $ps.RunspacePool = $rsp
                    $pipelines.Add($ps)

                    $iasyncResult = $ps.BeginInvoke()
                    $asyncResults.Add($iasyncResult)
                }
      
                $waitHandles = @($asyncResults | % AsyncWaitHandle)

                $index = [System.Threading.WaitHandle]::WaitAny($waitHandles, -1)
                $completedJobs++
                $ps = $pipelines[$index]
                $ps.EndInvoke($asyncResults[$index])
                $ps.Streams.Error
                $ps.Streams.Verbose
                $ps.dispose()
                $null = $pipelines.Remove($pipelines[$index])
                $null = $asyncResults.Remove($asyncResults[$index])

                Write-Progress -Activity "Downloading from $fulluri" -status 'Status' -PercentComplete ((($completedJobs)/$totalJobs)*100)
            
            } while(($start -lt $cLength) -or ($asyncResults.Count -gt 0))
            Write-Progress -Activity "Downloading from $fulluri" -Status 'Status' -Completed
        }
        finally
        {
            $rsp.Dispose()
            $mmpFile.Dispose()
        }
    }
}

function Save-RSTBlobItem
{
    param(
        $fullUri = $(throw 'fullUri cannot be null.'),
        $mmpFile = $(throw 'mmpFile cannot be null.'),
        $byteStart,
        $byteEnd        
    )
        
    $length = $byteEnd - $byteStart + 1
    if ($length -le 0)
    {
        throw 'byteEnd is greaterthan byteStart'
    }

    $sizePerIteration = 4mb

    $mmvs = $mmpFile.CreateViewStream($byteStart, $length, 'Write')
    try
    {
      for($start = $byteStart; $start -le $byteEnd; $start = $end + 1)
      {
            $end = $start + $sizePerIteration - 1;
            if ($end -ge $byteEnd) 
            { 
                $end = $byteEnd
            }

            $headers = @{
                'x-ms-range' = "bytes=$start-$End";
                'x-ms-version'='2015-02-21';
                'x-ms-client-request-id'='ContainerImageProvider'
            }  

            #$data = Invoke-WebRequest $fullUri -Headers $headers -Method GET
            #$data.RawContentStream.CopyTo($mmvs)

            $streamContent = Invoke-HTTPClient -FullUri $fullUri -Headers $headers -httpMethod Get -retryCount 4
            $copyTask = $streamContent.CopyToAsync($mmvs)
            $null = $copyTask.AsyncWaitHandle.WaitOne()
            $copyTask.Dispose()
            $streamContent.Dispose()
      }
    }
    catch
    {
       Write-Error "$_"
    }
    finally
    {
        $mmvs.Close()
    }
}

function Invoke-HTTPClient
{
   param(
      [Uri] $FullUri,
      [Hashtable] $Headers,
      [ValidateSet('Get','Head')]
      [string] $httpMethod,
      [int] $retryCount = 0
   )

   $poshExtensionExist = ([psobject].Assembly.GetType('Microsoft.PowerShell.CoreCLR.AssemblyExtensions'))
   if ($poshExtensionExist)
   {
        # Nano case
        $snhAssemblyPath = join-path $env:windir "System32\DotNetCore\v1.0\System.Net.Http.dll"
        $null = [Microsoft.PowerShell.CoreCLR.AssemblyExtensions]::LoadFrom($snhAssemblyPath )
   }
   else
   {
        # Non-Nano case
        Add-Type -AssemblyName System.Net.Http
   }

   do
   {
        $httpClient = [System.Net.Http.HttpClient]::new()
        foreach($headerKey in $Headers.Keys)
        {
            $httpClient.DefaultRequestHeaders.Add($headerKey, $Headers[$headerKey])
        }
  
        $HttpCompletionOption = 'ResponseContentRead'
        if ($httpMethod -eq 'Get')
        {   
            $httpRequestMessage = [System.Net.Http.HttpRequestMessage]::new([System.Net.Http.HttpMethod]::Get, $fullUri)
        }
        else
        {
            $httpRequestMessage = [System.Net.Http.HttpRequestMessage]::new([System.Net.Http.HttpMethod]::Head, $fullUri)
            $HttpCompletionOption = 'ResponseHeadersRead'
        }  
   
        $result = $httpClient.SendAsync($httpRequestMessage, $HttpCompletionOption)
        $null = $result.AsyncWaitHandle.WaitOne()

        if ($result.Result.IsSuccessStatusCode)
        {
            break;
        }

        $retryCount--;
        $msg = 'RetryCount: {0}, Http.GetAsync did not return successful status code. Status Code: {1}, {2}' -f `
                    $retryCount, $result.Result.StatusCode, $result.Result.ReasonPhrase 
        $msg = $msg + ('Result Reason Phrase: {0}' -f $result.Result.ReasonPhrase)

   } while($retryCount -gt 0)

   if (-not $result.Result.IsSuccessStatusCode)
   {
       $msg = 'Http.GetAsync did not return successful status code. Status Code: {0}, {1}' -f `
                    $result.Result.StatusCode, $result.Result.ReasonPhrase    
       throw $msg
   }

   return $result.Result.Content
}