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"
    #=================================================
}