Eigenverft.Manifested.Drydock.DriveMapping.ps1
|
function New-NetworkDriveMapping { <# .SYNOPSIS Creates a new network drive mapping after clearing cached Explorer data. .DESCRIPTION Removes any existing mapping for the specified drive letter, cleans related HKCU:\Network and MountPoints2 entries, then creates a new mapping so that File Explorer sees the updated connection. .PARAMETER DriveLetter Single drive letter without colon (for example Z). .PARAMETER RemotePath UNC path to map to (for example \\server\share). .PARAMETER NonPersistent If set, the mapping will not be stored persistently in the user profile. .EXAMPLE New-NetworkDriveMapping -DriveLetter Z -RemotePath '\\server\share' .EXAMPLE New-NetworkDriveMapping -DriveLetter Z -RemotePath '\\server\share' -NonPersistent .NOTES This function is supported only on Windows. On non-Windows platforms it will throw a terminating error before attempting any Windows-specific operations. #> [CmdletBinding()] param( [Parameter(Mandatory=$true)] [ValidatePattern('^[A-Za-z]$')] [string]$DriveLetter, [Parameter(Mandatory=$true)] [ValidatePattern('^\\\\')] [string]$RemotePath, [Parameter()] [switch]$NonPersistent ) function local:_Write-StandardMessage { [Diagnostics.CodeAnalysis.SuppressMessage("PSUseApprovedVerbs","")] param( [Parameter(Mandatory=$true)][AllowEmptyString()][string]$Message, [Parameter()][ValidateSet('TRC','DBG','INF','WRN','ERR','FTL')][string]$Level='INF', [Parameter()][ValidateSet('TRC','DBG','INF','WRN','ERR','FTL')][string]$MinLevel ) if ($null -eq $Message) { $Message = [string]::Empty } $sevMap=@{TRC=0;DBG=1;INF=2;WRN=3;ERR=4;FTL=5} if(-not $PSBoundParameters.ContainsKey('MinLevel')){ $gv=Get-Variable ConsoleLogMinLevel -Scope Global -ErrorAction SilentlyContinue $MinLevel=if($gv -and $gv.Value -and -not [string]::IsNullOrEmpty([string]$gv.Value)){[string]$gv.Value}else{'INF'} } $lvl=$Level.ToUpperInvariant() $min=$MinLevel.ToUpperInvariant() $sev=$sevMap[$lvl];if($null -eq $sev){$lvl='INF';$sev=$sevMap['INF']} $gate=$sevMap[$min];if($null -eq $gate){$min='INF';$gate=$sevMap['INF']} if($sev -ge 4 -and $sev -lt $gate -and $gate -ge 4){$lvl=$min;$sev=$gate} if($sev -lt $gate){return} $ts=[DateTime]::UtcNow.ToString('yy-MM-dd HH:mm:ss.ff') $stack=Get-PSCallStack ; $helperName=$MyInvocation.MyCommand.Name ; $helperScript=$MyInvocation.MyCommand.ScriptBlock.File ; $caller=$null if($stack){ # 1: prefer first non-underscore function not defined in the helper's own file for($i=0;$i -lt $stack.Count;$i++){ $f=$stack[$i];$fn=$f.FunctionName;$sn=$f.ScriptName if($fn -and $fn -ne $helperName -and -not $fn.StartsWith('_') -and (-not $helperScript -or -not $sn -or $sn -ne $helperScript)){$caller=$f;break} } # 2: fallback to first non-underscore function (any file) if(-not $caller){ for($i=0;$i -lt $stack.Count;$i++){ $f=$stack[$i];$fn=$f.FunctionName if($fn -and $fn -ne $helperName -and -not $fn.StartsWith('_')){$caller=$f;break} } } # 3: fallback to first non-helper frame not from helper's own file if(-not $caller){ for($i=0;$i -lt $stack.Count;$i++){ $f=$stack[$i];$fn=$f.FunctionName;$sn=$f.ScriptName if($fn -and $fn -ne $helperName -and (-not $helperScript -or -not $sn -or $sn -ne $helperScript)){$caller=$f;break} } } # 4: final fallback to first non-helper frame if(-not $caller){ for($i=0;$i -lt $stack.Count;$i++){ $f=$stack[$i];$fn=$f.FunctionName if($fn -and $fn -ne $helperName){$caller=$f;break} } } } if(-not $caller){$caller=[pscustomobject]@{ScriptName=$PSCommandPath;FunctionName=$null}} $lineNumber=$null ; $p=$caller.PSObject.Properties['ScriptLineNumber'];if($p -and $p.Value){$lineNumber=[string]$p.Value} if(-not $lineNumber){ $p=$caller.PSObject.Properties['Position'] if($p -and $p.Value){ $sp=$p.Value.PSObject.Properties['StartLineNumber'];if($sp -and $sp.Value){$lineNumber=[string]$sp.Value} } } if(-not $lineNumber){ $p=$caller.PSObject.Properties['Location'] if($p -and $p.Value){ $m=[regex]::Match([string]$p.Value,':(\d+)\s+char:','IgnoreCase');if($m.Success -and $m.Groups.Count -gt 1){$lineNumber=$m.Groups[1].Value} } } $file=if($caller.ScriptName){Split-Path -Leaf $caller.ScriptName}else{'cmd'} if($file -ne 'console' -and $lineNumber){$file="{0}:{1}" -f $file,$lineNumber} $prefix="[$ts " $suffix="] [$file] $Message" $cfg=@{TRC=@{Fore='DarkGray';Back=$null};DBG=@{Fore='Cyan';Back=$null};INF=@{Fore='Green';Back=$null};WRN=@{Fore='Yellow';Back=$null};ERR=@{Fore='Red';Back=$null};FTL=@{Fore='Red';Back='DarkRed'}}[$lvl] $fore=$cfg.Fore $back=$cfg.Back $isInteractive = [System.Environment]::UserInteractive if($isInteractive -and ($fore -or $back)){ Write-Host -NoNewline $prefix if($fore -and $back){Write-Host -NoNewline $lvl -ForegroundColor $fore -BackgroundColor $back} elseif($fore){Write-Host -NoNewline $lvl -ForegroundColor $fore} elseif($back){Write-Host -NoNewline $lvl -BackgroundColor $back} Write-Host $suffix } else { Write-Host "$prefix$lvl$suffix" } if($sev -ge 4 -and $ErrorActionPreference -eq 'Stop'){throw ("ConsoleLog.{0}: {1}" -f $lvl,$Message)} } _Write-StandardMessage -Message '--- New-NetworkDriveMapping: create or refresh a network drive mapping ---' # Ensure the function runs only on Windows $platform = [System.Environment]::OSVersion.Platform if ($platform -ne [System.PlatformID]::Win32NT) { _Write-StandardMessage -Message '[ERR] New-NetworkDriveMapping is only supported on Windows.' -Level 'ERR' throw 'New-NetworkDriveMapping is only supported on Windows.' } $normalizedLetter = $DriveLetter.TrimEnd(':') $driveRoot = ($normalizedLetter + ':').ToUpperInvariant() $uncPath = $RemotePath # Create WScript.Network COM object (Explorer-visible mapping) try { $wscriptNetwork = New-Object -ComObject WScript.Network } catch { $errMsg = '[ERR] Failed to create WScript.Network COM object. Verify that Windows Script Host is enabled.' _Write-StandardMessage -Message $errMsg -Level 'ERR' throw $errMsg } # Read current SMB mapping if the cmdlets are available $currentMapping = $null $getSmbMappingCmd = Get-Command -Name Get-SmbMapping -ErrorAction SilentlyContinue if ($null -ne $getSmbMappingCmd) { try { $currentMapping = Get-SmbMapping -LocalPath $driveRoot -ErrorAction SilentlyContinue } catch { _Write-StandardMessage -Message '[WRN] Get-SmbMapping failed. Continuing without SMB mapping cleanup.' -Level 'WRN' } } else { _Write-StandardMessage -Message '[WRN] Get-SmbMapping not available. Skipping SMB mapping cleanup.' -Level 'WRN' } # Remove COM / profile mapping (if any) try { _Write-StandardMessage -Message "[STATUS] Removing existing COM mapping for $driveRoot (if present)." -Level 'INF' $wscriptNetwork.RemoveNetworkDrive($driveRoot, $true, $true) } catch { _Write-StandardMessage -Message "[WRN] RemoveNetworkDrive reported a problem. The mapping may not have existed." -Level 'WRN' } # Remove SMB mapping (if present) if ($null -ne $currentMapping) { try { _Write-StandardMessage -Message "[STATUS] Removing existing SMB mapping for $driveRoot." -Level 'INF' $currentMapping | Remove-SmbMapping -Force -ErrorAction SilentlyContinue } catch { _Write-StandardMessage -Message '[WRN] Remove-SmbMapping reported a problem. The SMB mapping may already be gone.' -Level 'WRN' } } # Remove HKCU:\Network\<DriveLetter> $networkKeyPath = 'HKCU:\Network\{0}' -f $normalizedLetter if (Test-Path -LiteralPath $networkKeyPath) { try { _Write-StandardMessage -Message "[STATUS] Removing registry key $networkKeyPath." -Level 'INF' Remove-Item -LiteralPath $networkKeyPath -Recurse -Force -ErrorAction Stop } catch { $err = "[ERR] Failed to remove registry key $networkKeyPath." _Write-StandardMessage -Message $err -Level 'ERR' throw $err } } # Clean MountPoints2 entries related to this drive and UNC $mountPointsBase = 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\MountPoints2' $oldSanitized = $null if ($null -ne $currentMapping -and $null -ne $currentMapping.RemotePath) { $oldSanitized = '##' + $currentMapping.RemotePath.TrimStart('\').Replace('\', '#') } $newSanitized = '##' + $uncPath.TrimStart('\').Replace('\', '#') if (Test-Path -LiteralPath $mountPointsBase) { try { _Write-StandardMessage -Message '[STATUS] Cleaning MountPoints2 entries for the drive and UNC path.' -Level 'INF' Get-ChildItem -LiteralPath $mountPointsBase -ErrorAction SilentlyContinue | ForEach-Object { $childName = $_.PSChildName $matchDriveKey = ($childName -eq $driveRoot) -or ($childName -eq $normalizedLetter) $matchOld = $false if ($null -ne $oldSanitized) { if ($childName -like ($oldSanitized + '*')) { $matchOld = $true } } $matchNew = $false if ($childName -like ($newSanitized + '*')) { $matchNew = $true } if ($matchDriveKey -or $matchOld -or $matchNew) { try { Remove-Item -LiteralPath $_.PSPath -Recurse -Force -ErrorAction Stop } catch { _Write-StandardMessage -Message "[WRN] Failed to remove MountPoints2 entry $childName." -Level 'WRN' } } } } catch { _Write-StandardMessage -Message '[WRN] Failed while enumerating MountPoints2 entries.' -Level 'WRN' } } # Map new drive via COM (Explorer-compatible mapping) try { $persistentFlag = $true if ($NonPersistent.IsPresent) { $persistentFlag = $false } _Write-StandardMessage -Message "[STATUS] Creating new network drive mapping $driveRoot -> $uncPath." -Level 'INF' $wscriptNetwork.MapNetworkDrive($driveRoot, $uncPath, $persistentFlag) } catch { $err = "[ERR] Failed to create network drive mapping $driveRoot -> $uncPath." _Write-StandardMessage -Message $err -Level 'ERR' throw $err } # Optional: force the visible label in Explorer to match the UNC try { $shellApplication = New-Object -ComObject Shell.Application $folder = $shellApplication.NameSpace($driveRoot + '\') if ($null -ne $folder -and $null -ne $folder.Self) { $folder.Self.Name = $uncPath } } catch { _Write-StandardMessage -Message '[WRN] Failed to update the Explorer folder label. Mapping itself is still valid.' -Level 'WRN' } # Notify Explorer to refresh drive info try { $explorerRefreshType = 'ExplorerRefresh' -as [type] if ($null -eq $explorerRefreshType) { Add-Type @' using System; using System.Runtime.InteropServices; public static class ExplorerRefresh { const uint SHCNE_ASSOCCHANGED = 0x08000000; const uint SHCNE_UPDATEDIR = 0x00001000; const uint SHCNF_IDLIST = 0x0000; const uint SHCNF_PATHW = 0x0005; [DllImport("shell32.dll", CharSet = CharSet.Unicode)] public static extern void SHChangeNotify(uint wEventId, uint uFlags, string dwItem1, string dwItem2); public static void RefreshDrive(string path) { SHChangeNotify(SHCNE_ASSOCCHANGED, SHCNF_IDLIST, null, null); if (!string.IsNullOrEmpty(path)) { SHChangeNotify(SHCNE_UPDATEDIR, SHCNF_PATHW, path, null); } } } '@ $explorerRefreshType = 'ExplorerRefresh' -as [type] } if ($null -ne $explorerRefreshType) { [ExplorerRefresh]::RefreshDrive($driveRoot + '\') _Write-StandardMessage -Message "[OK] Explorer has been notified about $driveRoot." -Level 'INF' } } catch { _Write-StandardMessage -Message '[WRN] Failed to send a refresh notification to Explorer.' -Level 'WRN' } } function Remove-NetworkDriveMapping { <# .SYNOPSIS Removes a network drive mapping and cleans related Explorer cache. .DESCRIPTION Removes the mapping for the specified drive letter using WScript.Network and SMB (when available), deletes HKCU:\Network\<DriveLetter> and related MountPoints2 entries, then notifies Explorer so that the user interface is updated. .PARAMETER DriveLetter Single drive letter without colon (for example Z). .EXAMPLE Remove-NetworkDriveMapping -DriveLetter Z .NOTES This function is supported only on Windows. On non-Windows platforms it will throw a terminating error before attempting any Windows-specific operations. #> [CmdletBinding()] param( [Parameter(Mandatory=$true)] [ValidatePattern('^[A-Za-z]$')] [string]$DriveLetter ) function local:_Write-StandardMessage { [Diagnostics.CodeAnalysis.SuppressMessage("PSUseApprovedVerbs","")] param( [Parameter(Mandatory=$true)][AllowEmptyString()][string]$Message, [Parameter()][ValidateSet('TRC','DBG','INF','WRN','ERR','FTL')][string]$Level='INF', [Parameter()][ValidateSet('TRC','DBG','INF','WRN','ERR','FTL')][string]$MinLevel ) if ($null -eq $Message) { $Message = [string]::Empty } $sevMap=@{TRC=0;DBG=1;INF=2;WRN=3;ERR=4;FTL=5} if(-not $PSBoundParameters.ContainsKey('MinLevel')){ $gv=Get-Variable ConsoleLogMinLevel -Scope Global -ErrorAction SilentlyContinue $MinLevel=if($gv -and $gv.Value -and -not [string]::IsNullOrEmpty([string]$gv.Value)){[string]$gv.Value}else{'INF'} } $lvl=$Level.ToUpperInvariant() $min=$MinLevel.ToUpperInvariant() $sev=$sevMap[$lvl];if($null -eq $sev){$lvl='INF';$sev=$sevMap['INF']} $gate=$sevMap[$min];if($null -eq $gate){$min='INF';$gate=$sevMap['INF']} if($sev -ge 4 -and $sev -lt $gate -and $gate -ge 4){$lvl=$min;$sev=$gate} if($sev -lt $gate){return} $ts=[DateTime]::UtcNow.ToString('yy-MM-dd HH:mm:ss.ff') $stack=Get-PSCallStack ; $helperName=$MyInvocation.MyCommand.Name ; $helperScript=$MyInvocation.MyCommand.ScriptBlock.File ; $caller=$null if($stack){ # 1: prefer first non-underscore function not defined in the helper's own file for($i=0;$i -lt $stack.Count;$i++){ $f=$stack[$i];$fn=$f.FunctionName;$sn=$f.ScriptName if($fn -and $fn -ne $helperName -and -not $fn.StartsWith('_') -and (-not $helperScript -or -not $sn -or $sn -ne $helperScript)){$caller=$f;break} } # 2: fallback to first non-underscore function (any file) if(-not $caller){ for($i=0;$i -lt $stack.Count;$i++){ $f=$stack[$i];$fn=$f.FunctionName if($fn -and $fn -ne $helperName -and -not $fn.StartsWith('_')){$caller=$f;break} } } # 3: fallback to first non-helper frame not from helper's own file if(-not $caller){ for($i=0;$i -lt $stack.Count;$i++){ $f=$stack[$i];$fn=$f.FunctionName;$sn=$f.ScriptName if($fn -and $fn -ne $helperName -and (-not $helperScript -or -not $sn -or $sn -ne $helperScript)){$caller=$f;break} } } # 4: final fallback to first non-helper frame if(-not $caller){ for($i=0;$i -lt $stack.Count;$i++){ $f=$stack[$i];$fn=$f.FunctionName if($fn -and $fn -ne $helperName){$caller=$f;break} } } } if(-not $caller){$caller=[pscustomobject]@{ScriptName=$PSCommandPath;FunctionName=$null}} $lineNumber=$null ; $p=$caller.PSObject.Properties['ScriptLineNumber'];if($p -and $p.Value){$lineNumber=[string]$p.Value} if(-not $lineNumber){ $p=$caller.PSObject.Properties['Position'] if($p -and $p.Value){ $sp=$p.Value.PSObject.Properties['StartLineNumber'];if($sp -and $sp.Value){$lineNumber=[string]$sp.Value} } } if(-not $lineNumber){ $p=$caller.PSObject.Properties['Location'] if($p -and $p.Value){ $m=[regex]::Match([string]$p.Value,':(\d+)\s+char:','IgnoreCase');if($m.Success -and $m.Groups.Count -gt 1){$lineNumber=$m.Groups[1].Value} } } $file=if($caller.ScriptName){Split-Path -Leaf $caller.ScriptName}else{'cmd'} if($file -ne 'console' -and $lineNumber){$file="{0}:{1}" -f $file,$lineNumber} $prefix="[$ts " $suffix="] [$file] $Message" $cfg=@{TRC=@{Fore='DarkGray';Back=$null};DBG=@{Fore='Cyan';Back=$null};INF=@{Fore='Green';Back=$null};WRN=@{Fore='Yellow';Back=$null};ERR=@{Fore='Red';Back=$null};FTL=@{Fore='Red';Back='DarkRed'}}[$lvl] $fore=$cfg.Fore $back=$cfg.Back $isInteractive = [System.Environment]::UserInteractive if($isInteractive -and ($fore -or $back)){ Write-Host -NoNewline $prefix if($fore -and $back){Write-Host -NoNewline $lvl -ForegroundColor $fore -BackgroundColor $back} elseif($fore){Write-Host -NoNewline $lvl -ForegroundColor $fore} elseif($back){Write-Host -NoNewline $lvl -BackgroundColor $back} Write-Host $suffix } else { Write-Host "$prefix$lvl$suffix" } if($sev -ge 4 -and $ErrorActionPreference -eq 'Stop'){throw ("ConsoleLog.{0}: {1}" -f $lvl,$Message)} } _Write-StandardMessage -Message '--- Remove-NetworkDriveMapping: remove a network drive mapping ---' # Ensure the function runs only on Windows $platform = [System.Environment]::OSVersion.Platform if ($platform -ne [System.PlatformID]::Win32NT) { _Write-StandardMessage -Message '[ERR] Remove-NetworkDriveMapping is only supported on Windows.' -Level 'ERR' throw 'Remove-NetworkDriveMapping is only supported on Windows.' } $normalizedLetter = $DriveLetter.TrimEnd(':') $driveRoot = ($normalizedLetter + ':').ToUpperInvariant() # Create WScript.Network COM object (Explorer-visible mapping) try { $wscriptNetwork = New-Object -ComObject WScript.Network } catch { $errMsg = '[ERR] Failed to create WScript.Network COM object. Verify that Windows Script Host is enabled.' _Write-StandardMessage -Message $errMsg -Level 'ERR' throw $errMsg } # Read current SMB mapping if the cmdlets are available $currentMapping = $null $getSmbMappingCmd = Get-Command -Name Get-SmbMapping -ErrorAction SilentlyContinue if ($null -ne $getSmbMappingCmd) { try { $currentMapping = Get-SmbMapping -LocalPath $driveRoot -ErrorAction SilentlyContinue } catch { _Write-StandardMessage -Message '[WRN] Get-SmbMapping failed. Continuing without SMB mapping cleanup.' -Level 'WRN' } } else { _Write-StandardMessage -Message '[WRN] Get-SmbMapping not available. Skipping SMB mapping cleanup.' -Level 'WRN' } # Remove COM / profile mapping (if any) try { _Write-StandardMessage -Message "[STATUS] Removing existing COM mapping for $driveRoot (if present)." -Level 'INF' $wscriptNetwork.RemoveNetworkDrive($driveRoot, $true, $true) } catch { _Write-StandardMessage -Message "[WRN] RemoveNetworkDrive reported a problem. The mapping may not have existed." -Level 'WRN' } # Remove SMB mapping (if present) if ($null -ne $currentMapping) { try { _Write-StandardMessage -Message "[STATUS] Removing existing SMB mapping for $driveRoot." -Level 'INF' $currentMapping | Remove-SmbMapping -Force -ErrorAction SilentlyContinue } catch { _Write-StandardMessage -Message '[WRN] Remove-SmbMapping reported a problem. The SMB mapping may already be gone.' -Level 'WRN' } } # Remove HKCU:\Network\<DriveLetter> $networkKeyPath = 'HKCU:\Network\{0}' -f $normalizedLetter if (Test-Path -LiteralPath $networkKeyPath) { try { _Write-StandardMessage -Message "[STATUS] Removing registry key $networkKeyPath." -Level 'INF' Remove-Item -LiteralPath $networkKeyPath -Recurse -Force -ErrorAction Stop } catch { $err = "[ERR] Failed to remove registry key $networkKeyPath." _Write-StandardMessage -Message $err -Level 'ERR' throw $err } } # Clean MountPoints2 entries related to this drive and previous UNC $mountPointsBase = 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\MountPoints2' $oldSanitized = $null if ($null -ne $currentMapping -and $null -ne $currentMapping.RemotePath) { $oldSanitized = '##' + $currentMapping.RemotePath.TrimStart('\').Replace('\', '#') } if (Test-Path -LiteralPath $mountPointsBase) { try { _Write-StandardMessage -Message '[STATUS] Cleaning MountPoints2 entries for the drive and previous UNC path.' -Level 'INF' Get-ChildItem -LiteralPath $mountPointsBase -ErrorAction SilentlyContinue | ForEach-Object { $childName = $_.PSChildName $matchDriveKey = ($childName -eq $driveRoot) -or ($childName -eq $normalizedLetter) $matchOld = $false if ($null -ne $oldSanitized) { if ($childName -like ($oldSanitized + '*')) { $matchOld = $true } } if ($matchDriveKey -or $matchOld) { try { Remove-Item -LiteralPath $_.PSPath -Recurse -Force -ErrorAction Stop } catch { _Write-StandardMessage -Message "[WRN] Failed to remove MountPoints2 entry $childName." -Level 'WRN' } } } } catch { _Write-StandardMessage -Message '[WRN] Failed while enumerating MountPoints2 entries.' -Level 'WRN' } } # Notify Explorer to refresh drive info try { $explorerRefreshType = 'ExplorerRefresh' -as [type] if ($null -eq $explorerRefreshType) { Add-Type @' using System; using System.Runtime.InteropServices; public static class ExplorerRefresh { const uint SHCNE_ASSOCCHANGED = 0x08000000; const uint SHCNE_UPDATEDIR = 0x00001000; const uint SHCNF_IDLIST = 0x0000; const uint SHCNF_PATHW = 0x0005; [DllImport("shell32.dll", CharSet = CharSet.Unicode)] public static extern void SHChangeNotify(uint wEventId, uint uFlags, string dwItem1, string dwItem2); public static void RefreshDrive(string path) { SHChangeNotify(SHCNE_ASSOCCHANGED, SHCNF_IDLIST, null, null); if (!string.IsNullOrEmpty(path)) { SHChangeNotify(SHCNE_UPDATEDIR, SHCNF_PATHW, path, null); } } } '@ $explorerRefreshType = 'ExplorerRefresh' -as [type] } if ($null -ne $explorerRefreshType) { [ExplorerRefresh]::RefreshDrive($driveRoot + '\') _Write-StandardMessage -Message "[OK] Explorer has been notified about $driveRoot." -Level 'INF' } } catch { _Write-StandardMessage -Message '[WRN] Failed to send a refresh notification to Explorer.' -Level 'WRN' } } |