Skip to content

Instantly share code, notes, and snippets.

@mavaddat
Last active October 12, 2023 17:00
Show Gist options
  • Save mavaddat/26146d111abf62f6160b1bd02a392ba8 to your computer and use it in GitHub Desktop.
Save mavaddat/26146d111abf62f6160b1bd02a392ba8 to your computer and use it in GitHub Desktop.
This correctly downloads neutral and x64 packages but untested for arm and 32bit systems. The path must point to a folder.
# Usage (for one URI):
<#
Import-Module -Name Invoke-DownloadAppxPackage.ps1
$URI = 'https://www.microsoft.com/store/productId/9P6RC76MSMMJ' # From Windows Store 'share'
if( Get-Command -Name Get-AppxPackageDownload -CommandType Function ) {
Get-AppxPackageDownload -Uri $URI -Path $env:TEMP # Use -Force to skip confirmation
} else {
Write-Host 'Get-AppxPackageDownload function not found'
}
#>
# Usage (for multiple URIs):
<#
Import-Module -Name Invoke-DownloadAppxPackage.ps1
$URIs = @('https://www.microsoft.com/store/productId/9P6RC76MSMMJ', 'https://www.microsoft.com/store/productId/9N0866FS04W8', 'https://www.microsoft.com/store/productId/9NH2GPH4JZS4')
if( Get-Command -Name Get-AppxPackageDownload -CommandType Function ) {
$URIs | ForEach-Object {
Get-AppxPackageDownload -Uri $_ -Path $env:TEMP -Force # Use -Force to skip confirmation
}
} else {
Write-Host 'Get-AppxPackageDownload function not found'
}
#>
function Invoke-DownloadAppxPackage
{
[CmdletBinding(SupportsShouldProcess)]
param (
[Parameter(Mandatory, ValueFromPipeline)]
[ValidateScript({ [uri]::TryCreate($_, [UriKind]::Absolute, [ref]$null) })]
[string]$Uri,
[ValidateScript({ Test-Path -Path $_ -PathType Container })]
[string]$OutputDir = $env:TEMP
)
process
{
if ($WhatIfPreference)
{
$OutputDir = $env:TEMP
}
else
{
$OutputDir = (Resolve-Path -Path $OutputDir).Path
}
#Get Urls to download
$WebResponse = Invoke-WebRequest -Uri 'https://store.rg-adguard.net/api/GetFiles' -Method Post -Body "type=url&url=$Uri&ring=Retail" -ContentType 'application/x-www-form-urlencoded'
$LinksMatch = $WebResponse.Links | Where-Object { $_ -like '*.appx*' } | Where-Object { $_ -like '*_neutral_*' -or $_ -like '*_' + $env:PROCESSOR_ARCHITECTURE.Replace('AMD', 'X').Replace('IA', 'X') + '_*' } | Select-String -Pattern '(?<=a href=").+(?=" r)' | Select-Object -ExpandProperty Matches
$DownloadLinks = $LinksMatch.Value
function private:Resolve-NameConflict
{
#Accepts Path to a FILE and changes it so there are no name conflicts
param(
[string]$OutputDir
)
$newPath = $OutputDir
if (Test-Path $OutputDir -PathType Leaf)
{
$i = 0
$item = (Get-Item $OutputDir)
while ( (Test-Path $newPath -PathType Leaf) -and ($i -lt [int]([math]::Sqrt([int]::MaxValue))))
{
$i += 1
$newPath = Join-Path $item.DirectoryName ($item.BaseName + "($i)" + $item.Extension)
}
}
return $newPath
}
$InstallQueue = [System.Collections.Generic.Queue[string]]::new(($DownloadLinks.Count))
#Download Urls
foreach ($url in $DownloadLinks)
{
$FileRequest = $null
try
{
$FileRequest = Invoke-WebRequest -Uri $url -ErrorAction SilentlyContinue
}
catch
{
Write-Warning "Failed to download '$url' - $_"
}
$AppxFileName = ($FileRequest.Headers['Content-Disposition'] | Select-String -Pattern '(?<=filename=).+').Matches.Value
$AppxFilePath = Join-Path -Path $OutputDir -ChildPath $AppxFileName
$AppxFilePath = Resolve-NameConflict -OutputDir $OutputDir
try{
[System.IO.File]::WriteAllBytes($AppxFilePath, $FileRequest.Content) | Out-Null
}
catch{
Write-Warning "Failed to write to '$AppxFilePath' - $_"
}
if ( -not $ConfirmPreference -or $PSCmdlet.ShouldProcess($AppxFileName, 'Add Appx Package'))
{
try { Add-AppxPackage -Path $AppxFilePath -ErrorAction SilentlyContinue } catch { $InstallQueue.EnQueue($AppxFilePath) }
}
}
while ($InstallQueue.Count -gt 0)
{
Write-Verbose "Retrying Add-AppxPackage on '$($InstallQueue.Peek())'"
Add-AppxPackage -Path ($InstallQueue.DeQueue())
}
}
}
@xd003
Copy link

xd003 commented Oct 10, 2023

Ah good to know, gonna test the new script now. I feel bit safe in first checking all the downloaded packages before installing them manually. Maybe you can add a parameter to skip download like say Download-AppxPackage -Uri "$URL" -OutputDir"$PATH" -skipinstall

i tried tinkering around with the script to only have it download the packages

function Invoke-DownloadAppxPackage
{
  [CmdletBinding(SupportsShouldProcess)]
  param (
    [Parameter(Mandatory, ValueFromPipeline)]
    [ValidateScript({ [uri]::TryCreate($_, [UriKind]::Absolute, [ref]$null) })]
    [string]$Uri,
    [ValidateScript({ Test-Path -Path $_ -PathType Container })]
    [string]$OutputDir = $env:TEMP
  )

  process
  {
    if ($WhatIfPreference)
    {
      $OutputDir = $env:TEMP
    }
    else
    {
      $OutputDir = (Resolve-Path -Path $OutputDir).Path
    }
    # Get Urls to download
    $WebResponse = Invoke-WebRequest -Uri 'https://store.rg-adguard.net/api/GetFiles' -Method Post -Body "type=url&url=$Uri&ring=Retail" -ContentType 'application/x-www-form-urlencoded'
    $LinksMatch = $WebResponse.Links | Where-Object { $_ -like '*.appx*' } | Where-Object { $_ -like '*_neutral_*' -or $_ -like '*_' + $env:PROCESSOR_ARCHITECTURE.Replace('AMD', 'X').Replace('IA', 'X') + '_*' } | Select-String -Pattern '(?<=a href=").+(?=" r)' | Select-Object -ExpandProperty Matches
    $DownloadLinks = $LinksMatch.Value

    function private:Resolve-NameConflict
    {
      # Accepts Path to a FILE and changes it so there are no name conflicts
      param(
        [string]$OutputDir
      )
      $newPath = $OutputDir
      if (Test-Path $OutputDir -PathType Leaf)
      {
        $i = 0
        $item = (Get-Item $OutputDir)
        while ((Test-Path $newPath -PathType Leaf) -and ($i -lt [int]([math]::Sqrt([int]::MaxValue))))
        {
          $i += 1
          $newPath = Join-Path $item.DirectoryName ($item.BaseName + "($i)" + $item.Extension)
        }
      }
      return $newPath
    }

    # Download Urls
    foreach ($url in $DownloadLinks)
    {
      $FileRequest = $null
      try
      {
        $FileRequest = Invoke-WebRequest -Uri $url -ErrorAction SilentlyContinue
      }
      catch
      {
        Write-Warning "Failed to download '$url' - $_"
      }
      $AppxFileName = ($FileRequest.Headers['Content-Disposition'] | Select-String -Pattern '(?<=filename=).+').Matches.Value
      $AppxFilePath = Join-Path -Path $OutputDir -ChildPath $AppxFileName
      $AppxFilePath = Resolve-NameConflict -OutputDir $OutputDir
      try
      {
        [System.IO.File]::WriteAllBytes($AppxFilePath, $FileRequest.Content) | Out-Null
      }
      catch
      {
        Write-Warning "Failed to write to '$AppxFilePath' - $_"
      }
    }
  }
}

Invoke-DownloadAppxPackage -Uri 'https://www.microsoft.com/store/productId/9NKSQGP7F2NH' -OutputDir "$env:USERPROFILE\Downloads"

i am getting these errors

PowerShell 7.3.7
WARNING: Failed to write to 'C:\Users\Administrator\Downloads' - Exception calling "WriteAllBytes" with "2" argument(s): "Access to the path 'C:\Users\Administrator\Downloads' is denied."

i didn't got these errors in the earlier version of the script

@xd003
Copy link

xd003 commented Oct 12, 2023

i think have found the issue, although i have no idea how do i fix it

$AppxFileName = ($FileRequest.Headers['Content-Disposition'] | Select-String -Pattern '(?<=filename=).+').Matches.Value

$AppxFileName in your script is not storing any value whatsoever

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment