#Requires -Version 5.1 <# .SYNOPSIS Large Folder Search - find what is filling up your drive .DESCRIPTION Scans a drive (C:\ by default) and lists the folders and files taking up the most space, as a simple tree. Use it when the drive is full, Windows updates fail for lack of space, or the PC has slowed down. The script does NOT delete, move or change anything, and sends nothing anywhere. The only things it writes are the results folder and zip file. Works as a normal user. Run as administrator for complete totals: without admin rights a few system folders can't be read, so they show smaller than they really are (the results say so when that happens). .NOTES Source : https://techs4u.com.au/resources Version : 2.0 Requires : Windows 10/11, Windows PowerShell 5.1 or PowerShell 7+ Licence : MIT - free to use and modify. Provided as-is, without warranty. HOW TO RUN 1. Download - your browser saves it to your Downloads folder. 2. Run it - right-click LargeFolderSearch.ps1 > Run with PowerShell (Windows 11: right-click > Show more options first). 3. Results - saved to your Downloads folder, which opens for you. IF RIGHT-CLICK DOESN'T WORK (the most reliable method): 1. Copy the whole script (the "Copy script" button on the web page, or open the file in Notepad, Ctrl+A, Ctrl+C). 2. Open Windows PowerShell from the Start menu. 3. Paste it in (right-click inside the window, or Ctrl+V) and press Enter. If Windows warns about pasting multiple lines, choose "Paste anyway". FOR COMPLETE TOTALS (optional): right-click the Start button > "Terminal (Admin)" or "Windows PowerShell (Admin)", then paste the script as above. A full C: drive can take 5-15 minutes to scan. PRIVACY: the results list folder and file names on your PC. Review them before sharing. #> # ---- Settings: change these before running if you want ---- $ScanPath = 'C:\' # Drive or folder to scan $MinFolderGB = 2 # Only show folders at least this big $Depth = 3 # How many folder levels deep to show $MinFileGB = 1 # List individual files at least this big $TopFiles = 25 # How many of the biggest files to list # ============================================================================ # TECHS4U SCRIPT STANDARD - output to Downloads, zip, Explorer, wait for Enter # ============================================================================ $ScriptTitle = 'Large Folder Search' $OutputName = 'LargeFolderSearch' function Get-DownloadsFolder { $path = $null try { $path = (New-Object -ComObject Shell.Application).Namespace('shell:Downloads').Self.Path } catch {} if (-not $path -or -not (Test-Path $path)) { try { $raw = (Get-ItemProperty 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\User Shell Folders' -ErrorAction Stop).'{374DE290-123F-4565-9164-39C4925E467B}' if ($raw) { $path = [Environment]::ExpandEnvironmentVariables($raw) } } catch {} } if (-not $path -or -not (Test-Path $path)) { $path = Join-Path $env:USERPROFILE 'Downloads' } if (-not (Test-Path $path)) { $path = $env:TEMP } return $path } function Wait-BeforeClose { Write-Host "" Write-Host "Press Enter to close this window..." -ForegroundColor DarkGray [void](Read-Host) } $IsAdmin = (New-Object Security.Principal.WindowsPrincipal([Security.Principal.WindowsIdentity]::GetCurrent())).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) Clear-Host Write-Host "==============================================" -ForegroundColor Cyan Write-Host " $ScriptTitle" -ForegroundColor Cyan Write-Host " techs4u.com.au" -ForegroundColor Cyan Write-Host "==============================================" -ForegroundColor Cyan Write-Host "" Write-Host "This script only READS folder sizes - it doesn't delete or change anything." -ForegroundColor Gray if ($IsAdmin) { Write-Host "Running as administrator: full totals." -ForegroundColor Gray } else { Write-Host "Running as a normal user: a few system folders may show smaller than they are." -ForegroundColor DarkYellow } Write-Host "" Start-Sleep -Seconds 2 try { $Folder = Join-Path (Get-DownloadsFolder) ("{0}_{1}" -f $OutputName, (Get-Date -Format 'yyyyMMdd-HHmm')) New-Item -ItemType Directory -Path $Folder -Force -ErrorAction Stop | Out-Null $ResultsPath = Join-Path $Folder 'Results.txt' $LogPath = Join-Path $Folder 'Log.txt' } catch { Write-Host "SETUP FAILED - could not create the results folder: $($_.Exception.Message)" -ForegroundColor Red Wait-BeforeClose exit } function Write-Result { param([string]$Text, [string]$Color = 'White') Write-Host $Text -ForegroundColor $Color Add-Content -Path $ResultsPath -Value $Text -Encoding UTF8 } function Write-Log { param([string]$Text) Add-Content -Path $LogPath -Value ("{0} {1}" -f (Get-Date -Format 'yyyy-MM-dd HH:mm:ss'), $Text) -Encoding UTF8 } function Format-GB { param([long]$Bytes) "{0,8:N2} GB" -f ($Bytes / 1GB) } if (-not (Test-Path -LiteralPath $ScanPath)) { Write-Host "Can't find $ScanPath - check the ScanPath setting at the top of the script." -ForegroundColor Red Wait-BeforeClose exit } # ---- Scan: one pass over the drive ---- # Each file is read once and its size added to every parent folder down to # $Depth levels, so the scan doesn't re-read the drive for each level. # Junctions/links are skipped (they point at folders already counted), and # OneDrive "online-only" files are skipped (they don't use disk space). $root = (Get-Item -LiteralPath $ScanPath -Force).FullName $rootInfo = New-Object System.IO.DirectoryInfo $root $sizes = @{} $children = @{} $sizes[$root] = [long]0 $children[$root] = New-Object System.Collections.Generic.List[string] $bigFiles = New-Object System.Collections.Generic.List[object] $denied = New-Object System.Collections.Generic.List[string] $minFolderBytes = [long]($MinFolderGB * 1GB) $minFileBytes = [long]($MinFileGB * 1GB) # Offline (0x1000), RecallOnOpen (0x40000) and RecallOnDataAccess (0x400000) mark # OneDrive online-only files. Plain numbers: the last two aren't named in the # .NET FileAttributes enum, so casting them to that type throws. $cloudMask = 0x1000 -bor 0x40000 -bor 0x400000 $fileCount = [long]0 $totalBytes = [long]0 $cloudBytes = [long]0 $start = Get-Date $lastProgress = Get-Date Write-Log "Scan started: $root (admin: $IsAdmin, depth: $Depth, min folder: $MinFolderGB GB)" Write-Host "Scanning $root - a full drive can take 5-15 minutes..." -ForegroundColor Cyan $stack = New-Object 'System.Collections.Generic.Stack[object]' $stack.Push([pscustomobject]@{ Dir = $rootInfo; Depth = 0; Keys = [string[]]@($root) }) while ($stack.Count -gt 0) { $node = $stack.Pop() $dir = $node.Dir try { foreach ($f in $dir.EnumerateFiles()) { $fileCount++ $len = $f.Length if (([int]$f.Attributes) -band $cloudMask) { $cloudBytes += $len; continue } $totalBytes += $len foreach ($k in $node.Keys) { $sizes[$k] += $len } if ($len -ge $minFileBytes) { $bigFiles.Add($f) } } } catch { $denied.Add($dir.FullName) Write-Log "Could not read files in '$($dir.FullName)': $($_.Exception.Message)" } try { foreach ($d in $dir.EnumerateDirectories()) { if ($d.Attributes -band [IO.FileAttributes]::ReparsePoint) { continue } $childDepth = $node.Depth + 1 if ($childDepth -le $Depth) { $sizes[$d.FullName] = [long]0 $children[$d.FullName] = New-Object System.Collections.Generic.List[string] $children[$dir.FullName].Add($d.FullName) $keys = [string[]]($node.Keys + $d.FullName) } else { $keys = $node.Keys } $stack.Push([pscustomobject]@{ Dir = $d; Depth = $childDepth; Keys = $keys }) } } catch { if (-not $denied.Contains($dir.FullName)) { $denied.Add($dir.FullName) } Write-Log "Could not list folders in '$($dir.FullName)': $($_.Exception.Message)" } if (((Get-Date) - $lastProgress).TotalMilliseconds -ge 500) { Write-Progress -Activity "Scanning $root" -Status ("{0:N0} files checked ({1:N1} GB) - {2}" -f $fileCount, ($totalBytes / 1GB), $dir.FullName) $lastProgress = Get-Date } } Write-Progress -Activity "Scanning $root" -Completed $elapsed = (Get-Date) - $start Write-Log ("Scan finished in {0:N1} minutes: {1:N0} files, {2} folders unreadable" -f $elapsed.TotalMinutes, $fileCount, $denied.Count) # ---- Results ---- Write-Host "" Write-Result "LARGE FOLDER SEARCH (techs4u.com.au)" 'Cyan' Write-Result ("Scanned {0} on {1} | {2:N0} files | took {3:N1} minutes" -f $root, (Get-Date -Format 'dd MMM yyyy HH:mm'), $fileCount, $elapsed.TotalMinutes) 'DarkGray' try { $di = New-Object System.IO.DriveInfo $root if ($di.IsReady) { $usedPct = [math]::Round((1 - $di.AvailableFreeSpace / $di.TotalSize) * 100) Write-Result ("Drive {0} size {1:N1} GB | free {2:N1} GB | {3}% used{4}" -f $di.Name, ($di.TotalSize / 1GB), ($di.AvailableFreeSpace / 1GB), $usedPct, $(if ($usedPct -ge 90) { " <-- VERY LOW SPACE" } else { "" })) 'DarkGray' } } catch {} if ($cloudBytes -gt 0) { Write-Result ("Skipped {0:N1} GB of OneDrive online-only files (they don't use disk space)." -f ($cloudBytes / 1GB)) 'DarkGray' } if (-not $IsAdmin -and $denied.Count -gt 0) { Write-Result ("{0} folders couldn't be read without admin rights, so some totals are lower than real." -f $denied.Count) 'Yellow' Write-Result "For full totals: right-click Start > Terminal (Admin), then paste the script again." 'Yellow' } Write-Result "" Write-Result ("FOLDERS OF {0} GB OR MORE (up to {1} levels deep)" -f $MinFolderGB, $Depth) 'Green' Write-Result ("=" * 64) 'Green' function Write-Tree { param([string]$Parent, [string]$Indent, [int]$Level) if (-not $children.ContainsKey($Parent)) { return } $kids = $children[$Parent] | Where-Object { $sizes[$_] -ge $minFolderBytes } | Sort-Object { $sizes[$_] } -Descending foreach ($k in $kids) { $color = if ($Level -eq 1) { 'White' } else { 'Cyan' } Write-Result ("{0}[{1}] {2}" -f $Indent, (Format-GB $sizes[$k]), (Split-Path $k -Leaf)) $color Write-Tree -Parent $k -Indent ($Indent + " ") -Level ($Level + 1) if ($Level -eq 1) { Write-Result "" } } } Write-Tree -Parent $root -Indent "" -Level 1 Write-Result "" Write-Result ("LARGEST FILES ({0} GB OR MORE)" -f $MinFileGB) 'Green' Write-Result ("=" * 64) 'Green' if ($bigFiles.Count -gt 0) { $bigFiles | Sort-Object Length -Descending | Select-Object -First $TopFiles | ForEach-Object { Write-Result ("[{0}] {1}" -f (Format-GB $_.Length), $_.FullName) 'Cyan' } } else { Write-Result "None found." 'DarkGray' } Write-Result "" Write-Result "WHERE TO START" 'Green' Write-Result ("=" * 64) 'Green' Write-Result "- Usually safe to clear yourself: your Downloads folder, the Recycle Bin, and" Write-Result " Settings > System > Storage > Temporary files." Write-Result "- C:\Windows\WinSxS often looks bigger than it really is. Don't delete it by hand." Write-Result "- Don't delete anything inside C:\Windows or C:\Program Files manually." Write-Result "- Not sure? Send this file to your IT support (or techs4u.com.au/contact)." # ============================================================================ # FINISH - zip, show the result, wait for Enter # ============================================================================ Write-Host "" $ZipPath = "$Folder.zip" try { Compress-Archive -Path (Join-Path $Folder '*') -DestinationPath $ZipPath -Force -ErrorAction Stop } catch { $ZipPath = $null } Write-Host "========================================" -ForegroundColor Green Write-Host "DONE. Results are in your Downloads folder:" -ForegroundColor Green Write-Host " $ResultsPath" -ForegroundColor Green if ($ZipPath) { Write-Host "To send them to someone, use this one file:" -ForegroundColor Green Write-Host " $ZipPath" -ForegroundColor Green } Write-Host "========================================" -ForegroundColor Green Start-Process explorer.exe -ArgumentList "/select,`"$(if ($ZipPath) { $ZipPath } else { $ResultsPath })`"" # Kept on one line so a pasted run pauses here correctly. Wait-BeforeClose; exit