mirror of
https://github.com/ScoopInstaller/Scoop.git
synced 2025-10-30 06:07:56 +00:00
refactor(download): Move download-related functions to 'download.ps1' (#6095)
This commit is contained in:
@@ -2,7 +2,11 @@
|
|||||||
|
|
||||||
### Bug Fixes
|
### Bug Fixes
|
||||||
|
|
||||||
- **decompress**: `Expand-7zipArchive` Only delete temp dir / `$extractDir` if it is empty ([#6092](https://github.com/ScoopInstaller/Scoop/issues/6092))
|
- **decompress**: `Expand-7zipArchive` only delete temp dir / `$extractDir` if it is empty ([#6092](https://github.com/ScoopInstaller/Scoop/issues/6092))
|
||||||
|
|
||||||
|
### Code Refactoring
|
||||||
|
|
||||||
|
- **download:** Move download-related functions to 'download.ps1' ([#6095](https://github.com/ScoopInstaller/Scoop/issues/6095))
|
||||||
|
|
||||||
## [v0.5.2](https://github.com/ScoopInstaller/Scoop/compare/v0.5.1...v0.5.2) - 2024-07-26
|
## [v0.5.2](https://github.com/ScoopInstaller/Scoop/compare/v0.5.1...v0.5.2) - 2024-07-26
|
||||||
|
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ param(
|
|||||||
. "$PSScriptRoot\..\lib\autoupdate.ps1"
|
. "$PSScriptRoot\..\lib\autoupdate.ps1"
|
||||||
. "$PSScriptRoot\..\lib\json.ps1"
|
. "$PSScriptRoot\..\lib\json.ps1"
|
||||||
. "$PSScriptRoot\..\lib\versions.ps1"
|
. "$PSScriptRoot\..\lib\versions.ps1"
|
||||||
. "$PSScriptRoot\..\lib\install.ps1"
|
. "$PSScriptRoot\..\lib\download.ps1"
|
||||||
|
|
||||||
$Dir = Convert-Path $Dir
|
$Dir = Convert-Path $Dir
|
||||||
if ($ForceUpdate) { $Update = $true }
|
if ($ForceUpdate) { $Update = $true }
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ param(
|
|||||||
|
|
||||||
. "$PSScriptRoot\..\lib\core.ps1"
|
. "$PSScriptRoot\..\lib\core.ps1"
|
||||||
. "$PSScriptRoot\..\lib\manifest.ps1"
|
. "$PSScriptRoot\..\lib\manifest.ps1"
|
||||||
. "$PSScriptRoot\..\lib\install.ps1"
|
. "$PSScriptRoot\..\lib\download.ps1"
|
||||||
|
|
||||||
$Dir = Convert-Path $Dir
|
$Dir = Convert-Path $Dir
|
||||||
$Queue = @()
|
$Queue = @()
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ param(
|
|||||||
. "$PSScriptRoot\..\lib\buckets.ps1"
|
. "$PSScriptRoot\..\lib\buckets.ps1"
|
||||||
. "$PSScriptRoot\..\lib\json.ps1"
|
. "$PSScriptRoot\..\lib\json.ps1"
|
||||||
. "$PSScriptRoot\..\lib\versions.ps1"
|
. "$PSScriptRoot\..\lib\versions.ps1"
|
||||||
. "$PSScriptRoot\..\lib\install.ps1" # needed for hash generation
|
. "$PSScriptRoot\..\lib\download.ps1"
|
||||||
|
|
||||||
if ($App -ne '*' -and (Test-Path $App -PathType Leaf)) {
|
if ($App -ne '*' -and (Test-Path $App -PathType Leaf)) {
|
||||||
$Dir = Split-Path $App
|
$Dir = Split-Path $App
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ param(
|
|||||||
. "$PSScriptRoot\..\lib\core.ps1"
|
. "$PSScriptRoot\..\lib\core.ps1"
|
||||||
. "$PSScriptRoot\..\lib\manifest.ps1"
|
. "$PSScriptRoot\..\lib\manifest.ps1"
|
||||||
. "$PSScriptRoot\..\lib\description.ps1"
|
. "$PSScriptRoot\..\lib\description.ps1"
|
||||||
|
. "$PSScriptRoot\..\lib\download.ps1"
|
||||||
|
|
||||||
$Dir = Convert-Path $Dir
|
$Dir = Convert-Path $Dir
|
||||||
$Queue = @()
|
$Queue = @()
|
||||||
|
|||||||
@@ -1,4 +1,17 @@
|
|||||||
# Must included with 'json.ps1'
|
# Must included with 'json.ps1'
|
||||||
|
|
||||||
|
function format_hash([String] $hash) {
|
||||||
|
$hash = $hash.toLower()
|
||||||
|
switch ($hash.Length) {
|
||||||
|
32 { $hash = "md5:$hash" } # md5
|
||||||
|
40 { $hash = "sha1:$hash" } # sha1
|
||||||
|
64 { $hash = $hash } # sha256
|
||||||
|
128 { $hash = "sha512:$hash" } # sha512
|
||||||
|
default { $hash = $null }
|
||||||
|
}
|
||||||
|
return $hash
|
||||||
|
}
|
||||||
|
|
||||||
function find_hash_in_rdf([String] $url, [String] $basename) {
|
function find_hash_in_rdf([String] $url, [String] $basename) {
|
||||||
$xml = $null
|
$xml = $null
|
||||||
try {
|
try {
|
||||||
|
|||||||
176
lib/core.ps1
176
lib/core.ps1
@@ -63,18 +63,6 @@ function Optimize-SecurityProtocol {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function Get-Encoding($wc) {
|
|
||||||
if ($null -ne $wc.ResponseHeaders -and $wc.ResponseHeaders['Content-Type'] -match 'charset=([^;]*)') {
|
|
||||||
return [System.Text.Encoding]::GetEncoding($Matches[1])
|
|
||||||
} else {
|
|
||||||
return [System.Text.Encoding]::GetEncoding('utf-8')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function Get-UserAgent() {
|
|
||||||
return "Scoop/1.0 (+http://scoop.sh/) PowerShell/$($PSVersionTable.PSVersion.Major).$($PSVersionTable.PSVersion.Minor) (Windows NT $([System.Environment]::OSVersion.Version.Major).$([System.Environment]::OSVersion.Version.Minor); $(if(${env:ProgramFiles(Arm)}){'ARM64; '}elseif($env:PROCESSOR_ARCHITECTURE -eq 'AMD64'){'Win64; x64; '})$(if($env:PROCESSOR_ARCHITEW6432 -in 'AMD64','ARM64'){'WOW64; '})$PSEdition)"
|
|
||||||
}
|
|
||||||
|
|
||||||
function Show-DeprecatedWarning {
|
function Show-DeprecatedWarning {
|
||||||
<#
|
<#
|
||||||
.SYNOPSIS
|
.SYNOPSIS
|
||||||
@@ -228,35 +216,6 @@ function Complete-ConfigChange {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function setup_proxy() {
|
|
||||||
# note: '@' and ':' in password must be escaped, e.g. 'p@ssword' -> p\@ssword'
|
|
||||||
$proxy = get_config PROXY
|
|
||||||
if(!$proxy) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
$credentials, $address = $proxy -split '(?<!\\)@'
|
|
||||||
if(!$address) {
|
|
||||||
$address, $credentials = $credentials, $null # no credentials supplied
|
|
||||||
}
|
|
||||||
|
|
||||||
if($address -eq 'none') {
|
|
||||||
[net.webrequest]::defaultwebproxy = $null
|
|
||||||
} elseif($address -ne 'default') {
|
|
||||||
[net.webrequest]::defaultwebproxy = new-object net.webproxy "http://$address"
|
|
||||||
}
|
|
||||||
|
|
||||||
if($credentials -eq 'currentuser') {
|
|
||||||
[net.webrequest]::defaultwebproxy.credentials = [net.credentialcache]::defaultcredentials
|
|
||||||
} elseif($credentials) {
|
|
||||||
$username, $password = $credentials -split '(?<!\\):' | ForEach-Object { $_ -replace '\\([@:])','$1' }
|
|
||||||
[net.webrequest]::defaultwebproxy.credentials = new-object net.networkcredential($username, $password)
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
warn "Failed to use proxy '$proxy': $($_.exception.message)"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function Invoke-Git {
|
function Invoke-Git {
|
||||||
[CmdletBinding()]
|
[CmdletBinding()]
|
||||||
[OutputType([String])]
|
[OutputType([String])]
|
||||||
@@ -584,10 +543,6 @@ function Test-HelperInstalled {
|
|||||||
return ![String]::IsNullOrWhiteSpace((Get-HelperPath -Helper $Helper))
|
return ![String]::IsNullOrWhiteSpace((Get-HelperPath -Helper $Helper))
|
||||||
}
|
}
|
||||||
|
|
||||||
function Test-Aria2Enabled {
|
|
||||||
return (Test-HelperInstalled -Helper Aria2) -and (get_config 'aria2-enabled' $true)
|
|
||||||
}
|
|
||||||
|
|
||||||
function app_status($app, $global) {
|
function app_status($app, $global) {
|
||||||
$status = @{}
|
$status = @{}
|
||||||
$status.installed = installed $app $global
|
$status.installed = installed $app $global
|
||||||
@@ -639,28 +594,6 @@ function fname($path) { split-path $path -leaf }
|
|||||||
function strip_ext($fname) { $fname -replace '\.[^\.]*$', '' }
|
function strip_ext($fname) { $fname -replace '\.[^\.]*$', '' }
|
||||||
function strip_filename($path) { $path -replace [regex]::escape((fname $path)) }
|
function strip_filename($path) { $path -replace [regex]::escape((fname $path)) }
|
||||||
function strip_fragment($url) { $url -replace (new-object uri $url).fragment }
|
function strip_fragment($url) { $url -replace (new-object uri $url).fragment }
|
||||||
|
|
||||||
function url_filename($url) {
|
|
||||||
(split-path $url -leaf).split('?') | Select-Object -First 1
|
|
||||||
}
|
|
||||||
# Unlike url_filename which can be tricked by appending a
|
|
||||||
# URL fragment (e.g. #/dl.7z, useful for coercing a local filename),
|
|
||||||
# this function extracts the original filename from the URL.
|
|
||||||
function url_remote_filename($url) {
|
|
||||||
$uri = (New-Object URI $url)
|
|
||||||
$basename = Split-Path $uri.PathAndQuery -Leaf
|
|
||||||
If ($basename -match ".*[?=]+([\w._-]+)") {
|
|
||||||
$basename = $matches[1]
|
|
||||||
}
|
|
||||||
If (($basename -notlike "*.*") -or ($basename -match "^[v.\d]+$")) {
|
|
||||||
$basename = Split-Path $uri.AbsolutePath -Leaf
|
|
||||||
}
|
|
||||||
If (($basename -notlike "*.*") -and ($uri.Fragment -ne "")) {
|
|
||||||
$basename = $uri.Fragment.Trim('/', '#')
|
|
||||||
}
|
|
||||||
return $basename
|
|
||||||
}
|
|
||||||
|
|
||||||
function ensure($dir) {
|
function ensure($dir) {
|
||||||
if (!(Test-Path -Path $dir)) {
|
if (!(Test-Path -Path $dir)) {
|
||||||
New-Item -Path $dir -ItemType Directory | Out-Null
|
New-Item -Path $dir -ItemType Directory | Out-Null
|
||||||
@@ -1282,112 +1215,6 @@ function substitute($entity, [Hashtable] $params, [Bool]$regexEscape = $false) {
|
|||||||
return $newentity
|
return $newentity
|
||||||
}
|
}
|
||||||
|
|
||||||
function format_hash([String] $hash) {
|
|
||||||
$hash = $hash.toLower()
|
|
||||||
switch ($hash.Length)
|
|
||||||
{
|
|
||||||
32 { $hash = "md5:$hash" } # md5
|
|
||||||
40 { $hash = "sha1:$hash" } # sha1
|
|
||||||
64 { $hash = $hash } # sha256
|
|
||||||
128 { $hash = "sha512:$hash" } # sha512
|
|
||||||
default { $hash = $null }
|
|
||||||
}
|
|
||||||
return $hash
|
|
||||||
}
|
|
||||||
|
|
||||||
function format_hash_aria2([String] $hash) {
|
|
||||||
$hash = $hash -split ':' | Select-Object -Last 1
|
|
||||||
switch ($hash.Length)
|
|
||||||
{
|
|
||||||
32 { $hash = "md5=$hash" } # md5
|
|
||||||
40 { $hash = "sha-1=$hash" } # sha1
|
|
||||||
64 { $hash = "sha-256=$hash" } # sha256
|
|
||||||
128 { $hash = "sha-512=$hash" } # sha512
|
|
||||||
default { $hash = $null }
|
|
||||||
}
|
|
||||||
return $hash
|
|
||||||
}
|
|
||||||
|
|
||||||
function get_hash([String] $multihash) {
|
|
||||||
$type, $hash = $multihash -split ':'
|
|
||||||
if(!$hash) {
|
|
||||||
# no type specified, assume sha256
|
|
||||||
$type, $hash = 'sha256', $multihash
|
|
||||||
}
|
|
||||||
|
|
||||||
if(@('md5','sha1','sha256', 'sha512') -notcontains $type) {
|
|
||||||
return $null, "Hash type '$type' isn't supported."
|
|
||||||
}
|
|
||||||
|
|
||||||
return $type, $hash.ToLower()
|
|
||||||
}
|
|
||||||
|
|
||||||
function Get-GitHubToken {
|
|
||||||
return $env:SCOOP_GH_TOKEN, (get_config GH_TOKEN) | Where-Object -Property Length -Value 0 -GT | Select-Object -First 1
|
|
||||||
}
|
|
||||||
|
|
||||||
function handle_special_urls($url)
|
|
||||||
{
|
|
||||||
# FossHub.com
|
|
||||||
if ($url -match "^(?:.*fosshub.com\/)(?<name>.*)(?:\/|\?dwl=)(?<filename>.*)$") {
|
|
||||||
$Body = @{
|
|
||||||
projectUri = $Matches.name;
|
|
||||||
fileName = $Matches.filename;
|
|
||||||
source = 'CF';
|
|
||||||
isLatestVersion = $true
|
|
||||||
}
|
|
||||||
if ((Invoke-RestMethod -Uri $url) -match '"p":"(?<pid>[a-f0-9]{24}).*?"r":"(?<rid>[a-f0-9]{24})') {
|
|
||||||
$Body.Add("projectId", $Matches.pid)
|
|
||||||
$Body.Add("releaseId", $Matches.rid)
|
|
||||||
}
|
|
||||||
$url = Invoke-RestMethod -Method Post -Uri "https://api.fosshub.com/download/" -ContentType "application/json" -Body (ConvertTo-Json $Body -Compress)
|
|
||||||
if ($null -eq $url.error) {
|
|
||||||
$url = $url.data.url
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Sourceforge.net
|
|
||||||
if ($url -match "(?:downloads\.)?sourceforge.net\/projects?\/(?<project>[^\/]+)\/(?:files\/)?(?<file>.*?)(?:$|\/download|\?)") {
|
|
||||||
# Reshapes the URL to avoid redirections
|
|
||||||
$url = "https://downloads.sourceforge.net/project/$($matches['project'])/$($matches['file'])"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Github.com
|
|
||||||
if ($url -match 'github.com/(?<owner>[^/]+)/(?<repo>[^/]+)/releases/download/(?<tag>[^/]+)/(?<file>[^/#]+)(?<filename>.*)' -and ($token = Get-GitHubToken)) {
|
|
||||||
$headers = @{ "Authorization" = "token $token" }
|
|
||||||
$privateUrl = "https://api.github.com/repos/$($Matches.owner)/$($Matches.repo)"
|
|
||||||
$assetUrl = "https://api.github.com/repos/$($Matches.owner)/$($Matches.repo)/releases/tags/$($Matches.tag)"
|
|
||||||
|
|
||||||
if ((Invoke-RestMethod -Uri $privateUrl -Headers $headers).Private) {
|
|
||||||
$url = ((Invoke-RestMethod -Uri $assetUrl -Headers $headers).Assets | Where-Object -Property Name -EQ -Value $Matches.file).Url, $Matches.filename -join ''
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return $url
|
|
||||||
}
|
|
||||||
|
|
||||||
function get_magic_bytes($file) {
|
|
||||||
if(!(Test-Path $file)) {
|
|
||||||
return ''
|
|
||||||
}
|
|
||||||
|
|
||||||
if((Get-Command Get-Content).parameters.ContainsKey('AsByteStream')) {
|
|
||||||
# PowerShell Core (6.0+) '-Encoding byte' is replaced by '-AsByteStream'
|
|
||||||
return Get-Content $file -AsByteStream -TotalCount 8
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
return Get-Content $file -Encoding byte -TotalCount 8
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function get_magic_bytes_pretty($file, $glue = ' ') {
|
|
||||||
if(!(Test-Path $file)) {
|
|
||||||
return ''
|
|
||||||
}
|
|
||||||
|
|
||||||
return (get_magic_bytes $file | ForEach-Object { $_.ToString('x2') }) -join $glue
|
|
||||||
}
|
|
||||||
|
|
||||||
function Out-UTF8File {
|
function Out-UTF8File {
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory = $True, Position = 0)]
|
[Parameter(Mandatory = $True, Position = 0)]
|
||||||
@@ -1473,6 +1300,3 @@ $scoopPathEnvVar = switch (get_config USE_ISOLATED_PATH) {
|
|||||||
|
|
||||||
# OS information
|
# OS information
|
||||||
$WindowsBuild = [System.Environment]::OSVersion.Version.Build
|
$WindowsBuild = [System.Environment]::OSVersion.Version.Build
|
||||||
|
|
||||||
# Setup proxy globally
|
|
||||||
setup_proxy
|
|
||||||
|
|||||||
758
lib/download.ps1
Normal file
758
lib/download.ps1
Normal file
@@ -0,0 +1,758 @@
|
|||||||
|
# Description: Functions for downloading files
|
||||||
|
|
||||||
|
## Meta downloader
|
||||||
|
|
||||||
|
function Invoke-ScoopDownload ($app, $version, $manifest, $bucket, $architecture, $dir, $use_cache = $true, $check_hash = $true) {
|
||||||
|
# we only want to show this warning once
|
||||||
|
if (!$use_cache) { warn 'Cache is being ignored.' }
|
||||||
|
|
||||||
|
# can be multiple urls: if there are, then installer should go first to make 'installer.args' section work
|
||||||
|
$urls = @(script:url $manifest $architecture)
|
||||||
|
|
||||||
|
# can be multiple cookies: they will be used for all HTTP requests.
|
||||||
|
$cookies = $manifest.cookie
|
||||||
|
|
||||||
|
# download first
|
||||||
|
if (Test-Aria2Enabled) {
|
||||||
|
Invoke-CachedAria2Download $app $version $manifest $architecture $dir $cookies $use_cache $check_hash
|
||||||
|
} else {
|
||||||
|
foreach ($url in $urls) {
|
||||||
|
$fname = url_filename $url
|
||||||
|
|
||||||
|
try {
|
||||||
|
Invoke-CachedDownload $app $version $url "$dir\$fname" $cookies $use_cache
|
||||||
|
} catch {
|
||||||
|
Write-Host -ForegroundColor DarkRed $_
|
||||||
|
abort "URL $url is not valid"
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($check_hash) {
|
||||||
|
$manifest_hash = hash_for_url $manifest $url $architecture
|
||||||
|
$ok, $err = check_hash "$dir\$fname" $manifest_hash $(show_app $app $bucket)
|
||||||
|
if (!$ok) {
|
||||||
|
error $err
|
||||||
|
$cached = cache_path $app $version $url
|
||||||
|
if (Test-Path $cached) {
|
||||||
|
# rm cached file
|
||||||
|
Remove-Item -Force $cached
|
||||||
|
}
|
||||||
|
if ($url.Contains('sourceforge.net')) {
|
||||||
|
Write-Host -ForegroundColor Yellow 'SourceForge.net is known for causing hash validation fails. Please try again before opening a ticket.'
|
||||||
|
}
|
||||||
|
abort $(new_issue_msg $app $bucket 'hash check failed')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $urls.ForEach({ url_filename $_ })
|
||||||
|
}
|
||||||
|
|
||||||
|
## [System.Net] downloader
|
||||||
|
|
||||||
|
function Invoke-CachedDownload ($app, $version, $url, $to, $cookies = $null, $use_cache = $true) {
|
||||||
|
$cached = cache_path $app $version $url
|
||||||
|
|
||||||
|
if (!(Test-Path $cached) -or !$use_cache) {
|
||||||
|
ensure $cachedir | Out-Null
|
||||||
|
Start-Download $url "$cached.download" $cookies
|
||||||
|
Move-Item "$cached.download" $cached -Force
|
||||||
|
} else { Write-Host "Loading $(url_remote_filename $url) from cache" }
|
||||||
|
|
||||||
|
if (!($null -eq $to)) {
|
||||||
|
if ($use_cache) {
|
||||||
|
Copy-Item $cached $to
|
||||||
|
} else {
|
||||||
|
Move-Item $cached $to -Force
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Start-Download ($url, $to, $cookies) {
|
||||||
|
$progress = [console]::isoutputredirected -eq $false -and
|
||||||
|
$host.name -ne 'Windows PowerShell ISE Host'
|
||||||
|
|
||||||
|
try {
|
||||||
|
$url = handle_special_urls $url
|
||||||
|
Invoke-Download $url $to $cookies $progress
|
||||||
|
} catch {
|
||||||
|
$e = $_.exception
|
||||||
|
if ($e.Response.StatusCode -eq 'Unauthorized') {
|
||||||
|
warn 'Token might be misconfigured.'
|
||||||
|
}
|
||||||
|
if ($e.innerexception) { $e = $e.innerexception }
|
||||||
|
throw $e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Invoke-Download ($url, $to, $cookies, $progress) {
|
||||||
|
# download with filesize and progress indicator
|
||||||
|
$reqUrl = ($url -split '#')[0]
|
||||||
|
$wreq = [Net.WebRequest]::Create($reqUrl)
|
||||||
|
if ($wreq -is [Net.HttpWebRequest]) {
|
||||||
|
$wreq.UserAgent = Get-UserAgent
|
||||||
|
if (-not ($url -match 'sourceforge\.net' -or $url -match 'portableapps\.com')) {
|
||||||
|
$wreq.Referer = strip_filename $url
|
||||||
|
}
|
||||||
|
if ($url -match 'api\.github\.com/repos') {
|
||||||
|
$wreq.Accept = 'application/octet-stream'
|
||||||
|
$wreq.Headers['Authorization'] = "Bearer $(Get-GitHubToken)"
|
||||||
|
$wreq.Headers['X-GitHub-Api-Version'] = '2022-11-28'
|
||||||
|
}
|
||||||
|
if ($cookies) {
|
||||||
|
$wreq.Headers.Add('Cookie', (cookie_header $cookies))
|
||||||
|
}
|
||||||
|
|
||||||
|
get_config PRIVATE_HOSTS | Where-Object { $_ -ne $null -and $url -match $_.match } | ForEach-Object {
|
||||||
|
(ConvertFrom-StringData -StringData $_.Headers).GetEnumerator() | ForEach-Object {
|
||||||
|
$wreq.Headers[$_.Key] = $_.Value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$wres = $wreq.GetResponse()
|
||||||
|
} catch [System.Net.WebException] {
|
||||||
|
$exc = $_.Exception
|
||||||
|
$handledCodes = @(
|
||||||
|
[System.Net.HttpStatusCode]::MovedPermanently, # HTTP 301
|
||||||
|
[System.Net.HttpStatusCode]::Found, # HTTP 302
|
||||||
|
[System.Net.HttpStatusCode]::SeeOther, # HTTP 303
|
||||||
|
[System.Net.HttpStatusCode]::TemporaryRedirect # HTTP 307
|
||||||
|
)
|
||||||
|
|
||||||
|
# Only handle redirection codes
|
||||||
|
$redirectRes = $exc.Response
|
||||||
|
if ($handledCodes -notcontains $redirectRes.StatusCode) {
|
||||||
|
throw $exc
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get the new location of the file
|
||||||
|
if ((-not $redirectRes.Headers) -or ($redirectRes.Headers -notcontains 'Location')) {
|
||||||
|
throw $exc
|
||||||
|
}
|
||||||
|
|
||||||
|
$newUrl = $redirectRes.Headers['Location']
|
||||||
|
info "Following redirect to $newUrl..."
|
||||||
|
|
||||||
|
# Handle manual file rename
|
||||||
|
if ($url -like '*#/*') {
|
||||||
|
$null, $postfix = $url -split '#/'
|
||||||
|
$newUrl = "$newUrl#/$postfix"
|
||||||
|
}
|
||||||
|
|
||||||
|
Invoke-Download $newUrl $to $cookies $progress
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
$total = $wres.ContentLength
|
||||||
|
if ($total -eq -1 -and $wreq -is [net.ftpwebrequest]) {
|
||||||
|
$total = ftp_file_size($url)
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($progress -and ($total -gt 0)) {
|
||||||
|
[console]::CursorVisible = $false
|
||||||
|
function Trace-DownloadProgress ($read) {
|
||||||
|
Write-DownloadProgress $read $total $url
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Write-Host "Downloading $url ($(filesize $total))..."
|
||||||
|
function Trace-DownloadProgress {
|
||||||
|
#no op
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$s = $wres.getresponsestream()
|
||||||
|
$fs = [io.file]::openwrite($to)
|
||||||
|
$buffer = New-Object byte[] 2048
|
||||||
|
$totalRead = 0
|
||||||
|
$sw = [diagnostics.stopwatch]::StartNew()
|
||||||
|
|
||||||
|
Trace-DownloadProgress $totalRead
|
||||||
|
while (($read = $s.read($buffer, 0, $buffer.length)) -gt 0) {
|
||||||
|
$fs.write($buffer, 0, $read)
|
||||||
|
$totalRead += $read
|
||||||
|
if ($sw.elapsedmilliseconds -gt 100) {
|
||||||
|
$sw.restart()
|
||||||
|
Trace-DownloadProgress $totalRead
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$sw.stop()
|
||||||
|
Trace-DownloadProgress $totalRead
|
||||||
|
} finally {
|
||||||
|
if ($progress) {
|
||||||
|
[console]::CursorVisible = $true
|
||||||
|
Write-Host
|
||||||
|
}
|
||||||
|
if ($fs) {
|
||||||
|
$fs.close()
|
||||||
|
}
|
||||||
|
if ($s) {
|
||||||
|
$s.close()
|
||||||
|
}
|
||||||
|
$wres.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Format-DownloadProgress ($url, $read, $total, $console) {
|
||||||
|
$filename = url_remote_filename $url
|
||||||
|
|
||||||
|
# calculate current percentage done
|
||||||
|
$p = [math]::Round($read / $total * 100, 0)
|
||||||
|
|
||||||
|
# pre-generate LHS and RHS of progress string
|
||||||
|
# so we know how much space we have
|
||||||
|
$left = "$filename ($(filesize $total))"
|
||||||
|
$right = [string]::Format('{0,3}%', $p)
|
||||||
|
|
||||||
|
# calculate remaining width for progress bar
|
||||||
|
$midwidth = $console.BufferSize.Width - ($left.Length + $right.Length + 8)
|
||||||
|
|
||||||
|
# calculate how many characters are completed
|
||||||
|
$completed = [math]::Abs([math]::Round(($p / 100) * $midwidth, 0) - 1)
|
||||||
|
|
||||||
|
# generate dashes to symbolise completed
|
||||||
|
if ($completed -gt 1) {
|
||||||
|
$dashes = [string]::Join('', ((1..$completed) | ForEach-Object { '=' }))
|
||||||
|
}
|
||||||
|
|
||||||
|
# this is why we calculate $completed - 1 above
|
||||||
|
$dashes += switch ($p) {
|
||||||
|
100 { '=' }
|
||||||
|
default { '>' }
|
||||||
|
}
|
||||||
|
|
||||||
|
# the remaining characters are filled with spaces
|
||||||
|
$spaces = switch ($dashes.Length) {
|
||||||
|
$midwidth { [string]::Empty }
|
||||||
|
default {
|
||||||
|
[string]::Join('', ((1..($midwidth - $dashes.Length)) | ForEach-Object { ' ' }))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
"$left [$dashes$spaces] $right"
|
||||||
|
}
|
||||||
|
|
||||||
|
function Write-DownloadProgress ($read, $total, $url) {
|
||||||
|
$console = $Host.UI.RawUI
|
||||||
|
$left = $console.CursorPosition.X
|
||||||
|
$top = $console.CursorPosition.Y
|
||||||
|
$width = $console.BufferSize.Width
|
||||||
|
|
||||||
|
if ($read -eq 0) {
|
||||||
|
$maxOutputLength = $(Format-DownloadProgress $url 100 $total $console).Length
|
||||||
|
if (($left + $maxOutputLength) -gt $width) {
|
||||||
|
# not enough room to print progress on this line
|
||||||
|
# print on new line
|
||||||
|
Write-Host
|
||||||
|
$left = 0
|
||||||
|
$top = $top + 1
|
||||||
|
if ($top -gt $console.CursorPosition.Y) { $top = $console.CursorPosition.Y }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host $(Format-DownloadProgress $url $read $total $console) -NoNewline
|
||||||
|
[console]::SetCursorPosition($left, $top)
|
||||||
|
}
|
||||||
|
|
||||||
|
## Aria2 downloader
|
||||||
|
|
||||||
|
function Test-Aria2Enabled {
|
||||||
|
return (Test-HelperInstalled -Helper Aria2) -and (get_config 'aria2-enabled' $true)
|
||||||
|
}
|
||||||
|
|
||||||
|
function aria_exit_code($exitcode) {
|
||||||
|
$codes = @{
|
||||||
|
0 = 'All downloads were successful'
|
||||||
|
1 = 'An unknown error occurred'
|
||||||
|
2 = 'Timeout'
|
||||||
|
3 = 'Resource was not found'
|
||||||
|
4 = 'Aria2 saw the specified number of "resource not found" error. See --max-file-not-found option'
|
||||||
|
5 = 'Download aborted because download speed was too slow. See --lowest-speed-limit option'
|
||||||
|
6 = 'Network problem occurred.'
|
||||||
|
7 = 'There were unfinished downloads. This error is only reported if all finished downloads were successful and there were unfinished downloads in a queue when aria2 exited by pressing Ctrl-C by an user or sending TERM or INT signal'
|
||||||
|
8 = 'Remote server did not support resume when resume was required to complete download'
|
||||||
|
9 = 'There was not enough disk space available'
|
||||||
|
10 = 'Piece length was different from one in .aria2 control file. See --allow-piece-length-change option'
|
||||||
|
11 = 'Aria2 was downloading same file at that moment'
|
||||||
|
12 = 'Aria2 was downloading same info hash torrent at that moment'
|
||||||
|
13 = 'File already existed. See --allow-overwrite option'
|
||||||
|
14 = 'Renaming file failed. See --auto-file-renaming option'
|
||||||
|
15 = 'Aria2 could not open existing file'
|
||||||
|
16 = 'Aria2 could not create new file or truncate existing file'
|
||||||
|
17 = 'File I/O error occurred'
|
||||||
|
18 = 'Aria2 could not create directory'
|
||||||
|
19 = 'Name resolution failed'
|
||||||
|
20 = 'Aria2 could not parse Metalink document'
|
||||||
|
21 = 'FTP command failed'
|
||||||
|
22 = 'HTTP response header was bad or unexpected'
|
||||||
|
23 = 'Too many redirects occurred'
|
||||||
|
24 = 'HTTP authorization failed'
|
||||||
|
25 = 'Aria2 could not parse bencoded file (usually ".torrent" file)'
|
||||||
|
26 = '".torrent" file was corrupted or missing information that aria2 needed'
|
||||||
|
27 = 'Magnet URI was bad'
|
||||||
|
28 = 'Bad/unrecognized option was given or unexpected option argument was given'
|
||||||
|
29 = 'The remote server was unable to handle the request due to a temporary overloading or maintenance'
|
||||||
|
30 = 'Aria2 could not parse JSON-RPC request'
|
||||||
|
31 = 'Reserved. Not used'
|
||||||
|
32 = 'Checksum validation failed'
|
||||||
|
}
|
||||||
|
if ($null -eq $codes[$exitcode]) {
|
||||||
|
return 'An unknown error occurred'
|
||||||
|
}
|
||||||
|
return $codes[$exitcode]
|
||||||
|
}
|
||||||
|
|
||||||
|
function get_filename_from_metalink($file) {
|
||||||
|
$bytes = get_magic_bytes_pretty $file ''
|
||||||
|
# check if file starts with '<?xml'
|
||||||
|
if (!($bytes.StartsWith('3c3f786d6c'))) {
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
|
||||||
|
# Add System.Xml for reading metalink files
|
||||||
|
Add-Type -AssemblyName 'System.Xml'
|
||||||
|
$xr = [System.Xml.XmlReader]::Create($file)
|
||||||
|
$filename = $null
|
||||||
|
try {
|
||||||
|
$xr.ReadStartElement('metalink')
|
||||||
|
if ($xr.ReadToFollowing('file') -and $xr.MoveToFirstAttribute()) {
|
||||||
|
$filename = $xr.Value
|
||||||
|
}
|
||||||
|
} catch [System.Xml.XmlException] {
|
||||||
|
return $null
|
||||||
|
} finally {
|
||||||
|
$xr.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
return $filename
|
||||||
|
}
|
||||||
|
|
||||||
|
function Invoke-CachedAria2Download ($app, $version, $manifest, $architecture, $dir, $cookies = $null, $use_cache = $true, $check_hash = $true) {
|
||||||
|
$data = @{}
|
||||||
|
$urls = @(script:url $manifest $architecture)
|
||||||
|
|
||||||
|
# aria2 input file
|
||||||
|
$urlstxt = Join-Path $cachedir "$app.txt"
|
||||||
|
$urlstxt_content = ''
|
||||||
|
$download_finished = $true
|
||||||
|
|
||||||
|
# aria2 options
|
||||||
|
$options = @(
|
||||||
|
"--input-file='$urlstxt'"
|
||||||
|
"--user-agent='$(Get-UserAgent)'"
|
||||||
|
'--allow-overwrite=true'
|
||||||
|
'--auto-file-renaming=false'
|
||||||
|
"--retry-wait=$(get_config 'aria2-retry-wait' 2)"
|
||||||
|
"--split=$(get_config 'aria2-split' 5)"
|
||||||
|
"--max-connection-per-server=$(get_config 'aria2-max-connection-per-server' 5)"
|
||||||
|
"--min-split-size=$(get_config 'aria2-min-split-size' '5M')"
|
||||||
|
'--console-log-level=warn'
|
||||||
|
'--enable-color=false'
|
||||||
|
'--no-conf=true'
|
||||||
|
'--follow-metalink=true'
|
||||||
|
'--metalink-preferred-protocol=https'
|
||||||
|
'--min-tls-version=TLSv1.2'
|
||||||
|
"--stop-with-process=$PID"
|
||||||
|
'--continue'
|
||||||
|
'--summary-interval=0'
|
||||||
|
'--auto-save-interval=1'
|
||||||
|
)
|
||||||
|
|
||||||
|
if ($cookies) {
|
||||||
|
$options += "--header='Cookie: $(cookie_header $cookies)'"
|
||||||
|
}
|
||||||
|
|
||||||
|
$proxy = get_config PROXY
|
||||||
|
if ($proxy -ne 'none') {
|
||||||
|
if ([Net.Webrequest]::DefaultWebProxy.Address) {
|
||||||
|
$options += "--all-proxy='$([Net.Webrequest]::DefaultWebProxy.Address.Authority)'"
|
||||||
|
}
|
||||||
|
if ([Net.Webrequest]::DefaultWebProxy.Credentials.UserName) {
|
||||||
|
$options += "--all-proxy-user='$([Net.Webrequest]::DefaultWebProxy.Credentials.UserName)'"
|
||||||
|
}
|
||||||
|
if ([Net.Webrequest]::DefaultWebProxy.Credentials.Password) {
|
||||||
|
$options += "--all-proxy-passwd='$([Net.Webrequest]::DefaultWebProxy.Credentials.Password)'"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$more_options = get_config 'aria2-options'
|
||||||
|
if ($more_options) {
|
||||||
|
$options += $more_options
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($url in $urls) {
|
||||||
|
$data.$url = @{
|
||||||
|
'target' = Join-Path $dir (url_filename $url)
|
||||||
|
'cachename' = fname (cache_path $app $version $url)
|
||||||
|
'source' = cache_path $app $version $url
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((Test-Path $data.$url.source) -and -not((Test-Path "$($data.$url.source).aria2") -or (Test-Path $urlstxt)) -and $use_cache) {
|
||||||
|
Write-Host 'Loading ' -NoNewline
|
||||||
|
Write-Host $(url_remote_filename $url) -ForegroundColor Cyan -NoNewline
|
||||||
|
Write-Host ' from cache.'
|
||||||
|
} else {
|
||||||
|
$download_finished = $false
|
||||||
|
# create aria2 input file content
|
||||||
|
try {
|
||||||
|
$try_url = handle_special_urls $url
|
||||||
|
} catch {
|
||||||
|
if ($_.Exception.Response.StatusCode -eq 'Unauthorized') {
|
||||||
|
warn 'Token might be misconfigured.'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$urlstxt_content += "$try_url`n"
|
||||||
|
if (!$url.Contains('sourceforge.net')) {
|
||||||
|
$urlstxt_content += " referer=$(strip_filename $url)`n"
|
||||||
|
}
|
||||||
|
$urlstxt_content += " dir=$cachedir`n"
|
||||||
|
$urlstxt_content += " out=$($data.$url.cachename)`n"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not($download_finished)) {
|
||||||
|
# write aria2 input file
|
||||||
|
if ($urlstxt_content -ne '') {
|
||||||
|
ensure $cachedir | Out-Null
|
||||||
|
# Write aria2 input-file with UTF8NoBOM encoding
|
||||||
|
$urlstxt_content | Out-UTF8File -FilePath $urlstxt
|
||||||
|
}
|
||||||
|
|
||||||
|
# build aria2 command
|
||||||
|
$aria2 = "& '$(Get-HelperPath -Helper Aria2)' $($options -join ' ')"
|
||||||
|
|
||||||
|
# handle aria2 console output
|
||||||
|
Write-Host 'Starting download with aria2 ...'
|
||||||
|
|
||||||
|
# Set console output encoding to UTF8 for non-ASCII characters printing
|
||||||
|
$oriConsoleEncoding = [Console]::OutputEncoding
|
||||||
|
[Console]::OutputEncoding = New-Object System.Text.UTF8Encoding
|
||||||
|
|
||||||
|
Invoke-Command ([scriptblock]::Create($aria2)) | ForEach-Object {
|
||||||
|
# Skip blank lines
|
||||||
|
if ([String]::IsNullOrWhiteSpace($_)) { return }
|
||||||
|
|
||||||
|
# Prevent potential overlaping of text when one line is shorter
|
||||||
|
$len = $Host.UI.RawUI.WindowSize.Width - $_.Length - 20
|
||||||
|
$blank = if ($len -gt 0) { ' ' * $len } else { '' }
|
||||||
|
$color = 'Gray'
|
||||||
|
|
||||||
|
if ($_.StartsWith('(OK):')) {
|
||||||
|
$noNewLine = $true
|
||||||
|
$color = 'Green'
|
||||||
|
} elseif ($_.StartsWith('[') -and $_.EndsWith(']')) {
|
||||||
|
$noNewLine = $true
|
||||||
|
$color = 'Cyan'
|
||||||
|
} elseif ($_.StartsWith('Download Results:')) {
|
||||||
|
$noNewLine = $false
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "`rDownload: $_$blank" -ForegroundColor $color -NoNewline:$noNewLine
|
||||||
|
}
|
||||||
|
Write-Host ''
|
||||||
|
|
||||||
|
if ($lastexitcode -gt 0) {
|
||||||
|
error "Download failed! (Error $lastexitcode) $(aria_exit_code $lastexitcode)"
|
||||||
|
error $urlstxt_content
|
||||||
|
error $aria2
|
||||||
|
abort $(new_issue_msg $app $bucket 'download via aria2 failed')
|
||||||
|
}
|
||||||
|
|
||||||
|
# remove aria2 input file when done
|
||||||
|
if (Test-Path $urlstxt, "$($data.$url.source).aria2*") {
|
||||||
|
Remove-Item $urlstxt -Force -ErrorAction SilentlyContinue
|
||||||
|
Remove-Item "$($data.$url.source).aria2*" -Force -ErrorAction SilentlyContinue
|
||||||
|
}
|
||||||
|
|
||||||
|
# Revert console encoding
|
||||||
|
[Console]::OutputEncoding = $oriConsoleEncoding
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($url in $urls) {
|
||||||
|
|
||||||
|
$metalink_filename = get_filename_from_metalink $data.$url.source
|
||||||
|
if ($metalink_filename) {
|
||||||
|
Remove-Item $data.$url.source -Force
|
||||||
|
Rename-Item -Force (Join-Path -Path $cachedir -ChildPath $metalink_filename) $data.$url.source
|
||||||
|
}
|
||||||
|
|
||||||
|
# run hash checks
|
||||||
|
if ($check_hash) {
|
||||||
|
$manifest_hash = hash_for_url $manifest $url $architecture
|
||||||
|
$ok, $err = check_hash $data.$url.source $manifest_hash $(show_app $app $bucket)
|
||||||
|
if (!$ok) {
|
||||||
|
error $err
|
||||||
|
if (Test-Path $data.$url.source) {
|
||||||
|
# rm cached file
|
||||||
|
Remove-Item $data.$url.source -Force -ErrorAction SilentlyContinue
|
||||||
|
Remove-Item "$($data.$url.source).aria2*" -Force -ErrorAction SilentlyContinue
|
||||||
|
}
|
||||||
|
if ($url.Contains('sourceforge.net')) {
|
||||||
|
Write-Host -f yellow 'SourceForge.net is known for causing hash validation fails. Please try again before opening a ticket.'
|
||||||
|
}
|
||||||
|
abort $(new_issue_msg $app $bucket 'hash check failed')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# copy or move file to target location
|
||||||
|
if (!(Test-Path $data.$url.source) ) {
|
||||||
|
abort $(new_issue_msg $app $bucket 'cached file not found')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!($dir -eq $cachedir)) {
|
||||||
|
if ($use_cache) {
|
||||||
|
Copy-Item $data.$url.source $data.$url.target
|
||||||
|
} else {
|
||||||
|
Move-Item $data.$url.source $data.$url.target -Force
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
## Helper functions
|
||||||
|
|
||||||
|
### Downloader parameters
|
||||||
|
|
||||||
|
function cookie_header($cookies) {
|
||||||
|
if (!$cookies) { return }
|
||||||
|
|
||||||
|
$vals = $cookies.psobject.properties | ForEach-Object {
|
||||||
|
"$($_.name)=$($_.value)"
|
||||||
|
}
|
||||||
|
|
||||||
|
[string]::join(';', $vals)
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-Encoding($wc) {
|
||||||
|
if ($null -ne $wc.ResponseHeaders -and $wc.ResponseHeaders['Content-Type'] -match 'charset=([^;]*)') {
|
||||||
|
return [System.Text.Encoding]::GetEncoding($Matches[1])
|
||||||
|
} else {
|
||||||
|
return [System.Text.Encoding]::GetEncoding('utf-8')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-UserAgent() {
|
||||||
|
return "Scoop/1.0 (+http://scoop.sh/) PowerShell/$($PSVersionTable.PSVersion.Major).$($PSVersionTable.PSVersion.Minor) (Windows NT $([System.Environment]::OSVersion.Version.Major).$([System.Environment]::OSVersion.Version.Minor); $(if(${env:ProgramFiles(Arm)}){'ARM64; '}elseif($env:PROCESSOR_ARCHITECTURE -eq 'AMD64'){'Win64; x64; '})$(if($env:PROCESSOR_ARCHITEW6432 -in 'AMD64','ARM64'){'WOW64; '})$PSEdition)"
|
||||||
|
}
|
||||||
|
|
||||||
|
function setup_proxy() {
|
||||||
|
# note: '@' and ':' in password must be escaped, e.g. 'p@ssword' -> p\@ssword'
|
||||||
|
$proxy = get_config PROXY
|
||||||
|
if (!$proxy) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
$credentials, $address = $proxy -split '(?<!\\)@'
|
||||||
|
if (!$address) {
|
||||||
|
$address, $credentials = $credentials, $null # no credentials supplied
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($address -eq 'none') {
|
||||||
|
[net.webrequest]::defaultwebproxy = $null
|
||||||
|
} elseif ($address -ne 'default') {
|
||||||
|
[net.webrequest]::defaultwebproxy = New-Object net.webproxy "http://$address"
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($credentials -eq 'currentuser') {
|
||||||
|
[net.webrequest]::defaultwebproxy.credentials = [net.credentialcache]::defaultcredentials
|
||||||
|
} elseif ($credentials) {
|
||||||
|
$username, $password = $credentials -split '(?<!\\):' | ForEach-Object { $_ -replace '\\([@:])', '$1' }
|
||||||
|
[net.webrequest]::defaultwebproxy.credentials = New-Object net.networkcredential($username, $password)
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
warn "Failed to use proxy '$proxy': $($_.exception.message)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-GitHubToken {
|
||||||
|
return $env:SCOOP_GH_TOKEN, (get_config GH_TOKEN) | Where-Object -Property Length -Value 0 -GT | Select-Object -First 1
|
||||||
|
}
|
||||||
|
|
||||||
|
function github_ratelimit_reached {
|
||||||
|
$api_link = 'https://api.github.com/rate_limit'
|
||||||
|
$ret = (download_json $api_link).rate.remaining -eq 0
|
||||||
|
if ($ret) {
|
||||||
|
Write-Host "GitHub API rate limit reached.`r`nPlease try again later or configure your API token using 'scoop config gh_token <your token>'."
|
||||||
|
}
|
||||||
|
$ret
|
||||||
|
}
|
||||||
|
|
||||||
|
### URL handling
|
||||||
|
|
||||||
|
function handle_special_urls($url) {
|
||||||
|
# FossHub.com
|
||||||
|
if ($url -match '^(?:.*fosshub.com\/)(?<name>.*)(?:\/|\?dwl=)(?<filename>.*)$') {
|
||||||
|
$Body = @{
|
||||||
|
projectUri = $Matches.name
|
||||||
|
fileName = $Matches.filename
|
||||||
|
source = 'CF'
|
||||||
|
isLatestVersion = $true
|
||||||
|
}
|
||||||
|
if ((Invoke-RestMethod -Uri $url) -match '"p":"(?<pid>[a-f0-9]{24}).*?"r":"(?<rid>[a-f0-9]{24})') {
|
||||||
|
$Body.Add('projectId', $Matches.pid)
|
||||||
|
$Body.Add('releaseId', $Matches.rid)
|
||||||
|
}
|
||||||
|
$url = Invoke-RestMethod -Method Post -Uri 'https://api.fosshub.com/download/' -ContentType 'application/json' -Body (ConvertTo-Json $Body -Compress)
|
||||||
|
if ($null -eq $url.error) {
|
||||||
|
$url = $url.data.url
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Sourceforge.net
|
||||||
|
if ($url -match '(?:downloads\.)?sourceforge.net\/projects?\/(?<project>[^\/]+)\/(?:files\/)?(?<file>.*?)(?:$|\/download|\?)') {
|
||||||
|
# Reshapes the URL to avoid redirections
|
||||||
|
$url = "https://downloads.sourceforge.net/project/$($matches['project'])/$($matches['file'])"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Github.com
|
||||||
|
if ($url -match 'github.com/(?<owner>[^/]+)/(?<repo>[^/]+)/releases/download/(?<tag>[^/]+)/(?<file>[^/#]+)(?<filename>.*)' -and ($token = Get-GitHubToken)) {
|
||||||
|
$headers = @{ 'Authorization' = "token $token" }
|
||||||
|
$privateUrl = "https://api.github.com/repos/$($Matches.owner)/$($Matches.repo)"
|
||||||
|
$assetUrl = "https://api.github.com/repos/$($Matches.owner)/$($Matches.repo)/releases/tags/$($Matches.tag)"
|
||||||
|
|
||||||
|
if ((Invoke-RestMethod -Uri $privateUrl -Headers $headers).Private) {
|
||||||
|
$url = ((Invoke-RestMethod -Uri $assetUrl -Headers $headers).Assets | Where-Object -Property Name -EQ -Value $Matches.file).Url, $Matches.filename -join ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $url
|
||||||
|
}
|
||||||
|
|
||||||
|
### Remote file information
|
||||||
|
|
||||||
|
function download_json($url) {
|
||||||
|
$githubtoken = Get-GitHubToken
|
||||||
|
$authheader = @{}
|
||||||
|
if ($githubtoken) {
|
||||||
|
$authheader = @{'Authorization' = "token $githubtoken" }
|
||||||
|
}
|
||||||
|
$ProgressPreference = 'SilentlyContinue'
|
||||||
|
$result = Invoke-WebRequest $url -UseBasicParsing -Headers $authheader | Select-Object -ExpandProperty content | ConvertFrom-Json
|
||||||
|
$ProgressPreference = 'Continue'
|
||||||
|
$result
|
||||||
|
}
|
||||||
|
|
||||||
|
function get_magic_bytes($file) {
|
||||||
|
if (!(Test-Path $file)) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((Get-Command Get-Content).parameters.ContainsKey('AsByteStream')) {
|
||||||
|
# PowerShell Core (6.0+) '-Encoding byte' is replaced by '-AsByteStream'
|
||||||
|
return Get-Content $file -AsByteStream -TotalCount 8
|
||||||
|
} else {
|
||||||
|
return Get-Content $file -Encoding byte -TotalCount 8
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function get_magic_bytes_pretty($file, $glue = ' ') {
|
||||||
|
if (!(Test-Path $file)) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
return (get_magic_bytes $file | ForEach-Object { $_.ToString('x2') }) -join $glue
|
||||||
|
}
|
||||||
|
|
||||||
|
Function Get-RemoteFileSize ($Uri) {
|
||||||
|
$response = Invoke-WebRequest -Uri $Uri -Method HEAD -UseBasicParsing
|
||||||
|
if (!$response.Headers.StatusCode) {
|
||||||
|
$response.Headers.'Content-Length' | ForEach-Object { [int]$_ }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function ftp_file_size($url) {
|
||||||
|
$request = [net.ftpwebrequest]::create($url)
|
||||||
|
$request.method = [net.webrequestmethods+ftp]::getfilesize
|
||||||
|
$request.getresponse().contentlength
|
||||||
|
}
|
||||||
|
|
||||||
|
function url_filename($url) {
|
||||||
|
(Split-Path $url -Leaf).split('?') | Select-Object -First 1
|
||||||
|
}
|
||||||
|
|
||||||
|
function url_remote_filename($url) {
|
||||||
|
# Unlike url_filename which can be tricked by appending a
|
||||||
|
# URL fragment (e.g. #/dl.7z, useful for coercing a local filename),
|
||||||
|
# this function extracts the original filename from the URL.
|
||||||
|
$uri = (New-Object URI $url)
|
||||||
|
$basename = Split-Path $uri.PathAndQuery -Leaf
|
||||||
|
If ($basename -match '.*[?=]+([\w._-]+)') {
|
||||||
|
$basename = $matches[1]
|
||||||
|
}
|
||||||
|
If (($basename -notlike '*.*') -or ($basename -match '^[v.\d]+$')) {
|
||||||
|
$basename = Split-Path $uri.AbsolutePath -Leaf
|
||||||
|
}
|
||||||
|
If (($basename -notlike '*.*') -and ($uri.Fragment -ne '')) {
|
||||||
|
$basename = $uri.Fragment.Trim('/', '#')
|
||||||
|
}
|
||||||
|
return $basename
|
||||||
|
}
|
||||||
|
|
||||||
|
### Hash-related functions
|
||||||
|
|
||||||
|
function hash_for_url($manifest, $url, $arch) {
|
||||||
|
$hashes = @(hash $manifest $arch) | Where-Object { $_ -ne $null }
|
||||||
|
|
||||||
|
if ($hashes.length -eq 0) { return $null }
|
||||||
|
|
||||||
|
$urls = @(script:url $manifest $arch)
|
||||||
|
|
||||||
|
$index = [array]::IndexOf($urls, $url)
|
||||||
|
if ($index -eq -1) { abort "Couldn't find hash in manifest for '$url'." }
|
||||||
|
|
||||||
|
@($hashes)[$index]
|
||||||
|
}
|
||||||
|
|
||||||
|
function check_hash($file, $hash, $app_name) {
|
||||||
|
# returns (ok, err)
|
||||||
|
if (!$hash) {
|
||||||
|
warn "Warning: No hash in manifest. SHA256 for '$(fname $file)' is:`n $((Get-FileHash -Path $file -Algorithm SHA256).Hash.ToLower())"
|
||||||
|
return $true, $null
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host 'Checking hash of ' -NoNewline
|
||||||
|
Write-Host $(url_remote_filename $url) -ForegroundColor Cyan -NoNewline
|
||||||
|
Write-Host ' ... ' -NoNewline
|
||||||
|
$algorithm, $expected = get_hash $hash
|
||||||
|
if ($null -eq $algorithm) {
|
||||||
|
return $false, "Hash type '$algorithm' isn't supported."
|
||||||
|
}
|
||||||
|
|
||||||
|
$actual = (Get-FileHash -Path $file -Algorithm $algorithm).Hash.ToLower()
|
||||||
|
$expected = $expected.ToLower()
|
||||||
|
|
||||||
|
if ($actual -ne $expected) {
|
||||||
|
$msg = "Hash check failed!`n"
|
||||||
|
$msg += "App: $app_name`n"
|
||||||
|
$msg += "URL: $url`n"
|
||||||
|
if (Test-Path $file) {
|
||||||
|
$msg += "First bytes: $((get_magic_bytes_pretty $file ' ').ToUpper())`n"
|
||||||
|
}
|
||||||
|
if ($expected -or $actual) {
|
||||||
|
$msg += "Expected: $expected`n"
|
||||||
|
$msg += "Actual: $actual"
|
||||||
|
}
|
||||||
|
return $false, $msg
|
||||||
|
}
|
||||||
|
Write-Host 'ok.' -f Green
|
||||||
|
return $true, $null
|
||||||
|
}
|
||||||
|
|
||||||
|
function get_hash([String] $multihash) {
|
||||||
|
$type, $hash = $multihash -split ':'
|
||||||
|
if (!$hash) {
|
||||||
|
# no type specified, assume sha256
|
||||||
|
$type, $hash = 'sha256', $multihash
|
||||||
|
}
|
||||||
|
|
||||||
|
if (@('md5', 'sha1', 'sha256', 'sha512') -notcontains $type) {
|
||||||
|
return $null, "Hash type '$type' isn't supported."
|
||||||
|
}
|
||||||
|
|
||||||
|
return $type, $hash.ToLower()
|
||||||
|
}
|
||||||
|
|
||||||
|
# Setup proxy globally
|
||||||
|
setup_proxy
|
||||||
566
lib/install.ps1
566
lib/install.ps1
@@ -81,576 +81,10 @@ function install_app($app, $architecture, $global, $suggested, $use_cache = $tru
|
|||||||
show_notes $manifest $dir $original_dir $persist_dir
|
show_notes $manifest $dir $original_dir $persist_dir
|
||||||
}
|
}
|
||||||
|
|
||||||
function Invoke-CachedDownload ($app, $version, $url, $to, $cookies = $null, $use_cache = $true) {
|
|
||||||
$cached = cache_path $app $version $url
|
|
||||||
|
|
||||||
if (!(Test-Path $cached) -or !$use_cache) {
|
|
||||||
ensure $cachedir | Out-Null
|
|
||||||
Start-Download $url "$cached.download" $cookies
|
|
||||||
Move-Item "$cached.download" $cached -Force
|
|
||||||
} else { Write-Host "Loading $(url_remote_filename $url) from cache" }
|
|
||||||
|
|
||||||
if (!($null -eq $to)) {
|
|
||||||
if ($use_cache) {
|
|
||||||
Copy-Item $cached $to
|
|
||||||
} else {
|
|
||||||
Move-Item $cached $to -Force
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function Start-Download ($url, $to, $cookies) {
|
|
||||||
$progress = [console]::isoutputredirected -eq $false -and
|
|
||||||
$host.name -ne 'Windows PowerShell ISE Host'
|
|
||||||
|
|
||||||
try {
|
|
||||||
$url = handle_special_urls $url
|
|
||||||
Invoke-Download $url $to $cookies $progress
|
|
||||||
} catch {
|
|
||||||
$e = $_.exception
|
|
||||||
if ($e.Response.StatusCode -eq 'Unauthorized') {
|
|
||||||
warn 'Token might be misconfigured.'
|
|
||||||
}
|
|
||||||
if ($e.innerexception) { $e = $e.innerexception }
|
|
||||||
throw $e
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function aria_exit_code($exitcode) {
|
|
||||||
$codes = @{
|
|
||||||
0 = 'All downloads were successful'
|
|
||||||
1 = 'An unknown error occurred'
|
|
||||||
2 = 'Timeout'
|
|
||||||
3 = 'Resource was not found'
|
|
||||||
4 = 'Aria2 saw the specified number of "resource not found" error. See --max-file-not-found option'
|
|
||||||
5 = 'Download aborted because download speed was too slow. See --lowest-speed-limit option'
|
|
||||||
6 = 'Network problem occurred.'
|
|
||||||
7 = 'There were unfinished downloads. This error is only reported if all finished downloads were successful and there were unfinished downloads in a queue when aria2 exited by pressing Ctrl-C by an user or sending TERM or INT signal'
|
|
||||||
8 = 'Remote server did not support resume when resume was required to complete download'
|
|
||||||
9 = 'There was not enough disk space available'
|
|
||||||
10 = 'Piece length was different from one in .aria2 control file. See --allow-piece-length-change option'
|
|
||||||
11 = 'Aria2 was downloading same file at that moment'
|
|
||||||
12 = 'Aria2 was downloading same info hash torrent at that moment'
|
|
||||||
13 = 'File already existed. See --allow-overwrite option'
|
|
||||||
14 = 'Renaming file failed. See --auto-file-renaming option'
|
|
||||||
15 = 'Aria2 could not open existing file'
|
|
||||||
16 = 'Aria2 could not create new file or truncate existing file'
|
|
||||||
17 = 'File I/O error occurred'
|
|
||||||
18 = 'Aria2 could not create directory'
|
|
||||||
19 = 'Name resolution failed'
|
|
||||||
20 = 'Aria2 could not parse Metalink document'
|
|
||||||
21 = 'FTP command failed'
|
|
||||||
22 = 'HTTP response header was bad or unexpected'
|
|
||||||
23 = 'Too many redirects occurred'
|
|
||||||
24 = 'HTTP authorization failed'
|
|
||||||
25 = 'Aria2 could not parse bencoded file (usually ".torrent" file)'
|
|
||||||
26 = '".torrent" file was corrupted or missing information that aria2 needed'
|
|
||||||
27 = 'Magnet URI was bad'
|
|
||||||
28 = 'Bad/unrecognized option was given or unexpected option argument was given'
|
|
||||||
29 = 'The remote server was unable to handle the request due to a temporary overloading or maintenance'
|
|
||||||
30 = 'Aria2 could not parse JSON-RPC request'
|
|
||||||
31 = 'Reserved. Not used'
|
|
||||||
32 = 'Checksum validation failed'
|
|
||||||
}
|
|
||||||
if ($null -eq $codes[$exitcode]) {
|
|
||||||
return 'An unknown error occurred'
|
|
||||||
}
|
|
||||||
return $codes[$exitcode]
|
|
||||||
}
|
|
||||||
|
|
||||||
function get_filename_from_metalink($file) {
|
|
||||||
$bytes = get_magic_bytes_pretty $file ''
|
|
||||||
# check if file starts with '<?xml'
|
|
||||||
if (!($bytes.StartsWith('3c3f786d6c'))) {
|
|
||||||
return $null
|
|
||||||
}
|
|
||||||
|
|
||||||
# Add System.Xml for reading metalink files
|
|
||||||
Add-Type -AssemblyName 'System.Xml'
|
|
||||||
$xr = [System.Xml.XmlReader]::Create($file)
|
|
||||||
$filename = $null
|
|
||||||
try {
|
|
||||||
$xr.ReadStartElement('metalink')
|
|
||||||
if ($xr.ReadToFollowing('file') -and $xr.MoveToFirstAttribute()) {
|
|
||||||
$filename = $xr.Value
|
|
||||||
}
|
|
||||||
} catch [System.Xml.XmlException] {
|
|
||||||
return $null
|
|
||||||
} finally {
|
|
||||||
$xr.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
return $filename
|
|
||||||
}
|
|
||||||
|
|
||||||
function Invoke-CachedAria2Download ($app, $version, $manifest, $architecture, $dir, $cookies = $null, $use_cache = $true, $check_hash = $true) {
|
|
||||||
$data = @{}
|
|
||||||
$urls = @(script:url $manifest $architecture)
|
|
||||||
|
|
||||||
# aria2 input file
|
|
||||||
$urlstxt = Join-Path $cachedir "$app.txt"
|
|
||||||
$urlstxt_content = ''
|
|
||||||
$download_finished = $true
|
|
||||||
|
|
||||||
# aria2 options
|
|
||||||
$options = @(
|
|
||||||
"--input-file='$urlstxt'"
|
|
||||||
"--user-agent='$(Get-UserAgent)'"
|
|
||||||
'--allow-overwrite=true'
|
|
||||||
'--auto-file-renaming=false'
|
|
||||||
"--retry-wait=$(get_config 'aria2-retry-wait' 2)"
|
|
||||||
"--split=$(get_config 'aria2-split' 5)"
|
|
||||||
"--max-connection-per-server=$(get_config 'aria2-max-connection-per-server' 5)"
|
|
||||||
"--min-split-size=$(get_config 'aria2-min-split-size' '5M')"
|
|
||||||
'--console-log-level=warn'
|
|
||||||
'--enable-color=false'
|
|
||||||
'--no-conf=true'
|
|
||||||
'--follow-metalink=true'
|
|
||||||
'--metalink-preferred-protocol=https'
|
|
||||||
'--min-tls-version=TLSv1.2'
|
|
||||||
"--stop-with-process=$PID"
|
|
||||||
'--continue'
|
|
||||||
'--summary-interval=0'
|
|
||||||
'--auto-save-interval=1'
|
|
||||||
)
|
|
||||||
|
|
||||||
if ($cookies) {
|
|
||||||
$options += "--header='Cookie: $(cookie_header $cookies)'"
|
|
||||||
}
|
|
||||||
|
|
||||||
$proxy = get_config PROXY
|
|
||||||
if ($proxy -ne 'none') {
|
|
||||||
if ([Net.Webrequest]::DefaultWebProxy.Address) {
|
|
||||||
$options += "--all-proxy='$([Net.Webrequest]::DefaultWebProxy.Address.Authority)'"
|
|
||||||
}
|
|
||||||
if ([Net.Webrequest]::DefaultWebProxy.Credentials.UserName) {
|
|
||||||
$options += "--all-proxy-user='$([Net.Webrequest]::DefaultWebProxy.Credentials.UserName)'"
|
|
||||||
}
|
|
||||||
if ([Net.Webrequest]::DefaultWebProxy.Credentials.Password) {
|
|
||||||
$options += "--all-proxy-passwd='$([Net.Webrequest]::DefaultWebProxy.Credentials.Password)'"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$more_options = get_config 'aria2-options'
|
|
||||||
if ($more_options) {
|
|
||||||
$options += $more_options
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach ($url in $urls) {
|
|
||||||
$data.$url = @{
|
|
||||||
'target' = Join-Path $dir (url_filename $url)
|
|
||||||
'cachename' = fname (cache_path $app $version $url)
|
|
||||||
'source' = cache_path $app $version $url
|
|
||||||
}
|
|
||||||
|
|
||||||
if ((Test-Path $data.$url.source) -and -not((Test-Path "$($data.$url.source).aria2") -or (Test-Path $urlstxt)) -and $use_cache) {
|
|
||||||
Write-Host 'Loading ' -NoNewline
|
|
||||||
Write-Host $(url_remote_filename $url) -f Cyan -NoNewline
|
|
||||||
Write-Host ' from cache.'
|
|
||||||
} else {
|
|
||||||
$download_finished = $false
|
|
||||||
# create aria2 input file content
|
|
||||||
try {
|
|
||||||
$try_url = handle_special_urls $url
|
|
||||||
} catch {
|
|
||||||
if ($_.Exception.Response.StatusCode -eq 'Unauthorized') {
|
|
||||||
warn 'Token might be misconfigured.'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
$urlstxt_content += "$try_url`n"
|
|
||||||
if (!$url.Contains('sourceforge.net')) {
|
|
||||||
$urlstxt_content += " referer=$(strip_filename $url)`n"
|
|
||||||
}
|
|
||||||
$urlstxt_content += " dir=$cachedir`n"
|
|
||||||
$urlstxt_content += " out=$($data.$url.cachename)`n"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (-not($download_finished)) {
|
|
||||||
# write aria2 input file
|
|
||||||
if ($urlstxt_content -ne '') {
|
|
||||||
ensure $cachedir | Out-Null
|
|
||||||
# Write aria2 input-file with UTF8NoBOM encoding
|
|
||||||
$urlstxt_content | Out-UTF8File -FilePath $urlstxt
|
|
||||||
}
|
|
||||||
|
|
||||||
# build aria2 command
|
|
||||||
$aria2 = "& '$(Get-HelperPath -Helper Aria2)' $($options -join ' ')"
|
|
||||||
|
|
||||||
# handle aria2 console output
|
|
||||||
Write-Host 'Starting download with aria2 ...'
|
|
||||||
|
|
||||||
# Set console output encoding to UTF8 for non-ASCII characters printing
|
|
||||||
$oriConsoleEncoding = [Console]::OutputEncoding
|
|
||||||
[Console]::OutputEncoding = New-Object System.Text.UTF8Encoding
|
|
||||||
|
|
||||||
Invoke-Command ([scriptblock]::Create($aria2)) | ForEach-Object {
|
|
||||||
# Skip blank lines
|
|
||||||
if ([String]::IsNullOrWhiteSpace($_)) { return }
|
|
||||||
|
|
||||||
# Prevent potential overlaping of text when one line is shorter
|
|
||||||
$len = $Host.UI.RawUI.WindowSize.Width - $_.Length - 20
|
|
||||||
$blank = if ($len -gt 0) { ' ' * $len } else { '' }
|
|
||||||
$color = 'Gray'
|
|
||||||
|
|
||||||
if ($_.StartsWith('(OK):')) {
|
|
||||||
$noNewLine = $true
|
|
||||||
$color = 'Green'
|
|
||||||
} elseif ($_.StartsWith('[') -and $_.EndsWith(']')) {
|
|
||||||
$noNewLine = $true
|
|
||||||
$color = 'Cyan'
|
|
||||||
} elseif ($_.StartsWith('Download Results:')) {
|
|
||||||
$noNewLine = $false
|
|
||||||
}
|
|
||||||
|
|
||||||
Write-Host "`rDownload: $_$blank" -ForegroundColor $color -NoNewline:$noNewLine
|
|
||||||
}
|
|
||||||
Write-Host ''
|
|
||||||
|
|
||||||
if ($lastexitcode -gt 0) {
|
|
||||||
error "Download failed! (Error $lastexitcode) $(aria_exit_code $lastexitcode)"
|
|
||||||
error $urlstxt_content
|
|
||||||
error $aria2
|
|
||||||
abort $(new_issue_msg $app $bucket 'download via aria2 failed')
|
|
||||||
}
|
|
||||||
|
|
||||||
# remove aria2 input file when done
|
|
||||||
if (Test-Path $urlstxt, "$($data.$url.source).aria2*") {
|
|
||||||
Remove-Item $urlstxt -Force -ErrorAction SilentlyContinue
|
|
||||||
Remove-Item "$($data.$url.source).aria2*" -Force -ErrorAction SilentlyContinue
|
|
||||||
}
|
|
||||||
|
|
||||||
# Revert console encoding
|
|
||||||
[Console]::OutputEncoding = $oriConsoleEncoding
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach ($url in $urls) {
|
|
||||||
|
|
||||||
$metalink_filename = get_filename_from_metalink $data.$url.source
|
|
||||||
if ($metalink_filename) {
|
|
||||||
Remove-Item $data.$url.source -Force
|
|
||||||
Rename-Item -Force (Join-Path -Path $cachedir -ChildPath $metalink_filename) $data.$url.source
|
|
||||||
}
|
|
||||||
|
|
||||||
# run hash checks
|
|
||||||
if ($check_hash) {
|
|
||||||
$manifest_hash = hash_for_url $manifest $url $architecture
|
|
||||||
$ok, $err = check_hash $data.$url.source $manifest_hash $(show_app $app $bucket)
|
|
||||||
if (!$ok) {
|
|
||||||
error $err
|
|
||||||
if (Test-Path $data.$url.source) {
|
|
||||||
# rm cached file
|
|
||||||
Remove-Item $data.$url.source -Force -ErrorAction SilentlyContinue
|
|
||||||
Remove-Item "$($data.$url.source).aria2*" -Force -ErrorAction SilentlyContinue
|
|
||||||
}
|
|
||||||
if ($url.Contains('sourceforge.net')) {
|
|
||||||
Write-Host -f yellow 'SourceForge.net is known for causing hash validation fails. Please try again before opening a ticket.'
|
|
||||||
}
|
|
||||||
abort $(new_issue_msg $app $bucket 'hash check failed')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# copy or move file to target location
|
|
||||||
if (!(Test-Path $data.$url.source) ) {
|
|
||||||
abort $(new_issue_msg $app $bucket 'cached file not found')
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!($dir -eq $cachedir)) {
|
|
||||||
if ($use_cache) {
|
|
||||||
Copy-Item $data.$url.source $data.$url.target
|
|
||||||
} else {
|
|
||||||
Move-Item $data.$url.source $data.$url.target -Force
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# download with filesize and progress indicator
|
|
||||||
function Invoke-Download ($url, $to, $cookies, $progress) {
|
|
||||||
$reqUrl = ($url -split '#')[0]
|
|
||||||
$wreq = [Net.WebRequest]::Create($reqUrl)
|
|
||||||
if ($wreq -is [Net.HttpWebRequest]) {
|
|
||||||
$wreq.UserAgent = Get-UserAgent
|
|
||||||
if (-not ($url -match 'sourceforge\.net' -or $url -match 'portableapps\.com')) {
|
|
||||||
$wreq.Referer = strip_filename $url
|
|
||||||
}
|
|
||||||
if ($url -match 'api\.github\.com/repos') {
|
|
||||||
$wreq.Accept = 'application/octet-stream'
|
|
||||||
$wreq.Headers['Authorization'] = "Bearer $(Get-GitHubToken)"
|
|
||||||
$wreq.Headers['X-GitHub-Api-Version'] = '2022-11-28'
|
|
||||||
}
|
|
||||||
if ($cookies) {
|
|
||||||
$wreq.Headers.Add('Cookie', (cookie_header $cookies))
|
|
||||||
}
|
|
||||||
|
|
||||||
get_config PRIVATE_HOSTS | Where-Object { $_ -ne $null -and $url -match $_.match } | ForEach-Object {
|
|
||||||
(ConvertFrom-StringData -StringData $_.Headers).GetEnumerator() | ForEach-Object {
|
|
||||||
$wreq.Headers[$_.Key] = $_.Value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
$wres = $wreq.GetResponse()
|
|
||||||
} catch [System.Net.WebException] {
|
|
||||||
$exc = $_.Exception
|
|
||||||
$handledCodes = @(
|
|
||||||
[System.Net.HttpStatusCode]::MovedPermanently, # HTTP 301
|
|
||||||
[System.Net.HttpStatusCode]::Found, # HTTP 302
|
|
||||||
[System.Net.HttpStatusCode]::SeeOther, # HTTP 303
|
|
||||||
[System.Net.HttpStatusCode]::TemporaryRedirect # HTTP 307
|
|
||||||
)
|
|
||||||
|
|
||||||
# Only handle redirection codes
|
|
||||||
$redirectRes = $exc.Response
|
|
||||||
if ($handledCodes -notcontains $redirectRes.StatusCode) {
|
|
||||||
throw $exc
|
|
||||||
}
|
|
||||||
|
|
||||||
# Get the new location of the file
|
|
||||||
if ((-not $redirectRes.Headers) -or ($redirectRes.Headers -notcontains 'Location')) {
|
|
||||||
throw $exc
|
|
||||||
}
|
|
||||||
|
|
||||||
$newUrl = $redirectRes.Headers['Location']
|
|
||||||
info "Following redirect to $newUrl..."
|
|
||||||
|
|
||||||
# Handle manual file rename
|
|
||||||
if ($url -like '*#/*') {
|
|
||||||
$null, $postfix = $url -split '#/'
|
|
||||||
$newUrl = "$newUrl#/$postfix"
|
|
||||||
}
|
|
||||||
|
|
||||||
Invoke-Download $newUrl $to $cookies $progress
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
$total = $wres.ContentLength
|
|
||||||
if ($total -eq -1 -and $wreq -is [net.ftpwebrequest]) {
|
|
||||||
$total = ftp_file_size($url)
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($progress -and ($total -gt 0)) {
|
|
||||||
[console]::CursorVisible = $false
|
|
||||||
function Trace-DownloadProgress ($read) {
|
|
||||||
Write-DownloadProgress $read $total $url
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Write-Host "Downloading $url ($(filesize $total))..."
|
|
||||||
function Trace-DownloadProgress {
|
|
||||||
#no op
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
$s = $wres.getresponsestream()
|
|
||||||
$fs = [io.file]::openwrite($to)
|
|
||||||
$buffer = New-Object byte[] 2048
|
|
||||||
$totalRead = 0
|
|
||||||
$sw = [diagnostics.stopwatch]::StartNew()
|
|
||||||
|
|
||||||
Trace-DownloadProgress $totalRead
|
|
||||||
while (($read = $s.read($buffer, 0, $buffer.length)) -gt 0) {
|
|
||||||
$fs.write($buffer, 0, $read)
|
|
||||||
$totalRead += $read
|
|
||||||
if ($sw.elapsedmilliseconds -gt 100) {
|
|
||||||
$sw.restart()
|
|
||||||
Trace-DownloadProgress $totalRead
|
|
||||||
}
|
|
||||||
}
|
|
||||||
$sw.stop()
|
|
||||||
Trace-DownloadProgress $totalRead
|
|
||||||
} finally {
|
|
||||||
if ($progress) {
|
|
||||||
[console]::CursorVisible = $true
|
|
||||||
Write-Host
|
|
||||||
}
|
|
||||||
if ($fs) {
|
|
||||||
$fs.close()
|
|
||||||
}
|
|
||||||
if ($s) {
|
|
||||||
$s.close()
|
|
||||||
}
|
|
||||||
$wres.close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function Format-DownloadProgress ($url, $read, $total, $console) {
|
|
||||||
$filename = url_remote_filename $url
|
|
||||||
|
|
||||||
# calculate current percentage done
|
|
||||||
$p = [math]::Round($read / $total * 100, 0)
|
|
||||||
|
|
||||||
# pre-generate LHS and RHS of progress string
|
|
||||||
# so we know how much space we have
|
|
||||||
$left = "$filename ($(filesize $total))"
|
|
||||||
$right = [string]::Format('{0,3}%', $p)
|
|
||||||
|
|
||||||
# calculate remaining width for progress bar
|
|
||||||
$midwidth = $console.BufferSize.Width - ($left.Length + $right.Length + 8)
|
|
||||||
|
|
||||||
# calculate how many characters are completed
|
|
||||||
$completed = [math]::Abs([math]::Round(($p / 100) * $midwidth, 0) - 1)
|
|
||||||
|
|
||||||
# generate dashes to symbolise completed
|
|
||||||
if ($completed -gt 1) {
|
|
||||||
$dashes = [string]::Join('', ((1..$completed) | ForEach-Object { '=' }))
|
|
||||||
}
|
|
||||||
|
|
||||||
# this is why we calculate $completed - 1 above
|
|
||||||
$dashes += switch ($p) {
|
|
||||||
100 { '=' }
|
|
||||||
default { '>' }
|
|
||||||
}
|
|
||||||
|
|
||||||
# the remaining characters are filled with spaces
|
|
||||||
$spaces = switch ($dashes.Length) {
|
|
||||||
$midwidth { [string]::Empty }
|
|
||||||
default {
|
|
||||||
[string]::Join('', ((1..($midwidth - $dashes.Length)) | ForEach-Object { ' ' }))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
"$left [$dashes$spaces] $right"
|
|
||||||
}
|
|
||||||
|
|
||||||
function Write-DownloadProgress ($read, $total, $url) {
|
|
||||||
$console = $host.UI.RawUI
|
|
||||||
$left = $console.CursorPosition.X
|
|
||||||
$top = $console.CursorPosition.Y
|
|
||||||
$width = $console.BufferSize.Width
|
|
||||||
|
|
||||||
if ($read -eq 0) {
|
|
||||||
$maxOutputLength = $(Format-DownloadProgress $url 100 $total $console).length
|
|
||||||
if (($left + $maxOutputLength) -gt $width) {
|
|
||||||
# not enough room to print progress on this line
|
|
||||||
# print on new line
|
|
||||||
Write-Host
|
|
||||||
$left = 0
|
|
||||||
$top = $top + 1
|
|
||||||
if ($top -gt $console.CursorPosition.Y) { $top = $console.CursorPosition.Y }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Write-Host $(Format-DownloadProgress $url $read $total $console) -NoNewline
|
|
||||||
[console]::SetCursorPosition($left, $top)
|
|
||||||
}
|
|
||||||
|
|
||||||
function Invoke-ScoopDownload ($app, $version, $manifest, $bucket, $architecture, $dir, $use_cache = $true, $check_hash = $true) {
|
|
||||||
# we only want to show this warning once
|
|
||||||
if (!$use_cache) { warn 'Cache is being ignored.' }
|
|
||||||
|
|
||||||
# can be multiple urls: if there are, then installer should go first to make 'installer.args' section work
|
|
||||||
$urls = @(script:url $manifest $architecture)
|
|
||||||
|
|
||||||
# can be multiple cookies: they will be used for all HTTP requests.
|
|
||||||
$cookies = $manifest.cookie
|
|
||||||
|
|
||||||
# download first
|
|
||||||
if (Test-Aria2Enabled) {
|
|
||||||
Invoke-CachedAria2Download $app $version $manifest $architecture $dir $cookies $use_cache $check_hash
|
|
||||||
} else {
|
|
||||||
foreach ($url in $urls) {
|
|
||||||
$fname = url_filename $url
|
|
||||||
|
|
||||||
try {
|
|
||||||
Invoke-CachedDownload $app $version $url "$dir\$fname" $cookies $use_cache
|
|
||||||
} catch {
|
|
||||||
Write-Host -f darkred $_
|
|
||||||
abort "URL $url is not valid"
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($check_hash) {
|
|
||||||
$manifest_hash = hash_for_url $manifest $url $architecture
|
|
||||||
$ok, $err = check_hash "$dir\$fname" $manifest_hash $(show_app $app $bucket)
|
|
||||||
if (!$ok) {
|
|
||||||
error $err
|
|
||||||
$cached = cache_path $app $version $url
|
|
||||||
if (Test-Path $cached) {
|
|
||||||
# rm cached file
|
|
||||||
Remove-Item -Force $cached
|
|
||||||
}
|
|
||||||
if ($url.Contains('sourceforge.net')) {
|
|
||||||
Write-Host -f yellow 'SourceForge.net is known for causing hash validation fails. Please try again before opening a ticket.'
|
|
||||||
}
|
|
||||||
abort $(new_issue_msg $app $bucket 'hash check failed')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return $urls.ForEach({ url_filename $_ })
|
|
||||||
}
|
|
||||||
|
|
||||||
function cookie_header($cookies) {
|
|
||||||
if (!$cookies) { return }
|
|
||||||
|
|
||||||
$vals = $cookies.psobject.properties | ForEach-Object {
|
|
||||||
"$($_.name)=$($_.value)"
|
|
||||||
}
|
|
||||||
|
|
||||||
[string]::join(';', $vals)
|
|
||||||
}
|
|
||||||
|
|
||||||
function is_in_dir($dir, $check) {
|
function is_in_dir($dir, $check) {
|
||||||
$check -match "^$([regex]::Escape("$dir"))([/\\]|$)"
|
$check -match "^$([regex]::Escape("$dir"))([/\\]|$)"
|
||||||
}
|
}
|
||||||
|
|
||||||
function ftp_file_size($url) {
|
|
||||||
$request = [net.ftpwebrequest]::create($url)
|
|
||||||
$request.method = [net.webrequestmethods+ftp]::getfilesize
|
|
||||||
$request.getresponse().contentlength
|
|
||||||
}
|
|
||||||
|
|
||||||
# hashes
|
|
||||||
function hash_for_url($manifest, $url, $arch) {
|
|
||||||
$hashes = @(hash $manifest $arch) | Where-Object { $_ -ne $null }
|
|
||||||
|
|
||||||
if ($hashes.length -eq 0) { return $null }
|
|
||||||
|
|
||||||
$urls = @(script:url $manifest $arch)
|
|
||||||
|
|
||||||
$index = [array]::indexof($urls, $url)
|
|
||||||
if ($index -eq -1) { abort "Couldn't find hash in manifest for '$url'." }
|
|
||||||
|
|
||||||
@($hashes)[$index]
|
|
||||||
}
|
|
||||||
|
|
||||||
# returns (ok, err)
|
|
||||||
function check_hash($file, $hash, $app_name) {
|
|
||||||
if (!$hash) {
|
|
||||||
warn "Warning: No hash in manifest. SHA256 for '$(fname $file)' is:`n $((Get-FileHash -Path $file -Algorithm SHA256).Hash.ToLower())"
|
|
||||||
return $true, $null
|
|
||||||
}
|
|
||||||
|
|
||||||
Write-Host 'Checking hash of ' -NoNewline
|
|
||||||
Write-Host $(url_remote_filename $url) -f Cyan -NoNewline
|
|
||||||
Write-Host ' ... ' -NoNewline
|
|
||||||
$algorithm, $expected = get_hash $hash
|
|
||||||
if ($null -eq $algorithm) {
|
|
||||||
return $false, "Hash type '$algorithm' isn't supported."
|
|
||||||
}
|
|
||||||
|
|
||||||
$actual = (Get-FileHash -Path $file -Algorithm $algorithm).Hash.ToLower()
|
|
||||||
$expected = $expected.ToLower()
|
|
||||||
|
|
||||||
if ($actual -ne $expected) {
|
|
||||||
$msg = "Hash check failed!`n"
|
|
||||||
$msg += "App: $app_name`n"
|
|
||||||
$msg += "URL: $url`n"
|
|
||||||
if (Test-Path $file) {
|
|
||||||
$msg += "First bytes: $((get_magic_bytes_pretty $file ' ').ToUpper())`n"
|
|
||||||
}
|
|
||||||
if ($expected -or $actual) {
|
|
||||||
$msg += "Expected: $expected`n"
|
|
||||||
$msg += "Actual: $actual"
|
|
||||||
}
|
|
||||||
return $false, $msg
|
|
||||||
}
|
|
||||||
Write-Host 'ok.' -f Green
|
|
||||||
return $true, $null
|
|
||||||
}
|
|
||||||
|
|
||||||
function Invoke-Installer {
|
function Invoke-Installer {
|
||||||
[CmdletBinding()]
|
[CmdletBinding()]
|
||||||
param (
|
param (
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
. "$PSScriptRoot\..\lib\json.ps1" # 'autoupdate.ps1' (indirectly)
|
. "$PSScriptRoot\..\lib\json.ps1" # 'autoupdate.ps1' (indirectly)
|
||||||
. "$PSScriptRoot\..\lib\autoupdate.ps1" # 'generate_user_manifest' (indirectly)
|
. "$PSScriptRoot\..\lib\autoupdate.ps1" # 'generate_user_manifest' (indirectly)
|
||||||
. "$PSScriptRoot\..\lib\manifest.ps1" # 'generate_user_manifest' 'Get-Manifest'
|
. "$PSScriptRoot\..\lib\manifest.ps1" # 'generate_user_manifest' 'Get-Manifest'
|
||||||
. "$PSScriptRoot\..\lib\install.ps1"
|
. "$PSScriptRoot\..\lib\download.ps1"
|
||||||
if (get_config USE_SQLITE_CACHE) {
|
if (get_config USE_SQLITE_CACHE) {
|
||||||
. "$PSScriptRoot\..\lib\database.ps1"
|
. "$PSScriptRoot\..\lib\database.ps1"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
. "$PSScriptRoot\..\lib\getopt.ps1"
|
. "$PSScriptRoot\..\lib\getopt.ps1"
|
||||||
. "$PSScriptRoot\..\lib\manifest.ps1" # 'Get-Manifest'
|
. "$PSScriptRoot\..\lib\manifest.ps1" # 'Get-Manifest'
|
||||||
. "$PSScriptRoot\..\lib\versions.ps1" # 'Get-InstalledVersion'
|
. "$PSScriptRoot\..\lib\versions.ps1" # 'Get-InstalledVersion'
|
||||||
|
. "$PSScriptRoot\..\lib\download.ps1" # 'Get-RemoteFileSize'
|
||||||
|
|
||||||
$opt, $app, $err = getopt $args 'v' 'verbose'
|
$opt, $app, $err = getopt $args 'v' 'verbose'
|
||||||
if ($err) { error "scoop info: $err"; exit 1 }
|
if ($err) { error "scoop info: $err"; exit 1 }
|
||||||
@@ -166,7 +167,7 @@ if ($status.installed) {
|
|||||||
$cached = $null
|
$cached = $null
|
||||||
}
|
}
|
||||||
|
|
||||||
$urlLength = (Invoke-WebRequest $url -Method Head).Headers.'Content-Length' | ForEach-Object { [int]$_ }
|
$urlLength = Get-RemoteFileSize $url
|
||||||
$totalPackage += $urlLength
|
$totalPackage += $urlLength
|
||||||
} catch [System.Management.Automation.RuntimeException] {
|
} catch [System.Management.Automation.RuntimeException] {
|
||||||
$totalPackage = 0
|
$totalPackage = 0
|
||||||
|
|||||||
@@ -33,6 +33,7 @@
|
|||||||
. "$PSScriptRoot\..\lib\manifest.ps1" # 'generate_user_manifest' 'Get-Manifest' 'Select-CurrentVersion' (indirectly)
|
. "$PSScriptRoot\..\lib\manifest.ps1" # 'generate_user_manifest' 'Get-Manifest' 'Select-CurrentVersion' (indirectly)
|
||||||
. "$PSScriptRoot\..\lib\system.ps1"
|
. "$PSScriptRoot\..\lib\system.ps1"
|
||||||
. "$PSScriptRoot\..\lib\install.ps1"
|
. "$PSScriptRoot\..\lib\install.ps1"
|
||||||
|
. "$PSScriptRoot\..\lib\download.ps1"
|
||||||
. "$PSScriptRoot\..\lib\decompress.ps1"
|
. "$PSScriptRoot\..\lib\decompress.ps1"
|
||||||
. "$PSScriptRoot\..\lib\shortcuts.ps1"
|
. "$PSScriptRoot\..\lib\shortcuts.ps1"
|
||||||
. "$PSScriptRoot\..\lib\psmodules.ps1"
|
. "$PSScriptRoot\..\lib\psmodules.ps1"
|
||||||
|
|||||||
@@ -10,15 +10,10 @@ param($query)
|
|||||||
|
|
||||||
. "$PSScriptRoot\..\lib\manifest.ps1" # 'manifest'
|
. "$PSScriptRoot\..\lib\manifest.ps1" # 'manifest'
|
||||||
. "$PSScriptRoot\..\lib\versions.ps1" # 'Get-LatestVersion'
|
. "$PSScriptRoot\..\lib\versions.ps1" # 'Get-LatestVersion'
|
||||||
|
. "$PSScriptRoot\..\lib\download.ps1"
|
||||||
|
|
||||||
$list = [System.Collections.Generic.List[PSCustomObject]]::new()
|
$list = [System.Collections.Generic.List[PSCustomObject]]::new()
|
||||||
|
|
||||||
$githubtoken = Get-GitHubToken
|
|
||||||
$authheader = @{}
|
|
||||||
if ($githubtoken) {
|
|
||||||
$authheader = @{'Authorization' = "token $githubtoken" }
|
|
||||||
}
|
|
||||||
|
|
||||||
function bin_match($manifest, $query) {
|
function bin_match($manifest, $query) {
|
||||||
if (!$manifest.bin) { return $false }
|
if (!$manifest.bin) { return $false }
|
||||||
$bins = foreach ($bin in $manifest.bin) {
|
$bins = foreach ($bin in $manifest.bin) {
|
||||||
@@ -122,23 +117,6 @@ function search_bucket_legacy($bucket, $query) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function download_json($url) {
|
|
||||||
$ProgressPreference = 'SilentlyContinue'
|
|
||||||
$result = Invoke-WebRequest $url -UseBasicParsing -Headers $authheader | Select-Object -ExpandProperty content | ConvertFrom-Json
|
|
||||||
$ProgressPreference = 'Continue'
|
|
||||||
$result
|
|
||||||
}
|
|
||||||
|
|
||||||
function github_ratelimit_reached {
|
|
||||||
$api_link = 'https://api.github.com/rate_limit'
|
|
||||||
$ret = (download_json $api_link).rate.remaining -eq 0
|
|
||||||
if ($ret) {
|
|
||||||
Write-Host "GitHub API rate limit reached.
|
|
||||||
Please try again later or configure your API token using 'scoop config gh_token <your token>'."
|
|
||||||
}
|
|
||||||
$ret
|
|
||||||
}
|
|
||||||
|
|
||||||
function search_remote($bucket, $query) {
|
function search_remote($bucket, $query) {
|
||||||
$uri = [System.Uri](known_bucket_repo $bucket)
|
$uri = [System.Uri](known_bucket_repo $bucket)
|
||||||
if ($uri.AbsolutePath -match '/([a-zA-Z0-9]*)/([a-zA-Z0-9-]*)(?:.git|/)?') {
|
if ($uri.AbsolutePath -match '/([a-zA-Z0-9]*)/([a-zA-Z0-9-]*)(?:.git|/)?') {
|
||||||
|
|||||||
@@ -24,6 +24,7 @@
|
|||||||
. "$PSScriptRoot\..\lib\versions.ps1"
|
. "$PSScriptRoot\..\lib\versions.ps1"
|
||||||
. "$PSScriptRoot\..\lib\depends.ps1"
|
. "$PSScriptRoot\..\lib\depends.ps1"
|
||||||
. "$PSScriptRoot\..\lib\install.ps1"
|
. "$PSScriptRoot\..\lib\install.ps1"
|
||||||
|
. "$PSScriptRoot\..\lib\download.ps1"
|
||||||
if (get_config USE_SQLITE_CACHE) {
|
if (get_config USE_SQLITE_CACHE) {
|
||||||
. "$PSScriptRoot\..\lib\database.ps1"
|
. "$PSScriptRoot\..\lib\database.ps1"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,7 +31,7 @@
|
|||||||
. "$PSScriptRoot\..\lib\getopt.ps1"
|
. "$PSScriptRoot\..\lib\getopt.ps1"
|
||||||
. "$PSScriptRoot\..\lib\manifest.ps1" # 'Get-Manifest'
|
. "$PSScriptRoot\..\lib\manifest.ps1" # 'Get-Manifest'
|
||||||
. "$PSScriptRoot\..\lib\json.ps1" # 'json_path'
|
. "$PSScriptRoot\..\lib\json.ps1" # 'json_path'
|
||||||
. "$PSScriptRoot\..\lib\install.ps1" # 'hash_for_url'
|
. "$PSScriptRoot\..\lib\download.ps1" # 'hash_for_url'
|
||||||
. "$PSScriptRoot\..\lib\depends.ps1" # 'Get-Dependency'
|
. "$PSScriptRoot\..\lib\depends.ps1" # 'Get-Dependency'
|
||||||
|
|
||||||
$opt, $apps, $err = getopt $args 'asnup' @('all', 'scan', 'no-depends', 'no-update-scoop', 'passthru')
|
$opt, $apps, $err = getopt $args 'asnup' @('all', 'scan', 'no-depends', 'no-update-scoop', 'passthru')
|
||||||
@@ -86,11 +86,6 @@ Function ConvertTo-VirusTotalUrlId ($url) {
|
|||||||
$url_id
|
$url_id
|
||||||
}
|
}
|
||||||
|
|
||||||
Function Get-RemoteFileSize ($url) {
|
|
||||||
$response = Invoke-WebRequest -Uri $url -Method HEAD -UseBasicParsing
|
|
||||||
$response.Headers.'Content-Length' | ForEach-Object { [System.Convert]::ToInt32($_) }
|
|
||||||
}
|
|
||||||
|
|
||||||
Function Get-VirusTotalResultByHash ($hash, $url, $app) {
|
Function Get-VirusTotalResultByHash ($hash, $url, $app) {
|
||||||
$hash = $hash.ToLower()
|
$hash = $hash.ToLower()
|
||||||
$api_url = "https://www.virustotal.com/api/v3/files/$hash"
|
$api_url = "https://www.virustotal.com/api/v3/files/$hash"
|
||||||
|
|||||||
@@ -73,28 +73,6 @@ Describe 'Test-HelperInstalled' -Tag 'Scoop' {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Describe 'Test-Aria2Enabled' -Tag 'Scoop' {
|
|
||||||
It 'should return true if aria2 is installed' {
|
|
||||||
Mock Test-HelperInstalled { $true }
|
|
||||||
Mock get_config { $true }
|
|
||||||
Test-Aria2Enabled | Should -BeTrue
|
|
||||||
}
|
|
||||||
|
|
||||||
It 'should return false if aria2 is not installed' {
|
|
||||||
Mock Test-HelperInstalled { $false }
|
|
||||||
Mock get_config { $false }
|
|
||||||
Test-Aria2Enabled | Should -BeFalse
|
|
||||||
|
|
||||||
Mock Test-HelperInstalled { $false }
|
|
||||||
Mock get_config { $true }
|
|
||||||
Test-Aria2Enabled | Should -BeFalse
|
|
||||||
|
|
||||||
Mock Test-HelperInstalled { $true }
|
|
||||||
Mock get_config { $false }
|
|
||||||
Test-Aria2Enabled | Should -BeFalse
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Describe 'Test-CommandAvailable' -Tag 'Scoop' {
|
Describe 'Test-CommandAvailable' -Tag 'Scoop' {
|
||||||
It 'should return true if command exists' {
|
It 'should return true if command exists' {
|
||||||
Test-CommandAvailable 'Write-Host' | Should -BeTrue
|
Test-CommandAvailable 'Write-Host' | Should -BeTrue
|
||||||
|
|||||||
49
test/Scoop-Download.Tests.ps1
Normal file
49
test/Scoop-Download.Tests.ps1
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
BeforeAll {
|
||||||
|
. "$PSScriptRoot\Scoop-TestLib.ps1"
|
||||||
|
. "$PSScriptRoot\..\lib\core.ps1"
|
||||||
|
. "$PSScriptRoot\..\lib\download.ps1"
|
||||||
|
}
|
||||||
|
|
||||||
|
Describe 'Test-Aria2Enabled' -Tag 'Scoop' {
|
||||||
|
It 'should return true if aria2 is installed' {
|
||||||
|
Mock Test-HelperInstalled { $true }
|
||||||
|
Mock get_config { $true }
|
||||||
|
Test-Aria2Enabled | Should -BeTrue
|
||||||
|
}
|
||||||
|
|
||||||
|
It 'should return false if aria2 is not installed' {
|
||||||
|
Mock Test-HelperInstalled { $false }
|
||||||
|
Mock get_config { $false }
|
||||||
|
Test-Aria2Enabled | Should -BeFalse
|
||||||
|
|
||||||
|
Mock Test-HelperInstalled { $false }
|
||||||
|
Mock get_config { $true }
|
||||||
|
Test-Aria2Enabled | Should -BeFalse
|
||||||
|
|
||||||
|
Mock Test-HelperInstalled { $true }
|
||||||
|
Mock get_config { $false }
|
||||||
|
Test-Aria2Enabled | Should -BeFalse
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Describe 'url_filename' -Tag 'Scoop' {
|
||||||
|
It 'should extract the real filename from an url' {
|
||||||
|
url_filename 'http://example.org/foo.txt' | Should -Be 'foo.txt'
|
||||||
|
url_filename 'http://example.org/foo.txt?var=123' | Should -Be 'foo.txt'
|
||||||
|
}
|
||||||
|
|
||||||
|
It 'can be tricked with a hash to override the real filename' {
|
||||||
|
url_filename 'http://example.org/foo-v2.zip#/foo.zip' | Should -Be 'foo.zip'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Describe 'url_remote_filename' -Tag 'Scoop' {
|
||||||
|
It 'should extract the real filename from an url' {
|
||||||
|
url_remote_filename 'http://example.org/foo.txt' | Should -Be 'foo.txt'
|
||||||
|
url_remote_filename 'http://example.org/foo.txt?var=123' | Should -Be 'foo.txt'
|
||||||
|
}
|
||||||
|
|
||||||
|
It 'can not be tricked with a hash to override the real filename' {
|
||||||
|
url_remote_filename 'http://example.org/foo-v2.zip#/foo.zip' | Should -Be 'foo-v2.zip'
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,28 +12,6 @@ Describe 'appname_from_url' -Tag 'Scoop' {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Describe 'url_filename' -Tag 'Scoop' {
|
|
||||||
It 'should extract the real filename from an url' {
|
|
||||||
url_filename 'http://example.org/foo.txt' | Should -Be 'foo.txt'
|
|
||||||
url_filename 'http://example.org/foo.txt?var=123' | Should -Be 'foo.txt'
|
|
||||||
}
|
|
||||||
|
|
||||||
It 'can be tricked with a hash to override the real filename' {
|
|
||||||
url_filename 'http://example.org/foo-v2.zip#/foo.zip' | Should -Be 'foo.zip'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Describe 'url_remote_filename' -Tag 'Scoop' {
|
|
||||||
It 'should extract the real filename from an url' {
|
|
||||||
url_remote_filename 'http://example.org/foo.txt' | Should -Be 'foo.txt'
|
|
||||||
url_remote_filename 'http://example.org/foo.txt?var=123' | Should -Be 'foo.txt'
|
|
||||||
}
|
|
||||||
|
|
||||||
It 'can not be tricked with a hash to override the real filename' {
|
|
||||||
url_remote_filename 'http://example.org/foo-v2.zip#/foo.zip' | Should -Be 'foo-v2.zip'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Describe 'is_in_dir' -Tag 'Scoop', 'Windows' {
|
Describe 'is_in_dir' -Tag 'Scoop', 'Windows' {
|
||||||
It 'should work correctly' {
|
It 'should work correctly' {
|
||||||
is_in_dir 'C:\test' 'C:\foo' | Should -BeFalse
|
is_in_dir 'C:\test' 'C:\foo' | Should -BeFalse
|
||||||
|
|||||||
Reference in New Issue
Block a user