4

On my target computer in PowerShell I run command $FolderSize =(Get-ChildItem "C:\Users\JDoe" -force -Recurse -ErrorAction SilentlyContinue | Measure-Object length -sum).sum

and I get a value of 0.76 gb, which accurately corresponds to the compressed size of the folder on disk. However, when I try to run the command on a remote computer using

$folderSize = Invoke-Command -ComputerName "computername" {(Get-ChildItem -Path "C:\Users\JDoe" -Recurse -force -ErrorAction SilentlyContinue | Measure-Object -Property Length -sum).sum} I get a different, MUCH larger number, 17 gb.

I tried running the first command in a pssession but still get the 17gb result. I also tried using

psexec \\\computername powershell "(Get-ChildItem "C:\Users\JDoe" -force -Recurse -ErrorAction SilentlyContinue | Measure-Object length -sum).sum" but still get the larger number.

I don't understand why the results obtained remotely are different than the actual size of the folder when I examine it locally. At least all the remote results are consistent, which tells me they are all measuring the same thing.

CrookedJ
  • 300
  • 1
  • 7
MusicMan
  • 65
  • 6

3 Answers3

1

This is due to a junction in AppData\Local named Application Data that points back to AppData\Local

It appears that you can access this junction remotely (even from explorer using \\COMPUTER01\C$\Users\JDoe\AppData\Local\Application Data) so this is why you're getting different sizes, as it's recursively counting the same stuff up to the MAX_PATH limit.

Compare the following command's outputs on remote vs local:
Get-ChildItem 'C:\Users\JDoe\AppData\Local\Application Data' -Force

Local

PS C:\> Get-ChildItem 'C:\Users\JDoe\AppData\Local\Application Data' -Force
Get-ChildItem : Access to the path 'C:\Users\JDoe\AppData\Local\Application Data' is denied.
At line:1 char:1
+ Get-ChildItem 'C:\Users\JDoe\AppData\Local\Application Data' -Forc ...
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo          : PermissionDenied: (C:\users\JDoe\A...plication Data\:String) [Get-ChildItem], UnauthorizedAccessException
+ FullyQualifiedErrorId : DirUnauthorizedAccessError,Microsoft.PowerShell.Commands.GetChildItemCommand

Remote

PS C:\> Invoke-Command -ComputerName COMPUTER01 -ScriptBlock { Get-ChildItem 'C:\Users\JDoe\AppData\Local\Application Data' -Force }


    Directory: C:\Users\JDoe\AppData\Local\Application Data


Mode                LastWriteTime         Length Name                        PSComputerName
----                -------------         ------ ----                        --------------
d--hsl        4/16/2020   4:46 PM                Application Data            COMPUTER01
d-----       10/31/2019   9:43 AM                ConnectedDevicesPlatform    COMPUTER01
d-----       10/31/2019   9:52 AM                ElevatedDiagnostics         COMPUTER01
d-----       10/31/2019   9:43 AM                Google                      COMPUTER01
d--hsl        4/16/2020   4:46 PM                History                     COMPUTER01
d-----        4/16/2020   4:50 PM                Microsoft                   COMPUTER01
d-----        9/16/2019   8:14 PM                Microsoft Help              COMPUTER01
d-----       10/31/2019   9:43 AM                MicrosoftEdge               COMPUTER01
d-----       10/31/2019   9:53 AM                OpenShell                   COMPUTER01
d-----        4/16/2020   4:47 PM                Packages                    COMPUTER01
d-----       10/31/2019   9:43 AM                PlaceholderTileLogoFolder   COMPUTER01
d-----       10/31/2019   9:43 AM                Publishers                  COMPUTER01
d-----        3/18/2019  11:52 PM                Temp                        COMPUTER01
d--hsl        4/16/2020   4:46 PM                Temporary Internet Files    COMPUTER01
d-----       10/31/2019   9:43 AM                VirtualStore                COMPUTER01

You will need to recurse separately from Get-ChildItem by using a recursive function like the one in this answer.

CrookedJ
  • 300
  • 1
  • 7
  • Interesting. Although I am familiar with symbolic links, I have never encountered a junction. So I need to exclude "Application Data". I will give that a try and see what results I get. Thank you! – MusicMan Aug 03 '20 at 17:07
  • I'm unaware of any others like this but you should be able to run it locally on other folders without suppressing errors and detect it pretty easily. (You'll get errors like `Could not find a part of the path 'C:\Users\JDoe\AppData\Local\Application Data\Application Data\Application Data\Application Data\Application Data\...` You're welcome and good luck! – CrookedJ Aug 03 '20 at 17:12
  • I have had a little success, I added "where {$_.attributes -notlike "*ReparsePoint*"}", but it only seems to exclude them from reporting the full name, it still is counting the size. Do I need to create a subroutine to exclude those folders with that attribute? – MusicMan Aug 03 '20 at 18:01
  • 1
    Excellent sleuthing; this discrepancy should be considered a bug; you can find all these hidden junctions, which exist for backward compatibility only, with the following command `cmd /c dir /s /ashld $env:USERPROFILE` (in the user's profile) or `cmd /c dir /s /ashld C:\` (entire C: drive). Background info: https://www.svrops.com/svrops/articles/jpoints.htm – mklement0 Aug 03 '20 at 18:27
  • Looks like the easiest way to filter them out is by using `Get-ChildItem -Attributes !ReparsePoint`. I will update my answer – CrookedJ Aug 03 '20 at 19:28
  • 1
    It's crazy, though, even with the exclusion it is still adding the byte count to the total regardless. I've tried running powershell dir command remotely, everything I do remotely follows junctions/symbolic links no matter what. it always runs through the network shell. I guess the only way around it would be to run something strictly local, such as through a scheduled task, unless I find some 3rd party application. I'll keep trying. Thank you! – MusicMan Aug 03 '20 at 19:29
  • Ah, the recursion still traverses down them but does not return them. It looks like you will need to use a recursive function, such as the one in [this answer](https://superuser.com/a/528499/970923) – CrookedJ Aug 03 '20 at 19:39
1

Thanks for all the suggestions! Ultimately, I opted to use Sysinternals "du" app and capture the output in a remote job, so as to minimize network traffic.

Thanks again!

MusicMan
  • 65
  • 6
0

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

}
mklement0
  • 382,024
  • 64
  • 607
  • 775