Private/steps-osd/Step-OSDCloudRemoveUSBDrives.ps1

function Step-OSDCloudRemoveUSBDrives {
    <#
    .SYNOPSIS
    Removes access paths for detected USB partitions during OSDCloud workflow steps.
 
    .DESCRIPTION
    Step-OSDCloudRemoveUSBDrives iterates USB partitions collected in OSD core
    device state and attempts to remove each partition access path. Removal is
    only performed when running in WinPE (system drive X:) and is skipped with
    verbose logging in other environments.
 
    .EXAMPLE
    Step-OSDCloudRemoveUSBDrives
    Removes access paths for USB partitions in WinPE to avoid drive letter
    conflicts during OSDCloud deployment steps.
 
    .LINK
    https://github.com/OSDeploy/OSD/tree/master/docs
 
    .NOTES
    Author: David Segura - Recast Software
    2026-07-16 - Added comment-based help block
    #>

    [CmdletBinding()]
    param ()
    #=================================================
    Write-Host -ForegroundColor DarkGray "[$(Get-Date -format s)] [$($MyInvocation.MyCommand.Name)]"
    #=================================================
    if (-not $global:OSDCoreDevice.USBPartitions) {
        # If no USB partitions were discovered earlier, there is nothing to remove.
        Write-Verbose "[$(Get-Date -format s)] [$($MyInvocation.MyCommand.Name)] No USB partitions detected"
        Write-Verbose "[$(Get-Date -format s)] [$($MyInvocation.MyCommand.Name)] End"
        return
    }

    # Build a quick lookup table so we can find volume labels by drive letter.
    $usbVolumesByDriveLetter = @{}
    foreach ($volume in @($global:OSDCoreDevice.USBVolumes)) {
        if (-not [string]::IsNullOrWhiteSpace($volume.DriveLetter)) {
            $usbVolumesByDriveLetter[[string]$volume.DriveLetter] = $volume
        }
    }

    foreach ($Item in $global:OSDCoreDevice.USBPartitions) {
        # A partition can have one or more access paths (drive letter or mount path).
        $accessPaths = @($Item.AccessPaths) | Where-Object { -not [string]::IsNullOrWhiteSpace($_) }
        if (-not $accessPaths) {
            Write-Verbose "[$(Get-Date -format s)] [$($MyInvocation.MyCommand.Name)] Skipping USB partition without access paths (Disk $($Item.DiskNumber), Partition $($Item.PartitionNumber))"
            continue
        }

        # Capture the drive letter (when present) so logs can include a friendly label.
        $driveLetter = $null
        foreach ($path in $accessPaths) {
            if ($path -match '^(?<DriveLetter>[A-Z]):\\$') {
                $driveLetter = $Matches.DriveLetter
                break
            }
        }

        $fileSystemLabel = $null
        if ($driveLetter -and $usbVolumesByDriveLetter.ContainsKey($driveLetter)) {
            $fileSystemLabel = $usbVolumesByDriveLetter[$driveLetter].FileSystemLabel
        }

        foreach ($accessPath in $accessPaths) {
            # Do not attempt to remove canonical volume GUID paths.
            if ($accessPath -match '^\\\\\?\\Volume\{[0-9a-fA-F\-]+\}\\$') {
                Write-Verbose "[$(Get-Date -format s)] [$($MyInvocation.MyCommand.Name)] Skipped GUID access path '$accessPath'"
                continue
            }

            Write-Host -ForegroundColor DarkGray "[$(Get-Date -format s)] Removing USB Volume $fileSystemLabel on AccessPath $accessPath"
            try {
                # Remove this path so USB media does not interfere with deployment steps.
                Remove-PartitionAccessPath -AccessPath $accessPath -DiskNumber $Item.DiskNumber -PartitionNumber $Item.PartitionNumber -ErrorAction Stop
            }
            catch {
                # Keep going if one partition fails so others can still be processed.
                Write-Verbose "[$(Get-Date -format s)] [$($MyInvocation.MyCommand.Name)] Unable to remove access path '$accessPath' (Disk $($Item.DiskNumber), Partition $($Item.PartitionNumber)): $($_.Exception.Message)"
            }
        }
    }
    #=================================================
    Write-Verbose "[$(Get-Date -format s)] [$($MyInvocation.MyCommand.Name)] End"
    #=================================================
}