Files
FFU/FFUDevelopment/FFUUI.Core/FFUUI.Core.Drivers.Lenovo.psm1
T
rbalsleyMSFT d6689888b2 Refactor: Remove redundant Success property assignment
Removes the logic that adds a `Success` property to the returned object when an existing driver package is found.

This property is handled by the calling function, making this assignment unnecessary and simplifying the driver-saving task functions.
2025-06-28 00:57:38 -07:00

451 lines
25 KiB
PowerShell

# Function to get the list of Lenovo models using the PSREF API
function Get-LenovoDriversModelList {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]$ModelSearchTerm, # User input for model/machine type
[Parameter(Mandatory = $true)]
[hashtable]$Headers,
[Parameter(Mandatory = $true)]
[string]$UserAgent
)
WriteLog "Querying Lenovo PSREF API for model/machine type: $ModelSearchTerm"
$url = "https://psref.lenovo.com/api/search/DefinitionFilterAndSearch/Suggest?kw=$([uri]::EscapeDataString($ModelSearchTerm))"
$models = [System.Collections.Generic.List[PSCustomObject]]::new()
try {
$OriginalVerbosePreference = $VerbosePreference
$VerbosePreference = 'SilentlyContinue'
$response = Invoke-WebRequest -Uri $url -UseBasicParsing -Headers $Headers -UserAgent $UserAgent -ErrorAction Stop
$VerbosePreference = $OriginalVerbosePreference
WriteLog "PSREF API query complete."
$jsonResponse = $response.Content | ConvertFrom-Json
if ($null -ne $jsonResponse.data -and $jsonResponse.data.Count -gt 0) {
foreach ($item in $jsonResponse.data) {
$productName = $item.ProductName
$machineTypes = $item.MachineType -split " / " # Split if multiple machine types are listed
foreach ($machineTypeRaw in $machineTypes) {
$machineType = $machineTypeRaw.Trim()
# Only add if machine type is not empty
if (-not [string]::IsNullOrWhiteSpace($machineType)) {
# Create the combined display string
$displayModel = "$productName ($machineType)"
# Add each combination as a separate entry
$models.Add([PSCustomObject]@{
Make = 'Lenovo'
Model = $displayModel
ProductName = $productName
MachineType = $machineType
})
}
else {
WriteLog "Skipping entry for product '$productName' due to missing machine type."
}
}
}
WriteLog "Found $($models.Count) potential model/machine type combinations for '$ModelSearchTerm'."
}
else {
WriteLog "No models found matching '$ModelSearchTerm' in Lenovo PSREF."
}
}
catch {
WriteLog "Error querying Lenovo PSREF API: $($_.Exception.Message)"
# Return empty list on error
}
return $models
}
# Function to download and extract drivers for a specific Lenovo model (Background Task)
function Save-LenovoDriversTask {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[PSCustomObject]$DriverItemData, # Contains Model (ProductName) and MachineType
[Parameter(Mandatory = $true)]
[string]$DriversFolder,
[Parameter(Mandatory = $true)]
[int]$WindowsRelease,
[Parameter(Mandatory = $true)]
[hashtable]$Headers,
[Parameter(Mandatory = $true)]
[string]$UserAgent,
[Parameter()] # Made optional
[System.Collections.Concurrent.ConcurrentQueue[hashtable]]$ProgressQueue = $null,
[Parameter()]
[bool]$CompressToWim = $false
)
# The Model property from the UI already contains the combined "ProductName (MachineType)" string
$identifier = $DriverItemData.Model
$machineType = $DriverItemData.MachineType
$make = "Lenovo"
$sanitizedIdentifier = $identifier -replace '[\\/:"*?<>|]', '_'
$status = "Starting..."
$success = $false
# Define paths
$makeDriversPath = Join-Path -Path $DriversFolder -ChildPath $Make
# Use the identifier (which contains the model name and machine type) and sanitize it for the path
$modelPath = Join-Path -Path $makeDriversPath -ChildPath $sanitizedIdentifier
$driverRelativePath = Join-Path -Path $make -ChildPath $sanitizedIdentifier # Relative path for the driver folder
$tempDownloadPath = Join-Path -Path $makeDriversPath -ChildPath "_TEMP_$($machineType)_$($PID)" # Temp folder for catalog/package XMLs
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $identifier -Status "Checking..." }
try {
# Check for existing drivers
$existingDriver = Test-ExistingDriver -Make $make -Model $sanitizedIdentifier -DriversFolder $DriversFolder -Identifier $identifier -ProgressQueue $ProgressQueue
if ($null -ne $existingDriver) {
# The return object from Test-ExistingDriver uses 'Model' as the identifier key.
# We need to return 'Identifier' for Lenovo's logic.
$existingDriver | Add-Member -MemberType NoteProperty -Name 'Identifier' -Value $identifier -Force
$existingDriver.PSObject.Properties.Remove('Model')
# Special handling for existing folders that need compression
if ($CompressToWim -and $existingDriver.Status -eq 'Already downloaded') {
$wimFilePath = Join-Path -Path $makeDriversPath -ChildPath "$($sanitizedIdentifier).wim"
$sourceFolderPath = Join-Path -Path $makeDriversPath -ChildPath $sanitizedIdentifier
WriteLog "Attempting compression of existing folder '$sourceFolderPath' to '$wimFilePath'."
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $identifier -Status "Compressing existing..." }
try {
Compress-DriverFolderToWim -SourceFolderPath $sourceFolderPath -DestinationWimPath $wimFilePath -WimName $identifier -WimDescription "Drivers for $identifier" -ErrorAction Stop
$existingDriver.Status = "Already downloaded & Compressed"
$existingDriver.DriverPath = Join-Path -Path $make -ChildPath "$($sanitizedIdentifier).wim"
$existingDriver.Success = $true
WriteLog "Successfully compressed existing drivers for $identifier to $wimFilePath."
}
catch {
WriteLog "Error compressing existing drivers for $($identifier): $($_.Exception.Message)"
$existingDriver.Status = "Already downloaded (Compression failed)"
$existingDriver.Success = $false
}
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $identifier -Status $existingDriver.Status }
}
return $existingDriver
}
# Ensure base directories exist
if (-not (Test-Path -Path $makeDriversPath)) { New-Item -Path $makeDriversPath -ItemType Directory -Force | Out-Null }
if (-not (Test-Path -Path $modelPath)) { New-Item -Path $modelPath -ItemType Directory -Force | Out-Null }
if (-not (Test-Path -Path $tempDownloadPath)) { New-Item -Path $tempDownloadPath -ItemType Directory -Force | Out-Null }
# 2. Construct and Download Catalog URL
$modelRelease = $machineType + "_Win" + $WindowsRelease
$catalogUrl = "https://download.lenovo.com/catalog/$modelRelease.xml"
$lenovoCatalogXML = Join-Path -Path $tempDownloadPath -ChildPath "$modelRelease.xml"
$status = "Downloading Catalog..."
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $identifier -Status $status }
WriteLog "Downloading Lenovo Driver catalog for '$identifier' from $catalogUrl"
# Check URL accessibility first
try {
$request = [System.Net.WebRequest]::Create($catalogUrl); $request.Method = 'HEAD'; $response = $request.GetResponse(); $response.Close()
}
catch { throw "Lenovo Driver catalog URL is not accessible: $catalogUrl. Error: $($_.Exception.Message)" }
Start-BitsTransferWithRetry -Source $catalogUrl -Destination $lenovoCatalogXML
WriteLog "Catalog download Complete: $lenovoCatalogXML"
# 3. Parse Catalog and Process Packages
$status = "Parsing Catalog..."
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $identifier -Status $status }
[xml]$xmlContent = Get-Content -Path $lenovoCatalogXML -Encoding UTF8
$packages = @($xmlContent.packages.package) # Ensure it's an array
$totalPackages = $packages.Count
$processedPackages = 0
WriteLog "Found $totalPackages packages in catalog for '$identifier'."
foreach ($package in $packages) {
$processedPackages++
$category = $package.category
$packageUrl = $package.location # URL to the package's *XML* file
# Skip BIOS/Firmware based on category
if ($category -like 'BIOS*' -or $category -like 'Firmware*') {
WriteLog "($processedPackages/$totalPackages) Skipping BIOS/Firmware package: $category"
continue
}
# Sanitize category for path
$categoryClean = $category -replace '[\\/:"*?<>|]', '_'
if ($categoryClean -eq 'Motherboard Devices Backplanes core chipset onboard video PCIe switches') {
$categoryClean = 'Motherboard Devices' # Shorten long category name
}
$packageName = [System.IO.Path]::GetFileName($packageUrl)
$packageXMLPath = Join-Path -Path $tempDownloadPath -ChildPath $packageName
$baseURL = $packageUrl -replace [regex]::Escape($packageName), "" # Base URL for the driver file
$status = "($processedPackages/$totalPackages) Getting package info..."
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $identifier -Status $status }
# Download the package XML
WriteLog "($processedPackages/$totalPackages) Downloading package XML: $packageUrl"
try {
Start-BitsTransferWithRetry -Source $packageUrl -Destination $packageXMLPath
}
catch {
WriteLog "($processedPackages/$totalPackages) Failed to download package XML '$packageUrl'. Skipping. Error: $($_.Exception.Message)"
continue # Skip this package
}
# Load and parse the package XML
[xml]$packageXmlContent = Get-Content -Path $packageXMLPath -Encoding UTF8
$packageType = $packageXmlContent.Package.PackageType.type
$packageTitleRaw = $packageXmlContent.Package.title.InnerText
# Filter out non-driver packages (Type 2 = Driver)
if ($packageType -ne 2) {
WriteLog "($processedPackages/$totalPackages) Skipping package '$packageTitleRaw' (Type: $packageType) - Not a driver."
Remove-Item -Path $packageXMLPath -Force -ErrorAction SilentlyContinue # Clean up package XML
continue
}
# Sanitize title for folder name
$packageTitle = $packageTitleRaw -replace '[\\/:"*?<>|]', '_' -replace ' - .*', ''
# Extract driver file name and extract command
$driverFileName = $null
$extractCommand = $null
try {
$driverFileName = $packageXmlContent.Package.Files.Installer.File.Name
$extractCommand = $packageXmlContent.Package.ExtractCommand
}
catch {
WriteLog "($processedPackages/$totalPackages) Error parsing package XML '$packageXMLPath' for file name/command. Skipping. Error: $($_.Exception.Message)"
Remove-Item -Path $packageXMLPath -Force -ErrorAction SilentlyContinue
continue
}
# Skip if essential info is missing
if ([string]::IsNullOrWhiteSpace($driverFileName) -or [string]::IsNullOrWhiteSpace($extractCommand)) {
WriteLog "($processedPackages/$totalPackages) Skipping package '$packageTitleRaw' - Missing driver file name or extract command in XML."
Remove-Item -Path $packageXMLPath -Force -ErrorAction SilentlyContinue
continue
}
# Construct paths
$driverUrl = $baseURL + $driverFileName
$categoryPath = Join-Path -Path $modelPath -ChildPath $categoryClean
$downloadFolder = Join-Path -Path $categoryPath -ChildPath $packageTitle # Final destination subfolder
$driverFilePath = Join-Path -Path $downloadFolder -ChildPath $driverFileName
$extractFolder = Join-Path -Path $downloadFolder -ChildPath ($driverFileName -replace '\.exe$', '') # Extract to subfolder named after exe
# Check if already extracted
if (Test-Path -Path $extractFolder -PathType Container) {
$extractSize = (Get-ChildItem -Path $extractFolder -Recurse | Measure-Object -Property Length -Sum -ErrorAction SilentlyContinue).Sum
if ($extractSize -gt 1KB) {
WriteLog "($processedPackages/$totalPackages) Driver '$packageTitleRaw' already extracted to '$extractFolder'. Skipping."
Remove-Item -Path $packageXMLPath -Force -ErrorAction SilentlyContinue # Clean up package XML
continue
}
}
# Ensure download folder exists
if (-not (Test-Path -Path $downloadFolder)) {
New-Item -Path $downloadFolder -ItemType Directory -Force | Out-Null
}
# Download the driver .exe
$status = "($processedPackages/$totalPackages) Downloading $packageTitle..."
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $identifier -Status $status }
WriteLog "($processedPackages/$totalPackages) Downloading driver: $driverUrl to $driverFilePath"
try {
Start-BitsTransferWithRetry -Source $driverUrl -Destination $driverFilePath
WriteLog "($processedPackages/$totalPackages) Driver downloaded: $driverFileName"
}
catch {
WriteLog "($processedPackages/$totalPackages) Failed to download driver '$driverUrl'. Skipping. Error: $($_.Exception.Message)"
Remove-Item -Path $packageXMLPath -Force -ErrorAction SilentlyContinue # Clean up package XML
continue # Skip this driver
}
# --- Extraction Logic ---
$status = "($processedPackages/$totalPackages) Extracting $packageTitle..."
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $identifier -Status $status }
# Always use a temporary extraction path to avoid long path issues
$originalExtractFolder = $extractFolder # Store the originally intended final path
$extractionSucceeded = $false
$tempExtractBase = $null # Initialize
# Create randomized number for use with temp folder name
$randomNumber = Get-Random -Minimum 1000 -Maximum 9999
$tempExtractBase = Join-Path $env:TEMP "LenovoDriverExtract_$randomNumber"
$extractFolder = Join-Path $tempExtractBase ($driverFileName -replace '\.exe$', '') # Actual temp extraction folder
WriteLog "($processedPackages/$totalPackages) Using temporary extraction path: $extractFolder"
# Ensure the base temp directory exists
if (-not (Test-Path -Path $tempExtractBase)) {
New-Item -Path $tempExtractBase -ItemType Directory -Force | Out-Null
}
# Ensure the target temporary extraction folder exists
if (-not (Test-Path -Path $extractFolder)) {
New-Item -Path $extractFolder -ItemType Directory -Force | Out-Null
}
# Modify the extract command to point to the temporary folder
$modifiedExtractCommand = $extractCommand -replace '%PACKAGEPATH%', "`"$extractFolder`""
WriteLog "($processedPackages/$totalPackages) Extracting driver: $driverFilePath using command: $modifiedExtractCommand"
try {
Invoke-Process -FilePath $driverFilePath -ArgumentList $modifiedExtractCommand -Wait $true | Out-Null
WriteLog "($processedPackages/$totalPackages) Driver extracted to temporary path: $extractFolder"
$extractionSucceeded = $true
}
catch {
WriteLog "($processedPackages/$totalPackages) Failed to extract driver '$driverFilePath' to temporary path. Skipping. Error: $($_.Exception.Message)"
# Don't delete the downloaded exe yet if extraction fails
Remove-Item -Path $packageXMLPath -Force -ErrorAction SilentlyContinue # Clean up package XML
# Clean up temp folder if extraction failed
if ($tempExtractBase -and (Test-Path -Path $tempExtractBase)) {
Remove-Item -Path $tempExtractBase -Recurse -Force -ErrorAction SilentlyContinue
}
continue # Skip further processing for this driver
}
# --- Post-Extraction Handling (Move from Temp to Final Destination) ---
if ($extractionSucceeded) {
WriteLog "($processedPackages/$totalPackages) Performing post-extraction move from temp to final destination..."
try {
# Ensure the *original* final destination folder exists and is empty
if (Test-Path -Path $originalExtractFolder) {
WriteLog "($processedPackages/$totalPackages) Clearing existing final destination folder: $originalExtractFolder"
Get-ChildItem -Path $originalExtractFolder -Recurse | Remove-Item -Recurse -Force -ErrorAction SilentlyContinue
}
else {
WriteLog "($processedPackages/$totalPackages) Creating final destination folder: $originalExtractFolder"
New-Item -Path $originalExtractFolder -ItemType Directory -Force | Out-Null
}
# Get all items (files and folders) directly inside the temp extraction folder
$extractedItems = Get-ChildItem -Path $extractFolder -ErrorAction Stop
foreach ($item in $extractedItems) {
$itemName = $item.Name
$finalDestinationPath = $null
# Check if it's a directory containing 'Liteon'
if ($item.PSIsContainer -and $itemName -like '*Liteon*') {
# Rename Liteon folders with a random number suffix
$randomNumber = Get-Random -Minimum 1000 -Maximum 9999
$finalFolderName = "Liteon_$randomNumber"
$finalDestinationPath = Join-Path -Path $originalExtractFolder -ChildPath $finalFolderName
WriteLog "($processedPackages/$totalPackages) Moving Liteon folder '$itemName' to '$finalDestinationPath'"
}
else {
# For other files/folders, move them directly
$finalDestinationPath = Join-Path -Path $originalExtractFolder -ChildPath $itemName
WriteLog "($processedPackages/$totalPackages) Moving item '$itemName' to '$finalDestinationPath'"
}
# Perform the move
try {
Move-Item -Path $item.FullName -Destination $finalDestinationPath -Force -ErrorAction Stop
}
catch {
WriteLog "($processedPackages/$totalPackages) Failed to move item '$($item.FullName)' to '$finalDestinationPath'. Error: $($_.Exception.Message)"
# Decide if this should stop the whole process or just skip this item
# For now, we'll log and continue, but mark overall success as false
$extractionSucceeded = $false
}
} # End foreach ($item in $extractedItems)
if ($extractionSucceeded) {
WriteLog "($processedPackages/$totalPackages) All driver contents moved successfully from temp to final destination."
}
else {
WriteLog "($processedPackages/$totalPackages) Some driver contents failed to move. Check logs."
}
}
catch {
WriteLog "($processedPackages/$totalPackages) Error during post-extraction move: $($_.Exception.Message). Files might remain in temp."
$extractionSucceeded = $false # Mark as failed for cleanup logic below
}
finally {
# Clean up the base temporary directory regardless of move success/failure
if ($tempExtractBase -and (Test-Path -Path $tempExtractBase)) {
WriteLog "($processedPackages/$totalPackages) Cleaning up temporary extraction base: $tempExtractBase"
Remove-Item -Path $tempExtractBase -Recurse -Force -ErrorAction SilentlyContinue
}
}
}
# --- Final Cleanup ---
# Delete the downloaded .exe only if extraction AND move were successful
if ($extractionSucceeded) {
WriteLog "($processedPackages/$totalPackages) Deleting driver installation file: $driverFilePath"
Remove-Item -Path $driverFilePath -Force -ErrorAction SilentlyContinue
}
else {
WriteLog "($processedPackages/$totalPackages) Keeping driver installation file due to extraction/move failure: $driverFilePath"
}
# Always delete the package XML
WriteLog "($processedPackages/$totalPackages) Deleting package XML file: $packageXMLPath"
Remove-Item -Path $packageXMLPath -Force -ErrorAction SilentlyContinue
} # End foreach package
# --- Compress to WIM if requested (after all drivers processed) ---
if ($CompressToWim) {
$status = "Compressing..."
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $identifier -Status $status }
$wimFileName = "$($sanitizedIdentifier).wim" # Use sanitized identifier for filename
$destinationWimPath = Join-Path -Path $makeDriversPath -ChildPath $wimFileName
$driverRelativePath = Join-Path -Path $make -ChildPath $wimFileName # Update relative path to the WIM file
WriteLog "Compressing '$modelPath' to '$destinationWimPath'..."
try {
$compressResult = Compress-DriverFolderToWim -SourceFolderPath $modelPath -DestinationWimPath $destinationWimPath -WimName $identifier -WimDescription $identifier -ErrorAction Stop
if ($compressResult) {
WriteLog "Compression successful for '$identifier'."
$status = "Completed & Compressed"
}
else {
WriteLog "Compression failed for '$identifier'. Check verbose/error output from Compress-DriverFolderToWim."
$status = "Completed (Compression Failed)"
}
}
catch {
WriteLog "Error during compression for '$identifier': $($_.Exception.Message)"
$status = "Completed (Compression Error)"
}
}
else {
$status = "Completed"
}
# --- End Compression ---
$success = $true # Mark success as download/extract was okay
}
catch {
$status = "Error: $($_.Exception.Message.Split('.')[0])" # Shorten error message
WriteLog "Error saving Lenovo drivers for '$identifier': $($_.Exception.ToString())" # Log full exception string
$success = $false
# Enqueue the error status before returning
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $identifier -Status $status }
# Ensure return object is created even on error
return [PSCustomObject]@{ Identifier = $identifier; Status = $status; Success = $success; DriverPath = $null }
}
finally {
# Clean up the main catalog XML and temp folder
WriteLog "Cleaning up temporary download folder: $tempDownloadPath"
Remove-Item -Path $tempDownloadPath -Recurse -Force -ErrorAction SilentlyContinue
}
# Enqueue the final status (success or error) before returning
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $identifier -Status $status }
# Return the final status
return [PSCustomObject]@{ Identifier = $identifier; Status = $status; Success = $success; DriverPath = $driverRelativePath }
}
Export-ModuleMember -Function *