Skip to content

Instantly share code, notes, and snippets.

@mavaddat
Last active October 12, 2023 17:00
Show Gist options
  • Star 5 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • 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())
}
}
}
@yell0wsuit
Copy link

yell0wsuit commented Apr 25, 2022

Is it possible that you can write a script for batch downloading appx files?

@mavaddat
Copy link
Author

Is it possible that you can write a script for batch downloading appx files?

Here is a simple approach using the above Get-AppxPackageDownload function:

#  Example apps to install
$URIs = @('https://www.microsoft.com/store/productId/9P6RC76MSMMJ', 'https://www.microsoft.com/store/productId/9N0866FS04W8', 'https://www.microsoft.com/store/productId/9NH2GPH4JZS4')
$URIs | Foreach-Object {
  Get-AppxPackageDownload -Uri $_ -Path $env:TEMP
}

@xd003
Copy link

xd003 commented Oct 10, 2023

image

i tried the script but its ending instantly without doing anything, not sure if i am missing something here ?

@mavaddat
Copy link
Author

mavaddat commented Oct 10, 2023

image

i tried the script but its ending instantly without doing anything, not sure if i am missing something here ?

I believe it is because you are using PowerShell 6+, yes? This script currently only works on PowerShell 5.1. Please give me a few minutes and I will update it to work on PSEdition Core.

Actually, I tested it more carefully and the issue was the MS Store was refusing cross-origin requests. I was wrong that my version of the script required basic parsing — it actually doesn't require any parsing. Links are provided by Invoke-WebRequest without parsing.

@xd003
Copy link

xd003 commented Oct 10, 2023

There were some syntax errors in the script. Also -Force command is not valid for AppxPackage command. After some fiddling around, i have fixed the issue. But a script working properly on PSEdition Core would be much more ideal considering how the the Desktop edition is kind of depreciated. i am dropping the script which worked perfectly fine on PS 5.1 if it helps in any way. Gonna switch to the Core edition script as you drop it. Thanks for making this, its pretty useful, wasted so many hours on this before i finally got to this script :)

function Download-AppxPackage {
    [CmdletBinding(SupportsShouldProcess=$true)]
    param (
        [Parameter(Mandatory=$true,ValueFromPipeline)]
        [ValidateScript({[uri]::TryCreate($_, [UriKind]::Absolute, [ref]$null)})]
        [string]$Uri,
        [ValidateScript({Test-Path -Path $_ -PathType Container})]
        [string]$Path = $env:TEMP
    )
   
    process {
        #Requires -PSEdition Desktop # Basic parsing unavailable in Core
        if ($WhatIfPreference) {
            $Path = $env:TEMP
        } else {
            $Path = (Resolve-Path $Path).Path
        }
        
        # Get Urls to download
        $WebResponse = Invoke-WebRequest -UseBasicParsing -Method 'POST' -Uri 'https://store.rg-adguard.net/api/GetFiles' -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)'
        $DownloadLinks = $LinksMatch.matches.value 

        function private:Resolve-NameConflict {
            # Accepts Path to a FILE and changes it so there are no name conflicts
            param(
                [string]$Path
            )
            $newPath = $Path
            if (Test-Path $Path -PathType Leaf) {
                $i = 0;
                $item = (Get-Item $Path)
                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 -UseBasicParsing -ErrorAction SilentlyContinue 
            } catch { 
                continue 
            }
            $FileName = ($FileRequest.Headers["Content-Disposition"] | Select-String -Pattern '(?<=filename=).+').matches.value
            $FilePath = Join-Path $Path $FileName
            $FilePath = Resolve-NameConflict($FilePath)
            [System.IO.File]::WriteAllBytes($FilePath, $FileRequest.content) | Out-Null
            if (($ConfirmPreference -eq $false) -or $PSCmdlet.ShouldProcess($FileName, "Add Appx Package")) {
                try { 
                    Add-AppxPackage -Path $FilePath -ErrorAction SilentlyContinue 
                } catch { 
                    $installQueue.Enqueue($FilePath) 
                }
            }
        }
        
        while ($InstallQueue.Count -gt 0) {
            Write-Verbose "Retrying Add-AppxPackage on '$($InstallQueue.Peek())'"
            Add-AppxPackage -Path ($InstallQueue.Dequeue())
        }
    }
}

$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 Download-AppxPackage -CommandType Function) {
    $URIs | ForEach-Object {
        Download-AppxPackage -Uri $_ -Path "$env:USERPROFILE\Downloads"
    }
}

@mavaddat
Copy link
Author

Actually, I tested it more carefully and the issue was the MS Store was refusing cross-origin requests. I was wrong that my version of the script required basic parsing — it actually doesn't require any parsing. Links are provided by Invoke-WebRequest without parsing.

Also, I don't see what you changed in the version you shared in your latest comment (except for whitespace)!

@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