CrookedJ's helpful answer provides the crucial pointer:
Due to the network-type logons that PowerShell remoting uses - see this GitHub issue - PowerShell code that runs remotely unexpectedly has the required privileges to recurse into the hidden system junctions. However, these hidden junctions exist solely for backward compatibility with pre-Vista Windows versions and are not meant to be traversed themselves: they simply redirect to the current locations of the well-known folders they represent.
E.g., the hidden "$HOME\My Documents"
junction points to "$HOME\Documents"
- see this article for background information.
Locally executing code - even if run as admin - is by design not allowed to access the contents of these hidden junctions.
When you use Get-ChildItem -Recurse
:
Windows PowerShell reports access-denied errors while encountering these hidden junctions during recursive traversal, because it tries to recurse into them.
PowerShell [Core] v6+ more sensibly quietly skips these junctions during recursive traversal - both in local and remote execution, so your problem wouldn't arise. Generally, directory symlinks/junctions are not followed by default, unless -FollowSymlink
is specified; even then, however, no error occurs - only a warning is emitted for each hidden junction, in recognition that the redirected-to real directories have already been traversed.
In Windows PowerShell, your remotely executing code therefore counts the files in certain directories (at least) twice - once as the content of the hidden junction, and again in the actual directory pointed to.
Therefore, there are two potential solutions:
If the target machine has PowerShell [Core] v6+ installed and remoting enabled, target it with your remoting command (which you can do even when calling from Windows PowerShell:
- Simply add a
-ConfigurationName PowerShell.<majorVersion>
argument to your Invoke-Command
call, e.g., -ConfigurationName PowerShell.7
for PowerShell [Core] 7.x versions.
Otherwise - if you must target Windows PowerShell - you need a workaround, which in your case is to use a custom Get-ChildItem
variant that explicitly skips the hidden junctions during recursion:
# Note:
# * Hidden items other than the hidden junctions are invariably included.
# * (Other, non-system) directory reparse points are reported, but not recursed into.
# * Supports only a subset of Get-ChildItem functionality, notably NOT wildcard patterns
# and filters.
function Get-ChildItemExcludeHiddenJunctions {
[CmdletBinding(DefaultParameterSetName = 'Default')]
param(
[Parameter(ValueFromPipelineByPropertyName, Position = 0)] [Alias('lp', 'PSPath')]
[string] $LiteralPath,
[Parameter(ParameterSetName = 'DirsOnly')]
[switch] $Directory,
[Parameter(ParameterSetName = 'FilesOnly')]
[switch] $File,
[switch] $Recurse
)
# Get all child items except for the hidden junctions.
# Note: Due to the -Attributes filter, -Force is effectively implied.
# That is, hidden items other than hidden junctions are invariably included.
$htLitPathArg = if ($LiteralPath) { @{ LiteralPath = $LiteralPath } } else { @{ } }
$items = Get-ChildItem @htLitPathArg -Attributes !Directory, !Hidden, !System, !ReparsePoint
# Split into subdirs. and files.
$dirs, $files = $items.Where( { $_.PSIsContainer }, 'Split')
# Output the child items of interest on this level.
if (-not $File) { $dirs }
if (-not $Directory) { $files }
# Recurse on subdirs., if requested
if ($Recurse) {
$PSBoundParameters.Remove('LiteralPath')
foreach ($dir in $dirs) {
if ($dir.Target) { continue } # Don't recurse into (other, non-system) directory reparse points.
Get-ChildItemExcludeHiddenJunctions -LiteralPath $dir.FullName @PSBoundParameters
}
}
}
To use this function in a remote script block, you'll (also) have to define it there:
# Assuming the Get-ChildItemExcludeHiddenJunctions function is already defined as above:
# Get a string representation of its definition (function body).
$funcDef = "${function:Get-ChildItemExcludeHiddenJunctions}"
$folderSize = Invoke-Command -ComputerName "computername" {
# Define the Get-ChildItemExcludeHiddenJunctions function in the remote sesson.
${function:get-ChildItemExcludeHiddenJunctions} = $using:funcDef
(Get-ChildItemExcludeHiddenJunctions "C:\Users\JDoe" -Recurse -File |
Measure-Object -Property Length -Sum).Sum
}