Public/Network/WebTools/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 ) begin { $InformationPreference = 'Continue' 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 } } } } process { $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 } } } |