Public/Network/Invoke-FileDownload.ps1

function Invoke-FileDownload {
  # .SYNOPSIS
  # Downloads files
  # .DESCRIPTION
  # Can handle file downloads from an array of @(urls), and can create different names to prevent overwritting file downloads in same dl_folder.
  # .EXAMPLE
  # $Url = (
  # 'https://cdimage.debian.org/debian-cd/current/i386/iso-cd/debian-8.8.0-i386-CD-1.iso',
  # 'https://cdimage.debian.org/debian-cd/current/i386/iso-cd/debian-8.8.0-i386-CD-2.iso'
  # )
  # Invoke-FileDownload -Urls $Url -OutPath $DownloadsFolder
  # .EXAMPLE
  # Invoke-FileDownload -Url @( 'https://community.chocolatey.org/7za.exe', 'https://community.chocolatey.org/7za.exe') -OutPath .\7za
  # When there are duplicate urls, THe output files will be numbered (ie: 7za.exe and 7za1.exe)
  # .INPUTS
  # [String]
  # .OUTPUTS
  # [String]
  param (
    # Name with extention of the file
    [Parameter(Mandatory = $false)]
    [Alias('BaseName')]
    [string]$Name,
    # File download link
    [Parameter(Mandatory = $true)]
    [Alias('FileUrl', 'Urls')]
    [string[]]$Url,
    # Output directory of the downloaded file
    [Parameter(Mandatory = $false)]
    [Alias('FileDirectory')]
    [String]$OutPath,
    [Parameter(Mandatory = $false)]
    [hashtable]$ProxyConfiguration,
    #TODO: Add support for AWS_BUCKET (!Comming soon!)
    [Parameter(Mandatory = $false)]
    [string]$AwsBucketName,
    [Parameter(Mandatory = $false)]
    [int]$Retries = 20,
    # Forcefully do things. like overwriting files with sme name
    [Parameter(Mandatory = $false)]
    [switch]$Force,
    # Don't show the progress bar (Silent)
    [Parameter(Mandatory = $false)]
    [switch]$Silent
  )
  $InformationPreference = 'Continue'
  $OutPath = if ($PSBoundParameters.Keys.Contains('OutPath')) { $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($OutPath) }else { [System.IO.Path]::Combine($([IO.Path]::GetTempPath()), $((New-Guid).Guid.trim()).ToUpper()) }
  Write-Debug "`$OutPath = $OutPath"
  if (!(Test-Path $OutPath -Type Container)) { New-Item -Path $OutPath -Type Directory | Out-Null; [bool]$OutPathWasnotThere = $true }
  $testCNs = ('142.250.188.238', '20.112.52.29', '20.103.85.33', '20.81.111.85', '20.53.203.50', '20.84.181.62')
  try {
    if (!(Test-Connection -ComputerName $testCNs[0] -Count 2 -Delay 1 -ErrorAction SilentlyContinue )) {
      throw [System.Net.NetworkInformation.PingException]::New('Testing connection failed: Error due to lack of resources')
    }
    # TODO: Add a cool progressbar
    $Progress = @{}
    $Count = 0
    $Timer = New-Object System.Timers.Timer
    $timer.Interval = 100
    $timer.AutoReset = $true
    $timer.Start()
    $WebClients = [System.Collections.ArrayList]::new()
    foreach ($link in $Url) {
      $w = New-Object System.Net.WebClient
      $null = Register-ObjectEvent -InputObject $w -EventName DownloadProgressChanged -Action ([scriptblock]::Create(
          "`$Percent = 100 * `$eventargs.BytesReceived / `$eventargs.TotalBytesToReceive; `$null = New-Event -SourceIdentifier MyDownloadUpdate -MessageData @($count,`$Percent)"
        )
      )
      [int]$fn = 0
      do {
        $Name = if ($PSBoundParameters.Keys.Contains('Name')) {
          if ($fn -eq 0) { $Name }else {
            $x = $Name.Split('.') # $x = extention
            $(($x.Split($x[-1]) | Where-Object { $_.Trim().Length -ne 0 }) -join '.') + $fn + '.' + $x[-1]
          }
        } else {
          if ($fn -eq 0) { $link.Split('/')[-1] }else {
            $x = $link.Split('/')[-1].Split('.')
            $(($x.Split($x[-1]) | Where-Object { $_.Trim().Length -ne 0 }) -join '.') + $fn + '.' + $x[-1]
          }
        }
        $fn++
        $OutFile = Join-Path "$OutPath" $Name
      } while (Test-Path $OutFile -Type Leaf)
      Write-Info "`nDownloading`t" -NoNewline -ForegroundColor Green; Write-Info $Name -NoNewline -ForegroundColor DarkCyan; Write-Info "`t..." -NoNewline -ForegroundColor Green
      $w.DownloadFileAsync($link, "$OutFile")
      Write-Info "`tDone" -ForegroundColor Green
      $Count++
      $WebClients.Add($w) | Out-Null

      $dlevent = Register-EngineEvent -SourceIdentifier MyDownloadUpdate -Action {
        $progress[$dlevent.MessageData[0]] = $dlevent.MessageData[1]
      }

      Register-ObjectEvent -InputObject $Timer -EventName Elapsed -Action {
        if ($Progress.Values.Count -gt 0) {
          $PercentComplete = 100 * ($Progress.values | Measure-Object -Sum | Select-Object -ExpandProperty Sum) / $Progress.Values.Count
          Write-Progress -Activity "Download Progress" -PercentComplete $PercentComplete
        }
      } | Out-Null
    }
    if ([bool]@($WebClients | ForEach-Object { $_.IsBusy -eq $false })) {
      # "I guess at this point it should be finished"
      $timer.Stop()
    }
    $WebClients.Clear()
  } catch [System.Net.WebException], [System.IO.IOException] {
    if ($OutPathWasnotThere) { Remove-Item -Path $OutPath -Force }
    Write-Warning "Unable to download $Name from $Url`n"; Write-Info "[Info] $($Error.exeption.message)"; Start-Sleep -Seconds 3;
    return
  } catch [System.Net.NetworkInformation.PingException], [System.Management.ManagementException], [System.Net.Sockets.SocketException] {
    Write-Warning "Network connection failed!"
    Write-Info "[Info] Please Check your Internet Connection, and try again."; Start-Sleep -Seconds 3;
    return
  } catch {
    if ($OutPathWasnotThere) { Remove-Item -Path $OutPath -Force }
    Write-Info "`n[Info] An error occurred that could not be resolved."; Start-Sleep -Seconds 3;
    return
  }
}
# learn from:
function Get-WebFile {
  <#
    .SYNOPSIS
    Downloads a file from an HTTP/HTTPS location. Prefer HTTPS when
    available.
 
    .DESCRIPTION
    This will download a file from an HTTP/HTTPS location, saving the file
    to the FileName location specified.
 
    .NOTES
    This is a low-level function and not recommended for use in package
    scripts. It is recommended you call `Get-ChocolateyWebFile` instead.
 
    Starting in 0.9.10, will automatically call Set-PowerShellExitCode to
    set the package exit code to 404 if the resource is not found.
 
    .INPUTS
    None
 
    .OUTPUTS
    None
 
    .PARAMETER Url
    This is the url to download the file from. Prefer HTTPS when available.
 
    .PARAMETER FileName
    This is the full path to the file to create. If downloading to the
    package folder next to the install script, the path will be like
    `"$(Split-Path -Parent $MyInvocation.MyCommand.Definition)\\file.exe"`
 
    .PARAMETER UserAgent
    The user agent to use as part of the request. Defaults to 'chocolatey
    command line'.
 
    .PARAMETER PassThru
    DO NOT USE - holdover from original function.
 
    .PARAMETER Quiet
    Silences the progress output.
 
    .PARAMETER Options
    OPTIONAL - Specify custom headers. Available in 0.9.10+.
 
    .PARAMETER IgnoredArguments
    Allows splatting with arguments that do not apply. Do not use directly.
 
    .LINK
    Get-ChocolateyWebFile
 
    .LINK
    Get-FtpFile
 
    .LINK
    Get-WebHeaders
 
    .LINK
    Get-WebFileName
    #>

  param(
    [parameter(Mandatory = $false, Position = 0)][string] $url = '', #(Read-Host "The URL to download"),
    [parameter(Mandatory = $false, Position = 1)][string] $fileName = $null,
    [parameter(Mandatory = $false, Position = 2)][string] $userAgent = 'chocolatey command line',
    [parameter(Mandatory = $false)][switch] $Passthru,
    [parameter(Mandatory = $false)][switch] $quiet,
    [parameter(Mandatory = $false)][hashtable] $options = @{Headers = @{} }
  )

  DynamicParam {
    $DynamicParams = [System.Management.Automation.RuntimeDefinedParameterDictionary]::new()
    #region IgnoredArguments
    $attributeCollection = [System.Collections.ObjectModel.Collection[System.Attribute]]::new()
    $attributes = [System.Management.Automation.ParameterAttribute]::new(); $attHash = @{
      Position                        = 6
      ParameterSetName                = '__AllParameterSets'
      Mandatory                       = $False
      ValueFromPipeline               = $true
      ValueFromPipelineByPropertyName = $true
      ValueFromRemainingArguments     = $true
      HelpMessage                     = 'Allows splatting with arguments that do not apply. Do not use directly.'
      DontShow                        = $False
    }; $attHash.Keys | ForEach-Object { $attributes.$_ = $attHash.$_ }
    $attributeCollection.Add($attributes)
    # $attributeCollection.Add([System.Management.Automation.ValidateSetAttribute]::new([System.Object[]]$ValidateSetOption))
    # $attributeCollection.Add([System.Management.Automation.ValidateRangeAttribute]::new([System.Int32[]]$ValidateRange))
    # $attributeCollection.Add([System.Management.Automation.ValidateNotNullOrEmptyAttribute]::new())
    # $attributeCollection.Add([System.Management.Automation.AliasAttribute]::new([System.String[]]$Aliases))
    $RuntimeParam = [System.Management.Automation.RuntimeDefinedParameter]::new("IgnoredArguments", [Object[]], $attributeCollection)
    $DynamicParams.Add("IgnoredArguments", $RuntimeParam)
    #endregion IgnoredArguments
    return $DynamicParams
  }

  Process {
    $PsCmdlet.MyInvocation.BoundParameters.GetEnumerator() | ForEach-Object { New-Variable -Name $_.Key -Value $_.Value -ea 'SilentlyContinue' }
    # Log Invocation and Parameters used. $MyInvocation, $PSBoundParameters

    try {
      $uri = [System.Uri]$url
      if ($uri.IsFile()) {
        Write-Debug "Url is local file, setting destination"
        if ($url.LocalPath -ne $fileName) {
          Copy-Item $uri.LocalPath -Destination $fileName -Force
        }

        return
      }
    } catch {
      #continue on
      $null
    }

    $req = [System.Net.HttpWebRequest]::Create($url);
    $defaultCreds = [System.Net.CredentialCache]::DefaultCredentials
    if ($null -ne $defaultCreds) {
      $req.Credentials = $defaultCreds
    }

    $webclient = New-Object System.Net.WebClient
    if ($null -ne $defaultCreds) {
      $webClient.Credentials = $defaultCreds
    }

    # check if a proxy is required
    $explicitProxy = $env:dotfilesProxyLocation
    $explicitProxyUser = $env:dotfilesProxyUser
    $explicitProxyPassword = $env:dotfilesProxyPassword
    $explicitProxyBypassList = $env:dotfilesProxyBypassList
    $explicitProxyBypassOnLocal = $env:dotfilesProxyBypassOnLocal
    if ($null -ne $explicitProxy) {
      # explicit proxy
      $proxy = New-Object System.Net.WebProxy($explicitProxy, $true)
      if ($null -ne $explicitProxyPassword) {
        $passwd = ConvertTo-SecureString $explicitProxyPassword
        $proxy.Credentials = New-Object System.Management.Automation.PSCredential ($explicitProxyUser, $passwd)
      }

      if ($null -ne $explicitProxyBypassList -and $explicitProxyBypassList -ne '') {
        $proxy.BypassList = $explicitProxyBypassList.Split(',', [System.StringSplitOptions]::RemoveEmptyEntries)
      }
      if ($explicitProxyBypassOnLocal -eq 'true') { $proxy.BypassProxyOnLocal = $true; }

      Write-Info "Using explicit proxy server '$explicitProxy'."
      $req.Proxy = $proxy
    } elseif ($webclient.Proxy -and !$webclient.Proxy.IsBypassed($url)) {
      # system proxy (pass through)
      $creds = [net.CredentialCache]::DefaultCredentials
      if ($null -eq $creds) {
        Write-Debug "Default credentials were null. Attempting backup method"
        $cred = Get-Credential
        $creds = $cred.GetNetworkCredential();
      }
      $proxyaddress = $webclient.Proxy.GetProxy($url).Authority
      Write-Info "Using system proxy server '$proxyaddress'."
      $proxy = New-Object System.Net.WebProxy($proxyaddress)
      $proxy.Credentials = $creds
      $proxy.BypassProxyOnLocal = $true
      $req.Proxy = $proxy
    }

    $req.Accept = "*/*"
    $req.AllowAutoRedirect = $true
    $req.MaximumAutomaticRedirections = 20
    #$req.KeepAlive = $true
    $req.AutomaticDecompression = [System.Net.DecompressionMethods]::GZip -bor [System.Net.DecompressionMethods]::Deflate
    $req.Timeout = 30000
    if ($null -ne $env:dotfilesRequestTimeout -and $env:dotfilesRequestTimeout -ne '') {
      Write-Debug "Setting request timeout to $env:dotfilesRequestTimeout"
      $req.Timeout = $env:dotfilesRequestTimeout
    }
    if ($null -ne $env:dotfilesResponseTimeout -and $env:dotfilesResponseTimeout -ne '') {
      Write-Debug "Setting read/write timeout to $env:dotfilesResponseTimeout"
      $req.ReadWriteTimeout = $env:dotfilesResponseTimeout
    }

    #http://stackoverflow.com/questions/518181/too-many-automatic-redirections-were-attempted-error-message-when-using-a-httpw
    $req.CookieContainer = New-Object System.Net.CookieContainer
    if ($null -ne $userAgent) {
      Write-Debug "Setting the UserAgent to `'$userAgent`'"
      $req.UserAgent = $userAgent
    }

    if ($options.Headers.Count -gt 0) {
      Write-Debug "Setting custom headers"
      foreach ($key in $options.headers.keys) {
        $uri = (New-Object -TypeName system.uri $url)
        switch ($key) {
          'Accept' { $req.Accept = $options.headers.$key }
          'Cookie' { $req.CookieContainer.SetCookies($uri, $options.headers.$key) }
          'Referer' { $req.Referer = $options.headers.$key }
          'User-Agent' { $req.UserAgent = $options.headers.$key }
          Default { $req.Headers.Add($key, $options.headers.$key) }
        }
      }
    }

    try {
      $res = $req.GetResponse();

      try {
        $headers = @{}
        foreach ($key in $res.Headers) {
          $value = $res.Headers[$key];
          if ($value) {
            $headers.Add("$key", "$value")
          }
        }

        $binaryIsTextCheckFile = "$fileName.istext"
        if (Test-Path($binaryIsTextCheckFile)) { Remove-Item $binaryIsTextCheckFile -Force -EA SilentlyContinue; }

        if ($headers.ContainsKey("Content-Type")) {
          $contentType = $headers['Content-Type']
          if ($null -ne $contentType) {
            if ($contentType.ToLower().Contains("text/html") -or $contentType.ToLower().Contains("text/plain")) {
              Write-Warning "$fileName is of content type $contentType"
              Set-Content -Path $binaryIsTextCheckFile -Value "$fileName has content type $contentType" -Encoding UTF8 -Force
            }
          }
        }
      } catch {
        # not able to get content-type header
        Write-Debug "Error getting content type - $($_.Exception.Message)"
      }

      if ($fileName -and !(Split-Path $fileName)) {
        $fileName = Join-Path (Get-Location -PSProvider "FileSystem") $fileName
      } elseif ((!$Passthru -and ($null -eq $fileName)) -or (($null -ne $fileName) -and (Test-Path -PathType "Container" $fileName))) {
        [string]$fileName = ([regex]'(?i)filename=(.*)$').Match( $res.Headers["Content-Disposition"] ).Groups[1].Value
        $fileName = $fileName.trim("\/""'")
        if (!$fileName) {
          $fileName = $res.ResponseUri.Segments[-1]
          $fileName = $fileName.trim("\/")
          if (!$fileName) {
            $fileName = Read-Host "Please provide a file name"
          }
          $fileName = $fileName.trim("\/")
          if (!([IO.FileInfo]$fileName).Extension) {
            $fileName = $fileName + "." + $res.ContentType.Split(";")[0].Split("/")[1]
          }
        }
        $fileName = Join-Path (Get-Location -PSProvider "FileSystem") $fileName
      }
      if ($Passthru) {
        $encoding = [System.Text.Encoding]::GetEncoding( $res.CharacterSet )
        [string]$output = ""
      }

      if ($res.StatusCode -eq 401 -or $res.StatusCode -eq 403 -or $res.StatusCode -eq 404) {
        $env:dotfilesExitCode = $res.StatusCode
        throw "Remote file either doesn't exist, is unauthorized, or is forbidden for '$url'."
      }

      if ($res.StatusCode -eq 200) {
        [long]$goal = $res.ContentLength
        $goalFormatted = Format-ItemSize $goal
        $reader = $res.GetResponseStream()

        if ($fileName) {
          $fileDirectory = $([System.IO.Path]::GetDirectoryName($fileName))
          if (!(Test-Path($fileDirectory))) {
            [System.IO.Directory]::CreateDirectory($fileDirectory) | Out-Null
          }

          try {
            $writer = New-Object System.IO.FileStream $fileName, "Create"
          } catch {
            throw $_.Exception
          }
        }

        [byte[]]$buffer = New-Object byte[] 1048576
        [long]$total = [long]$count = [long]$iterLoop = 0

        $OgEAP = $ErrorActionPreference
        $ErrorActionPreference = 'Stop'
        try {
          do {
            $count = $reader.Read($buffer, 0, $buffer.Length);
            if ($fileName) {
              $writer.Write($buffer, 0, $count);
            }

            if ($Passthru) {
              $output += $encoding.GetString($buffer, 0, $count)
            } elseif (!$quiet) {
              $total += $count
              $totalFormatted = Format-ItemSize $total
              if ($goal -gt 0 -and ++$iterLoop % 10 -eq 0) {
                $percentComplete = [Math]::Truncate(($total / $goal) * 100)
                Write-Progress "Downloading $url to $fileName" "Saving $totalFormatted of $goalFormatted" -Id 0 -PercentComplete $percentComplete
              }

              if ($total -eq $goal -and $count -eq 0) {
                Write-Progress "Completed download of $url." "Completed download of $fileName ($goalFormatted)." -Id 0 -Completed -PercentComplete 100
              }
            }
          } while ($count -gt 0)
          Write-Info ""
          Write-Info "Download of $([System.IO.Path]::GetFileName($fileName)) ($goalFormatted) completed."
        } catch {
          throw $_.Exception
        } finally {
          $ErrorActionPreference = $OgEAP
        }

        $reader.Close()
        if ($fileName) {
          $writer.Flush()
          $writer.Close()
        }
        if ($Passthru) {
          $output
        }
      }
    } catch {
      if ($null -ne $req) {
        $req.ServicePoint.MaxIdleTime = 0
        $req.Abort();
        # ruthlessly remove $req to ensure it isn't reused
        Remove-Variable req
        Start-Sleep 1
        [System.GC]::Collect()
      }

      Set-PowerShellExitCode 404
      if ($env:DownloadCacheAvailable -eq 'true') {
        throw "The remote file either doesn't exist, is unauthorized, or is forbidden for url '$url'. $($_.Exception.Message) `nThis package is likely not broken for licensed users - see https://docs.chocolatey.org/en-us/features/private-cdn."
      } else {
        throw "The remote file either doesn't exist, is unauthorized, or is forbidden for url '$url'. $($_.Exception.Message)"
      }
    } finally {
      if ($null -ne $res) {
        $res.Close()
      }

      Start-Sleep 1
    }
  }
}
# this could be cleaned up with http://learn-powershell.net/2013/02/08/powershell-and-events-object-events/
# learn from:
function Get-FtpFile {
  <#
    .SYNOPSIS
    Downloads a file from a File Transfter Protocol (FTP) location.
 
    .DESCRIPTION
    This will download a file from an FTP location, saving the file to the
    FileName location specified.
 
    .NOTES
    This is a low-level function and not recommended for use in package
    scripts. It is recommended you call `Get-ChocolateyWebFile` instead.
 
    Starting in 0.9.10, will automatically call Set-PowerShellExitCode to
    set the package exit code to 404 if the resource is not found.
 
    .INPUTS
    None
 
    .OUTPUTS
    None
 
    .PARAMETER Url
    This is the url to download the file from.
 
    .PARAMETER FileName
    This is the full path to the file to create. If FTPing to the
    package folder next to the install script, the path will be like
    `"$(Split-Path -Parent $MyInvocation.MyCommand.Definition)\\file.exe"`
 
    .PARAMETER UserName
    The user account to connect to FTP with.
 
    .PARAMETER Password
    The password for the user account on the FTP server.
 
    .PARAMETER Quiet
    Silences the progress output.
 
    .PARAMETER IgnoredArguments
    Allows splatting with arguments that do not apply. Do not use directly.
 
    .LINK
    Get-ChocolateyWebFile
 
    .LINK
    Get-WebFile
    #>

  param(
    [parameter(Mandatory = $false, Position = 0)][string] $url = '',
    [parameter(Mandatory = $true, Position = 1)][string] $fileName = $null,
    [parameter(Mandatory = $false, Position = 2)][string] $username = $null,
    [parameter(Mandatory = $false, Position = 3)][SecureString] $password = $null,
    [parameter(Mandatory = $false)][switch] $quiet,
    [parameter(ValueFromRemainingArguments = $true)][Object[]] $ignoredArguments
  )

  # Log Invocation and Parameters used. $MyInvocation, $PSBoundParameters

  if ($url -eq $null -or $url -eq '') {
    Write-Warning "Url parameter is empty, Get-FtpFile has nothing to do."
    return
  }

  if ($fileName -eq $null -or $fileName -eq '') {
    Write-Warning "FileName parameter is empty, Get-FtpFile cannot save the output."
    return
  }

  try {
    $uri = [System.Uri]$url
    if ($uri.IsFile()) {
      Write-Debug "Url is local file, setting destination"
      if ($url.LocalPath -ne $fileName) {
        Copy-Item $uri.LocalPath -Destination $fileName -Force
      }
      return
    }
  } catch {
    $null
  }

  # Create a FTPWebRequest object to handle the connection to the ftp server
  $ftprequest = [System.Net.FtpWebRequest]::create($url)

  # check if a proxy is required
  $explicitProxy = $env:dotfilesProxyLocation
  $explicitProxyUser = $env:dotfilesProxyUser
  $explicitProxyPassword = $env:dotfilesProxyPassword
  $explicitProxyBypassList = $env:dotfilesProxyBypassList
  $explicitProxyBypassOnLocal = $env:dotfilesProxyBypassOnLocal
  if ($null -ne $explicitProxy) {
    # explicit proxy
    $proxy = New-Object System.Net.WebProxy($explicitProxy, $true)
    if ($null -ne $explicitProxyPassword) {
      $passwd = ConvertTo-SecureString $explicitProxyPassword
      $proxy.Credentials = New-Object System.Management.Automation.PSCredential ($explicitProxyUser, $passwd)
    }

    if ($null -ne $explicitProxyBypassList -and $explicitProxyBypassList -ne '') {
      $proxy.BypassList = $explicitProxyBypassList.Split(',', [System.StringSplitOptions]::RemoveEmptyEntries)
    }
    if ($explicitProxyBypassOnLocal -eq 'true') { $proxy.BypassProxyOnLocal = $true; }

    Write-Info "Using explicit proxy server '$explicitProxy'."
    $ftprequest.Proxy = $proxy
  }

  # set the request's network credentials for an authenticated connection
  $ftprequest.Credentials = New-Object System.Net.NetworkCredential($username, $password)

  $ftprequest.Method = [System.Net.WebRequestMethods+Ftp]::DownloadFile
  $ftprequest.UseBinary = $true
  $ftprequest.KeepAlive = $false

  # use the default request timeout of 100000
  if ($null -ne $env:dotfilesRequestTimeout -and $env:dotfilesRequestTimeout -ne '') {
    $ftprequest.Timeout = $env:dotfilesRequestTimeout
  }
  if ($null -ne $env:dotfilesResponseTimeout -and $env:dotfilesResponseTimeout -ne '') {
    $ftprequest.ReadWriteTimeout = $env:dotfilesResponseTimeout
  }

  try {
    # send the ftp request to the server
    $ftpresponse = $ftprequest.GetResponse()
    [long]$goal = $ftpresponse.ContentLength
    $goalFormatted = Format-ItemSize $goal

    # get a download stream from the server response
    $reader = $ftpresponse.GetResponseStream()

    # create the target file on the local system and the download buffer
    $writer = New-Object IO.FileStream ($fileName, [IO.FileMode]::Create)
    [byte[]]$buffer = New-Object byte[] 1048576
    [long]$total = [long]$count = 0

    $OgEAP = $ErrorActionPreference
    $ErrorActionPreference = 'Stop'
    try {
      # loop through the download stream and send the data to the target file
      do {
        $count = $reader.Read($buffer, 0, $buffer.Length);
        $writer.Write($buffer, 0, $count);
        if (!$quiet) {
          $total += $count
          $totalFormatted = Format-ItemSize $total
          if ($goal -gt 0) {
            $percentComplete = [Math]::Truncate(($total / $goal) * 100)
            Write-Progress "Downloading $url to $fileName" "Saving $totalFormatted of $goalFormatted ($total/$goal)" -Id 0 -PercentComplete $percentComplete
          } else {
            Write-Progress "Downloading $url to $fileName" "Saving $total bytes..." -Id 0 -Completed
          }
          if ($total -eq $goal -and $count -eq 0) {
            Write-Progress "Completed download of $url." "Completed a total of $total bytes of $fileName" -Id 0 -Completed -PercentComplete 100
          }
        }
      } while ($count -ne 0)
      Write-Info ""
      Write-Info "Download of $([System.IO.Path]::GetFileName($fileName)) ($goalFormatted) completed."
    } finally {
      $ErrorActionPreference = $OgEAP
    }
    $writer.Flush() # closed in finally block
  } catch {
    if ($null -ne $ftprequest) {
      $ftprequest.ServicePoint.MaxIdleTime = 0
      $ftprequest.Abort();
      # ruthlessly remove $ftprequest to ensure it isn't reused
      Remove-Variable ftprequest
      Start-Sleep 1
      [System.GC]::Collect()
    }

    Set-PowerShellExitCode 404
    if ($env:DownloadCacheAvailable -eq 'true') {
      throw "The remote file either doesn't exist, is unauthorized, or is forbidden for url '$url'. $($_.Exception.Message) `nThis package is likely not broken for licensed users - see https://docs.chocolatey.org/en-us/features/private-cdn."
    } else {
      throw "The remote file either doesn't exist, is unauthorized, or is forbidden for url '$url'. $($_.Exception.Message)"
    }
  } finally {

    if ($null -ne $reader) {
      try { $reader.Close(); } catch { $null }
    }

    if ($null -ne $writer) {
      try { $writer.Close(); } catch { $null }
    }

    if ($null -ne $ftpresponse) {
      try { $ftpresponse.Close(); } catch { $null }
    }
    Start-Sleep 1
  }
}