By: Mason Prewett
Introduction
As an admin of many server types, I often encounter the issue of a machine running out of disk space. Tracking the culprit folder that was hogging up disk space used to be a troublesome, manual effort. However, I recently coded a way to speed this process up with PowerShell. No more clicking folder by folder, inspecting their properties, and determining folder size one by one. With a bit of PowerShell code (that I’ll share with you) and an HTML file, I’ll demonstrate how to create a tree view of your machine’s file structure – with a size breakdown included – so you can quickly find your space hog.
How It Works
Part 1: The PowerShell Code
The PowerShell code below works by dynamically generating html that will produce the file directory tree view hierarchy. In order to do this, it is necessary to recursively (more detail on recursion) go through the folder structure and generate the html per folder.
Once the html is produced, it will be inserted into an html file template that is covered in the next section. When complete, you can simply open the html file to view the results! With a quick scan of the file tree view, you’ll be able to see exactly which file(s) is taking up space.
Once you have the HTML file in place and you’re ready to try this, make sure to set the variables at the top of the below PowerShell code to accommodate your configuration. You can set the starting folder location to analyze, which will help you avoid returning too much unnecessary information.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 |
#Variables that need to be set for each run $startFolder = "C:\Program Files"; #The starting folder to analyze $sourceHTMLFile = "C:\finalTemplate.html"; #The html source template file $destinationHTMLFile = "C:\final.html"; #The final html file that will be produced, #does not need to exist $htmlLines = @(); #Function that creates a folder detail record function CreateFolderDetailRecord { param([string]$FolderPath) #Get the total size of the folder by recursively summing its children $subFolderItems = Get-ChildItem $FolderPath -recurse -force | Where-Object {$_.PSIsContainer -eq $false} | Measure-Object -property Length -sum | Select-Object Sum $folderSizeRaw = 0; $folderSize = 0; $units = ""; #Account for no children if($subFolderItems.sum -gt 0) { $folderSizeRaw = $subFolderItems.sum; } #Determine units for a more friendly output if(($subFolderItems.sum / 1GB) -ge 1) { $units = "GB" $folderSize = [math]::Round(($subFolderItems.sum / 1GB),2) } else { if(($subFolderItems.sum / 1MB) -ge 1) { $units = "MB" $folderSize = [math]::Round(($subFolderItems.sum / 1MB),2) } else { $units = "KB" $folderSize = [math]::Round(($subFolderItems.sum / 1KB),2) } } #Create an object with the given properties $newFolderRecord = New-Object –TypeName PSObject $newFolderRecord | Add-Member –MemberType NoteProperty –Name FolderPath –Value $FolderPath; $newFolderRecord | Add-Member –MemberType NoteProperty –Name FolderSizeRaw –Value $folderSizeRaw $newFolderRecord | Add-Member –MemberType NoteProperty –Name FolderSizeInUnits –Value $folderSize; $newFolderRecord | Add-Member –MemberType NoteProperty –Name Units –Value $units; return $newFolderRecord; } #Function that recursively creates the html for the output, given a starting location function GetAllFolderDetails { param([string]$FolderPath) $recursiveHTML = @(); #Get properties used for processing $folderItem = Get-Item -Path $FolderPath $folderDetails = CreateFolderDetailRecord -FolderPath $FolderPath $subFolders = Get-ChildItem $FolderPath | Where-Object {$_.PSIsContainer -eq $true} | Sort-Object #If has subfolders, create hmtl drilldown. if($subFolders.Count -gt 0) { $recursiveHTML += "<li><span class='caret'>" + $folderItem.Name + " (<span style='color:red'>" + $folderDetails.FolderSizeInUnits + " " + $folderDetails.Units + "</span>)" + "</span>" $recursiveHTML += "<ul class='nested'>" } else { $recursiveHTML += "<li>" + $folderItem.Name + " (<span style='color:red'>" + $folderDetails.FolderSizeInUnits + " " + $folderDetails.Units + "</span>)"; } #Recursively call this function for all subfolders foreach($subFolder in $subFolders) { $recursiveHTML += GetAllFolderDetails -FolderPath $subFolder.FullName; } #Close up all tags if($subFolders.Count -gt 0) { $recursiveHTML += "</ul>"; } $recursiveHTML += "</li>"; return $recursiveHTML } #Processing Starts Here #Opening html $htmlLines += "<ul id='myUL'>" #This function call will return all of the recursive html for the startign folder and below $htmlLines += GetAllFolderDetails -FolderPath $startFolder #Closing html $htmlLines += "</ul>" #Get the html template, replace the template with generated code and write to the final html file $sourceHTML = Get-Content -Path $sourceHTMLFile; $destinationHTML = $sourceHTML.Replace("[FinalHTML]", $htmlLines); $destinationHTML | Set-Content $destinationHTMLFile |
Part 2: The HTML Template
The HTML file serves as the template for the dynamically generated HTML code to be inserted into. This file is a dependency and MUST be present in order for the above script to succeed!
The template string that is located in the file is [FinalHTML]. If you wish to change this template string, you will need to update this line of code as well:
1 |
$destinationHTML = $sourceHTML.Replace("[FinalHTML]", $htmlLines); |
Note: The tree view html/JavaScript/css code was pulled from W3 Schools.
HTML Template Code (Create an html file and paste this in to it):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 |
<html> <head> <style> ul, #myUL { list-style-type: none; } #myUL { margin: 0; padding: 0; } .caret { cursor: pointer; -webkit-user-select: none; /* Safari 3.1+ */ -moz-user-select: none; /* Firefox 2+ */ -ms-user-select: none; /* IE 10+ */ user-select: none; } .caret::before { content: "\25B6"; color: black; display: inline-block; margin-right: 6px; } .caret-down::before { -ms-transform: rotate(90deg); /* IE 9 */ -webkit-transform: rotate(90deg); /* Safari */' transform: rotate(90deg); } .nested { display: none; } .active { display: block; } </style> </head> <body> [FinalHTML] <script> var toggler = document.getElementsByClassName("caret"); var i; for (i = 0; i < toggler.length; i++) { toggler[i].addEventListener("click", function() { this.parentElement.querySelector(".nested").classList.toggle("active"); this.classList.toggle("caret-down"); }); } </script> </body> </html> |
What the End Result Looks Like
Notes
- You may notice that the subfolder sizes do not always add up to the total size displayed of their parent. This is because the folder sizes include files that are in the folders, but the html file only displays the folder and subfolder breakdowns.
- The tree view html/JavaScript/css code was used from this W3 Schools tutorial.
Questions?
Thanks for reading! We hope you found this blog post to be useful. Do let us know if you have any questions or topic ideas related to BI, analytics, the cloud, machine learning, SQL Server, (Star Wars), or anything else of the like that you’d like us to write about. Simply leave us a comment below, and we’ll see what we can do!
Keep your data analytics sharp by subscribing to our mailing list
Get fresh Key2 content around Business Intelligence, Data Warehousing, Analytics, and more delivered right to your inbox!
Key2 Consulting is a data warehousing and business intelligence company located in Atlanta, Georgia. We create and deliver custom data warehouse solutions, business intelligence solutions, and custom applications.
hi! your script is very helpfull. thank you very much. but how to make it add the directory files in this tree?
Hi Bizo,
Thanks for your comment. This this line of code controls only getting only folders “$subFolders = Get-ChildItem $FolderPath | Where-Object {$_.PSIsContainer -eq $true} | Sort-Object”. Change this to “$subItems = Get-ChildItem $FolderPath | Sort-Object” and it will get all items in a folder. The script would require some additional changes in logic to be able to display the files, but this is what controls the addition of files.
Thanks,
Mason Prewett, Key2 Consulting
Hello, this is a great script and very helpful. I was able to modify it to include Part 1 and Part 2 all in one script so that the template html file is created and then injected with the Part 2 code. Also, did other things to make things easier. I’m not a PS coder so I may be missing some “best” procedures, but I’m happy with the progress.
However, I have 2 questions.
1) When I run the script against to gather File Size of “C:\”; I get a lot of “Access to the path is denied.
2) Do you think adding a progress bar to the script is doable? I saw your other article https://126.595.myftpupload.com/powershell-how-to-display-job-progress/ but I haven’t quite figured out how to work it in to this script. I’ll keep playing with it.
Thank you again!
Jeremy,
Awesome that you were able to make some adjustments to make things easier. The less complexity the better.
Answers to your questions:
1) Make sure to open PowerShell ISE as an administrator. If you are still having issues, i would check the security on the drive and make sure your account has full control over that and everything underneath.
2) The GetAllFolderDetails function uses recursion, and progress bars with recursion are not fun. The article you mentioned actually is a way to accomplish this using jobs, but it would require some significant code changes. This topic could actually use an entire article on it. The concept would be to get an initial count of all of the folders that you will be processing, and increment a separate counter each time a folder is completed. The percentage complete would be the incremented counter divided by the total count * 100.
Thank you,
Mason Prewett
Hello
thanks for wonderful script
When i tried to run this in DevOps Pipeline it throws error
The ‘<' operator is reserved for future use – Line 70,75,87 etc
Hello,
Thank you for your comment. My guess is that it is considering the < character literally when it is meant to be in a string. Check to make sure that all double quotes are close and that the double quotes were not converted to a different character type. Maybe just delete the double quotes and add them back. - Mason Prewett, Key2 Consulting
Hello
thank you for response, when i run this script as Powershell Inline script in DevOps – it works but when script is provided as Path then it throws above error.
Anyways as of now its doing well for me in inline script mode.
But now i am seeing another problem –
Parent Disk Size is not matching with Children disk size (i saw your notes – and i dont have any files under parent directory – so parent should count the disk size correctly)
Hello
My bad,
I observed that folder was having some hidden folders which are not getting listed there
so i have updated -force parameter now its working fine
thanks