Get size of each folder inside a function

I am trying to obtain the size of each folder and its sub-folders along with the owner, path, and last modified date - also up to a depth of 5. I have everything except for the size of the folder completed; I am trying to get the size in MB. I am using PowerShell 4.

The structure I am trying to obtain:

 Date Modified Owner FullName                                            Size                                                                                                     
 ------------- ----- --------                                            ------                                                                
06/26/2017          /Users/demo/main/1slevel                             12.0MB
06/26/2017          /Users/demo/main/1slevel/2nlvel                       8.0MB
06/26/2017          /Users/demo/main/1slevel/2nlvel/3rdlvel               5.0MB
06/26/2017          /Users/demo/main/1slevel/2nlvel/3rdlvel/4thlev
06/26/2017          /Users/demo/main/1slevel/2nlvel/3rdlvel/4thlev/5thlevl
06/26/2017          /Users/demo/main/uns
06/26/2017          /Users/demo/main/uns/swan
06/26/2017          /Users/demo/main/uns/swan/drins
06/26/2017          /Users/demo/main/uns/swan/drins/furth
06/26/2017          /Users/demo/main/uns/swan/drins/furth/firf

The Code I have:

Function Get-Depth {
   Param(
        [String]$Path = '/Users/demo/main',
        [String]$Filter = "*",
        [Int]$ToDepth = 4,
        [Int]$CurrentDepth = 0
    )
    #incrimintation
    $CurrentDepth++

  #obtains the path and passes the filter values. KEEP in mind that level 1 is 0.
     Get-ChildItem $Path | %{
     $_ | ?{ $_.Name -Like $Filter }
     #if thier is a folder, use the depth and run function until to depth value is 4
     If ($_.PsIsContainer) {
     If ($CurrentDepth -le $ToDepth) {

       # Call to function
       #adds the filter values and depth to the path..
       Get-Depth -Path $_.FullName -Filter $Filter `
        -ToDepth $ToDepth -CurrentDepth $CurrentDepth
           }
         }
      }

  }


#just calling the function and and adding what we want!

Get-Depth|? {$_.PsIsContainer}| select @{Name='Date Modified'; 
Expression={$_.LastWriteTime.ToString('MM/dd/yyyy')}},
@{Name='Owner'; E={(($_.GetAccessControl().Owner.Split('\'))[1])}}, Fullname 

Thank you!

Folders don’t have a size; you’d need to get all the files within and add up their Length properties. Measure-Object can help with that.

@Don Jones, Thank you for the prompt reply. According to this article from MS (https://technet.microsoft.com/en-us/library/ff730945.aspx) It appear that I can obtain the size of a folder? Am I misinterpreting something? Thank you!

What that’s doing is getting all the files and adding up their Length property. Directories don’t have a Length property, because they don’t have a size in and of themselves.

@Dan Jones Thank you for input, after doing some research, I found this script from the script guys (Browse code samples | Microsoft Docs)
After doing some sample runs, it appears to do exactly what you say before (Adds lengths of all files), however; this script only lists out the first level directory folders and does not list out the size of its subfolders with fullpath, etc - is there anyway to expand this scripts functionality to list out more than the first level folders? (essentially trying to get the structure originally posted in the question)
Thank you!

# Get-DirStats.ps1
# Written by Bill Stewart (bstewart@iname.com)
# Outputs file system directory statistics.

#requires -version 2



[CmdletBinding(DefaultParameterSetName="Path")]
param(
  [parameter(Position=0,Mandatory=$false,ParameterSetName="Path",ValueFromPipeline=$true)]
    $Path='/Users/demo/main',
  [parameter(Position=0,Mandatory=$true,ParameterSetName="LiteralPath")]
    [String[]] $LiteralPath,
    [Switch] $Only,
    [Switch] $Every,
    [Switch] $FormatNumbers,
    [Switch] $Total
)

begin {
  $ParamSetName = $PSCmdlet.ParameterSetName
  if ( $ParamSetName -eq "Path" ) {
    $PipelineInput = ( -not $PSBoundParameters.ContainsKey("Path") ) -and ( -not $Path )
  }
  elseif ( $ParamSetName -eq "LiteralPath" ) {
    $PipelineInput = $false
  }

  # Script-level variables used with -Total.
  [UInt64] $script:totalcount = 0
  [UInt64] $script:totalbytes = 0

  # Returns a [System.IO.DirectoryInfo] object if it exists.
  function Get-Directory {
    param( $item )

    if ( $ParamSetName -eq "Path" ) {
      if ( Test-Path -Path $item -PathType Container ) {
        $item = Get-Item -Path $item -Force
      }
    }
    elseif ( $ParamSetName -eq "LiteralPath" ) {
      if ( Test-Path -LiteralPath $item -PathType Container ) {
        $item = Get-Item -LiteralPath $item -Force
      }
    }
    if ( $item -and ($item -is [System.IO.DirectoryInfo]) ) {
      return $item
    }
  }

  # Filter that outputs the custom object with formatted numbers.
  function Format-Output {
    process {
      $_ | Select-Object Path,
        @{Name="Files"; Expression={"{0:N0}" -f $_.Files}},
        @{Name="Size"; Expression={"{0:N0}" -f $_.Size}}
    }
  }

  # Outputs directory statistics for the specified directory. With -recurse,
  # the function includes files in all subdirectories of the specified
  # directory. With -format, numbers in the output objects are formatted with
  # the Format-Output filter.
  function Get-DirectoryStats {
    param( $directory, $recurse, $format )

    Write-Progress -Activity "Get-DirStats.ps1" -Status "Reading '$($directory.FullName)'"
    $files = $directory | Get-ChildItem -Force -Recurse:$recurse | Where-Object { -not $_.PSIsContainer }
    if ( $files ) {
      Write-Progress -Activity "Get-DirStats.ps1" -Status "Calculating '$($directory.FullName)'"
      $output = $files | Measure-Object -Sum -Property Length | Select-Object `
        @{Name="Path"; Expression={$directory.FullName}},
        @{Name="Files"; Expression={$_.Count; $script:totalcount += $_.Count}},
        @{Name="Size"; Expression={$_.Sum; $script:totalbytes += $_.Sum}}
    }
    else {
      $output = "" | Select-Object `
        @{Name="Path"; Expression={$directory.FullName}},
        @{Name="Files"; Expression={0}},
        @{Name="Size"; Expression={0}}
    }
    if ( -not $format ) { $output } else { $output | Format-Output }
  }
}

process {
  # Get the item to process, no matter whether the input comes from the
  # pipeline or not.
  if ( $PipelineInput ) {
    $item = $_
  }
  else {
    if ( $ParamSetName -eq "Path" ) {
      $item = $Path
    }
    elseif ( $ParamSetName -eq "LiteralPath" ) {
      $item = $LiteralPath
    }
  }

  # Write an error if the item is not a directory in the file system.
  $directory = Get-Directory -item $item
  if ( -not $directory ) {
    Write-Error -Message "Path '$item' is not a directory in the file system." -Category InvalidType
    return
  }

  # Get the statistics for the first-level directory.
  Get-DirectoryStats -directory $directory -recurse:$false -format:$FormatNumbers
  # -Only means no further processing past the first-level directory.
  if ( $Only ) { return }

  # Get the subdirectories of the first-level directory and get the statistics
  # for each of them.
  $directory | Get-ChildItem -Force -Recurse:$Every |
    Where-Object { $_.PSIsContainer } | ForEach-Object {
      Get-DirectoryStats -directory $_ -recurse:(-not $Every) -format:$FormatNumbers
    }
}

end {
  # If -Total specified, output summary object.
  if ( $Total ) {
    $output = "" | Select-Object `
      @{Name="Path"; Expression={""}},
      @{Name="Files"; Expression={$script:totalcount}},
      @{Name="Size"; Expression={$script:totalbytes}}
    if ( -not $FormatNumbers ) { $output } else { $output | Format-Output }
  }
}


 

Your process is:

  1. use Get-ChildItem and -recurse and -directory to get a list of JUST THE DIRECTORIES.

  2. ForEach directory, Get-ChildItem all the files and add up their Length, usually using Measure-Object.

  3. Construct an output object containing the directory full name and the total length for that directory.

You don’t need anything nearly as complex as what Bill wrote.

Get-ChildItem -Path /start/here -Recurse -Directory |
ForEach-Object {
 $size = Get-ChildItem -Path $_.FullName | Measure -Prop Length -Sum | Select -Expand Sum
 New-Object -Type PSObject -Prop @{'Path'=$_.FullName;'Size'=$size}
}

Something vaguely like that, I think. That’ll give you bytes; you’ll have to do whatever math you want if you want MB.

What Don provided will work for getting the total sum of all files in each directory at each level, but it will not the the size of the files and all sub directories.

If it were me, I would:

  1. Do a recursive search for all files from my root directory
  2. Group those by directory
  3. For each grouping get the directory name and the sum of all lengths (files in those directories)
  4. Store results in a variable
    *At this point you will have the same as what Don provided
  5. For each directory in the variable, Get all other directories in the variable who’s name begins with the same directory name
  6. Output the directory name being searched for and a sum of the length of the results
    *At this point every parent directory will contain the size of all file in it and in all sub directories

Again you don’t need anything near as complex as what Bill wrote.
(Hint: I was able to do the above in 15 lines of code, or 2 one liners if I didn’t care about readability :^)

Directory                                                     Size Size (KB) Size (MB) Size (GB)
---------                                                     ---- --------- --------- ---------
C:\Temp                                                  352320814 344063.29       336      0.33
C:\Temp\CheckLCStatus                                    140497837 137204.92    133.99      0.13
C:\Temp\CheckLCStatus\prerequisites\.NET Framework 4.5.2  69999448  68358.84     66.76      0.07
C:\Temp\CheckLCStatus\prerequisites\VS2013 vcredist x86    6503984   6351.55       6.2      0.01
C:\Temp\Old                                                9191936    8976.5      8.77      0.01
C:\Temp\SDelete                                             324234    316.63      0.31         0
C:\Temp\VF                                                       0         0         0         0

Give it a shot and see what you can come up with.

@Curtis Smith , @Don Jones

Thank you for your input.
I don’t believe I need that much detail just yet, I am still trying to get the size to work with my function.
Can you take a look at what I have:

 Function Get-Depth {
    Param(
        [String]$Path = 'H:\Demo',
        [String]$Filter = "*",
        [Int]$ToDepth = 5,
        [Int]$CurrentDepth = 0
    )
    #incrimintation
    $CurrentDepth++

#obtains the path and passes the filter values. KEEP in mind that level 1 is 0.
    Get-ChildItem $Path | %{
        $_ | ?{ $_.Name -Like $Filter }
#if thier is a folder, use the depth and run function until to depth value is 4
        If ($_.PsIsContainer) {
            If ($CurrentDepth -le $ToDepth) {

                # Call to function
                #adds the filter values and depth to the path..
                Get-Depth -Path $_.FullName -Filter $Filter `
                  -ToDepth $ToDepth -CurrentDepth $CurrentDepth
            }

     (Get-ChildItem -Path 'H:\Demo' -Directory -Recurse| ForEach-Object {
     $size= (Get-ChildItem $_.FullName -File -Recurse| Measure-Object -property length -sum).Sum
     if ($size) {[Math]::Round($size / 1MB,2,[MidPointRounding]::AwayFromZero)} else {0}
})

        }
    }
}


#just calling the function and and adding what we want!
Get-Depth|? {$_.PsIsContainer}| select @{Name='Date Modified'; Expression={$_.LastWriteTime.ToString('MM/dd/yyyy')}}, @{Name='Owner'; E={(($_.GetAccessControl().Owner.Split('\'))[1])}}, @{Name='File Path'; Expression={$_.FullName -replace '.*?(\\Demo*)','$1'}},size 

Thank you!

I assume you mean this section of your code

    (Get-ChildItem -Path 'H:\Demo' -Directory -Recurse| ForEach-Object {
     $size= (Get-ChildItem $_.FullName -File -Recurse| Measure-Object -property length -sum).Sum
     if ($size) {[Math]::Round($size / 1MB,2,[MidPointRounding]::AwayFromZero)} else {0}
})

Besides being inefficient because it has to query the same files for their size over and over again, there is nothing wrong here. It works.

I added $_.FullName to the output just so I could see what directory it was on and compare to my version and the numbers are correct.

(Get-ChildItem -Path 'C:\Temp' -Directory -Recurse| ForEach-Object {
    $_.FullName
     $size= (Get-ChildItem $_.FullName -File -Recurse| Measure-Object -property length -sum).Sum
     if ($size) {[Math]::Round($size / 1MB,2,[MidPointRounding]::AwayFromZero)} else {0}
})

Results

C:\Temp\1
0
C:\Temp\CheckLCStatus
133.99
C:\Temp\Old
8.77
C:\Temp\SDelete
0.31
C:\Temp\VF
0
C:\Temp\CheckLCStatus\prerequisites
72.96
C:\Temp\CheckLCStatus\prerequisites\.NET Framework 4.5.2
66.76
C:\Temp\CheckLCStatus\prerequisites\VS2013 vcredist x86
6.2

I guess part of the problem is that you have this in your ForEach-Object loop, so it is doing this piece of code for every folder it finds. With my sample, it would output the above results 8 times because their are 8 folders causing the full output to be generated 8 times.

Additionally, I’m not sure what you are intending to do here

$_ | ?{ $_.Name -Like $Filter }

It appears that you are attempting to only include those objects that match your filter, but then you are not doing anything with the output. It is just being sent to the Default Output.

@Curtis Smith,

Thank you for that, however; I am trying to have the sizes displayed in a table format along with the rest of the data; I was looking into achieving this sort of structure:

Date Modified Owner File Path                                                              size
------------- ----- ---------                                                              ----
06/26/2017     jh   /Users/demo/main/1slevel                                               2.3MB
06/29/2017          /Users/demo/main/1slevel/2nlvel                                        5.0MB ...
06/26/2017          /Users/demo/main/1slevel/2nlvel/3rdlvel
06/29/2017          /Users/demo/main/1slevel/2nlvel/3rdlvel/4thlev
06/26/2017          /Users/demo/main/1slevel/2nlvel/3rdlvel/4thlev/5thlevl
06/29/2017          /Users/demo/main/1slevel/2nlvel/3rdlvel/4thlev/5thlevl/6thlvl
06/26/2017          /Users/demo/main/uns
06/29/2017          /Users/demo/main/uns/swan
06/29/2017          /Users/demo/main/uns/swan/drins
06/29/2017          /Users/demo/main/uns/swan/drins/furth
06/26/2017          /Users/demo/main/uns/swan/drins/furth/firf
06/26/2017          /Users/demo/main/uns/swan/drins/furth/firf/zexs
(Get-ChildItem -Path 'C:\Temp' -Directory -Recurse| ForEach-Object {
    [pscustomobject]@{
        "Date Modified" = $_.LastWriteTime
        "Owner" = $_.GetAccessControl().Owner
        "File Path" = $_.FullName
        "Size" = [Math]::Round((Get-ChildItem $_.FullName -File -Recurse| Measure-Object -property length -sum).Sum / 1MB,2,[MidPointRounding]::AwayFromZero)
    }
})

Results:

Date Modified         Owner                  File Path                                                  Size
-------------         -----                  ---------                                                  ----
6/22/2016 12:06:27 AM domain\user            C:\Temp\1                                                     0
5/30/2017 8:55:14 AM  domain\user            C:\Temp\CheckLCStatus                                    133.99
1/11/2016 10:46:27 PM domain\user            C:\Temp\Old                                                8.77
7/14/2016 9:55:52 AM  domain\user            C:\Temp\SDelete                                            0.31
12/5/2016 8:29:43 AM  BUILTIN\Administrators C:\Temp\VF                                                    0
5/30/2017 8:55:19 AM  domain\user            C:\Temp\CheckLCStatus\prerequisites                       72.96
5/30/2017 8:55:14 AM  domain\user            C:\Temp\CheckLCStatus\prerequisites\.NET Framework 4.5.2  66.76
5/30/2017 8:55:19 AM  domain\user            C:\Temp\CheckLCStatus\prerequisites\VS2013 vcredist x86     6.2

thank you!
I’ve made the adjustments you asked, and the script runs a lot faster now.

Out of curiosity, for a beginner user of PowerShell like me, what resources should I utilize in order to further my understanding of Powershell?

Thanks!

One that I hear mentioned in this forum a lot is “Learn Windows PowerShell in a Month of Lunches”, written by one of the co-founders of this very site Mr. Don Jones. (Second Edition is now available)

For myself, it was a matter of practice and challenge. I find it very useful to be active in a forum such as this one to see what challenges people are facing, and what types of projects they are working on. Then I see how I would solve their issue. Quite frequently I come across ideas or challenges I’ve never faced and my capabilities are expanded by solving the problems of others or seeing how someone else solved the problem. Personally, experience is the best teacher, but a good solid foundation can be found in good solid resources like the one mentioned above. There are also several eBooks available for free in this site that cover many aspects of PowerShell including some of the gotchyas you may run into. Definitely worth a review.

Just for giggles, here is my method of doing it, and since it only does the Get-ChildItem once, it should run quicker.

$Directories = Get-ChildItem -Path C:\Temp -Recurse |
Group-Object -Property Directory |
Where-Object {$_.Name} |
Select-Object Name,
              @{label = "Size"; Expression={$_.group.length | Measure-Object -Sum | Select-Object -ExpandProperty Sum}}

$Directories |
ForEach-Object {
    $directory = Get-Item -Path $_.name
    $totalsize = ($Directories | Where-Object {$_.Name -like "$directory*"} | Measure-Object -Property Size -Sum).Sum / 1mb
    [PSCustomObject]@{"Directory" = $directory.FullName
                      "Date Modified" = $directory.LastWriteTime
                      "Owner" = $directory.GetAccessControl().Owner
                      "Size" = [math]::Round($totalsize,2)
                     }
} | Sort-Object Directory | FT -AutoSize

Results:

Directory                                                Date Modified         Owner                    Size
---------                                                -------------         -----                    ----
C:\Temp                                                  6/28/2017 9:23:04 AM  domain\user                 336
C:\Temp\CheckLCStatus                                    5/30/2017 8:55:14 AM  domain\user              133.99
C:\Temp\CheckLCStatus\prerequisites\.NET Framework 4.5.2 5/30/2017 8:55:14 AM  domain\user               66.76
C:\Temp\CheckLCStatus\prerequisites\VS2013 vcredist x86  5/30/2017 8:55:19 AM  domain\user                 6.2
C:\Temp\Old                                              1/11/2016 10:46:27 PM domain\user                8.77
C:\Temp\SDelete                                          7/14/2016 9:55:52 AM  domain\user                0.31
C:\Temp\VF                                               12/5/2016 8:29:43 AM  BUILTIN\Administrators      0