Includes/Download.provider.github.ps1
|
<#
Provider : github Dependencies : PowerShellForGitHub (Get-GithubRelease) v2 : assets are selected through the download.assets[] list (patterns tested in order, first match wins). The GitHub digest is verified automatically when present. When the matched asset declares a signature-file, it is downloaded from the same release and used to verify the asset's integrity. #> <# .SYNOPSIS Resolve the GitHub release matching the requested version. .DESCRIPTION 'latest' selects the most recent release ; any other value matches the first release whose tag contains it (substring match). TAGNAME is set in the mustache values table. Returns the release object. #> function Get-ReleaseFromGithub { [CmdletBinding()] [OutputType([object])] Param ( # Resolved package definition [Parameter(Mandatory = $true, ValueFromPipeLine = $true)][object]$pkgdef, # Template values (uppercase keys, mustache compatible) [Parameter(Mandatory = $false)][hashtable]$mustacheValues = @{} ) Begin { Write-EnterFunction } Process { $release = $null if ($pkgdef.version -eq "latest") { $release = Get-GithubRelease -Owner $pkgdef.download.owner -RepositoryName $pkgdef.download.repo -Latest } else { $releases = Get-GithubRelease -Owner $pkgdef.download.owner -RepositoryName $pkgdef.download.repo $release = @($releases | Where-Object { $_.tag_name -like "*$($pkgdef.version)*" }) | Select-Object -First 1 } if ($release) { Write-Verbose "Found release $($release.tag_name) matched requested version $($pkgdef.version)" } else { Write-Fatal "No release found for version $($pkgdef.version)" } # update mustacheValues if ($mustacheValues) { $mustacheValues.TAGNAME = $release.tag_name } return $release } End { Write-LeaveFunction } } <# .SYNOPSIS Select the asset of a GitHub release matching the ordered pattern list. #> function Get-AssetFromGithub { [CmdletBinding()] [OutputType([System.Object])] Param ( # Resolved package definition [Parameter(Mandatory = $true, ValueFromPipeLine = $true)][object]$pkgdef, # Template values (uppercase keys, mustache compatible) [Parameter(Mandatory = $false)][hashtable]$mustacheValues = @{}, # Release found with Get-ReleaseFromGithub [Parameter(Mandatory = $true, ValueFromPipeLine = $true)][System.Object]$Release ) Begin { Write-EnterFunction } Process { $assets = @($Release.assets) if (-not $assets.Count) { Write-Fatal "No assets found for release $($Release.tag_name)" } Write-Verbose "Found $($assets.Count) assets for release $($Release.tag_name)" $match = Select-GenericPackageAsset -Names (@($assets | ForEach-Object { $_.name })) -Assets $assets -AssetSpecs @($pkgdef.download.assets) -mustacheValues $mustacheValues if (-not $match) { Write-Fatal "No asset of release $($Release.tag_name) matches the declared patterns" } return $assets[$match.Index] } End { Write-LeaveFunction } } <# .SYNOPSIS Get asset information from a GitHub release without downloading. #> function Get-AssetInfoFromGithub { [CmdletBinding()] [OutputType([array])] Param ( [Parameter(Mandatory = $true, ValueFromPipeLine = $true)][object]$pkgdef, [Parameter(Mandatory = $false)][string]$CachePath, [Parameter(Mandatory = $false)][hashtable]$mustacheValues = @{} ) Begin { Write-EnterFunction $assets = @() } Process { $release = Get-ReleaseFromGithub -pkgdef $pkgdef -mustacheValues $mustacheValues $asset = Get-AssetFromGithub -pkgdef $pkgdef -mustacheValues $mustacheValues -Release $release if ($asset) { $assetInfo = @{ name = $asset.name download_url = $asset.browser_download_url local_path = "$CachePath/$($asset.name)" } if ($asset.digest) { $assetInfo.digest = $asset.digest } $assets += $assetInfo } } End { Write-LeaveFunction return $assets } } <# .SYNOPSIS Download an asset from a GitHub release. .DESCRIPTION Resolves the release, selects the asset with the ordered pattern list, verifies the GitHub digest when available, downloads the asset (skipping when already cached) and verifies the signature-file when declared. #> function Download-AssetFromGithub { [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseApprovedVerbs", "", Justification="Download is a more intuitive verb for this function because Get- is too generic.")] [CmdletBinding()] [OutputType([String])] Param ( # Resolved package definition [Parameter(Mandatory = $true, ValueFromPipeLine = $true)][object]$pkgdef, # Destination path (cache directory) [Parameter()][String]$DestinationPath, # Template values (uppercase keys, mustache compatible) [Parameter(Mandatory = $false)][hashtable]$mustacheValues = @{} ) Begin { Write-EnterFunction } Process { # Resolve DestinationPath: parameter > pkgdef.download.destination > temp path if ([string]::IsNullOrEmpty($DestinationPath)) { if ($pkgdef.download.destination) { $DestinationPath = $pkgdef.download.destination } else { $DestinationPath = [io.path]::GetTempPath() } } $release = Get-ReleaseFromGithub -pkgdef $pkgdef -mustacheValues $mustacheValues $asset = Get-AssetFromGithub -pkgdef $pkgdef -mustacheValues $mustacheValues -Release $release if (-not $asset) { return $null } $destFile = "$DestinationPath/$($asset.name)" if (Test-FileExist $destFile) { Write-Verbose "Asset $($asset.name) already exists in $DestinationPath, skipping download" return $destFile } # GitHub digest verification (when the API provides one) — checked # against the downloaded file, since PwSh.Fw.Web Download-File does # not accept digest parameters $DigestAlgo = $null $rc = [string]$asset.digest -match '(?<algo>[a-z]+[0-9]+):(?<hash>[a-fA-F0-9]+)' if ($rc) { switch ($Matches.algo) { "sha256" { $DigestAlgo = 'SHA256' } "sha1" { $DigestAlgo = 'SHA1' } "md5" { $DigestAlgo = 'MD5' } default { Write-Warning "Unable to detect hash algorithm in digest : $($asset.digest)" } } } Write-Verbose "Downloading $($asset.name) from $($asset.browser_download_url) to $DestinationPath" $null = Download-File -Url $asset.browser_download_url -To $DestinationPath -PassThru if (-not (Test-FileExist $destFile)) { Write-Fatal "Error downloading asset $($asset.name)" return $null } if ($DigestAlgo) { $ActualDigest = (Get-FileHash -Algorithm $DigestAlgo -Path $destFile).Hash.ToLower() if ($ActualDigest -ne $Matches.hash.ToLower()) { Write-Fatal "Digest mismatch for $($asset.name) : expected $($Matches.hash), got $ActualDigest" return $null } Write-Verbose "Digest verified ($DigestAlgo) for $($asset.name)" } # Signature-file verification (companion checksum from the same release) $spec = $null foreach ($a in @($pkgdef.download.assets)) { if ($a.pattern -and $asset.name -match (Render-MustacheField -value $a.pattern -mustacheValues $mustacheValues)) { $spec = $a break } } if ($spec -and $spec.'signature-file') { $sigPattern = Render-MustacheField -value $spec.'signature-file'.pattern -mustacheValues $mustacheValues $sigAsset = @($release.assets | Where-Object { $_.name -match $sigPattern }) | Select-Object -First 1 if (-not $sigAsset) { Write-Fatal "Signature file matching '$sigPattern' not found in release $($release.tag_name)" return $null } $sigFile = Download-File -Url $sigAsset.browser_download_url -To $DestinationPath -PassThru if (-not (Test-GenericPackageSignature -AssetPath $destFile -SignaturePath $sigFile -Algorithm $spec.'signature-file'.algorithm)) { return $null } } return $destFile } End { Write-LeaveFunction } } |