Public/Save-OSDBuilderDownload.ps1

<#
.SYNOPSIS
Downloads Microsoft Updates for use in OSDBuilder
 
.DESCRIPTION
Downloads Microsoft Updates for use in OSDBuilder
 
.LINK
https://osdbuilder.osdeploy.com
#>

function Save-OSDBuilderDownload {
    [CmdletBinding(DefaultParameterSetName='OSDUpdate')]
    param (

        #Download OneDrive Sync Client
        [Parameter(ParameterSetName='Content')]
        [ValidateSet(
            'OneDriveSetup Production',
            'OneDriveSetup Enterprise')]
        [string]$ContentDownload,
        
        #Download the selected Microsoft Updates
        #By default, updates are not downloaded
        [Parameter(ParameterSetName='OSDUpdate')]
        [switch]$Download,

        #Skip Feature Updates GridView
        #Be careful as this will automatically download
        [Parameter(ParameterSetName='FeatureUpdates')]
        [switch]$SkipGridView,

        #Downloads Feature Updates
        [Parameter(ParameterSetName='FeatureUpdates',Mandatory = $True)]
        [switch]$FeatureUpdates,

        #Feature Update Architecture
        [Parameter(ParameterSetName = 'FeatureUpdates')]
        [ValidateSet ('Windows 10','Windows 11')]
        [string]$FeatureOS,

        #Feature Update Architecture
        [Parameter(ParameterSetName = 'FeatureUpdates')]
        [ValidateSet ('x64','x86')]
        [string]$FeatureArch,

        #Feature Update Build
        [Parameter(ParameterSetName = 'FeatureUpdates')]
        [ValidateSet ('24H2','23H2','22H2','21H2','21H1','20H2',2004,1909,1903,1809)]
        [string]$FeatureBuild,

        #Feature Update Edition
        [Parameter(ParameterSetName = 'FeatureUpdates')]
        [ValidateSet ('Business','Consumer')]
        [string]$FeatureEdition,

        #Feature Update Language
        [Parameter(ParameterSetName = 'FeatureUpdates')]
        [ValidateSet (
            'ar-sa','bg-bg','cs-cz','da-dk','de-de','el-gr',
            'en-gb','en-us','es-es','es-mx','et-ee','fi-fi',
            'fr-ca','fr-fr','he-il','hr-hr','hu-hu','it-it',
            'ja-jp','ko-kr','lt-lt','lv-lv','nb-no','nl-nl',
            'pl-pl','pt-br','pt-pt','ro-ro','ru-ru','sk-sk',
            'sl-si','sr-latn-rs','sv-se','th-th','tr-tr',
            'uk-ua','zh-cn','zh-tw'
        )]
        [string[]]$FeatureLang,

        #Display the results in a GridView with PassThru enabled
        [Parameter(ParameterSetName='OSDUpdate')]
        [switch]$GridView,
        
        #Remove Superseded Updates that are no longer needed
        [Parameter(ParameterSetName = 'OSDUpdateSuperseded', Mandatory = $True)]
        [ValidateSet ('List','Remove')]
        [string]$Superseded,

        #Filter Microsoft Updates for a specific OS Architecture
        [Parameter(ParameterSetName = 'OSDUpdate')]
        [ValidateSet ('x64','x86')]
        [string]$UpdateArch,

        #Filter Microsoft Updates for a specific ReleaseId
        [Parameter(ParameterSetName='OSDUpdate')]
        [ValidateSet ('24H2','23H2','22H2','21H2','21H1','20H2',2004,1909,1903,1809,1803,1709,1703,1607,1511,1507,7601,7603)]
        [Alias('ReleaseId')]
        [string]$UpdateBuild,

        #Filter Microsoft Updates for a specific Update type
        [Parameter(ParameterSetName='OSDUpdate')]
        [ValidateSet(
            'SSU Servicing Stack Update',
            'LCU Latest Cumulative Update',
            'DUSU Setup Dynamic Update',
            'DUCU Component Dynamic Update',
            'Adobe Flash Player',
            'DotNet Framework',
            'Optional')]
        [string]$UpdateGroup,
        
        #Filter Microsoft Updates for a specific OS
        [Parameter(ParameterSetName='OSDUpdate')]
        [ValidateSet(
            'Windows 10',
            'Windows 11',
            'Windows Server 2019',
            'Windows Server 2022',
            'Windows Server')]
        [string]$UpdateOS,

        #Download updates using Webclient instead of BITS
        [Parameter(ParameterSetName='OSDUpdate')]
        [switch]$WebClient,

        [Parameter(ParameterSetName='OSDUpdate')]
        [switch]$CheckFileHash

    )

    Begin {
        Get-OSDBuilder -CreatePaths -HideDetails
        Block-StandardUser
    }

    Process {
        #=================================================
        # Test WebClient
        #=================================================
        $UseWebClient = $false
        $UseWebRequest = $false
        $UseCurl = $false

        if ($WebClient.IsPresent) {$UseWebClient = $true}
        if (([System.Net.WebRequest]::DefaultWebProxy).Address) {$UseWebClient = $true}
        if (Get-Command 'curl.exe' -ErrorAction SilentlyContinue) {
            $UseCurl = $true
        }
        else {
            $UseWebClient = $true
        }
        if ($UseWebClient -eq $true) {
            [Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls1
            $WebClientObj = New-Object System.Net.WebClient
        }
        #=================================================
        # FeatureUpdates
        #=================================================
        if ($FeatureUpdates.IsPresent) {
            #Write-Warning "FeatureUpdates are downloaded using BITS Transfer"
            #Write-Warning "Windows Server 2016 (1607) does not support decompressing ESD Files"
            #=================================================
            # Get FeatureUpdateDownloads
            #=================================================
            $FeatureUpdateDownloads = @()
            $FeatureUpdateDownloads = Get-FeatureUpdateDownloads
            #=================================================
            # Filters
            #=================================================
            if ($FeatureOS) { $FeatureUpdateDownloads = $FeatureUpdateDownloads | Where-Object { $_.UpdateOS -eq $FeatureOS}}
            if ($FeatureArch) { $FeatureUpdateDownloads = $FeatureUpdateDownloads | Where-Object { $_.UpdateArch -eq $FeatureArch}}
            if ($FeatureBuild) { $FeatureUpdateDownloads = $FeatureUpdateDownloads | Where-Object { $_.UpdateBuild -eq $FeatureBuild}}
            if ($FeatureEdition) {$FeatureUpdateDownloads = $FeatureUpdateDownloads | Where-Object {$_.FileName -match $FeatureEdition}}
            if ($FeatureLang) {
                $regex = $FeatureLang.ForEach({ [RegEx]::Escape($_) }) -join '|'
                $FeatureUpdateDownloads = $FeatureUpdateDownloads | Where-Object {$_.Name -match $regex}
            }
            #=================================================
            # Select-Object
            #=================================================
            $FeatureUpdateDownloads = $FeatureUpdateDownloads | Select-Object -Property OSDStatus, Title, UpdateOS,`
            UpdateBuild, UpdateArch, CreationDate, KBNumber, FileName, OriginUri, Hash
            #=================================================
            # Sorting
            #=================================================
            $FeatureUpdateDownloads = $FeatureUpdateDownloads | Sort-Object -Property Language -Descending
            #=================================================
            # Select Updates with GridView
            #=================================================
            if (! ($SkipGridView.IsPresent)) {
                $FeatureUpdateDownloads = $FeatureUpdateDownloads | Out-GridView -PassThru -Title 'Select ESD Files to Download and Build and press OK'
            }
            #=================================================
            # Download
            #=================================================
            foreach ($Item in $FeatureUpdateDownloads) {
                $DownloadFullPath = Join-Path $SetOSDBuilderPathFeatureUpdates $Item.FileName

                if (!(Test-Path $SetOSDBuilderPathFeatureUpdates)) {
                    New-Item -Path $SetOSDBuilderPathFeatureUpdates -ItemType Directory -Force | Out-Null
                }
                Write-Host "$DownloadFullPath" -ForegroundColor Cyan
                Write-Host "$($Item.OriginUri)" -ForegroundColor DarkGray
                if (!(Test-Path $DownloadFullPath)) {
                    #=================================================
                    # Download File
                    #=================================================
                    if ($UseWebClient -eq $true) {
                        $WebClientObj.DownloadFile("$($Item.OriginUri)","$DownloadFullPath")
                    }
                    elseif ($UseCurl -eq $true) {
                        if ($host.name -match 'ConsoleHost') {
                            Invoke-Expression "& curl.exe --insecure --location --output `"$DownloadFullPath`" --url `"$($Item.OriginUri)`""
                        }
                        else {
                            #PowerShell ISE will display a NativeCommandError, so progress will not be displayed
                            $Quiet = Invoke-Expression "& curl.exe --insecure --location --output `"$DownloadFullPath`" --url `"$($Item.OriginUri)`" 2>&1"
                        }
                    }
                    else {
                        Start-BitsTransfer -Source $Item.OriginUri -Destination $DownloadFullPath -ErrorAction Stop
                    }
                }
                #=================================================
                # Verify Download
                #=================================================
                if (! (Test-Path $DownloadFullPath)) {
                    Write-Warning "Could not complete download of $DownloadFullPath"
                    Break
                }

                $esdbasename = (Get-Item "$DownloadFullPath").Basename
                $esddirectory = Join-Path $SetOSDBuilderPathFeatureUpdates $esdbasename

                if (Test-Path "$esddirectory\Sources\Install.wim") {
                    Write-Verbose "Image already exists at $esddirectory\Sources\Install.wim" -Verbose
                } else {
                    Try {$esdinfo = Get-WindowsImage -ImagePath "$DownloadFullPath"}
                    Catch {
                        Write-Warning "Could not get ESD information"
                        Break
                    }
                    Write-Host "Creating $esddirectory" -ForegroundColor Cyan
                    New-Item -Path "$esddirectory" -Force -ItemType Directory | Out-Null
                    
                    foreach ($image in $esdinfo) {
                        if ($image.ImageName -eq 'Windows Setup Media') {
                            Write-Host "Expanding Index $($image.ImageIndex) $($image.ImageName) ..." -ForegroundColor Cyan
                            Expand-WindowsImage -ImagePath "$($image.ImagePath)" -ApplyPath "$esddirectory" -Index "$($image.ImageIndex)" -ErrorAction SilentlyContinue | Out-Null
                        } elseif ($image.ImageName -like "*Windows PE*") {
                            Write-Host "Exporting Index $($image.ImageIndex) $($image.ImageName) ..." -ForegroundColor Cyan
                            Export-WindowsImage -SourceImagePath "$($image.ImagePath)" -SourceIndex $($image.ImageIndex) -DestinationImagePath "$esddirectory\sources\boot.wim" -CompressionType Max -ErrorAction SilentlyContinue | Out-Null
                        } elseif ($image.ImageName -like "*Windows Setup*") {
                            Write-Host "Exporting Index $($image.ImageIndex) $($image.ImageName) ..." -ForegroundColor Cyan
                            Export-WindowsImage -SourceImagePath "$($image.ImagePath)" -SourceIndex $($image.ImageIndex) -DestinationImagePath "$esddirectory\sources\boot.wim" -CompressionType Max -Setbootable -ErrorAction SilentlyContinue | Out-Null
                        } else {
                            Write-Host "Exporting Index $($image.ImageIndex) $($image.ImageName) ..." -ForegroundColor Cyan
                            Export-WindowsImage -SourceImagePath "$($image.ImagePath)" -SourceIndex $($image.ImageIndex) -DestinationImagePath "$esddirectory\sources\install.wim" -CompressionType Max -ErrorAction SilentlyContinue | Out-Null
                        }
                    }
                }
            }
            Write-Warning "Use Import-OSMedia to import this Feature Update to OSMedia"
        }

        if ($PSCmdlet.ParameterSetName -eq 'Content') {
            #=================================================
            # Database
            #=================================================
            if ($ContentDownload -eq 'OneDriveSetup Production') {
                $DownloadUrl = 'https://go.microsoft.com/fwlink/?linkid=844652'
                $DownloadPath = $GetOSDBuilderPathContentOneDrive
                $DownloadFile = 'OneDriveSetup.exe'
            }
            if ($ContentDownload -eq 'OneDriveSetup Enterprise') {
                $DownloadUrl = 'https://go.microsoft.com/fwlink/p/?linkid=860987'
                $DownloadPath = $GetOSDBuilderPathContentOneDrive
                $DownloadFile = 'OneDriveSetup.exe'
            }
            #=================================================
            # Download
            #=================================================
            if (!(Test-Path "$DownloadPath")) {New-Item -Path $DownloadPath -ItemType Directory -Force | Out-Null}
            Write-Verbose "DownloadUrl: $DownloadUrl" -Verbose
            Write-Verbose "DownloadPath: $DownloadPath" -Verbose
            Write-Verbose "DownloadFile: $DownloadFile" -Verbose
            #=================================================
            # Download File
            #=================================================
            if ($UseWebClient -eq $true) {
                $WebClientObj.DownloadFile("$DownloadUrl","$DownloadPath\$DownloadFile")
            }
            elseif ($UseWebRequest -eq $true) {
                Invoke-WebRequest -Uri $DownloadUrl -OutFile "$DownloadPath\$DownloadFile"
            }
            elseif ($UseCurl -eq $true) {
                if ($host.name -match 'ConsoleHost') {
                    #Invoke-Expression "& curl.exe --insecure --location --output `"$DownloadFullPath`" --url `"$($Item.OriginUri)`""
                    Invoke-Expression "& curl.exe --insecure --location --output `"$DownloadPath\$DownloadFile`" --url `"$DownloadUrl`""
                }
                else {
                    #PowerShell ISE will display a NativeCommandError, so progress will not be displayed
                    #$Quiet = Invoke-Expression "& curl.exe --insecure --location --output `"$DownloadFullPath`" --url `"$($Item.OriginUri)`" 2>&1"
                    $Quiet = Invoke-Expression "& curl.exe --insecure --location --output `"$DownloadPath\$DownloadFile`" --url `"$DownloadUrl`" 2>&1"
                }
            }
            else {
                #Start-BitsTransfer -Source $Item.OriginUri -Destination $DownloadFullPath -ErrorAction Stop
            }

            if (Test-Path "$DownloadPath\$DownloadFile") {
                $OneDriveSetupInfo = Get-Item -Path "$DownloadPath\$DownloadFile" | Select-Object -Property *
                Write-Verbose "DownloadVersion: $($($OneDriveSetupInfo).VersionInfo.ProductVersion)" -Verbose
                Write-Verbose 'Complete' -Verbose
            } else {
                Write-Warning 'Content could not be downloaded'
            }
        }

        if (($PSCmdlet.ParameterSetName -eq 'OSDUpdate') -or ($PSCmdlet.ParameterSetName -eq 'OSDUpdateSuperseded')) {
            #=================================================
            # Information
            #=================================================
            if ($WebClient.IsPresent) {
                Write-Verbose "Downloading OSDUpdates using System.Net.WebClient" -Verbose
            } else {
                Write-Verbose "Downloading OSDUpdates using BITS-Transfer" -Verbose
                Write-Verbose "To use System.Net.WebClient, use the -WebClient Parameter" -Verbose
            }
            #=================================================
            # Get OSDUpdates
            #=================================================
            $OSDUpdates = @()
            $OSDUpdates = Get-OSDUpdates | Sort-Object CreationDate -Descending
            #=================================================
            # Superseded Updates
            #=================================================
            if ($Superseded) {
                $ExistingUpdates = @()
                if (!(Test-Path $SetOSDBuilderPathUpdates)) {New-Item $SetOSDBuilderPathUpdates -ItemType Directory -Force | Out-Null}
                $ExistingUpdates = Get-ChildItem -Path "$SetOSDBuilderPathUpdates\*\*" -Directory

                $SupersededUpdates = @()
                foreach ($Update in $ExistingUpdates) {
                    if ($OSDUpdates.Title -NotContains $Update.Name) {$SupersededUpdates += $Update.FullName}
                }
            
                if ($Superseded -eq 'List') {
                    Write-Warning 'Superseded Updates:'
                    foreach ($Update in $SupersededUpdates) {
                        Write-Host "$Update" -ForegroundColor Gray
                    }
                }
                if ($Superseded -eq 'Remove') {
                    Write-Warning 'Superseded Updates:'
                    foreach ($Update in $SupersededUpdates) {
                        Write-Warning "Deleting $Update"
                        Remove-Item $Update -Recurse -Force | Out-Null
                    }
                }
                Break
            }
            #=================================================
            # Filters
            #=================================================
            if ($UpdateOS) {$OSDUpdates = $OSDUpdates | Where-Object {$_.UpdateOS -eq $UpdateOS}}
            if ($UpdateArch) {$OSDUpdates = $OSDUpdates | Where-Object {$_.UpdateArch -eq $UpdateArch}}
            if ($UpdateBuild) {$OSDUpdates = $OSDUpdates | Where-Object {$_.UpdateBuild -eq $UpdateBuild}}
            #=================================================
            # UpdateGroup
            #=================================================
            if ($UpdateGroup -like "*Adobe*") {$OSDUpdates = $OSDUpdates | Where-Object {$_.UpdateGroup -eq 'AdobeSU'}}
            if ($UpdateGroup -like "*DotNet*") {$OSDUpdates = $OSDUpdates | Where-Object {$_.UpdateGroup -like "DotNet*"}}
            if ($UpdateGroup -like "*DUCU*") {$OSDUpdates = $OSDUpdates | Where-Object {$_.UpdateGroup -like "ComponentDU*"}}
            if ($UpdateGroup -like "*DUSU*") {$OSDUpdates = $OSDUpdates | Where-Object {$_.UpdateGroup -eq 'SetupDU'}}
            if ($UpdateGroup -like "*LCU*") {$OSDUpdates = $OSDUpdates | Where-Object {$_.UpdateGroup -eq 'LCU'}}
            if ($UpdateGroup -like "*SSU*") {$OSDUpdates = $OSDUpdates | Where-Object {$_.UpdateGroup -eq 'SSU'}}
            if ($UpdateGroup -eq 'Optional') {$OSDUpdates = $OSDUpdates | Where-Object {[String]::IsNullOrWhiteSpace($_.UpdateGroup) -or $_.UpdateGroup -eq 'Optional'}}
            #=================================================
            # Sorting
            #=================================================
            $OSDUpdates = $OSDUpdates | Sort-Object -Property CreationDate -Descending
            #=================================================
            # Select Updates with GridView
            #=================================================
            if ($GridView.IsPresent) {$OSDUpdates = $OSDUpdates | Out-GridView -PassThru -Title 'Select Updates to Download and press OK'}
            #=================================================
            # Download Updates
            #=================================================
            if ($Download.IsPresent) {
                if ($WebClient.IsPresent) {
                    [Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls1
                    $WebClientObj = New-Object System.Net.WebClient
                }
                foreach ($Update in $OSDUpdates) {
                    $DownloadPath = "$SetOSDBuilderPathUpdates"
                    $DownloadFullPath = Join-Path $DownloadPath $(Split-Path $Update.OriginUri -Leaf)

                    if (!(Test-Path $DownloadPath)) {New-Item -Path "$DownloadPath" -ItemType Directory -Force | Out-Null}
                    if (!(Test-Path $DownloadFullPath)) {
                        Write-Host "$DownloadFullPath" -ForegroundColor Cyan
                        Write-Host "$($Update.OriginUri)" -ForegroundColor DarkGray
                        #=================================================
                        # Download File
                        #=================================================
                        if ($UseWebClient -eq $true) {
                            $WebClientObj.DownloadFile("$($Update.OriginUri)","$DownloadFullPath")
                        }
                        elseif ($UseCurl -eq $true) {
                            if ($host.name -match 'ConsoleHost') {
                                Invoke-Expression "& curl.exe --insecure --location --output `"$DownloadFullPath`" --url `"$($Update.OriginUri)`""
                            }
                            else {
                                #PowerShell ISE will display a NativeCommandError, so progress will not be displayed
                                $Quiet = Invoke-Expression "& curl.exe --insecure --location --output `"$DownloadFullPath`" --url `"$($Update.OriginUri)`" 2>&1"
                            }
                        }
                        else {
                            Start-BitsTransfer -Source $Update.OriginUri -Destination $DownloadFullPath
                        }
                        if ($CheckFileHash.IsPresent -and ($Update.Hash -ne "")) {
                            $ActualHash = $null
                            $ActualHash = (Get-FileHash -Path $DownloadFullPath -Algorithm SHA1).Hash.ToLower()
                            $DeriredHash = Convert-ByteArrayToHex -Bytes $($update.Hash -split " ")
                            Write-Verbose "Desired SHA1 Hash: [$DeriredHash], Actual Hash [$ActualHash]"
                            if ($ActualHash -ne $DeriredHash) {
                                Write-Error -Exception "Hashes don't match - please investigate!" 
                            }
                            else {
                                Write-Verbose -Message "Hashes match."
                            }
                        }
                    } else {
                        #Write-Warning "Exists: $($Update.Title)"
                    }
                }
            } else {
                Return $OSDUpdates | Select-Object -Property Catalog, OSDVersion, OSDStatus, UpdateOS, UpdateBuild, UpdateArch, UpdateGroup, CreationDate, KBNumber, Title
            }
        }
    }

    End {
    }
}