Idera nSoftware Compellent

Cleaning Hard Drives and Finding Folders Total Size

Hard drives these days are huge, and still they run out of space all too soon. So maybe you'd like to find out where the storage hogs are located and which folder wastes the most space.

Or, you need to manage user profiles and would like to find out just how much space each consumes. Here is how.

Calculating a Folders' Total Size

Surprisingly, this isn't as straightforward as it seems. There is no "size" property on folders. You have to calculate the total size step by step.

To calculate a folders' total size, you need to enumerate all files contained in a folder and manually sum up their sizes. As it turns out, listing all files in a folder isn't that hard:

dir $home -recurse

Actually, Dir (an alias pointing to Get-Childitem) returns all files and folders in a folder. You can safely ignore folders because they have no length property. By specifying the -recurse switch parameter, you get all files including those contained in subfolders. Now, you just need to sum the length property of all those files. Measure-Object can do that for you:

dir $home -recurse | Measure-Object length -sum

The result is an object returned by Measure-Object which includes the count of files as well as the sum of their lengths. To just get back the sum, access the property you are after:

(dir $home -recurse | Measure-Object length -sum).Sum

If you'd like to get that information in another unit like MB or GB, you can now easily calculate with the information. Use the -f operator to format numbers. The next example returns the total size in GB rounded to two digits after the decimal:

'{0:0.00}' -f ((dir $home -recurse | Measure-Object length -sum).Sum/1GB)

Calculating Total Size of Subfolders

Maybe you don't want to get the total size of your $home folder but instead a list of all subfolders in $home with their aggregated size. To get that information, you first need a list of all subfolders in $home. Dir will get you both files and folders. To limit the output to folders only, filter the resulting objects. You could for example include only those that have a PSisContainer property of $true:

dir $home | Where-Object { $_.PSisContainer }

Next, pipe each folder into a Foreach-Object loop and calculate its total size like you did in the first example:

dir $home | Where-Object { $_.PSisContainer } | 
ForEach-Object { Dir $_.FullName -recurse |
Measure-Object length -sum }

You now get back the results from Measure-Object for each subfolder contained in $home. However, you no longer see the folder names. The line only outputs the result from Measure-Object which only tells you the file count and total size. To get a list of folder paths and total size, you can construct your very own return object and assign it the properties you are after.

dir $home | Where-Object { $_.PSisContainer } | 
ForEach-Object { $result = '' | Select-Object Path, Count, Size;
$result.path = $_.FullName; $temp = Dir $_.FullName -recurse |
Measure-Object length -sum; $result.count = $temp.Count;
$result.Size = $temp.Sum; $result }

$result = '' | Select-Object Path, Count, Size actually takes an empty string (two single quotes) and then uses Select-Object to add custom properties: Path, Count and Size. Next, the pipeline can store the information you need in these custom properties. At the end, it returns your custom object $result to output the data in a structured way.

The result now looks much better: you get back three columns: Path, Count and Size. Cool.

However, you may also get some red error messages. They occur when a folder is empty or when you have no access rights. To skip error messages, set the ErrorAction to "SilentlyContinue". This command will no longer throw exceptions on empty folders:

dir $home | Where-Object { $_.PSisContainer } | 
ForEach-Object { $result = '' | Select-Object Path, Count, Size;
$result.path = $_.FullName; $temp = Dir $_.FullName -recurse |
Measure-Object length -sum -ea SilentlyContinue ;
$result.count = $temp.Count; $result.Size = $temp.Sum; $result }

And this command will skip over error messages generated because of unsufficient access rights for subfolders:

dir $home | Where-Object { $_.PSisContainer } | 
ForEach-Object { $result = '' | Select-Object Path, Count, Size; $result.path = $_.FullName;
$temp = Dir $_.FullName -recurse -ea SilentlyContinue |
Measure-Object length -sum -ea SilentlyContinue ;
$result.count = $temp.Count; $result.Size = $temp.Sum; $result }

Adding Progress Indicators

Since calculating sizes for large folders can take considerable time, it would be nice to present some sort of status messages to the user.

Write-Progress can display a temporary status bar. You can add progress indicators by adding a Foreach-Object into your pipeline. Inside of it, display whatever status message you want for the current object travelling the pipeline. Just make sure you output the object ($_) at the end so following commands in the pipeline can pick it up and continue processing it:

dir $home | Where-Object { $_.PSisContainer } | 
ForEach-Object { Write-Progress 'Examining Folder' ($_.FullName); $_ } |
ForEach-Object { $result = '' | Select-Object Path, Count, Size;
$result.path = $_.FullName;
$temp = Dir $_.FullName -recurse -ea SilentlyContinue |
Measure-Object length -sum -ea SilentlyContinue ;
$result.count = $temp.Count; $result.Size = $temp.Sum; $result }

Changing Units to MB

Up till now, your custom object returns the total bytes for each folder as a numeric value. To return the size in other units like MB, you can again calculate.

dir $home | Where-Object { $_.PSisContainer } | 
ForEach-Object { Write-Progress 'Examining Folder' ($_.FullName); $_ } |
ForEach-Object { $result = '' | Select-Object Path, Count, SizeMB;
$result.path = $_.FullName;
$temp = Dir $_.FullName -recurse -ea SilentlyContinue |
Measure-Object length -sum -ea SilentlyContinue ;
$result.count = $temp.Count;
$result.SizeMB = [Int]($temp.Sum/1MB); $result }

If you'd like to stick to numbers, convert the result to Integer. To get a specific number of digits after the decimal, you would have to specify size as String.

dir $home | Where-Object { $_.PSisContainer } | 
ForEach-Object { Write-Progress 'Examining Folder' ($_.FullName); $_ } |
ForEach-Object { $result = '' | Select-Object Path, Count, Size;
$result.path = $_.FullName;
$temp = Dir $_.FullName -recurse -ea SilentlyContinue |
Measure-Object length -sum -ea SilentlyContinue ;
$result.count = $temp.Count;
$result.Size = '{0:0.00}' -f ($temp.Sum/1MB); $result }

Sorting and Outputting

With your progress indicators in place, you can safely add additional cmdlets to sort or output the results. Without progress indicators, when you sort results or output them to a file or xml, you would not get any feedback until the entire operation has completed which for large folders can take a long time. Progress indicators are therefore a really good idea:

dir $home | Where-Object { $_.PSisContainer } | 
ForEach-Object { Write-Progress 'Examining Folder' ($_.FullName); $_ } |
ForEach-Object { $result = '' | Select-Object Path, Count, Size;
$result.path = $_.FullName;
$temp = Dir $_.FullName -recurse -ea SilentlyContinue |
Measure-Object length -sum -ea SilentlyContinue ;
$result.count = $temp.Count; $result.Size = $temp.Sum; $result } |
Sort-Object Size -descending

Finding Special Folder Locations

Now that you can examine folder sizes, you may want to access special folders such as the user profile or a users' desktop folder. One great way of finding special folder paths is the .NET class System.Environment with its static method GetFolderPath(). For example, to find out the current Desktop folder, do this:

[System.Environment]::GetFolderPath('Desktop')

Note that in PowerShellPlus, you can place the cursor over 'Desktop' and press TAB (press SHIFT+TAB in the editor) to get a list of all available options. As it turns out, there are a lot of them. This call gets you your folder with all your personal documents:

[System.Environment]::GetFolderPath('MyDocuments')

Calculating Folder Content with Turboboost

Admittedly, the way PowerShell sums up folder content is a little time-consuming because PowerShell needs to visit each single file. There are faster ways to do this, and you could use .NET methods or DLLs via COM. One such solution uses the Scripting.FileSystemObject which can calculate the total size of a folder really fast:

(New-Object -COMObject Scripting.FileSystemObject).GetFolder($home).Size

However, this call may fail miserably and not return anything. It fails when you have no sufficient access privileges. Just one file inside of the folder structure with insufficient access privileges is enough to break this call. And: while external libraries calculate total folder size, there is no way for PowerShell to interrupt this operation. Even CTRL+C won't stop. PowerShell is single-threaded, and once it handed over control to some external DLL, it simply has to wait until that DLL finishes whatever it does.

That's why the PowerShell approach has a lot of important advantages that make up for the slower performance. It is fault-tolerant and gracefully ignores files you cannot access, and you can cancel the call anytime.

Have a great weekend!

-Tobias 

 


Posted Nov 25 2008, 09:52 PM by Tobias Weltner
Copyright 2010 PowerShell.com. All rights reserved.