Public/Save-StoreApp.ps1
|
function Save-StoreApp { <# .SYNOPSIS Downloads Windows Store packages to disk. .DESCRIPTION Accepts package metadata from Get-StoreLinks (via pipeline or parameter) and downloads each file. Validates SHA1 after download and skips files that already exist with matching name and size. Supports -WhatIf and -Confirm via ShouldProcess. .PARAMETER InputObject One or more package objects from Get-StoreLinks. Accepts pipeline input. .PARAMETER Path The output directory. Defaults to the current directory. Created if it does not exist. .PARAMETER Filter A wildcard pattern to filter which packages to download by filename. For example, '*.appx', '*.msixbundle', '*x64*'. .OUTPUTS System.IO.FileInfo for each successfully downloaded file. .EXAMPLE Get-StoreLinks -Data '9NBLGGH4NNS1' | Save-StoreApp -Path ./downloads Downloads all Retail-channel packages for the given product ID. .EXAMPLE Get-StoreLinks -Data '9NBLGGH4NNS1' -Channel All | Save-StoreApp -Filter '*.msixbundle' -Path C:\Packages Downloads only .msixbundle files from all channels. .EXAMPLE Get-StoreLinks -Data '9NBLGGH4NNS1' | Save-StoreApp -WhatIf Shows what would be downloaded without actually downloading. #> [CmdletBinding(SupportsShouldProcess)] [OutputType([System.IO.FileInfo])] param( [Parameter(Mandatory, ValueFromPipeline)] [PSCustomObject]$InputObject, [Parameter()] [string]$Path = '.', [Parameter()] [string]$Filter ) begin { $resolvedPath = $PSCmdlet.GetUnresolvedProviderPathFromPSPath($Path) if (-not (Test-Path -LiteralPath $resolvedPath -PathType Container)) { Write-Verbose "Creating output directory: $resolvedPath" $null = New-Item -Path $resolvedPath -ItemType Directory -Force -ErrorAction Stop } $queue = [System.Collections.Generic.List[PSCustomObject]]::new() } process { if ($Filter -and ($InputObject.Name -notlike $Filter)) { Write-Verbose "Skipping (filter mismatch): $($InputObject.Name)" return } $queue.Add($InputObject) } end { $total = $queue.Count if ($total -eq 0) { Write-Verbose 'No packages matched the filter.' return } for ($i = 0; $i -lt $total; $i++) { $pkg = $queue[$i] $fileName = $pkg.Name $outFile = Join-Path -Path $resolvedPath -ChildPath $fileName Write-Progress -Activity 'Downloading Store packages' ` -Status "$fileName ($($i + 1) of $total)" ` -PercentComplete ([math]::Floor(($i / $total) * 100)) # Skip if file already exists with matching size. if (Test-Path -LiteralPath $outFile -PathType Leaf) { $existing = Get-Item -LiteralPath $outFile $expectedBytes = Convert-SizeToBytes -SizeString $pkg.Size if ($null -ne $expectedBytes -and $existing.Length -eq $expectedBytes) { Write-Verbose "Skipping (exists, size matches): $fileName" Write-Output $existing continue } elseif ($null -eq $expectedBytes) { Write-Verbose "Skipping (exists, size not parseable): $fileName" Write-Output $existing continue } Write-Verbose "File exists but size mismatch (expected ~$expectedBytes, got $($existing.Length)). Re-downloading." } if (-not $PSCmdlet.ShouldProcess($fileName, 'Download')) { continue } Write-Verbose "Downloading: $fileName -> $outFile" try { $dlParams = @{ Uri = $pkg.URL OutFile = $outFile UseBasicParsing = $true ErrorAction = 'Stop' } Invoke-WebRequest @dlParams } catch { Write-Error "Failed to download ${fileName}: $_" continue } # SHA1 validation. if ($pkg.SHA1 -and $pkg.SHA1.Trim().Length -eq 40) { Write-Verbose "Validating SHA1 for $fileName..." $actualHash = (Get-FileHash -LiteralPath $outFile -Algorithm SHA1).Hash if ($actualHash -ne $pkg.SHA1.ToUpper()) { Write-Warning "SHA1 mismatch for ${fileName}: expected $($pkg.SHA1), got $actualHash" } else { Write-Verbose "SHA1 validated: $actualHash" } } Write-Output (Get-Item -LiteralPath $outFile) } Write-Progress -Activity 'Downloading Store packages' -Completed } } function Convert-SizeToBytes { <# .SYNOPSIS Converts a human-readable size string to bytes. #> [CmdletBinding()] [OutputType([long])] param( [Parameter()] [string]$SizeString ) if ([string]::IsNullOrWhiteSpace($SizeString)) { return $null } if ($SizeString -match '^([\d,.]+)\s*(Bytes?|KB|MB|GB|TB)?$') { $num = [double]($Matches[1] -replace ',', '') $unit = if ($Matches[2]) { $Matches[2] } else { 'Bytes' } switch -Wildcard ($unit) { 'Byte*' { return [long]$num } 'KB' { return [long]($num * 1KB) } 'MB' { return [long]($num * 1MB) } 'GB' { return [long]($num * 1GB) } 'TB' { return [long]($num * 1TB) } } } return $null } |