32 Commits

Author SHA1 Message Date
z-Fng
b592b38abd feat(install): Add separator at the end of notes, highlight suggestions (#6418) 2025-09-24 16:52:03 +08:00
z-Fng
25d35007c6 fix(core|manifest): Avoid error when searching non-existent 'deprecated' directory (#6471) 2025-09-24 14:45:01 +08:00
z-Fng
04b7ce79c7 fix(scoop-version): Fix logic error caused by missing brackets (#6463) 2025-08-27 17:46:16 +08:00
z-Fng
ff38652e01 fix(buckets|scoop-info): Switch git log date format to ISO 8601 to avoid locale issues (#6446) 2025-08-21 14:18:33 +08:00
HUMORCE
c9f0728ff7 fix(autoupdate): Use origin url to handle urls with fragment in github mode (#6455) 2025-08-20 13:59:47 +08:00
AkariiinMKII
691c23330c fix(scoop-download): Fix function nightly_version not defined error (#6386) 2025-08-14 17:51:19 +08:00
Hsiao-nan Cheung
b588a06e41 chore(release): Bump to version 0.5.3 (resync) (#6436) 2025-08-12 18:18:31 +08:00
Hsiao-nan Cheung
9246250808 Merge branch 'master' into develop 2025-08-12 15:21:48 +08:00
Hsiao-nan Cheung
8c51a50856 fix(autoupdate): Check hash file before use GitHub SHA256 (#6435) 2025-08-12 14:36:30 +08:00
Hsiao-nan Cheung
ebd8c036fa chore(release): Bump to version 0.5.3 (#6257)
* fix (decompress): `Expand-7zipArchive` only delete temp dir / `$extractDir` if it is empty (#6092)

Co-authored-by: Hsiao-nan Cheung <niheaven@gmail.com>

* refactor(download): Move download-related functions to 'download.ps1' (#6095)

* fix(download): Fallback to default downloader when aria2 fails (#4292)

* fix(commands): Handling broken aliases (#6141)

* fix(shim): properly check `wslpath`/`cygpath` command first (#6114)

Co-authored-by: Hsiao-nan Cheung <niheaven@gmail.com>

* fix(scoop-bucket): Add missing import for `no_junction` envs (#6181)

Signed-off-by: Chawye Hsu <su+git@chawyehsu.com>

* docs(chglog): Update to 0.5.3 (#6258)

* perf(shim): Update kiennq-shim to v3.1.2 (#6261)

* fix(decompress): Replace deprecated 7ZIPEXTRACT_USE_EXTERNAL config (#6327)

Co-authored-by: Hsiao-nan Cheung <niheaven@gmail.com>

* fix(scoop-uninstall): Fix uninstaller does not gain Global state (#6430)

* global arg

* changelog

* refactor(Get-Manifest): Select actual source for manifest (#6142)

* first step

* Revert "first step"

This reverts commit c5907c3e25.

* refactor(Get-Manifest): Select actual source for installed manifest

* rework sub-commands, `scoop-depends` is NOT working at this stage

* URI manifest

* opt

* deprecated manifest

* source of manifests

* source of manifest pt2

- Mark URI(path/URL/UNC/etc.) query as standalone manifest
- Drop `installed` and `available update` items for [query] and [installed] are different sources.

* remove variable preventing I forget it

* scoop-info: fix source of manifest on bucket

* fix `scoop-depends`

* Fix Standalone and Source detection

* fix global install

* Fix scoop-cat, scoop-home

- Query for remote manifest

* scoop-list: info +deprecated

* manifest: Fix first selected manifest

* gramma..

* Fix 61b3259

* length

* fix(scoop-depends-tests): Mocking `USE_EXTERNAL_7ZIP` as $false (#6431)

* fix(scoop-depends-tests): Mocking `USE_EXTERNAL_7ZIP` as $false to avoding error when it is $true

* CHANGELOG

* feat(autoupdate): GitHub predefined hashes support (#6416)

* feat(autoupdate): predefined hash case for GitHub

- Remove `sha256:` prefix in `format_hash()`
- Add GitHub support in `get_hash_for_app()`

Close #6381

* doc(chglog): GitHub auto hash update

* fix(autoupdate): remove prefix only

* docs(CHANGELOG): Update to 0.5.3 (#6432)

* docs(CHANGELOG): Update to 0.5.3

* 6416

---------

Signed-off-by: Chawye Hsu <su+git@chawyehsu.com>
Co-authored-by: Olav Rønnestad Birkeland <6450056+o-l-a-v@users.noreply.github.com>
Co-authored-by: kiennq <kien.n.quang@gmail.com>
Co-authored-by: HUMORCE <humorce@outlook.com>
Co-authored-by: Ryan <sitiom@proton.me>
Co-authored-by: Chawye Hsu <su+git@chawyehsu.com>
Co-authored-by: Bassel Rachid <101208715+basselworkforce@users.noreply.github.com>
Co-authored-by: Wordless Echo <wordless@echo.moe>
2025-08-11 00:38:38 +00:00
HUMORCE
ed6101a2ab docs(CHANGELOG): Update to 0.5.3 (#6432)
* docs(CHANGELOG): Update to 0.5.3

* 6416
2025-08-11 00:34:50 +00:00
Wordless Echo
26a03e2404 feat(autoupdate): GitHub predefined hashes support (#6416)
* feat(autoupdate): predefined hash case for GitHub

- Remove `sha256:` prefix in `format_hash()`
- Add GitHub support in `get_hash_for_app()`

Close #6381

* doc(chglog): GitHub auto hash update

* fix(autoupdate): remove prefix only
2025-08-11 00:11:46 +00:00
HUMORCE
f9c4f9e175 fix(scoop-depends-tests): Mocking USE_EXTERNAL_7ZIP as $false (#6431)
* fix(scoop-depends-tests): Mocking `USE_EXTERNAL_7ZIP` as $false to avoding error when it is $true

* CHANGELOG
2025-08-10 23:49:22 +00:00
HUMORCE
3321b969a4 refactor(Get-Manifest): Select actual source for manifest (#6142)
* first step

* Revert "first step"

This reverts commit c5907c3e25.

* refactor(Get-Manifest): Select actual source for installed manifest

* rework sub-commands, `scoop-depends` is NOT working at this stage

* URI manifest

* opt

* deprecated manifest

* source of manifests

* source of manifest pt2

- Mark URI(path/URL/UNC/etc.) query as standalone manifest
- Drop `installed` and `available update` items for [query] and [installed] are different sources.

* remove variable preventing I forget it

* scoop-info: fix source of manifest on bucket

* fix `scoop-depends`

* Fix Standalone and Source detection

* fix global install

* Fix scoop-cat, scoop-home

- Query for remote manifest

* scoop-list: info +deprecated

* manifest: Fix first selected manifest

* gramma..

* Fix 61b3259

* length
2025-08-10 23:04:34 +00:00
HUMORCE
22f1672fcb fix(scoop-uninstall): Fix uninstaller does not gain Global state (#6430)
* global arg

* changelog
2025-08-10 22:48:06 +00:00
Bassel Rachid
4e528e1655 fix(decompress): Replace deprecated 7ZIPEXTRACT_USE_EXTERNAL config (#6327)
Co-authored-by: Hsiao-nan Cheung <niheaven@gmail.com>
2025-04-17 11:19:22 +08:00
Hsiao-nan Cheung
ac6f1baaf4 perf(shim): Update kiennq-shim to v3.1.2 (#6261) 2025-01-03 14:34:38 +08:00
Hsiao-nan Cheung
dc9d198014 docs(chglog): Update to 0.5.3 (#6258) 2024-12-30 13:55:32 +08:00
Chawye Hsu
84e00fdb77 fix(scoop-bucket): Add missing import for no_junction envs (#6181)
Signed-off-by: Chawye Hsu <su+git@chawyehsu.com>
2024-10-11 14:20:43 +08:00
Ryan
7a309a1b00 fix(shim): properly check wslpath/cygpath command first (#6114)
Co-authored-by: Hsiao-nan Cheung <niheaven@gmail.com>
2024-10-09 19:10:42 +08:00
HUMORCE
3577f91d82 fix(commands): Handling broken aliases (#6141) 2024-09-20 14:18:13 +08:00
kiennq
e0c682de7c fix(download): Fallback to default downloader when aria2 fails (#4292) 2024-08-26 16:10:30 +08:00
Hsiao-nan Cheung
79cf33d0b7 refactor(download): Move download-related functions to 'download.ps1' (#6095) 2024-08-11 17:23:59 +08:00
Olav Rønnestad Birkeland
7f99c499d7 fix (decompress): Expand-7zipArchive only delete temp dir / $extractDir if it is empty (#6092)
Co-authored-by: Hsiao-nan Cheung <niheaven@gmail.com>
2024-08-09 11:03:51 +08:00
Hsiao-nan Cheung
859d1db51b chore(release): Bump to version 0.5.2 (#6080)
Co-authored-by: Hsiao-nan Cheung <niheaven@gmail.com>
Co-authored-by: Matt Cargile <74641867+mattcargile@users.noreply.github.com>
Co-authored-by: ccyykkcyk <91893497+ccyykkcyk@users.noreply.github.com>
2024-07-30 15:34:41 +08:00
Hsiao-nan Cheung
c7ec5c82b2 docs(chglog): Update CHANGELOG for v0.5.2 (#6081) 2024-07-25 20:29:02 +08:00
Hsiao-nan Cheung
46624b00c9 fix(core): Use 'Join-Path' to construct cache file path (#6079) 2024-07-25 13:54:26 +08:00
Hsiao-nan Cheung
49ee8ad6ec fix(scoop-hold): Use 'foreach' loop to allow 'continue' statement (#6078) 2024-07-25 13:00:36 +08:00
Hsiao-nan Cheung
5dc5dd22f8 fix(json): Fix JSON items counting function (#6073) 2024-07-23 19:55:40 +08:00
Hsiao-nan Cheung
5971f1dda8 builds(supporting): Update Json.Schema to 4.0.1 (#6072) 2024-07-23 19:13:00 +08:00
ccyykkcyk
0cb479abf0 fix(json): Don't serialize jsonpath return if only one result (#6066)
Co-authored-by: Hsiao-nan Cheung <niheaven@gmail.com>
2024-07-22 19:02:06 +08:00
Matt Cargile
429ba7af5b fix(scoop-alias): Fix 'Option --verbose not recognized.' (#6062) 2024-07-17 22:12:29 +08:00
46 changed files with 1148 additions and 921 deletions

View File

@@ -1,3 +1,56 @@
## [Unreleased](https://github.com/ScoopInstaller/Scoop/compare/v0.5.3...develop)
### Features
- **install:** Add separator at the end of notes, highlight suggestions ([#6418](https://github.com/ScoopInstaller/Scoop/issues/6418))
### Bug Fixes
- **scoop-download:** Fix function `nightly_version` not defined error ([#6386](https://github.com/ScoopInstaller/Scoop/issues/6386))
- **autoupdate:** Use origin URL to handle URLs with fragment in GitHub mode ([#6455](https://github.com/ScoopInstaller/Scoop/issues/6455))
- **buckets|scoop-info:** Switch git log date format to ISO 8601 to avoid locale issues ([#6446](https://github.com/ScoopInstaller/Scoop/issues/6446))
- **scoop-version:** Fix logic error caused by missing brackets ([#6463](https://github.com/ScoopInstaller/Scoop/issues/6463))
- **core|manifest:** Avoid error messages when searching non-existent 'deprecated' directory ([#6471](https://github.com/ScoopInstaller/Scoop/issues/6471))
## [v0.5.3](https://github.com/ScoopInstaller/Scoop/compare/v0.5.2...v0.5.3) - 2025-08-11
### Features
**autoupdate:** GitHub predefined hashes support ([#6416](https://github.com/ScoopInstaller/Scoop/issues/6416), [#6435](https://github.com/ScoopInstaller/Scoop/issues/6435))
### Bug Fixes
- **scoop-download|install|update:** Fallback to default downloader when aria2 fails ([#4292](https://github.com/ScoopInstaller/Scoop/issues/4292))
- **decompress**: `Expand-7zipArchive` only delete temp dir / `$extractDir` if it is empty ([#6092](https://github.com/ScoopInstaller/Scoop/issues/6092))
- **decompress**: Replace deprecated 7ZIPEXTRACT_USE_EXTERNAL config with USE_EXTERNAL_7ZIP ([#6327](https://github.com/ScoopInstaller/Scoop/issues/6327))
- **commands**: Handling broken aliases ([#6141](https://github.com/ScoopInstaller/Scoop/issues/6141))
- **shim:** Do not suppress `stderr`, properly check `wslpath`/`cygpath` command first ([#6114](https://github.com/ScoopInstaller/Scoop/issues/6114))
- **scoop-bucket:** Add missing import for `no_junction` envs ([#6181](https://github.com/ScoopInstaller/Scoop/issues/6181))
- **scoop-uninstall:** Fix uninstaller does not gain Global state ([#6430](https://github.com/ScoopInstaller/Scoop/issues/6430))
- **scoop-depends-tests:** Mocking `USE_EXTERNAL_7ZIP` as $false to avoding error when it is $true ([#6431](https://github.com/ScoopInstaller/Scoop/issues/6431))
### Code Refactoring
- **download:** Move download-related functions to 'download.ps1' ([#6095](https://github.com/ScoopInstaller/Scoop/issues/6095))
- **Get-Manifest:** Select actual source for manifest ([#6142](https://github.com/ScoopInstaller/Scoop/issues/6142))
### Performance Improvements
- **shim:** Update kiennq-shim to v3.1.2 ([#6261](https://github.com/ScoopInstaller/Scoop/issues/6261))
## [v0.5.2](https://github.com/ScoopInstaller/Scoop/compare/v0.5.1...v0.5.2) - 2024-07-26
### Bug Fixes
- **scoop-alias:** Fix 'Option --verbose not recognized.' ([#6062](https://github.com/ScoopInstaller/Scoop/issues/6062))
- **scoop-hold:** Use 'foreach' loop to allow 'continue' statement ([#6078](https://github.com/ScoopInstaller/Scoop/issues/6078))
- **core:** Use 'Join-Path' to construct cache file path ([#6079](https://github.com/ScoopInstaller/Scoop/issues/6079))
- **json:** Don't serialize jsonpath return if only one result ([#6066](https://github.com/ScoopInstaller/Scoop/issues/6066), [#6073](https://github.com/ScoopInstaller/Scoop/issues/6073))
### Builds
- **supporting:** Update Json.Schema to 4.0.1 ([#6072](https://github.com/ScoopInstaller/Scoop/issues/6072))
## [v0.5.1](https://github.com/ScoopInstaller/Scoop/compare/v0.5.0...v0.5.1) - 2024-07-16
### Bug Fixes
@@ -27,7 +80,7 @@
- **checkver:** Correct error messages ([#6024](https://github.com/ScoopInstaller/Scoop/issues/6024))
- **core:** Search for Git executable instead of any cmdlet ([#5998](https://github.com/ScoopInstaller/Scoop/issues/5998))
- **core:** Use correct path in 'bash' ([#6006](https://github.com/ScoopInstaller/Scoop/issues/6006))
- **core:** Limit the number of commands to get when search for git executable ([#6013](https://github.com/ScoopInstaller/Scoop/pull/6013))
- **core:** Limit the number of commands to get when search for git executable ([#6013](https://github.com/ScoopInstaller/Scoop/issues/6013))
- **decompress:** Match `extract_dir`/`extract_to` and archives ([#5983](https://github.com/ScoopInstaller/Scoop/issues/5983))
- **json:** Serialize jsonpath return ([#5921](https://github.com/ScoopInstaller/Scoop/issues/5921))
- **shim:** Restore original path for JAR cmd ([#6030](https://github.com/ScoopInstaller/Scoop/issues/6030))

View File

@@ -46,7 +46,7 @@ param(
. "$PSScriptRoot\..\lib\autoupdate.ps1"
. "$PSScriptRoot\..\lib\json.ps1"
. "$PSScriptRoot\..\lib\versions.ps1"
. "$PSScriptRoot\..\lib\install.ps1"
. "$PSScriptRoot\..\lib\download.ps1"
$Dir = Convert-Path $Dir
if ($ForceUpdate) { $Update = $true }

View File

@@ -28,7 +28,7 @@ param(
. "$PSScriptRoot\..\lib\core.ps1"
. "$PSScriptRoot\..\lib\manifest.ps1"
. "$PSScriptRoot\..\lib\install.ps1"
. "$PSScriptRoot\..\lib\download.ps1"
$Dir = Convert-Path $Dir
$Queue = @()

View File

@@ -73,7 +73,7 @@ param(
. "$PSScriptRoot\..\lib\buckets.ps1"
. "$PSScriptRoot\..\lib\json.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)) {
$Dir = Split-Path $App

View File

@@ -23,6 +23,7 @@ param(
. "$PSScriptRoot\..\lib\core.ps1"
. "$PSScriptRoot\..\lib\manifest.ps1"
. "$PSScriptRoot\..\lib\description.ps1"
. "$PSScriptRoot\..\lib\download.ps1"
$Dir = Convert-Path $Dir
$Queue = @()

View File

@@ -20,8 +20,8 @@ switch ($subCommand) {
}
({ $subCommand -in @('-v', '--version') }) {
Write-Host 'Current Scoop version:'
if (Test-GitAvailable -and (Test-Path "$PSScriptRoot\..\.git") -and (get_config SCOOP_BRANCH 'master') -ne 'master') {
Invoke-Git -Path "$PSScriptRoot\.." -ArgumentList @('log', 'HEAD', '-1', '--oneline')
if ((Test-GitAvailable) -and (Test-Path "$PSScriptRoot\..\.git") -and ((get_config SCOOP_BRANCH 'master') -ne 'master')) {
Invoke-Git -Path "$PSScriptRoot\.." -ArgumentList @('--no-pager', 'log', 'HEAD', '-1', '--oneline')
} else {
$version = Select-String -Pattern '^## \[(v[\d.]+)\].*?([\d-]+)$' -Path "$PSScriptRoot\..\CHANGELOG.md"
Write-Host $version.Matches.Groups[1].Value -ForegroundColor Cyan -NoNewline
@@ -31,9 +31,9 @@ switch ($subCommand) {
Get-LocalBucket | ForEach-Object {
$bucketLoc = Find-BucketDirectory $_ -Root
if (Test-GitAvailable -and (Test-Path "$bucketLoc\.git")) {
if ((Test-GitAvailable) -and (Test-Path "$bucketLoc\.git")) {
Write-Host "'$_' bucket:"
Invoke-Git -Path $bucketLoc -ArgumentList @('log', 'HEAD', '-1', '--oneline')
Invoke-Git -Path $bucketLoc -ArgumentList @('--no-pager', 'log', 'HEAD', '-1', '--oneline')
Write-Host ''
}
}

View File

@@ -1,4 +1,24 @@
# Must included with 'json.ps1'
function format_hash([String] $hash) {
$hash = $hash.toLower()
# Workaround for GitHub API:
# `"digest": "sha256:<SHA256_STRING>"`
if ($hash -like 'sha256:*') {
$hash = $hash.Substring(7) # Remove prefix 'sha256:'
}
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) {
$xml = $null
try {
@@ -191,13 +211,14 @@ function get_hash_for_app([String] $app, $config, [String] $version, [String] $u
$hash = $null
$hashmode = $config.mode
$originurl = strip_fragment $url
$basename = [System.Web.HttpUtility]::UrlDecode((url_remote_filename($url)))
$substitutions = $substitutions.Clone()
$substitutions.Add('$url', (strip_fragment $url))
$substitutions.Add('$baseurl', (strip_filename (strip_fragment $url)).TrimEnd('/'))
$substitutions.Add('$url', $originurl)
$substitutions.Add('$baseurl', (strip_filename $originurl).TrimEnd('/'))
$substitutions.Add('$basename', $basename)
$substitutions.Add('$urlNoExt', (strip_ext (strip_fragment $url)))
$substitutions.Add('$urlNoExt', (strip_ext $originurl))
$substitutions.Add('$basenameNoExt', (strip_ext $basename))
debug $substitutions
@@ -246,6 +267,10 @@ function get_hash_for_app([String] $app, $config, [String] $version, [String] $u
$hashmode = 'sourceforge'
}
if (!$hashfile_url -and $url -match 'https:\/\/github\.com\/(?<owner>[^\/]+)\/(?<repo>[^\/]+)\/releases\/download\/[^\/]+\/[^\/]+') {
$hashmode = 'github'
}
switch ($hashmode) {
'extract' {
$hash = find_hash_in_textfile $hashfile_url $substitutions $regex
@@ -273,6 +298,10 @@ function get_hash_for_app([String] $app, $config, [String] $version, [String] $u
$hashfile_url = (strip_filename (strip_fragment "https://sourceforge.net/projects/$($matches['project'])/files/$($matches['file'])")).TrimEnd('/')
$hash = find_hash_in_textfile $hashfile_url $substitutions '"$basename":.*?"sha1":\s*"([a-fA-F0-9]{40})"'
}
'github' {
$hashfile_url = "https://api.github.com/repos/$($matches['owner'])/$($matches['repo'])/releases"
$hash = find_hash_in_json $hashfile_url $substitutions ("$..assets[?(@.browser_download_url == '" + $originurl + "')].digest")
}
}
if ($hash) {

View File

@@ -108,7 +108,7 @@ function list_buckets {
$path = Find-BucketDirectory $_ -Root
if ((Test-Path (Join-Path $path '.git')) -and (Get-Command git -ErrorAction SilentlyContinue)) {
$bucket.Source = Invoke-Git -Path $path -ArgumentList @('config', 'remote.origin.url')
$bucket.Updated = Invoke-Git -Path $path -ArgumentList @('log', '--format=%aD', '-n', '1') | Get-Date
$bucket.Updated = Invoke-Git -Path $path -ArgumentList @('log', '--format=%aI', '-n', '1') | Get-Date
} else {
$bucket.Source = friendly_path $path
$bucket.Updated = (Get-Item "$path\bucket" -ErrorAction SilentlyContinue).LastWriteTime

View File

@@ -4,7 +4,7 @@
function command_files {
(Get-ChildItem "$PSScriptRoot\..\libexec") + (Get-ChildItem "$scoopdir\shims") |
Where-Object 'scoop-.*?\.ps1$' -Property Name -Match
Where-Object 'scoop-.*?\.ps1$' -Property Name -Match
}
function commands {
@@ -86,7 +86,9 @@ function rm_alias {
}
info "Removing alias '$name'..."
Remove-Item "$(shimdir $false)\scoop-$name.ps1"
if (Test-Path "$(shimdir $false)\scoop-$name.ps1") {
Remove-Item "$(shimdir $false)\scoop-$name.ps1"
}
$aliases.PSObject.Properties.Remove($name)
set_config ALIAS $aliases | Out-Null
}
@@ -98,11 +100,19 @@ function list_aliases {
$aliases = get_config ALIAS ([PSCustomObject]@{})
$alias_info = $aliases.PSObject.Properties.Name | Where-Object { $_ } | ForEach-Object {
# Mark the alias as <BROKEN>, if the alias script file does NOT exist.
if (!(Test-Path "$(shimdir $false)\scoop-$_.ps1")) {
[PSCustomObject]@{
Name = $_
Command = '<BROKEN>'
}
return
}
$content = Get-Content (command_path $_)
[PSCustomObject]@{
Name = $_
Summary = (summary $content).Trim()
Command = ($content | Select-Object -Skip 1).Trim()
Summary = (summary $content).Trim()
}
}
if (!$alias_info) {

View File

@@ -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 {
<#
.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 {
[CmdletBinding()]
[OutputType([String])]
@@ -407,7 +366,7 @@ function usermanifestsdir { "$(basedir)\workspace" }
function usermanifest($app) { "$(usermanifestsdir)\$app.json" }
function cache_path($app, $version, $url) {
$underscoredUrl = $url -replace '[^\w\.\-]+', '_'
$filePath = "$cachedir\$app#$version#$underscoredUrl"
$filePath = Join-Path $cachedir "$app#$version#$underscoredUrl"
# NOTE: Scoop cache files migration. Remove this 6 months after the feature ships.
if (Test-Path $filePath) {
@@ -584,10 +543,6 @@ function Test-HelperInstalled {
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) {
$status = @{}
$status.installed = installed $app $global
@@ -599,6 +554,9 @@ function app_status($app, $global) {
$status.failed = failed $app $global
$status.hold = ($install_info.hold -eq $true)
$deprecated_dir = (Find-BucketDirectory -Name $install_info.bucket -Root) + "\deprecated"
$status.deprecated = (Get-ChildItem $deprecated_dir -Filter "$(sanitary_path $app).json" -Recurse -ErrorAction Ignore).FullName
$manifest = manifest $app $install_info.bucket $install_info.url
$status.removed = (!$manifest)
if ($manifest.version) {
@@ -626,7 +584,6 @@ function app_status($app, $global) {
if ($deps) {
$status.missing_deps += , $deps
}
return $status
}
@@ -639,28 +596,6 @@ function fname($path) { split-path $path -leaf }
function strip_ext($fname) { $fname -replace '\.[^\.]*$', '' }
function strip_filename($path) { $path -replace [regex]::escape((fname $path)) }
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) {
if (!(Test-Path -Path $dir)) {
New-Item -Path $dir -ItemType Directory | Out-Null
@@ -1070,11 +1005,18 @@ function shim($path, $global, $name, $arg) {
) -join "`n" | Out-UTF8File $shim -NoNewLine
} else {
warn_on_overwrite "$shim.cmd" $path
$quoted_arg = if ($arg.Count -gt 0) { $arg | ForEach-Object { "`"$_`"" } }
@(
"@rem $resolved_path",
"@bash `"`$(wslpath -u '$resolved_path')`" $arg %* 2>nul",
'@if %errorlevel% neq 0 (',
" @bash `"`$(cygpath -u '$resolved_path')`" $arg %* 2>nul",
'@echo off',
'bash -c "command -v wslpath >/dev/null"',
'if %errorlevel% equ 0 (',
" bash `"`$(wslpath -u '$resolved_path')`" $quoted_arg %*",
') else (',
" set args=$quoted_arg %*",
' setlocal enabledelayedexpansion',
' if not "!args!"=="" set args=!args:"=""!',
" bash -c `"`$(cygpath -u '$resolved_path') !args!`"",
')'
) -join "`r`n" | Out-UTF8File "$shim.cmd"
@@ -1282,112 +1224,6 @@ function substitute($entity, [Hashtable] $params, [Bool]$regexEscape = $false) {
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 {
param(
[Parameter(Mandatory = $True, Position = 0)]
@@ -1473,6 +1309,3 @@ $scoopPathEnvVar = switch (get_config USE_ISOLATED_PATH) {
# OS information
$WindowsBuild = [System.Environment]::OSVersion.Version.Build
# Setup proxy globally
setup_proxy

View File

@@ -24,7 +24,7 @@ function Invoke-Extraction {
$extractFn = $null
switch -regex ($Name[$i]) {
'\.zip$' {
if ((Test-HelperInstalled -Helper 7zip) -or ((get_config 7ZIPEXTRACT_USE_EXTERNAL) -and (Test-CommandAvailable 7z))) {
if ((Test-HelperInstalled -Helper 7zip) -or ((get_config USE_EXTERNAL_7ZIP) -and (Test-CommandAvailable 7z))) {
$extractFn = 'Expand-7zipArchive'
} else {
$extractFn = 'Expand-ZipArchive'
@@ -123,15 +123,18 @@ function Expand-7zipArchive {
}
if (!$IsTar -and $ExtractDir) {
movedir "$DestinationPath\$ExtractDir" $DestinationPath | Out-Null
# Remove temporary directory
Remove-Item "$DestinationPath\$($ExtractDir -replace '[\\/].*')" -Recurse -Force -ErrorAction Ignore
# Remove temporary directory if it is empty
$ExtractDirTopPath = [string] "$DestinationPath\$($ExtractDir -replace '[\\/].*')"
if ((Get-ChildItem -Path $ExtractDirTopPath -Force -ErrorAction Ignore).Count -eq 0) {
Remove-Item -Path $ExtractDirTopPath -Recurse -Force -ErrorAction Ignore
}
}
if (Test-Path $LogPath) {
Remove-Item $LogPath -Force
}
if ($Removal) {
if (($Path -replace '.*\.([^\.]*)$', '$1') -eq '001') {
# Remove splited 7-zip archive parts
# Remove splitted 7-zip archive parts
Get-ChildItem "$($Path -replace '\.[^\.]*$', '').???" | Remove-Item -Force
} elseif (($Path -replace '.*\.part(\d+)\.rar$', '$1')[-1] -eq '1') {
# Remove splitted RAR archive parts

769
lib/download.ps1 Normal file
View File

@@ -0,0 +1,769 @@
# 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) {
warn "Download failed! (Error $lastexitcode) $(aria_exit_code $lastexitcode)"
warn $urlstxt_content
warn $aria2
warn $(new_issue_msg $app $bucket "download via aria2 failed")
Write-Host "Fallback to default downloader ..."
try {
foreach ($url in $urls) {
Invoke-CachedDownload $app $version $url "$($data.$url.target)" $cookies $use_cache
}
} catch {
Write-Host $_ -ForegroundColor DarkRed
abort "URL $url is not valid"
}
}
# 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

View File

@@ -81,576 +81,10 @@ function install_app($app, $architecture, $global, $suggested, $use_cache = $tru
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' = "$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) {
$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 {
[CmdletBinding()]
param (
@@ -925,6 +359,7 @@ function show_notes($manifest, $dir, $original_dir, $persist_dir) {
Write-Output 'Notes'
Write-Output '-----'
Write-Output (wraptext (substitute $manifest.notes @{ '$dir' = $dir; '$original_dir' = $original_dir; '$persist_dir' = $persist_dir }))
Write-Output '-----'
}
}
@@ -985,7 +420,7 @@ function show_suggestions($suggested) {
}
if (!$fulfilled) {
Write-Host "'$app' suggests installing '$([string]::join("' or '", $feature_suggestions))'."
Write-Host "'$app' suggests installing '$([string]::join("' or '", $feature_suggestions))'." -ForegroundColor DarkYellow
}
}
}

View File

@@ -110,7 +110,7 @@ function json_path([String] $json, [String] $jsonpath, [Hashtable] $substitution
# Return versions in reverse order
$result = [System.Linq.Enumerable]::Reverse($result)
}
if ($single) {
if ([System.Linq.Enumerable]::Count($result) -eq 1 -or $single) {
# Extract First value
$result = [System.Linq.Enumerable]::First($result)
# Convert first value to string

View File

@@ -40,30 +40,74 @@ function Get-Manifest($app) {
$app = appname_from_url $url
$manifest = url_manifest $url
} else {
$app, $bucket, $version = parse_app $app
if ($bucket) {
$manifest = manifest $app $bucket
# Check if the manifest is already installed
if (installed $app) {
$global = installed $app $true
$ver = Select-CurrentVersion -AppName $app -Global:$global
if (!$ver) {
$app, $bucket, $ver = parse_app $app
$ver = Select-CurrentVersion -AppName $app -Global:$global
}
$install_info_path = "$(versiondir $app $ver $global)\install.json"
if (Test-Path $install_info_path) {
$install_info = parse_json $install_info_path
$bucket = $install_info.bucket
if (!$bucket) {
$url = $install_info.url
if ($url -match '^(ht|f)tps?://|\\\\') {
$manifest = url_manifest $url
}
if (!$manifest) {
if (Test-Path $url) {
$manifest = parse_json $url
} else {
# Fallback to installed manifest
$manifest = installed_manifest $app $ver $global
}
}
} else {
$manifest = manifest $app $bucket
if (!$manifest) {
$deprecated_dir = (Find-BucketDirectory -Name $bucket -Root) + '\deprecated'
$manifest = parse_json (Get-ChildItem $deprecated_dir -Filter "$(sanitary_path $app).json" -Recurse -ErrorAction Ignore).FullName
}
}
}
} else {
foreach ($tekcub in Get-LocalBucket) {
$manifest = manifest $app $tekcub
if ($manifest) {
$bucket = $tekcub
break
$app, $bucket, $version = parse_app $app
if ($bucket) {
$manifest = manifest $app $bucket
} else {
$matched_buckets = @()
foreach ($tekcub in Get-LocalBucket) {
$current_manifest = manifest $app $tekcub
if (!$manifest -and $current_manifest) {
$manifest = $current_manifest
$bucket = $tekcub
}
if ($current_manifest) {
$matched_buckets += $tekcub
}
}
}
if (!$manifest) {
# couldn't find app in buckets: check if it's a local path
if (Test-Path $app) {
$url = Convert-Path $app
$app = appname_from_url $url
$manifest = parse_json $url
} else {
if (($app -match '\\/') -or $app.EndsWith('.json')) { $url = $app }
$app = appname_from_url $app
}
}
}
if (!$manifest) {
# couldn't find app in buckets: check if it's a local path
if (Test-Path $app) {
$url = Convert-Path $app
$app = appname_from_url $url
$manifest = url_manifest $url
} else {
if (($app -match '\\/') -or $app.EndsWith('.json')) { $url = $app }
$app = appname_from_url $app
}
}
}
if ($matched_buckets.Length -gt 1) {
warn "Multiple buckets contain manifest '$app', the current selection is '$bucket/$app'."
}
return $app, $manifest, $bucket, $url
}

View File

@@ -39,7 +39,7 @@ if ($SubCommand -notin $SubCommands) {
exit 1
}
$opt, $other, $err = getopt $Args 'v' , 'verbose'
$opt, $other, $err = getopt $Args 'v' 'verbose'
if ($err) { "scoop alias: $err"; exit 1 }
$name, $command, $description = $other

View File

@@ -19,6 +19,10 @@
# scoop bucket known
param($cmd, $name, $repo)
if (get_config NO_JUNCTION) {
. "$PSScriptRoot\..\lib\versions.ps1"
}
if (get_config USE_SQLITE_CACHE) {
. "$PSScriptRoot\..\lib\manifest.ps1"
. "$PSScriptRoot\..\lib\database.ps1"

View File

@@ -7,7 +7,9 @@
param($app)
. "$PSScriptRoot\..\lib\json.ps1" # 'ConvertToPrettyJson'
. "$PSScriptRoot\..\lib\versions.ps1" # 'Select-CurrentVersion'
. "$PSScriptRoot\..\lib\manifest.ps1" # 'Get-Manifest'
. "$PSScriptRoot\..\lib\download.ps1" # 'Get-UserAgent'
if (!$app) { error '<app> missing'; my_usage; exit 1 }

View File

@@ -3,7 +3,9 @@
. "$PSScriptRoot\..\lib\getopt.ps1"
. "$PSScriptRoot\..\lib\depends.ps1" # 'Get-Dependency'
. "$PSScriptRoot\..\lib\versions.ps1" # 'Select-CurrentVersion'
. "$PSScriptRoot\..\lib\manifest.ps1" # 'Get-Manifest' (indirectly)
. "$PSScriptRoot\..\lib\download.ps1" # 'Get-UserAgent'
$opt, $apps, $err = getopt $args 'a:' 'arch='
$app = $apps[0]
@@ -20,7 +22,14 @@ try {
$deps = @()
Get-Dependency $app $architecture | ForEach-Object {
$dep = [ordered]@{}
$dep.Source, $dep.Name = $_ -split '/'
$app, $null, $bucket, $url = Get-Manifest $_
if (!$url) {
$bucket, $app = $_ -split '/'
}
$dep.Source = if ($url) { $url } else { $bucket }
$dep.Name = $app
$deps += [PSCustomObject]$dep
}
$deps

View File

@@ -22,8 +22,10 @@
. "$PSScriptRoot\..\lib\getopt.ps1"
. "$PSScriptRoot\..\lib\json.ps1" # 'autoupdate.ps1' (indirectly)
. "$PSScriptRoot\..\lib\autoupdate.ps1" # 'generate_user_manifest' (indirectly)
. "$PSScriptRoot\..\lib\versions.ps1" # 'Select-CurrentVersion'
. "$PSScriptRoot\..\lib\manifest.ps1" # 'generate_user_manifest' 'Get-Manifest'
. "$PSScriptRoot\..\lib\install.ps1"
. "$PSScriptRoot\..\lib\download.ps1"
. "$PSScriptRoot\..\lib\install.ps1" # 'nightly_version'
if (get_config USE_SQLITE_CACHE) {
. "$PSScriptRoot\..\lib\database.ps1"
}

View File

@@ -29,14 +29,13 @@ if ($global -and !(is_admin)) {
exit 1
}
$apps | ForEach-Object {
$app = $_
foreach ($app in $apps) {
if ($app -eq 'scoop') {
$hold_update_until = [System.DateTime]::Now.AddDays(1)
set_config HOLD_UPDATE_UNTIL $hold_update_until.ToString('o') | Out-Null
success "$app is now held and might not be updated until $($hold_update_until.ToLocalTime())."
return
continue
}
if (!(installed $app $global)) {
if ($global) {
@@ -44,7 +43,7 @@ $apps | ForEach-Object {
} else {
error "'$app' is not installed."
}
return
continue
}
if (get_config NO_JUNCTION) {

View File

@@ -2,7 +2,9 @@
# Summary: Opens the app homepage
param($app)
. "$PSScriptRoot\..\lib\versions.ps1" # 'Select-CurrentVersion'
. "$PSScriptRoot\..\lib\manifest.ps1" # 'Get-Manifest'
. "$PSScriptRoot\..\lib\download.ps1" # 'Get-UserAgent'
if ($app) {
$null, $manifest, $bucket, $null = Get-Manifest $app

View File

@@ -5,9 +5,11 @@
. "$PSScriptRoot\..\lib\getopt.ps1"
. "$PSScriptRoot\..\lib\manifest.ps1" # 'Get-Manifest'
. "$PSScriptRoot\..\lib\versions.ps1" # 'Get-InstalledVersion'
. "$PSScriptRoot\..\lib\versions.ps1" # 'Get-InstalledVersion', 'Select-CurrentVersion'
. "$PSScriptRoot\..\lib\download.ps1" # 'Get-RemoteFileSize'
$opt, $app, $err = getopt $args 'v' 'verbose'
$original_app = $app
if ($err) { error "scoop info: $err"; exit 1 }
$verbose = $opt.v -or $opt.verbose
@@ -22,7 +24,7 @@ if (!$manifest) {
$global = installed $app $true
$status = app_status $app $global
$install = install_info $app $status.version $global
$status.installed = $bucket -and $install.bucket -eq $bucket
$status.installed = ($bucket -and $install.bucket -eq $bucket) -or (installed $app)
$version_output = $manifest.version
$manifest_file = if ($bucket) {
manifest_path $app $bucket
@@ -30,12 +32,30 @@ $manifest_file = if ($bucket) {
$url
}
# Standalone and Source detection
if ((Test-Path $original_app) -or ($original_app -match '^(ht|f)tps?://|\\\\')) {
$standalone = $true
if (Test-Path $original_app) {
$original_app = (Get-AbsolutePath "$original_app")
}
if ($install.url) {
if (Test-Path $install.url) {
$install_url = (Get-AbsolutePath $install.url)
} else {
$install_url = $install.url
}
}
if ($original_app -eq $install_url) {
$same_source = $true
}
}
if ($verbose) {
$dir = currentdir $app $global
$original_dir = versiondir $app $manifest.version $global
$persist_dir = persistdir $app $global
} else {
$dir, $original_dir, $persist_dir = "<root>", "<root>", "<root>"
$dir, $original_dir, $persist_dir = '<root>', '<root>', '<root>'
}
if ($status.installed) {
@@ -43,21 +63,39 @@ if ($status.installed) {
if ($install.url) {
$manifest_file = $install.url
}
if ($status.version -eq $manifest.version) {
if ($status.deprecated) {
$manifest_file = $status.deprecated
} elseif ($standalone -and !$same_source) {
$version_output = $manifest.version
} elseif ($status.version -eq $manifest.version) {
$version_output = $status.version
} else {
$version_output = "$($status.version) (Update to $($manifest.version) available)"
}
}
$item = [ordered]@{ Name = $app }
if ($status.deprecated) {
$item.Name += ' (DEPRECATED)'
}
if ($manifest.description) {
$item.Description = $manifest.description
}
$item.Version = $version_output
if ($bucket) {
$item.Bucket = $bucket
$item.Source = if ($standalone) {
$original_app
} else {
if ($install.bucket) {
$install.bucket
} elseif ($install.url) {
$install.url
} else {
$bucket
}
}
if ($manifest.homepage) {
$item.Website = $manifest.homepage.TrimEnd('/')
}
@@ -69,7 +107,7 @@ if ($manifest.license) {
$manifest.license
} elseif ($manifest.license -match '[|,]') {
if ($verbose) {
"$($manifest.license) ($(($manifest.license -Split "\||," | ForEach-Object { "https://spdx.org/licenses/$_.html" }) -join ', '))"
"$($manifest.license) ($(($manifest.license -Split '\||,' | ForEach-Object { "https://spdx.org/licenses/$_.html" }) -join ', '))"
} else {
$manifest.license
}
@@ -84,7 +122,7 @@ if ($manifest.depends) {
if (Test-Path $manifest_file) {
if (Get-Command git -ErrorAction Ignore) {
$gitinfo = (Invoke-Git -Path (Split-Path $manifest_file) -ArgumentList @('log', '-1', '-s', '--format=%aD#%an', $manifest_file) 2> $null) -Split '#'
$gitinfo = (Invoke-Git -Path (Split-Path $manifest_file) -ArgumentList @('log', '-1', '-s', '--format=%aI#%an', $manifest_file) 2> $null) -Split '#'
}
if ($gitinfo) {
$item.'Updated at' = $gitinfo[0] | Get-Date
@@ -100,11 +138,13 @@ if ($verbose) { $item.Manifest = $manifest_file }
if ($status.installed) {
# Show installed versions
$installed_output = @()
Get-InstalledVersion -AppName $app -Global:$global | ForEach-Object {
$installed_output += if ($verbose) { versiondir $app $_ $global } else { "$_$(if ($global) { " *global*" })" }
if (!$standalone -or $same_source) {
$installed_output = @()
Get-InstalledVersion -AppName $app -Global:$global | ForEach-Object {
$installed_output += if ($verbose) { versiondir $app $_ $global } else { "$_$(if ($global) { ' *global*' })" }
}
$item.Installed = $installed_output -join "`n"
}
$item.Installed = $installed_output -join "`n"
if ($verbose) {
# Show size of installation
@@ -161,12 +201,12 @@ if ($status.installed) {
foreach ($url in @(url $manifest (Get-DefaultArchitecture))) {
try {
if (Test-Path (cache_path $app $manifest.version $url)) {
$cached = " (latest version is cached)"
$cached = ' (latest version is cached)'
} else {
$cached = $null
}
$urlLength = (Invoke-WebRequest $url -Method Head).Headers.'Content-Length' | ForEach-Object { [int]$_ }
$urlLength = Get-RemoteFileSize $url
$totalPackage += $urlLength
} catch [System.Management.Automation.RuntimeException] {
$totalPackage = 0
@@ -196,7 +236,7 @@ if ($binaries) {
$binary_output += $_
}
}
$item.Binaries = $binary_output -join " | "
$item.Binaries = $binary_output -join ' | '
}
$shortcuts = @(arch_specific 'shortcuts' $manifest $install.architecture)
if ($shortcuts) {
@@ -204,7 +244,7 @@ if ($shortcuts) {
$shortcuts | ForEach-Object {
$shortcut_output += $_[1]
}
$item.Shortcuts = $shortcut_output -join " | "
$item.Shortcuts = $shortcut_output -join ' | '
}
$env_set = arch_specific 'env_set' $manifest $install.architecture
if ($env_set) {

View File

@@ -33,6 +33,7 @@
. "$PSScriptRoot\..\lib\manifest.ps1" # 'generate_user_manifest' 'Get-Manifest' 'Select-CurrentVersion' (indirectly)
. "$PSScriptRoot\..\lib\system.ps1"
. "$PSScriptRoot\..\lib\install.ps1"
. "$PSScriptRoot\..\lib\download.ps1"
. "$PSScriptRoot\..\lib\decompress.ps1"
. "$PSScriptRoot\..\lib\shortcuts.ps1"
. "$PSScriptRoot\..\lib\psmodules.ps1"

View File

@@ -5,8 +5,9 @@ param($query)
. "$PSScriptRoot\..\lib\versions.ps1" # 'Select-CurrentVersion'
. "$PSScriptRoot\..\lib\manifest.ps1" # 'parse_json' 'Select-CurrentVersion' (indirectly)
. "$PSScriptRoot\..\lib\download.ps1" # 'Get-UserAgent'
$def_arch = Get-DefaultArchitecture
$defaultArchitecture = Get-DefaultArchitecture
if (-not (Get-FormatData ScoopApps)) {
Update-FormatData "$PSScriptRoot\..\supporting\formats\ScoopTypes.Format.ps1xml"
}
@@ -47,10 +48,11 @@ $apps | Where-Object { !$query -or ($_.name -match $query) } | ForEach-Object {
$item.Updated = $updated
$info = @()
if ((app_status $app $global).deprecated) { $info += 'Deprecated package'}
if ($global) { $info += 'Global install' }
if (failed $app $global) { $info += 'Install failed' }
if ($install_info.hold) { $info += 'Held package' }
if ($install_info.architecture -and $def_arch -ne $install_info.architecture) {
if ($install_info.architecture -and $defaultArchitecture -ne $install_info.architecture) {
$info += $install_info.architecture
}
$item.Info = $info -join ', '

View File

@@ -10,15 +10,10 @@ param($query)
. "$PSScriptRoot\..\lib\manifest.ps1" # 'manifest'
. "$PSScriptRoot\..\lib\versions.ps1" # 'Get-LatestVersion'
. "$PSScriptRoot\..\lib\download.ps1"
$list = [System.Collections.Generic.List[PSCustomObject]]::new()
$githubtoken = Get-GitHubToken
$authheader = @{}
if ($githubtoken) {
$authheader = @{'Authorization' = "token $githubtoken" }
}
function bin_match($manifest, $query) {
if (!$manifest.bin) { return $false }
$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) {
$uri = [System.Uri](known_bucket_repo $bucket)
if ($uri.AbsolutePath -match '/([a-zA-Z0-9]*)/([a-zA-Z0-9-]*)(?:.git|/)?') {

View File

@@ -6,6 +6,7 @@
. "$PSScriptRoot\..\lib\manifest.ps1" # 'manifest' 'parse_json' "install_info"
. "$PSScriptRoot\..\lib\versions.ps1" # 'Select-CurrentVersion'
. "$PSScriptRoot\..\lib\download.ps1" # 'Get-UserAgent'
# check if scoop needs updating
$currentdir = versiondir 'scoop' 'current'
@@ -58,7 +59,7 @@ $true, $false | ForEach-Object { # local and global apps
Get-ChildItem $dir | Where-Object name -NE 'scoop' | ForEach-Object {
$app = $_.name
$status = app_status $app $global
if (!$status.outdated -and !$status.failed -and !$status.removed -and !$status.missing_deps) { return }
if (!$status.outdated -and !$status.failed -and !$status.deprecated -and !$status.removed -and !$status.missing_deps) { return }
$item = [ordered]@{}
$item.Name = $app
@@ -66,9 +67,10 @@ $true, $false | ForEach-Object { # local and global apps
$item.'Latest Version' = if ($status.outdated) { $status.latest_version } else { "" }
$item.'Missing Dependencies' = $status.missing_deps -Split ' ' -Join ' | '
$info = @()
if ($status.failed) { $info += 'Install failed' }
if ($status.hold) { $info += 'Held package' }
if ($status.removed) { $info += 'Manifest removed' }
if ($status.failed) { $info += 'Install failed' }
if ($status.hold) { $info += 'Held package' }
if ($status.deprecated) { $info += 'Deprecated' }
if ($status.removed) { $info += 'Manifest removed' }
$item.Info = $info -join ', '
$list += [PSCustomObject]$item
}

View File

@@ -74,7 +74,7 @@ if (!$apps) { exit 0 }
continue
}
Invoke-Installer -Path $dir -Manifest $manifest -ProcessorArchitecture $architecture -Uninstall
Invoke-Installer -Path $dir -Manifest $manifest -ProcessorArchitecture $architecture -Global $global -Uninstall
rm_shims $app $manifest $global $architecture
rm_startmenu_shortcuts $manifest $global $architecture

View File

@@ -24,6 +24,7 @@
. "$PSScriptRoot\..\lib\versions.ps1"
. "$PSScriptRoot\..\lib\depends.ps1"
. "$PSScriptRoot\..\lib\install.ps1"
. "$PSScriptRoot\..\lib\download.ps1"
if (get_config USE_SQLITE_CACHE) {
. "$PSScriptRoot\..\lib\database.ps1"
}

View File

@@ -29,15 +29,17 @@
# -p, --passthru Return reports as objects
. "$PSScriptRoot\..\lib\getopt.ps1"
. "$PSScriptRoot\..\lib\versions.ps1" # 'Select-CurrentVersion'
. "$PSScriptRoot\..\lib\manifest.ps1" # 'Get-Manifest'
. "$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'
$opt, $apps, $err = getopt $args 'asnup' @('all', 'scan', 'no-depends', 'no-update-scoop', 'passthru')
if ($err) { "scoop virustotal: $err"; exit 1 }
if (!$apps -and -$all) { my_usage; exit 1 }
$architecture = Format-ArchitectureString
$all = $apps -eq '*' -or $opt.a -or $opt.all
if (!$apps -and !$all) { my_usage; exit 1 }
$architecture = Get-DefaultArchitecture
if (is_scoop_outdated) {
if ($opt.u -or $opt.'no-update-scoop') {
@@ -47,11 +49,8 @@ if (is_scoop_outdated) {
}
}
$apps_param = $apps
if ($apps_param -eq '*' -or $opt.a -or $opt.all) {
$apps = installed_apps $false
$apps += installed_apps $true
if ($all) {
$apps = (installed_apps $false) + (installed_apps $true)
}
if (!$opt.n -and !$opt.'no-depends') {
@@ -88,11 +87,6 @@ Function ConvertTo-VirusTotalUrlId ($url) {
$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) {
$hash = $hash.ToLower()
$api_url = "https://www.virustotal.com/api/v3/files/$hash"
@@ -102,14 +96,14 @@ Function Get-VirusTotalResultByHash ($hash, $url, $app) {
$response = Invoke-WebRequest -Uri $api_url -Method GET -Headers $headers -UseBasicParsing
$result = $response.Content
$stats = json_path $result '$.data.attributes.last_analysis_stats'
[int]$malicious = json_path $stats '$[0].malicious' $null $false $true
[int]$suspicious = json_path $stats '$[0].suspicious' $null $false $true
[int]$timeout = json_path $stats '$[0].timeout' $null $false $true
[int]$undetected = json_path $stats '$[0].undetected' $null $false $true
[int]$malicious = json_path $stats '$.malicious'
[int]$suspicious = json_path $stats '$.suspicious'
[int]$timeout = json_path $stats '$.timeout'
[int]$undetected = json_path $stats '$.undetected'
[int]$unsafe = $malicious + $suspicious
[int]$total = $unsafe + $undetected
[int]$fileSize = json_path $result '$.data.attributes.size' $null $false $true
$report_hash = json_path $result '$.data.attributes.sha256' $null $false $true
[int]$fileSize = json_path $result '$.data.attributes.size'
$report_hash = json_path $result '$.data.attributes.sha256'
$report_url = "https://www.virustotal.com/gui/file/$report_hash"
if ($total -eq 0) {
info "$app`: Analysis in progress."

View File

@@ -1 +1 @@
410f84fe347cf55f92861ea3899d30b2d84a8bbc56bb3451d74697a4a0610b25 *shim.exe
140e3801d8adeda639a21b14e62b93a4c7d26b7a758421f43c82be59753be49b *shim.exe

View File

@@ -1 +1 @@
9ce94adf48f7a31ab5773465582728c39db6f11a560fc43316fe6c1ad0a7b69a76aa3f9b52bb6b2e3be8043e4920985c8ca0bf157be9bf1e4a5a4d7c4ed195ba *shim.exe
59d9da9f9714003b915bcafbe1b41f53b121dde206ecc23984f62273e957766eece8d64ffc53011c328d3a2ad627aa0f4f7c39bbec8e7b64d0d2ee7b7e771423 *shim.exe

Binary file not shown.

View File

@@ -1 +1 @@
v3.1.1
v3.1.2

View File

@@ -1,4 +1,4 @@
e1e27af7b07eeedf5ce71a9255f0422816a6fc5849a483c6714e1b472044fa9d *Newtonsoft.Json.dll
9f1a8f06c284a4ee01f704d89003ddc7061846f2008094071e9adf08267849f9 *Newtonsoft.Json.Schema.dll
d11b660612ce821ec03772b73aa3b8884a0479275c70085c7e143913a41a2d28 *Scoop.Validator.dll
7496d5349a123a6e3696085662b2ff17b156ccdb0e30e0c396ac72d2da36ce1c *Newtonsoft.Json.Schema.dll
83b1006443e8c340ca4c631614fc2ce0d5cb9a28c851e3b59724299f58b1397f *Scoop.Validator.dll
87f8f8db2202a3fbef6f431d0b7e20cec9d32095c441927402041f3c4076c1b6 *validator.exe

View File

@@ -1,4 +1,4 @@
56eb7f070929b239642dab729537dde2c2287bdb852ad9e80b5358c74b14bc2b2dded910d0e3b6304ea27eb587e5f19db0a92e1cbae6a70fb20b4ef05057e4ac *Newtonsoft.Json.dll
551e772fe2ee72b349d5c4ed5d5f8d8957d50cfcbbde7af5d5740d9652bcad626a2c00bc0d9223db7c874962187a90f9160397f243eadee1c594585ba2b155e0 *Newtonsoft.Json.Schema.dll
0a31d192c82bbd8ce50fb75dd5fe813c98bb870d54c112c600ae2e2436063cb2bd94bb206675dfe31ce89922e9a04a3d520ed579ab7198835190b67a6321a74e *Scoop.Validator.dll
78b12beb1e67ac4f6efa0fcba57b4b34ea6a31d8b369934d6b6a6617386ef9939ea453ac262916e5857ce0359eb809424ea33c676a87a8fdfd77a59b2ce96db0 *Newtonsoft.Json.Schema.dll
e9da4370aee4df47eedcf15d9749712eee513e5a9115b808617ddfcfde5bc47a0410edfb57508fcf51033c0be967611b2fd2c2ba944de7290c020cc67f77ac57 *Scoop.Validator.dll
58a0c37e98cac17822c7756bf6686a5fb74e711b8d986d13bd2f689f6b3b1f485fcd908d92cbc6a162a0e5974c2c5a43de57d15f1996be0aa405e41ec2ec8393 *validator.exe

View File

@@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="Newtonsoft.Json" version="13.0.3" targetFramework="net45" />
<package id="Newtonsoft.Json.Schema" version="3.0.15" targetFramework="net45" />
<package id="Microsoft.Net.Compilers.Toolset" version="4.9.2" targetFramework="net45" developmentDependency="true" />
<package id="Newtonsoft.Json.Schema" version="4.0.1" targetFramework="net45" />
<package id="Microsoft.Net.Compilers.Toolset" version="4.10.0" targetFramework="net45"
developmentDependency="true" />
</packages>

View File

@@ -1,8 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import
Project="packages\Microsoft.Net.Compilers.Toolset.4.9.2\build\Microsoft.Net.Compilers.Toolset.props"
Condition="Exists('packages\Microsoft.Net.Compilers.Toolset.4.9.2\build\Microsoft.Net.Compilers.Toolset.props')" />
Project="packages\Microsoft.Net.Compilers.Toolset.4.10.0\build\Microsoft.Net.Compilers.Toolset.props"
Condition="Exists('packages\Microsoft.Net.Compilers.Toolset.4.10.0\build\Microsoft.Net.Compilers.Toolset.props')" />
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props"
Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
@@ -23,9 +23,8 @@
<Private>True</Private>
</Reference>
<Reference
Include="Newtonsoft.Json.Schema, Version=3.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed">
<HintPath>
packages\Newtonsoft.Json.Schema.3.0.15\lib\net45\Newtonsoft.Json.Schema.dll</HintPath>
Include="Newtonsoft.Json.Schema, Version=4.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed">
<HintPath>packages\Newtonsoft.Json.Schema.4.0.1\lib\net45\Newtonsoft.Json.Schema.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="System" />
@@ -52,7 +51,7 @@
http://go.microsoft.com/fwlink/?LinkID=322105.The missing file is {0}.</ErrorText>
</PropertyGroup>
<Error
Condition="!Exists('packages\Microsoft.Net.Compilers.Toolset.4.9.2\build\Microsoft.Net.Compilers.Toolset.props')"
Text="$([System.String]::Format('$(ErrorText)', 'packages\Microsoft.Net.Compilers.Toolset.4.9.2\build\Microsoft.Net.Compilers.Toolset.props'))" />
Condition="!Exists('packages\Microsoft.Net.Compilers.Toolset.4.10.0\build\Microsoft.Net.Compilers.Toolset.props')"
Text="$([System.String]::Format('$(ErrorText)', 'packages\Microsoft.Net.Compilers.Toolset.4.10.0\build\Microsoft.Net.Compilers.Toolset.props'))" />
</Target>
</Project>

View File

@@ -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' {
It 'should return true if command exists' {
Test-CommandAvailable 'Write-Host' | Should -BeTrue

View File

@@ -25,7 +25,7 @@ Describe 'Decompression function' -Tag 'Scoop', 'Windows', 'Decompress' {
}
It 'Test cases should exist and hash should match' {
$testcases | Should -Exist
(Get-FileHash -Path $testcases -Algorithm SHA256).Hash.ToLower() | Should -Be 'afb86b0552187b8d630ce25d02835fb809af81c584f07e54cb049fb74ca134b6'
(Get-FileHash -Path $testcases -Algorithm SHA256).Hash.ToLower() | Should -Be '591072faabd419b77932b7023e5899b4e05c0bf8e6859ad367398e6bfe1eb203'
}
It 'Test cases should be extracted correctly' {
{ Microsoft.PowerShell.Archive\Expand-Archive -Path $testcases -DestinationPath $working_dir } | Should -Not -Throw
@@ -61,7 +61,7 @@ Describe 'Decompression function' -Tag 'Scoop', 'Windows', 'Decompress' {
$to = test_extract 'Expand-7zipArchive' $test1
$to | Should -Exist
"$to\empty" | Should -Exist
(Get-ChildItem $to).Count | Should -Be 3
(Get-ChildItem $to).Count | Should -Be 4
}
It 'extract "extract_dir" correctly' {
@@ -78,6 +78,14 @@ Describe 'Decompression function' -Tag 'Scoop', 'Windows', 'Decompress' {
(Get-ChildItem $to).Count | Should -Be 1
}
It 'extract "extract_dir" with nested folder with same name' {
$to = test_extract 'Expand-7zipArchive' $test1 $false 'keep\sub'
$to | Should -Exist
"$to\keep\empty" | Should -Exist
(Get-ChildItem $to).Count | Should -Be 1
(Get-ChildItem "$to\keep").Count | Should -Be 1
}
It 'extract nested compressed file' {
# file ext: tgz
$to = test_extract 'Expand-7zipArchive' $test2

View File

@@ -65,6 +65,7 @@ Describe 'Package Dependencies' -Tag 'Scoop' {
BeforeAll {
Mock Test-HelperInstalled { $false }
Mock get_config { $true } -ParameterFilter { $name -eq 'USE_LESSMSI' }
Mock get_config { $false } -ParameterFilter { $name -eq 'USE_EXTERNAL_7ZIP' }
Mock Get-Manifest { 'lessmsi', @{}, $null, $null } -ParameterFilter { $app -eq 'lessmsi' }
Mock Get-Manifest { '7zip', @{ url = 'test.msi' }, $null, $null } -ParameterFilter { $app -eq '7zip' }
Mock Get-Manifest { 'innounp', @{}, $null, $null } -ParameterFilter { $app -eq 'innounp' }

View 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'
}
}

View File

@@ -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' {
It 'should work correctly' {
is_in_dir 'C:\test' 'C:\foo' | Should -BeFalse

Binary file not shown.