Public/Invoke-PdqPkgDownload.ps1

<#
.SYNOPSIS
Downloads .pdqpkg files.
 
.DESCRIPTION
.pdqpkg files are what Deploy downloads when you download a package from the Package Library.
You can import these files into Deploy. They will show up as a standard package, not an Auto Download.
 
.INPUTS
None.
 
.OUTPUTS
System.Management.Automation.PSCustomObject
System.Object[]
 
.EXAMPLE
Invoke-PdqPkgDownload -PackageName 'Google Chrome Enterprise'
Downloads the latest version of Chrome into the current directory.
 
.EXAMPLE
Invoke-PdqPkgDownload -PackageName 'Google Chrome Enterprise', 'Mozilla Firefox' -DestinationFolder 'Downloads'
Downloads the latest version of Chrome and Firefox into the Downloads directory.
 
.EXAMPLE
Invoke-PdqPkgDownload -PackageName 'Google Chrome Enterprise', 'Mozilla Firefox' -PackageVersion '100.0.4896.60', '98.0.2'
Downloads specific versions of Chrome and Firefox.
 
.EXAMPLE
Invoke-PdqPkgDownload -PackageName '7-Zip', 'Mozilla Firefox', 'iTunes', 'Audacity' -PreviousVersionIndex 1, 0, 2, -1
Downloads the previous version of 7-Zip, the current version of Firefox, a version of iTunes 2 away from current,
and the oldest available version of Audacity.
 
.EXAMPLE
Invoke-PdqPkgDownload -PackageVersionId 8726, 9102
Downloads version 21.06 of 7-Zip and version 12.12.3.5 of iTunes.
#>


function Invoke-PdqPkgDownload {
    
    [CmdletBinding()]
    param (
        [Parameter(ParameterSetName = 'PackageName', Mandatory = $true)]
        [Parameter(ParameterSetName = 'PackageVersion', Mandatory = $true)]
        [Parameter(ParameterSetName = 'PreviousVersionIndex', Mandatory = $true)]
        # The names of the packages you would like to download from the Package Library.
        [String[]]$PackageName,
        
        [Parameter(ParameterSetName = 'PackageVersion', Mandatory = $true)]
        # The version numbers of the packages you want to download.
        # If you use this parameter, you must provide a version for every entry in -PackageName.
        [String[]]$PackageVersion,
        
        [Parameter(ParameterSetName = 'PreviousVersionIndex', Mandatory = $true)]
        <#
        A way to select previous versions without having to specify the exact version number.
 
        0 - Current version.
        1 - The previous version.
        2 - 2 versions ago.
        etc.
        -1 - The oldest available version.
        #>

        [Int32[]]$PreviousVersionIndex,

        [Parameter(ParameterSetName = 'PackageVersionId', Mandatory = $true)]
        # PackageVersionId can be found by using Select-PdqPackageLibraryPackage and examining the PackageVersion property.
        [UInt32[]]$PackageVersionId,

        # The folder you would like to save the .pdqpkg files to.
        [String]$DestinationFolder = $PWD,

        # A Deploy license in string form.
        # If you don't use this parameter, your Deploy license will be retrieved from the registry.
        [String]$License,

        # How long you want to wait before downloading a fresh copy of the Package Library catalog.
        [UInt32]$CacheTimeoutSeconds = 600,

        # The object that Get-PdqPackageLibraryCatalog emits.
        # Omitting this will cause Get-PdqPackageLibraryCatalog to be called.
        [XML]$LibraryCatalog
    )


    if ( $PackageVersion ) {

        if ( $PackageVersion.Count -ne $PackageName.Count ) {

            throw 'The count of versions does not match the count of names.'

        }

    }


    if ( $PreviousVersionIndex ) {

        if ( $PreviousVersionIndex.Count -ne $PackageName.Count ) {

            throw 'The count of indexes does not match the count of names.'

        }

    }


    # WebClient requires an absolute path.
    $DestinationFolder = (Resolve-Path -Path $DestinationFolder -ErrorAction 'Stop').Path.Trim('\')


    if ( $License ) {

        $License = Get-PdqLicense -License $License -Format 'URL'

    } else {

        $License = Get-PdqLicense -Product 'Deploy' -Format 'URL' -AllowFreeMode

    }


    if ( -not $LibraryCatalog ) {

        $LibraryCatalog = Get-PdqPackageLibraryCatalog -CacheTimeoutSeconds $CacheTimeoutSeconds

    }


    if ( -not $PackageVersionId ) {

        $PackageEntries = @(Select-PdqPackageLibraryPackage -PackageName $PackageName -LibraryCatalog $LibraryCatalog)
    
        for ( $Index = 0; $Index -lt $PackageName.Count; $Index ++ ) {

            $PackageEntry = $PackageEntries[$Index]
            $NameOfPackage = $PackageName[$Index]

            if ( $PackageEntry.PackageVersion.PackageVersionId.Count -eq 0 ) {

                throw "Unable to find any versions for '$NameOfPackage'. This should never happen."
    
            }

            if ( $PackageVersion ) {

                $VersionOfPackage = $PackageVersion[$Index]
                $FoundPackageVersionId = ($PackageEntry.PackageVersion | Where-Object 'VersionNumber' -eq $VersionOfPackage).PackageVersionId

                if ( -not $FoundPackageVersionId ) {

                    throw "Unable to find a PackageVersionId for '$NameOfPackage' with version '$VersionOfPackage'"

                }
                
                [UInt32[]]$PackageVersionId += $FoundPackageVersionId

            } elseif ( $PreviousVersionIndex ) {

                $IndexOfPreviousVersion = $PreviousVersionIndex[$Index]
                # I would love to use `Select-Object -Index` here, but it doesn't accept negative values :'(
                [UInt32[]]$PackageVersionId += @($PackageEntry.PackageVersion.PackageVersionId | Sort-Object -Descending)[$IndexOfPreviousVersion]

            } else {

                [UInt32[]]$PackageVersionId += $PackageEntry.PackageVersion.PackageVersionId | Sort-Object -Descending | Select-Object -First 1

            }

        }

    }

    foreach ( $IdOfPackageVersion in $PackageVersionId ) {

        <#
Dear PDQ.com,
 
The value for AA-Request-Context below is several years old. I'm surprised the API doesn't have replay protection,
but I'm glad it doesn't :). Please don't fix it. If you do, here are some suggestions in order of preference:
 
- Make AA-Request-Context optional. It's just telemetry data anyway.
- Accept the plaintext form of the data. The full license is being sent anyway, so there's nothing that needs to be
  encrypted. Also, connections are already over HTTPS.
- Accept randomly generated values. I tried it, but the service gave me a 400 error.
#>

        $Params = @{
            'Headers' = @{
                'Subscription-License' = $License
                'AA-Request-Context'   = 'BCA6FBB167CD558D4197A669ED446879B6D5AC73||KGVuY3J5cHRlZCkAEJ7jh1K/jqfSE/SFteGwZ7XzjNaZcPXt3QGIQX6rIOQqrr0Lj6lmAFRiEN/sWSPwzgamdp/LLWfsAxzGLpMe0vXqNvwxJVCexttDt60QjZ8t4BKC9hkfA1lRO5EKK7ezgAyJqkMxIZIDqYEDcPSV2znGc2kuKlkwMBLXCv9NYt3tBw31LkpnFkg8WBmNq6fdE5JMf/sFCwVa/lUncE5bUJ4wVoWUCOrB9EBXMpGJ/8iqFXTUIeSqdOu6/KSNKtYXWA=='
            }
            'Uri'     = "https://library.pdq.com/PackageLibrary?PackageVersionId=$IdOfPackageVersion"
        }
        $PackageVersionXml = (Invoke-RestMethod @Params).Package

        if ( $PackageVersionXml.Count -eq 0 ) {

            throw "Failed to download XML for PackageVersionId $IdOfPackageVersion"

        }

        $FinalName = $PackageVersionXml.Name
        $FinalVersion = $PackageVersionXml.PackageVersion.VersionNumber
        $DestinationPath = "$DestinationFolder\$FinalName $FinalVersion" + '.pdqpkg'

        Write-Verbose "Downloading $FinalName $FinalVersion"
        [System.Net.WebClient]::new().DownloadFile($PackageVersionXml.DownloadURL, $DestinationPath)

        # Save the output until the end to avoid scrambling the verbose messages.
        $Result += @(
            [PSCustomObject]@{
                'Name'           = $FinalName
                'Version Number' = $FinalVersion
                'Version Name'   = $PackageVersionXml.PackageVersion.Version
                'Path'           = $DestinationPath
            }
        )

    }

    $Result

}