public/Update-OSDeployCoreESD.ps1
|
#Requires -PSEdition Core #Requires -Version 7.4 function Update-OSDeployCoreESD { <# .SYNOPSIS Downloads Windows Enterprise ESD files from the latest OSDeploy OS catalog .DESCRIPTION Locates the newest XML file by name in the module operating system catalog directory, resolves its en-US Enterprise ESD entries, and downloads them to the version-specific directory under the OSDeployCore OSDCloud OS cache. The directory name is derived from the catalog filename, for example Windows 11 25H2 from 26200.8457-win11-25h2.xml. On an ARM64 host, considers only the ARM64 ESD. On other hosts, considers AMD64 first and ARM64 second. Each pending download receives a separate confirmation. Returns a latest-catalog file without downloading when its cached SHA256 matches. When that file is absent, searches older catalogs for a differently named cached file with a matching checksum. If found, asks whether to download the newer file; declining returns the verified older file. A mismatched latest file is offered for deletion to the Recycle Bin and is skipped if deletion is declined. Tests each pending URL before confirmation, then confirms all reachable downloads before starting transfers. Downloads support continuation, receive up to three automatic attempts, and are verified with SHA256. After automatic attempts fail, the command offers further manual retries. Files that are declined, unreachable, or not successfully verified are omitted from output. .PARAMETER Force Re-downloads each ESD file even when it already exists in the cache with a matching SHA256 checksum and automatically confirms each pending download. .PARAMETER Architecture Limits ESD selection to amd64 or arm64. When omitted, the command uses the host-based selection described above. Specifying amd64 on an ARM64 host produces no target. .EXAMPLE PS> Update-OSDeployCoreESD Returns verified cached ESD files and, for missing current files, tests reachability and prompts before downloading the host-supported architectures. .EXAMPLE PS> Update-OSDeployCoreESD -Force Bypasses current and older cache reuse, then automatically downloads each host-supported ESD file. .EXAMPLE PS> Update-OSDeployCoreESD -Architecture arm64 Downloads or returns the cached ARM64 Enterprise ESD only. .EXAMPLE PS> Update-OSDeployCoreESD -WhatIf Tests URL reachability and shows which changes would be made without downloading files. Confirmation prompts may still be displayed. .INPUTS None. This function does not accept pipeline input. .OUTPUTS System.IO.FileInfo. Returns each successfully downloaded and SHA256-verified current ESD file, each verified current cache file, or a verified older cache file retained after the newer download is declined. .NOTES Author: David Segura Company: Recast Software Version: 1.0.0 Date: 2026-08-28 Requires Windows 11 25H2 or later, PowerShell 7.4 or later installed from MSI, curl.exe, and Administrator rights. Downloads are sourced from Microsoft Content Delivery Network URLs in the catalog XML. Confirmed checksum-mismatched or failed files are sent to the Recycle Bin. #> [CmdletBinding(SupportsShouldProcess)] [OutputType([System.IO.FileInfo[]])] param ( [Parameter()] [switch]$Force, [Parameter()] [ValidateSet('amd64', 'arm64')] [System.String] $Architecture ) #================================================= Write-HostOSDeployBanner #================================================= # Require License for this function. If the license is not valid, return without executing the function. if (-not (Test-OSDeployLicenseGate -CommandName $MyInvocation.MyCommand.Name)) { return } #================================================= # Stop before resolving or downloading ESD content when a required host capability is missing. if (-not (Test-IsWindows11)) { throw "[$(Get-Date -Format s)] [$($MyInvocation.MyCommand.Name)] Windows 11 is required." } if (-not (Test-IsWindows1125H2)) { throw "[$(Get-Date -Format s)] [$($MyInvocation.MyCommand.Name)] Windows 11 25H2 (build 26200) is required." } if (-not (Test-PwshVersionMin)) { throw "[$(Get-Date -Format s)] [$($MyInvocation.MyCommand.Name)] PowerShell 7.4 or higher is required." } if (-not (Test-PwshPSHome)) { throw "[$(Get-Date -Format s)] [$($MyInvocation.MyCommand.Name)] The MSI installation of PowerShell 7 is required." } if (-not (Test-CommandCurl)) { throw "[$(Get-Date -Format s)] [$($MyInvocation.MyCommand.Name)] curl.exe is required but was not found in the current PATH. curl.exe ships with Windows 10 1803+." } if (-not (Test-IsAdministrator)) { throw "[$(Get-Date -Format s)] [$($MyInvocation.MyCommand.Name)] Administrator rights are required. Re-run PowerShell as Administrator and try again." } #================================================= Initialize-OSDeployCorePaths #================================================= # ------------------------------------------------------------------------- # Use the newest catalog as the download source and retain older catalogs for cache comparison. # ------------------------------------------------------------------------- $catalogDir = Join-Path $script:OSDeployModuleBase 'core\operatingsystems' $allXmlFiles = Get-ChildItem -Path $catalogDir -Filter '*.xml' -File | Sort-Object Name -Descending $latestXml = $allXmlFiles | Select-Object -First 1 $olderXmls = @($allXmlFiles | Select-Object -Skip 1) # Stop when no operating system catalog is available. if (-not $latestXml) { $PSCmdlet.ThrowTerminatingError( [System.Management.Automation.ErrorRecord]::new( [System.IO.FileNotFoundException]::new("No OS catalog XML files found in '$catalogDir'."), 'CatalogNotFound', [System.Management.Automation.ErrorCategory]::ObjectNotFound, $catalogDir ) ) } Write-Verbose "[$($MyInvocation.MyCommand.Name)] Using catalog: $($latestXml.Name)" if ($olderXmls.Count -gt 0) { Write-Verbose "[$($MyInvocation.MyCommand.Name)] Older catalog(s) available: $($olderXmls.Name -join ', ')" } Write-Host -ForegroundColor DarkGray "[$(Get-Date -format s)] [INFO] Importing OSDeploy PSModule OperatingSystem Catalog" Write-Host -ForegroundColor DarkGray "[$(Get-Date -format s)] [INFO] $($latestXml.FullName)" # ------------------------------------------------------------------------- # Parse the catalog # ------------------------------------------------------------------------- [xml]$catalog = Get-Content -Path $latestXml.FullName -Raw $allFiles = $catalog.MCT.Catalogs.Catalog.PublishedMedia.Files.File # ------------------------------------------------------------------------- # Select architectures supported by the host, then apply an explicit architecture filter. # On ARM64 Windows only offer ARM64; on AMD64 offer AMD64 first then ARM64. # ------------------------------------------------------------------------- $isArm64 = $env:PROCESSOR_ARCHITECTURE -eq 'ARM64' $targets = if ($isArm64) { @( [pscustomobject]@{ Architecture = 'ARM64'; Edition = 'Enterprise'; LanguageCode = 'en-us' } ) } else { @( [pscustomobject]@{ Architecture = 'x64'; Edition = 'Enterprise'; LanguageCode = 'en-us' } [pscustomobject]@{ Architecture = 'ARM64'; Edition = 'Enterprise'; LanguageCode = 'en-us' } ) } if ($Architecture) { $archMap = @{ amd64 = 'x64'; arm64 = 'ARM64' } $targets = @($targets | Where-Object { $_.Architecture -eq $archMap[$Architecture] }) Write-Verbose "[$($MyInvocation.MyCommand.Name)] Architecture filter applied: $Architecture" } $resolvedEntries = foreach ($target in $targets) { $entry = $allFiles | Where-Object { $_.LanguageCode -eq $target.LanguageCode -and $_.Edition -eq $target.Edition -and $_.Architecture -eq $target.Architecture } | Select-Object -First 1 if (-not $entry) { Write-Warning "[$(Get-Date -Format s)] No catalog entry found for $($target.Edition) $($target.Architecture) $($target.LanguageCode). Skipping." continue } $entry } # Stop when the catalog has no entries that match the selected architecture. if (-not $resolvedEntries) { Write-Warning "[$(Get-Date -Format s)] No matching ESD entries found in catalog '$($latestXml.Name)'." return } # ------------------------------------------------------------------------- # Derive OS folder name from the catalog filename # e.g. '26200.8457-win11-25h2.xml' → 'Windows 11 25H2' # ------------------------------------------------------------------------- $catalogBase = [System.IO.Path]::GetFileNameWithoutExtension($latestXml.Name) if ($catalogBase -notmatch '^\d+\.\d+-win(\d+)-(.+)$') { $PSCmdlet.ThrowTerminatingError( [System.Management.Automation.ErrorRecord]::new( [System.FormatException]::new("Cannot parse OS version from catalog name '$($latestXml.Name)'. Expected format: '<build>-win<version>-<release>.xml' (e.g. '26200.8457-win11-25h2.xml')."), 'CatalogNameUnrecognized', [System.Management.Automation.ErrorCategory]::InvalidData, $latestXml.Name ) ) } $osFolderName = "Windows $($Matches[1]) $($Matches[2].ToUpper())" Write-Verbose "[$($MyInvocation.MyCommand.Name)] OS folder name: $osFolderName" # ------------------------------------------------------------------------- # Prepare download directory # ------------------------------------------------------------------------- $downloadDir = Join-Path $script:OSDeployCorePath 'OSDCloud' 'OS' $osFolderName Write-HostDateTimeDarkCyan "OSDeployCoreESD files are saved in $downloadDir" # Honor WhatIf and Confirm before creating the ESD cache directory. if (-not (Test-Path -Path $downloadDir)) { if ($PSCmdlet.ShouldProcess($downloadDir, 'Create download directory')) { New-Item -ItemType Directory -Path $downloadDir -Force | Out-Null Write-Verbose "[$($MyInvocation.MyCommand.Name)] Created download directory: $downloadDir" } } $normalizeHash = { param([string]$hash) ($hash -replace '\s+', '').ToUpperInvariant() } $results = [System.Collections.Generic.List[System.IO.FileInfo]]::new() $pendingEntries = [System.Collections.Generic.List[object]]::new() # ------------------------------------------------------------------------- # Phase 1: Check cache and test URL reachability # ------------------------------------------------------------------------- foreach ($entry in $resolvedEntries) { $destPath = Join-Path $downloadDir $entry.FileName $expectedSha256 = & $normalizeHash $entry.Sha256 $checkForOlder = $false Write-Verbose "[$($MyInvocation.MyCommand.Name)] Checking: $($entry.FileName)" Write-Host -ForegroundColor DarkGray "[$(Get-Date -format s)] [INFO] Verifying OSDeployCoreESD for Windows 11 $($entry.Edition) $($entry.LanguageCode) $($entry.Architecture)" Write-Host -ForegroundColor DarkGray "[$(Get-Date -format s)] [INFO] FileName: $($entry.FileName)" # Return a verified cache entry unless Force requests a fresh download. if (-not $Force -and (Test-Path -Path $destPath -PathType Leaf)) { $actualHash = (Get-FileHash -Path $destPath -Algorithm SHA256).Hash.ToUpperInvariant() if ($actualHash -eq $expectedSha256) { Write-Verbose "[$($MyInvocation.MyCommand.Name)] '$($entry.FileName)' already cached and SHA256 verified. Skipping." # Write-Host -ForegroundColor DarkGray "[$(Get-Date -format s)] [INFO] OSDeployCoreESD is already cached and hash verified: $($entry.FileName)" Write-Host -ForegroundColor DarkGray "[$(Get-Date -format s)] [INFO] Hash SHA256: $actualHash" $results.Add((Get-Item -Path $destPath)) continue } Write-Warning "[$(Get-Date -Format s)] '$($entry.FileName)' exists but SHA256 does not match the catalog." Write-Host -ForegroundColor DarkGray "[$(Get-Date -format s)] [INFO] SHA256 mismatch: $($entry.FileName)" Write-Host -ForegroundColor DarkGray "[$(Get-Date -format s)] [INFO] Expected : $expectedSha256" Write-Host -ForegroundColor DarkGray "[$(Get-Date -format s)] [INFO] Actual : $actualHash" $deleteCaption = "SHA256 Mismatch – Delete and Re-download?" $deleteMessage = "File : $destPath`nExpected SHA256 : $expectedSha256`nActual SHA256 : $actualHash`n`nThe cached file does not match the catalog checksum. Delete it to the Recycle Bin and re-download?" # Require confirmation before removing an invalid cached file. if ($PSCmdlet.ShouldContinue($deleteMessage, $deleteCaption) -and $PSCmdlet.ShouldProcess($destPath, 'Delete mismatched cached file (Recycle Bin)')) { Add-Type -AssemblyName Microsoft.VisualBasic [Microsoft.VisualBasic.FileIO.FileSystem]::DeleteFile($destPath, 'OnlyErrorDialogs', 'SendToRecycleBin') Write-Verbose "[$($MyInvocation.MyCommand.Name)] Sent to Recycle Bin: $destPath" Write-Host -ForegroundColor DarkGray "[$(Get-Date -format s)] [INFO] Sent to Recycle Bin: $($entry.FileName)" } else { Write-Verbose "[$($MyInvocation.MyCommand.Name)] User declined to delete mismatched file. Skipping: $($entry.FileName)" continue } } else { # Latest ESD is not cached; check for an older cached version below $checkForOlder = -not $Force } # ----------------------------------------------------------------------- # Check for a valid older cached version when the latest ESD is not present # ----------------------------------------------------------------------- if ($checkForOlder -and $olderXmls.Count -gt 0) { $olderCachedFile = $null foreach ($olderXml in $olderXmls) { [xml]$olderCatalogXml = Get-Content -Path $olderXml.FullName -Raw $olderEntry = $olderCatalogXml.MCT.Catalogs.Catalog.PublishedMedia.Files.File | Where-Object { $_.LanguageCode -eq $entry.LanguageCode -and $_.Edition -eq $entry.Edition -and $_.Architecture -eq $entry.Architecture } | Select-Object -First 1 if ($olderEntry -and $olderEntry.FileName -ne $entry.FileName) { $olderDestPath = Join-Path $downloadDir $olderEntry.FileName if (Test-Path -Path $olderDestPath -PathType Leaf) { $olderExpectedSha256 = & $normalizeHash $olderEntry.Sha256 $olderActualHash = (Get-FileHash -Path $olderDestPath -Algorithm SHA256).Hash.ToUpperInvariant() if ($olderActualHash -eq $olderExpectedSha256) { $olderCachedFile = Get-Item -Path $olderDestPath break } } } } if ($olderCachedFile) { $fileSizeMB = [Math]::Round([long]$entry.Size / 1MB, 1) $fileLabel = "$($entry.Architecture) – $($entry.Edition) ($($entry.LanguageCode))" $upgradeCaption = "Newer Version Available – $fileLabel" $upgradeMessage = "Cached : $($olderCachedFile.Name)`nNewer : $($entry.FileName) ($fileSizeMB MB)`n`nA newer version is available from catalog '$($latestXml.Name)'.`nDownload the newer version?" Write-Host -ForegroundColor DarkGray "[$(Get-Date -format s)] [INFO] Newer ESD available: $($entry.FileName)" Write-Host -ForegroundColor DarkGray "[$(Get-Date -format s)] [INFO] Cached : $($olderCachedFile.Name)" Write-Host -ForegroundColor DarkGray "[$(Get-Date -format s)] [INFO] Newer : $($entry.FileName)" if (-not $PSCmdlet.ShouldContinue($upgradeMessage, $upgradeCaption)) { Write-Verbose "[$($MyInvocation.MyCommand.Name)] User kept older cached ESD: $($olderCachedFile.Name)" $results.Add($olderCachedFile) continue } Write-Verbose "[$($MyInvocation.MyCommand.Name)] User chose to download newer ESD: $($entry.FileName)" } } # Skip entries whose download URL is unreachable before prompting the user. Write-Verbose "[$($MyInvocation.MyCommand.Name)] Testing reachability: $($entry.FilePath)" Write-Host -ForegroundColor DarkGray "[$(Get-Date -format s)] [INFO] Testing availability of ESD:" Write-Host -ForegroundColor DarkGray "[$(Get-Date -format s)] [INFO] $($entry.FilePath)" $null = curl.exe --head --fail --silent --location --max-time 15 $entry.FilePath 2>&1 if ($LASTEXITCODE -ne 0) { Write-Warning "[$(Get-Date -Format s)] '$($entry.FileName)' is not reachable (curl exit $LASTEXITCODE). Skipping." continue } Write-Verbose "[$($MyInvocation.MyCommand.Name)] Reachable: $($entry.FileName)" Write-Host -ForegroundColor DarkGray "[$(Get-Date -format s)] [INFO] ESD is available: $($entry.FileName)" $pendingEntries.Add($entry) } # ------------------------------------------------------------------------- # Phase 2: Prompt for all pending downloads before starting any transfer # ------------------------------------------------------------------------- $confirmedEntries = [System.Collections.Generic.List[object]]::new() foreach ($entry in $pendingEntries) { $fileSizeMB = [Math]::Round([long]$entry.Size / 1MB, 1) $fileLabel = "$($entry.Architecture) – $($entry.Edition) ($($entry.LanguageCode))" $destPath = Join-Path $downloadDir $entry.FileName $confirmCaption = "Download $fileLabel" $confirmMessage = "File : $($entry.FileName)`nSize : $fileSizeMB MB`nDest : $destPath`n`nThis download can take between 5 - 30 minutes depending on your internet connection.`n`nDownload this file?" # Require confirmation before queuing each large ESD download unless Force is specified. if (($Force -or $PSCmdlet.ShouldContinue($confirmMessage, $confirmCaption)) -and $PSCmdlet.ShouldProcess($destPath, "Download $($entry.FileName)")) { $confirmedEntries.Add($entry) } else { Write-Verbose "[$($MyInvocation.MyCommand.Name)] Download declined by user: $($entry.FileName)" } } # ------------------------------------------------------------------------- # Phase 3: Download all confirmed entries # ------------------------------------------------------------------------- if ($confirmedEntries.Count -gt 0) { Write-Host -ForegroundColor DarkGray "[$(Get-Date -format s)] [INFO] Downloading $($confirmedEntries.Count) ESD file(s) from Microsoft. This process can take between 5 - 30 minutes depending on your internet connection." } $maxAutomaticAttempts = 3 # Initial attempt + 2 automatic retries $downloadedCount = 0 foreach ($entry in $confirmedEntries) { $destPath = Join-Path $downloadDir $entry.FileName $expectedSha256 = & $normalizeHash $entry.Sha256 Write-Verbose "[$($MyInvocation.MyCommand.Name)] Downloading: $($entry.FileName)" Write-Verbose "[$($MyInvocation.MyCommand.Name)] Source: $($entry.FilePath)" Write-Verbose "[$($MyInvocation.MyCommand.Name)] Destination: $destPath" Write-Host -ForegroundColor DarkGray "[$(Get-Date -format s)] [INFO] Downloading $($entry.FileName)" Write-Host -ForegroundColor DarkGray "[$(Get-Date -format s)] [INFO] $destPath" $remoteLength = [Int64]0 $remoteAcceptsRanges = $false try { Write-Verbose "[$($MyInvocation.MyCommand.Name)] Requesting HTTP HEAD for Content-Length and Accept-Ranges: $($entry.FilePath)" $remoteHead = Invoke-WebRequest -Method Head -Uri $entry.FilePath -ErrorAction Stop $remoteLengthHeader = $remoteHead.Headers.'Content-Length' | Select-Object -First 1 if ($remoteLengthHeader) { $remoteLength = [Int64]$remoteLengthHeader } $remoteAcceptsRanges = ($remoteHead.Headers.'Accept-Ranges' | Select-Object -First 1) -eq 'bytes' Write-Verbose "[$($MyInvocation.MyCommand.Name)] RemoteLength: $remoteLength" Write-Verbose "[$($MyInvocation.MyCommand.Name)] RemoteAcceptsRanges: $remoteAcceptsRanges" } catch { Write-Verbose "[$($MyInvocation.MyCommand.Name)] HTTP HEAD failed for '$($entry.FileName)'. Continuing without resume-length validation. $($_.Exception.Message)" } $curlArgs = @( '--location', '--fail', '--retry', '5', '--user-agent', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36', '--continue-at', '-', '--output', $destPath, $entry.FilePath ) $attempt = 0 $promptForManualRetry = $false $downloadSucceeded = $false $lastFailureReason = $null $lastCurlExitCode = 0 $actualHash = $null $localLength = [Int64]0 while (-not $downloadSucceeded) { $attempt++ if ($attempt -eq 1) { Write-Host -ForegroundColor DarkGray "[$(Get-Date -format s)] [INFO] Downloading $($entry.FileName)" } elseif (-not $promptForManualRetry) { Write-Host -ForegroundColor DarkGray "[$(Get-Date -format s)] [INFO] Automatic retry $($attempt - 1) of 2: $($entry.FileName)" } else { Write-Host -ForegroundColor DarkGray "[$(Get-Date -format s)] [INFO] Retrying download of $($entry.FileName) ..." } Write-Host -ForegroundColor DarkGray "[$(Get-Date -format s)] [INFO] $destPath" curl.exe @curlArgs $lastCurlExitCode = $LASTEXITCODE if ($lastCurlExitCode -ne 0) { $lastFailureReason = 'DownloadFailed' Write-Warning "[$(Get-Date -Format s)] curl.exe failed (exit $lastCurlExitCode) downloading '$($entry.FileName)'." Write-Host -ForegroundColor DarkGray "[$(Get-Date -format s)] [INFO] Download failed (curl exit $lastCurlExitCode): $($entry.FileName)" } elseif (-not (Test-Path -Path $destPath -PathType Leaf)) { $lastFailureReason = 'DownloadMissing' Write-Warning "[$(Get-Date -Format s)] Download completed but file is missing: '$($entry.FileName)'." Write-Host -ForegroundColor DarkGray "[$(Get-Date -format s)] [INFO] Download failed: output file was not created for $($entry.FileName)" } else { $localLength = (Get-Item -Path $destPath).Length if ($remoteAcceptsRanges -and $remoteLength -gt 0 -and $localLength -lt $remoteLength) { $lastFailureReason = 'DownloadIncomplete' Write-Warning "[$(Get-Date -Format s)] Incomplete download for '$($entry.FileName)' ($localLength of $remoteLength bytes)." Write-Host -ForegroundColor DarkGray "[$(Get-Date -format s)] [INFO] Incomplete download detected: $($entry.FileName)" Write-Host -ForegroundColor DarkGray "[$(Get-Date -format s)] [INFO] Local : $localLength" Write-Host -ForegroundColor DarkGray "[$(Get-Date -format s)] [INFO] Remote : $remoteLength" } else { $actualHash = (Get-FileHash -Path $destPath -Algorithm SHA256).Hash.ToUpperInvariant() if ($actualHash -eq $expectedSha256) { $downloadSucceeded = $true break } $lastFailureReason = 'ChecksumMismatch' Write-Warning "[$(Get-Date -Format s)] SHA256 mismatch for '$($entry.FileName)'." Write-Host -ForegroundColor DarkGray "[$(Get-Date -format s)] [INFO] SHA256 mismatch: $($entry.FileName)" Write-Host -ForegroundColor DarkGray "[$(Get-Date -format s)] [INFO] Expected : $expectedSha256" Write-Host -ForegroundColor DarkGray "[$(Get-Date -format s)] [INFO] Actual : $actualHash" } } if ($lastFailureReason -eq 'ChecksumMismatch' -and (Test-Path -Path $destPath -PathType Leaf)) { if ($PSCmdlet.ShouldProcess($destPath, 'Delete failed download (Recycle Bin)')) { Write-Host -ForegroundColor DarkGray "[$(Get-Date -format s)] [INFO] Sending to Recycle Bin: $($entry.FileName)" Add-Type -AssemblyName Microsoft.VisualBasic [Microsoft.VisualBasic.FileIO.FileSystem]::DeleteFile($destPath, 'OnlyErrorDialogs', 'SendToRecycleBin') Write-Verbose "[$($MyInvocation.MyCommand.Name)] Sent to Recycle Bin: $destPath" } } if (-not $promptForManualRetry -and $attempt -lt $maxAutomaticAttempts) { continue } $promptForManualRetry = $true $retryCaption = "Download Failed – Retry Download?" if ($lastFailureReason -eq 'ChecksumMismatch') { $retryMessage = "File : $destPath`nExpected SHA256 : $expectedSha256`nActual SHA256 : $actualHash`n`nThe downloaded file does not match the catalog checksum. Retry the download?" } elseif ($lastFailureReason -eq 'DownloadIncomplete') { $retryMessage = "File : $destPath`nLocal Bytes : $localLength`nRemote Bytes : $remoteLength`nSource : $($entry.FilePath)`n`nDownload is incomplete. Retry and continue the transfer?" } elseif ($lastFailureReason -eq 'DownloadFailed') { $retryMessage = "File : $destPath`nCurl Exit Code : $lastCurlExitCode`nSource : $($entry.FilePath)`n`nDownload failed. Retry the download?" } else { $retryMessage = "File : $destPath`nSource : $($entry.FilePath)`n`nDownload failed because the output file was not created. Retry the download?" } if (-not $PSCmdlet.ShouldContinue($retryMessage, $retryCaption)) { if (Test-Path -Path $destPath -PathType Leaf -and $PSCmdlet.ShouldProcess($destPath, 'Delete failed download after user declined retry (Recycle Bin)')) { Write-Host -ForegroundColor DarkGray "[$(Get-Date -format s)] [INFO] User declined retry. Sending failed download to Recycle Bin: $($entry.FileName)" Add-Type -AssemblyName Microsoft.VisualBasic [Microsoft.VisualBasic.FileIO.FileSystem]::DeleteFile($destPath, 'OnlyErrorDialogs', 'SendToRecycleBin') Write-Verbose "[$($MyInvocation.MyCommand.Name)] Sent to Recycle Bin after user declined retry: $destPath" } Write-Verbose "[$($MyInvocation.MyCommand.Name)] User declined retry for: $($entry.FileName)" break } if (-not $PSCmdlet.ShouldProcess($destPath, "Retry download $($entry.FileName)")) { Write-Verbose "[$($MyInvocation.MyCommand.Name)] Retry should-process declined for: $($entry.FileName)" break } } if (-not $downloadSucceeded) { continue } Write-Verbose "[$($MyInvocation.MyCommand.Name)] SHA256 verified: $($entry.FileName)" Write-Host -ForegroundColor DarkGray "[$(Get-Date -format s)] [INFO] SHA256 verified: $($entry.FileName)" $results.Add((Get-Item -Path $destPath)) $downloadedCount++ } Write-Verbose "[$($MyInvocation.MyCommand.Name)] Done. $($results.Count) file(s) ready." $cachedCount = $results.Count - $downloadedCount $summaryParts = [System.Collections.Generic.List[string]]::new() if ($downloadedCount -gt 0) { $summaryParts.Add("$downloadedCount file(s) downloaded") } if ($cachedCount -gt 0) { $summaryParts.Add("$cachedCount file(s) already cached") } $summary = if ($summaryParts.Count -gt 0) { $summaryParts -join ', ' } else { 'No ESD files were downloaded or cached' } Write-HostDateTimeDarkCyan "[$($MyInvocation.MyCommand.Name)] Done. $summary." return $results.ToArray() } |