public/Update-OSDeployBootUSB.ps1
|
function Update-OSDeployBootUSB { <# .SYNOPSIS Updates an existing OSDeploy USB drive from an OSDeploy Core BootImage build .DESCRIPTION Refreshes BootMedia files on existing OSDeploy USB volumes. Opens one selector for a completed BootImage build and a second selector for its bootmedia or bootmedia_ca2023 directory. It then locates every connected USB volume matching BootLabel, copies the selected directory to each volume with robocopy.exe, and writes BootMedia.json containing the selected BootImage object. When local OSDeployCore OSDCloud content exists, locates USB volumes matching DataLabel. For each volume, calculates the source size, verifies available space, and asks whether to copy the content to an OSDCloud directory. Declining or lacking space skips that data volume. No partitioning or formatting is performed. Use New-OSDeployBootUSB to create a new drive. The command disables drive-type AutoRun through the local machine Explorer policy. -WhatIf still performs prerequisite checks and selections, changes that policy, and may display the OSDCloud copy confirmation; it suppresses BootMedia copies, BootMedia.json writes, and confirmed OSDCloud copies. .PARAMETER BootLabel Specifies the volume label used to identify USB boot partitions. Accepts zero through 11 characters. Default is 'OSDEPLOY'. Every matching connected USB volume is updated. .PARAMETER DataLabel Specifies the volume label used to identify USB data partitions that can receive content from the local OSDeployCore OSDCloud directory. Accepts zero through 32 characters. Default is 'OSDCloud'. .EXAMPLE PS> Update-OSDeployBootUSB Updates all connected USB volumes labeled 'OSDEPLOY' with the selected BootImage build. .EXAMPLE PS> Update-OSDeployBootUSB -BootLabel 'WinPE' Updates every connected USB boot volume labeled 'WinPE' and uses the default 'OSDCloud' label for optional data-volume copies. .EXAMPLE PS> Update-OSDeployBootUSB -BootLabel 'OSDEPLOY' -DataLabel 'OSDCloud' -WhatIf Performs the interactive selections and shows copy and metadata operations without changing USB content. The AutoRun policy change is not covered by -WhatIf. .INPUTS None. This function does not accept pipeline input. .OUTPUTS System.String. Returns native robocopy.exe standard-output lines when a BootMedia or OSDCloud copy runs. Returns no objects when no copy runs or robocopy.exe produces no standard output. .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, Administrator rights, a completed BootImage build, and a prepared USB drive. Changes HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\Explorer\ NoDriveTypeAutorun to 0xFF without ShouldProcess protection. .LINK https://github.com/RecastSoftware/RecastOSDeploy #> [CmdletBinding(SupportsShouldProcess)] param ( # USB boot-volume label. Default is 'OSDEPLOY'. [ValidateLength(0, 11)] [string] $BootLabel = 'OSDEPLOY', # USB data-volume label for optional OSDCloud content. Default is 'OSDCloud'. [ValidateLength(0, 32)] [string] $DataLabel = 'OSDCloud' ) #================================================= # Stop before updating USB media 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." } #================================================= # Set Variables $ErrorActionPreference = 'Stop' #================================================= # Stop when no completed BootImage is selected. # Select a BootImage build $SelectBootImage = Select-OSDeployCoreBootMedia if ($null -eq $SelectBootImage) { Write-Warning "[$(Get-Date -Format s)] No OSDeployCore BootImage build was found or selected" return } Write-Host -ForegroundColor DarkGray "[$(Get-Date -format s)] [INFO] Selected BootImage: $($SelectBootImage.Name)" Write-Host -ForegroundColor DarkGray "[$(Get-Date -format s)] [INFO] BootImage path: $($SelectBootImage.Path)" #================================================= # Stop when the user cancels boot media selection. # Select a bootmedia folder Write-Host -ForegroundColor DarkGray "[$(Get-Date -format s)] [INFO] Select a bootmedia folder to copy to the USB (Cancel to exit)" $BootMediaObject = Get-ChildItem $SelectBootImage.Path -Directory | Where-Object { ($_.Name -eq 'bootmedia') -or ($_.Name -eq 'bootmedia_ca2023') } | Sort-Object Name, FullName | Select-Object Name, FullName | Out-GridView -Title 'Select a bootmedia folder to copy to the USB (Cancel to exit)' -OutputMode Single if ($null -eq $BootMediaObject) { Write-Warning "[$(Get-Date -Format s)] No bootmedia folder was found or selected" return } #================================================= # Disable Autorun Set-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\Explorer' -Name NoDriveTypeAutorun -Type DWord -Value 0xFF -ErrorAction SilentlyContinue #================================================= # Update only matching USB volumes when the selected media source is still available. # Update matching USB volumes if (Test-Path -Path $BootMediaObject.FullName) { $WinpeVolumes = Get-OSDeployVolumeUSB -FileSystemLabel $BootLabel if ($WinpeVolumes) { Write-Host -ForegroundColor DarkGray "[$(Get-Date -format s)] [INFO] Copying $($BootMediaObject.FullName) to $BootLabel partitions" foreach ($volume in $WinpeVolumes) { if (Test-Path -Path "$($volume.DriveLetter):\") { $bootTarget = "$($volume.DriveLetter):\" # Honor WhatIf and Confirm before replacing boot media on this volume. if ($PSCmdlet.ShouldProcess($bootTarget, "Copy BootMedia from $($BootMediaObject.FullName)")) { Write-Host -ForegroundColor DarkGray "[$(Get-Date -format s)] [INFO] Updating $bootTarget" robocopy.exe "$($BootMediaObject.FullName)" "$bootTarget" *.* /e /njh /ndl /r:0 /w:0 /xd '$RECYCLE.BIN' 'System Volume Information' /xj } $bootMediaJsonPath = "$($volume.DriveLetter):\BootMedia.json" if ($PSCmdlet.ShouldProcess($bootMediaJsonPath, 'Write BootMedia metadata')) { $SelectBootImage | ConvertTo-Json -Depth 5 | Out-File -FilePath $bootMediaJsonPath -Force } } } } else { Write-Warning "[$(Get-Date -Format s)] No USB partitions labeled '$BootLabel' were found" } } else { Write-Warning "[$(Get-Date -Format s)] BootMedia path not found: $($BootMediaObject.FullName)" } #================================================= # Copy OSDCloud content to USB data partition $OSDCloudSourcePath = Join-Path $script:OSDeployCorePath 'OSDCloud' # Skip optional data-partition content when its local source is unavailable. if (-not (Test-Path -Path $OSDCloudSourcePath)) { Write-Host -ForegroundColor DarkGray "[$(Get-Date -format s)] [INFO] OSDCloud source path not found, skipping data partition copy: $OSDCloudSourcePath" } else { $DataVolumes = Get-OSDeployVolumeUSB -FileSystemLabel $DataLabel if (-not $DataVolumes) { Write-Warning "[$(Get-Date -Format s)] No USB partitions labeled '$DataLabel' were found — skipping OSDCloud copy" } else { foreach ($DataVolume in $DataVolumes) { $DataDrive = $DataVolume.DriveLetter $OSDCloudDestPath = "$DataDrive`:\OSDCloud" Write-Host -ForegroundColor DarkGray "[$(Get-Date -format s)] [INFO] Checking OSDCloud content on $DataDrive`:\" # Calculate total source size for free space check $SourceFiles = Get-ChildItem -Path $OSDCloudSourcePath -File -Recurse $RequiredBytes = ($SourceFiles | Measure-Object -Property Length -Sum).Sum $DataVolumeInfo = Get-Volume -DriveLetter $DataDrive -ErrorAction SilentlyContinue $FreeBytes = $DataVolumeInfo.SizeRemaining # Skip this destination when free space cannot be confirmed or is insufficient. if ($null -eq $FreeBytes) { Write-Warning "[$(Get-Date -Format s)] Could not determine free space on $DataDrive`:\ — skipping OSDCloud copy" continue } if ($RequiredBytes -gt $FreeBytes) { $RequiredGB = [Math]::Round($RequiredBytes / 1GB, 2) $FreeGB = [Math]::Round($FreeBytes / 1GB, 2) Write-Warning "[$(Get-Date -Format s)] Insufficient space on $DataDrive`:\ — need $RequiredGB GB, $FreeGB GB free — skipping OSDCloud copy" continue } $copyCaption = "Copy OSDCloud – $DataDrive`:\" $copyMessage = "Source : $OSDCloudSourcePath`nDest : $OSDCloudDestPath`nFiles : $($SourceFiles.Count) file(s)`nSize : $([Math]::Round($RequiredBytes / 1GB, 2)) GB`nFree : $([Math]::Round($FreeBytes / 1GB, 2)) GB`n`nCopy OSDCloud content to the USB data partition?" if ($PSCmdlet.ShouldContinue($copyMessage, $copyCaption)) { if ($PSCmdlet.ShouldProcess($OSDCloudDestPath, "Copy OSDCloud content from $OSDCloudSourcePath")) { Write-Host -ForegroundColor DarkGray "[$(Get-Date -format s)] [INFO] Copying OSDCloud from $OSDCloudSourcePath to $OSDCloudDestPath" robocopy.exe "$OSDCloudSourcePath" "$OSDCloudDestPath" *.* /e /njh /ndl /r:0 /w:0 /xd '$RECYCLE.BIN' 'System Volume Information' /xj } } else { Write-Host -ForegroundColor DarkGray "[$(Get-Date -format s)] [INFO] OSDCloud copy declined by user for $DataDrive`:\" } } } } #================================================= Write-Verbose "[$($MyInvocation.MyCommand.Name)] End" #================================================= } |