public/Update-OSDeployBootUSB.ps1

function Update-OSDeployBootUSB {
    <#
    .SYNOPSIS
        Updates an existing OSDeploy USB drive with new BootMedia from an OSDeployCore BootImage build.
 
    .DESCRIPTION
        Refreshes the BootMedia files on one or more existing OSDeploy USB drives. The function:
        - Prompts for selection of a completed BootImage from %ProgramData%\OSDeployCore\boot
        - Prompts for the specific bootmedia folder (bootmedia or bootmedia_ca2023)
        - Locates all connected USB volumes whose label matches $BootLabel (default 'OSDEPLOY')
        - Copies the selected media to each matching USB volume using robocopy
        - Writes a BootMedia.json file to each updated volume
 
        No partitioning or formatting is performed. Use New-OSDeployBootUSB to create a new drive.
 
    .PARAMETER BootLabel
        Volume label used to identify USB boot partitions to update. Maximum 11 characters.
        Default is 'OSDEPLOY'.
 
    .PARAMETER DataLabel
        Volume label used to identify the USB data partition when copying ESD files.
        Maximum 32 characters. Default is 'OSDCloud'.
 
    .EXAMPLE
        Update-OSDeployBootUSB
        Updates all connected USB volumes labeled 'OSDEPLOY' with the selected BootImage build.
 
    .EXAMPLE
        Update-OSDeployBootUSB -BootLabel 'WinPE'
        Updates all connected USB volumes labeled 'WinPE'.
 
    .NOTES
        Author: David Segura
        Company: Recast Software
        Version: 0.1.0
        Date: April 2026
 
        Prerequisites:
            - PowerShell 5.0 or higher
            - Windows 10 or higher
            - Run as Administrator
            - At least one completed BootImage build in %ProgramData%\OSDeployCore\boot
            - One or more USB drives previously created with New-OSDeployBootUSB
 
    .LINK
        https://github.com/OSDeploy/OSDeploy
    #>


    [CmdletBinding(SupportsShouldProcess)]
    param (
        # Label used to identify USB boot partitions. Default is 'OSDEPLOY'.
        [ValidateLength(0, 11)]
        [string]
        $BootLabel = 'OSDEPLOY',

        # Label used to identify the USB data partition for ESD file copy. Default is 'OSDCloud'.
        [ValidateLength(0, 32)]
        [string]
        $DataLabel = 'OSDCloud'
    )
    #=================================================
    Write-OSDeployBanner
    $Error.Clear()
    Write-Verbose "[$(Get-Date -format G)] [$($MyInvocation.MyCommand.Name)] Start"
    #=================================================
    # Requires Run as Administrator
    if (-not (Test-IsAdministrator)) {
        Write-Warning "[$(Get-Date -format G)] [$($MyInvocation.MyCommand.Name)] This function must be Run as Administrator"
        return
    }
    #=================================================
    # Set Variables
    $ErrorActionPreference = 'Stop'
    #=================================================
    # Block
    Block-StandardUser
    Block-WindowsVersionNe10
    Block-PowerShellVersionLt5
    Block-WindowsReleaseIdLt1703
    #=================================================
    # Select a BootImage build
    $SelectBootImage = Select-OSDeployCoreBootImage
    if ($null -eq $SelectBootImage) {
        Write-Warning "[$(Get-Date -format G)] [$($MyInvocation.MyCommand.Name)] No OSDeployCore BootImage build was found or selected"
        return
    }
    Write-Host -ForegroundColor DarkGray "[$(Get-Date -format G)] Selected BootImage: $($SelectBootImage.Name)"
    Write-Host -ForegroundColor DarkGray "[$(Get-Date -format G)] BootImage path: $($SelectBootImage.Path)"
    #=================================================
    # Select a bootmedia folder
    Write-Host -ForegroundColor DarkGray "[$(Get-Date -format G)] 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 G)] [$($MyInvocation.MyCommand.Name)] 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 matching USB volumes
    if (Test-Path -Path $BootMediaObject.FullName) {
        $WinpeVolumes = Get-USBVolume | Where-Object { $_.FileSystemLabel -eq $BootLabel }

        if ($WinpeVolumes) {
            Write-Host -ForegroundColor DarkGray "[$(Get-Date -format G)] Copying $($BootMediaObject.FullName) to $BootLabel partitions"
            foreach ($volume in $WinpeVolumes) {
                if (Test-Path -Path "$($volume.DriveLetter):\") {
                    Write-Host -ForegroundColor DarkGray "[$(Get-Date -format G)] Updating $($volume.DriveLetter):\"
                    robocopy.exe "$($BootMediaObject.FullName)" "$($volume.DriveLetter):\" *.* /e /ndl /r:0 /w:0 /xd '$RECYCLE.BIN' 'System Volume Information' /xj
                    $SelectBootImage | ConvertTo-Json -Depth 5 | Out-File -FilePath "$($volume.DriveLetter):\BootMedia.json" -Force
                }
            }
        }
        else {
            Write-Warning "[$(Get-Date -format G)] [$($MyInvocation.MyCommand.Name)] No USB partitions labeled '$BootLabel' were found"
        }
    }
    else {
        Write-Warning "[$(Get-Date -format G)] [$($MyInvocation.MyCommand.Name)] BootMedia path not found: $($BootMediaObject.FullName)"
    }
    #=================================================
    # Verify ESD files are present on USB data partition
    $ESDSourcePath = Join-Path $script:OSDeployCorePath 'OSDCloud' 'OS'

    if (-not (Test-Path -Path $ESDSourcePath)) {
        Write-Host -ForegroundColor DarkGray "[$(Get-Date -format G)] ESD source path not found, skipping data partition sync: $ESDSourcePath"
    }
    else {
        $SourceFiles = Get-ChildItem -Path $ESDSourcePath -File -Recurse

        if (-not $SourceFiles) {
            Write-Host -ForegroundColor DarkGray "[$(Get-Date -format G)] No ESD files found in $ESDSourcePath, skipping data partition sync"
        }
        else {
            $DataVolumes = Get-USBVolume | Where-Object { $_.FileSystemLabel -eq $DataLabel }

            if (-not $DataVolumes) {
                Write-Warning "[$(Get-Date -format G)] [$($MyInvocation.MyCommand.Name)] No USB partitions labeled '$DataLabel' were found — skipping ESD copy"
            }
            else {
                foreach ($DataVolume in $DataVolumes) {
                    $DataDrive   = $DataVolume.DriveLetter
                    $ESDDestPath = "$DataDrive`:\OSDCloud\OS"

                    Write-Host -ForegroundColor DarkGray "[$(Get-Date -format G)] Checking ESD files on $DataDrive`:\"

                    # Determine which source files are missing from the destination
                    $MissingFiles = $SourceFiles | Where-Object {
                        $RelativePath = $_.FullName.Substring($ESDSourcePath.Length).TrimStart('\')
                        -not (Test-Path -Path (Join-Path $ESDDestPath $RelativePath))
                    }

                    if (-not $MissingFiles) {
                        Write-Host -ForegroundColor DarkGray "[$(Get-Date -format G)] ESD files already present on $DataDrive`:\"
                        continue
                    }

                    Write-Host -ForegroundColor DarkGray "[$(Get-Date -format G)] $($MissingFiles.Count) ESD file(s) missing from $DataDrive`:\"

                    # Verify sufficient free space before prompting
                    $RequiredBytes = ($MissingFiles | Measure-Object -Property Length -Sum).Sum
                    $DataVolumeInfo = Get-Volume -DriveLetter $DataDrive -ErrorAction SilentlyContinue
                    $FreeBytes      = $DataVolumeInfo.SizeRemaining

                    if ($null -eq $FreeBytes) {
                        Write-Warning "[$(Get-Date -format G)] [$($MyInvocation.MyCommand.Name)] Could not determine free space on $DataDrive`:\ — skipping ESD copy"
                        continue
                    }

                    if ($RequiredBytes -gt $FreeBytes) {
                        $RequiredGB = [Math]::Round($RequiredBytes / 1GB, 2)
                        $FreeGB     = [Math]::Round($FreeBytes / 1GB, 2)
                        Write-Warning "[$(Get-Date -format G)] [$($MyInvocation.MyCommand.Name)] Insufficient space on $DataDrive`:\ — need $RequiredGB GB, $FreeGB GB free — skipping ESD copy"
                        continue
                    }

                    # Prompt for confirmation before copying (style matches Update-OSDeployCoreESD)
                    $copyCaption = "Copy ESD Files – $DataDrive`:\"
                    $copyMessage = "Source : $ESDSourcePath`nDest : $ESDDestPath`nFiles : $($MissingFiles.Count) file(s) missing`nSize : $([Math]::Round($RequiredBytes / 1GB, 2)) GB`nFree : $([Math]::Round($FreeBytes / 1GB, 2)) GB`n`nCopy missing ESD files to the USB data partition?"

                    if ($PSCmdlet.ShouldContinue($copyMessage, $copyCaption)) {
                        Write-Host -ForegroundColor DarkGray "[$(Get-Date -format G)] Copying ESD files from $ESDSourcePath to $ESDDestPath"
                        robocopy.exe "$ESDSourcePath" "$ESDDestPath" *.* /e /ndl /r:0 /w:0 /xd '$RECYCLE.BIN' 'System Volume Information' /xj
                    }
                    else {
                        Write-Host -ForegroundColor DarkGray "[$(Get-Date -format G)] ESD copy declined by user for $DataDrive`:\"
                    }
                }
            }
        }
    }
    #=================================================
    Write-Verbose "[$(Get-Date -format G)] [$($MyInvocation.MyCommand.Name)] End"
    #=================================================
}