Skip to content

Instantly share code, notes, and snippets.

@DickHorner
Forked from SMSAgentSoftware/New-WizTreeDiskUsageReport.ps1
Last active November 8, 2025 21:40
Show Gist options
  • Select an option

  • Save DickHorner/4836f381d4a3d39856f8b6878858b3c8 to your computer and use it in GitHub Desktop.

Select an option

Save DickHorner/4836f381d4a3d39856f8b6878858b3c8 to your computer and use it in GitHub Desktop.
Creates csv and html disk usage reports and then some, using WizTree portable.
# Script to export html and csv reports of file and directory content on the system drive
param(
[switch]$DryRun,
[string]$LogFile,
[int]$TopN = 100,
[switch]$ExportFull,
[switch]$Trace,
[switch]$UseTextFieldParser
)
# Use to identify large files/directories for disk space cleanup
# Uses WizTree portable to quickly retrieve file and directory sizes from the Master File Table on disk
# Download and extract the WizTree64.exe and place in the same directory as this script
# Set the running location
$RunLocation = $PSScriptRoot
#$RunLocation = "D:\temp"
$TempLocation = "D:\temp"
# Default log file if not provided
if ([string]::IsNullOrEmpty($LogFile)) { $LogFile = Join-Path -Path $TempLocation -ChildPath 'wiztree_run.log' }
# Log helper
function Log {
param([string]$Message)
$ts = (Get-Date).ToString('s')
$line = "[$ts] $Message"
Write-Output $line
try { Add-Content -Path $LogFile -Value $line -ErrorAction SilentlyContinue } catch {}
}
# Trace helper
function Trace-Log { param([string]$m) if ($Trace) { Log "TRACE: $m" } }
# Ensure temporary folder exists (skip creation in DryRun)
if (!(Test-Path -Path $TempLocation)) {
if ($DryRun) {
Log "DRYRUN: Would create temp folder: $TempLocation"
} else {
try { New-Item -Path $TempLocation -ItemType Directory -Force | Out-Null } catch { Log "Warning: failed to create temp folder: $_" }
}
}
# Set Target share to copy the reports to
$TargetRoot = "D:\temp"
# Free disk space thresholds (percentages) for summary report
$script:Thresholds = @{}
$Thresholds.Warning = 80
$Thresholds.Critical = 90
# Custom function to exit with a specific code
function ExitWithCode {
param(
[int]$exitcode)
# Intentionally do NOT call host.SetShouldExit or exit here.
# Previously this forcibly closed the user's host/console.
Log "Exit requested: $exitcode (not closing host)."
return
}
# Function to set the progress bar colour based on the the threshold value in the summary report
function Set-PercentageColour {
param(
[int]$Value
)
If ($Value -lt $Thresholds.Warning)
{
$Hex = "#00ff00" # Green
}
If ($Value -ge $Thresholds.Warning -and $Value -lt $Thresholds.Critical)
{
$Hex = "#ff9900" # Amber
}
If ($Value -ge $Thresholds.Critical)
{
$Hex = "#FF0000" # Red
}
Return $Hex
}
# Define Html CSS style
$Style = @"
<style>
table {
border-collapse: collapse;
}
td, th {
border: 1px solid #ddd;
padding: 8px;
}
th {
padding-top: 12px;
padding-bottom: 12px;
text-align: left;
background-color: #4286f4;
color: white;
}
</style>
"@
# Set the filenames of WizTree csv's
$FilesCSV = "Files_$(Get-Date -Format 'yyyyMMdd_hhmmss').csv"
$FoldersCSV = "Folders_$(Get-Date -Format 'yyyyMMdd_hhmmss').csv"
# Set the filenames of customised csv's
$ExportedFilesCSV = "Exported_Files_$(Get-Date -Format 'yyyyMMdd_hhmmss').csv"
$ExportedFoldersCSV = "Exported_Folders_$(Get-Date -Format 'yyyyMMdd_hhmmss').csv"
# Set the filenames of html reports
$ExportedFilesHTML = "Largest_Files_$(Get-Date -Format 'yyyyMMdd_hhmmss').html"
$ExportedFoldersHTML = "Largest_Folders_$(Get-Date -Format 'yyyyMMdd_hhmmss').html"
$SummaryHTMLReport = "Disk_Usage_Summary_$(Get-Date -Format 'yyyyMMdd_hhmmss').html"
# Run the WizTree portable app if present
$WizTreePath = Join-Path -Path $RunLocation -ChildPath 'WizTree64.exe'
if (Test-Path -Path $WizTreePath) {
if ($DryRun) {
Log "DRYRUN: Would run WizTree to export files CSV to: $(Join-Path $TempLocation $FilesCSV)"
Log "DRYRUN: Would run WizTree to export folders CSV to: $(Join-Path $TempLocation $FoldersCSV)"
} else {
$t0 = Get-Date
try {
Start-Process -FilePath $WizTreePath -ArgumentList """$Env:SystemDrive"" /export=""$TempLocation\$FilesCSV"" /admin 1 /sortby=2 /exportfolders=0" -Verb runas -Wait
} catch { Log "Warning: Failed to run WizTree for files export: $_" }
try {
Start-Process -FilePath $WizTreePath -ArgumentList """$Env:SystemDrive"" /export=""$TempLocation\$FoldersCSV"" /admin 1 /sortby=2 /exportfiles=0" -Verb runas -Wait
} catch { Log "Warning: Failed to run WizTree for folders export: $_" }
Trace-Log "WizTree run took: $((Get-Date) - $t0)"
}
} else {
Log "WizTree executable not found at '$WizTreePath'. Skipping WizTree export steps."
}
#region Files
$FilesCsvPath = Join-Path $TempLocation $FilesCSV
$FoldersCsvPath = Join-Path $TempLocation $FoldersCSV
# If the expected timestamped CSVs do not exist (e.g. running against an existing export),
# pick the most recent matching Files_*.csv / Folders_*.csv in the temp folder.
if (!(Test-Path -Path $FilesCsvPath)) {
try {
$found = Get-ChildItem -Path $TempLocation -Filter 'Files_*.csv' -File -ErrorAction SilentlyContinue | Sort-Object LastWriteTime -Descending | Select-Object -First 1
if ($found) { $FilesCsvPath = $found.FullName; Log "Using existing Files CSV: $FilesCsvPath" }
} catch { }
}
if (!(Test-Path -Path $FoldersCsvPath)) {
try {
$found2 = Get-ChildItem -Path $TempLocation -Filter 'Folders_*.csv' -File -ErrorAction SilentlyContinue | Sort-Object LastWriteTime -Descending | Select-Object -First 1
if ($found2) { $FoldersCsvPath = $found2.FullName; Log "Using existing Folders CSV: $FoldersCsvPath" }
} catch { }
}
# Read Files CSV into $CSVContent variable (robust header detection & temporary cleaned CSV)
$FilesCsvPathClean = $null
if (Test-Path -Path $FilesCsvPath) {
try {
$rawLines = Get-Content -Path $FilesCsvPath -ReadCount 0
if ($rawLines.Count -eq 0) { $CSVContent = @() }
else {
# detect header line (look for common English/German header tokens)
$headerPattern = '(Dateiname|Datei|Name|Path|File|Größe|Groesse|Size|Bytes)'
$headerIndex = -1
for ($i = 0; $i -lt $rawLines.Count; $i++) {
if ($rawLines[$i] -match ',') {
if ($rawLines[$i] -match $headerPattern) { $headerIndex = $i; break }
}
}
if ($headerIndex -lt 0) {
# fallback: first comma-containing line
for ($i = 0; $i -lt $rawLines.Count; $i++) { if ($rawLines[$i] -match ',') { $headerIndex = $i; break } }
}
if ($headerIndex -lt 0) {
Log "No header-like line found in Files CSV"
$CSVContent = @()
} else {
Trace-Log "Files CSV header detected at line $headerIndex"
$cleanLines = $rawLines[$headerIndex..($rawLines.Count - 1)]
$tmp = [System.IO.Path]::GetTempFileName() + '.csv'
Set-Content -Path $tmp -Value $cleanLines -Encoding UTF8
$FilesCsvPathClean = $tmp
$CSVContent = Get-Content -Path $FilesCsvPathClean -ReadCount 0
Log "Using cleaned Files CSV: $FilesCsvPathClean"
}
}
} catch { Log "Failed to read Files CSV: $_"; $CSVContent = @() }
} else { $CSVContent = @() }
# Remove the first 2 rows from the CSVs to leave just the relevant data
# Constants (decimal)
# Use explicit numeric values to avoid environment-dependent token parsing issues
$KB = [decimal]1024
$MB = [decimal](1024 * 1024)
$GB = [decimal](1024 * 1024 * 1024)
# Detect if CSV contains quoted fields that span multiple lines (need TextFieldParser)
$autoUseTextFieldParser = $false
try {
$open = $false
foreach ($ln in $CSVContent) {
$q = ([regex]::Matches($ln, '"')).Count
if ($q % 2 -ne 0) { $open = -not $open }
if ($open) { $autoUseTextFieldParser = $true; break }
}
} catch {}
# Process files CSV using ConvertFrom-Csv for robust parsing and keep only TopN in memory
if ($CSVContent.Count -eq 0) {
Log "No file CSV content to process."
$TopFiles = @()
} else {
if ( ($UseTextFieldParser -or $autoUseTextFieldParser) -and (Test-Path $FilesCsvPath)) {
Trace-Log "Using TextFieldParser for files CSV"
try {
[void][Reflection.Assembly]::LoadWithPartialName('Microsoft.VisualBasic')
$parserPath = if ($FilesCsvPathClean) { $FilesCsvPathClean } else { $FilesCsvPath }
$parser = New-Object Microsoft.VisualBasic.FileIO.TextFieldParser($parserPath)
$parser.TextFieldType = 'Delimited'
$parser.SetDelimiters(',')
$parser.HasFieldsEnclosedInQuotes = $true
$headers = $parser.ReadFields()
$headerIndex = @{}
for ($h=0;$h -lt $headers.Length;$h++) { $headerIndex[$headers[$h]] = $h }
# Determine likely name/path and size columns
$nameProp = ($headers | Where-Object { $_ -match 'Name|Path|File' } | Select-Object -First 1)
if (-not $nameProp) { $nameProp = $headers[0] }
$sizeProp = ($headers | Where-Object { $_ -match 'Size|Bytes' } | Select-Object -First 1)
if (-not $sizeProp) { $sizeProp = ($headers | Select-Object -Index 1) }
$TopFiles = New-Object System.Collections.ArrayList
if ($ExportFull) { $FullFiles = New-Object System.Collections.ArrayList }
while (-not $parser.EndOfData) {
$fields = $parser.ReadFields()
$raw = ''
try { $raw = ($fields[$headerIndex[$sizeProp]] -as [string]) -replace '[^\d]', '' } catch { $raw = '0' }
$bytes = 0
try { $bytes = [int64]$raw } catch { $bytes = 0 }
$name = ''
try { $name = ($fields[$headerIndex[$nameProp]] -as [string]) } catch { $name = '' }
$entry = [pscustomobject]@{ Name = $name; SizeBytes = $bytes }
if ($TopFiles.Count -eq 0) { [void]$TopFiles.Add($entry) }
else {
$inserted = $false
for ($i = 0; $i -lt $TopFiles.Count; $i++) {
if ($bytes -gt $TopFiles[$i].SizeBytes) { [void]$TopFiles.Insert($i, $entry); $inserted = $true; break }
}
if (-not $inserted) { [void]$TopFiles.Add($entry) }
if ($TopFiles.Count -gt $TopN) { $TopFiles.RemoveAt($TopFiles.Count - 1) }
}
if ($ExportFull) { [void]$FullFiles.Add($entry) }
}
} finally { if ($parser) { $parser.Close() } }
} else {
# Robust CSV parsing: sanitize lines, detect header, dedupe header names, then use ConvertFrom-Csv
$csvRecords = @()
try {
$lines = $CSVContent | Where-Object { $_ -ne $null -and $_.Trim() -ne '' }
if ($lines.Count -eq 0) { throw 'No CSV lines' }
# Find first candidate header line (contains at least one comma and letters)
$headerIndex = -1
for ($i = 0; $i -lt $lines.Count; $i++) {
if ($lines[$i] -and ($lines[$i] -match ',') -and ($lines[$i] -match '[A-Za-z]')) { $headerIndex = $i; break }
}
if ($headerIndex -lt 0) { $headerIndex = 0 }
$headerLine = $lines[$headerIndex]
$dataLines = @()
if ($headerIndex -lt ($lines.Count - 1)) { $dataLines = $lines[($headerIndex + 1)..($lines.Count - 1)] } else { $dataLines = @() }
# Split header by comma (WizTree CSVs are simple, quotes around fields are uncommon for headers)
$rawCols = ($headerLine -split ',') | ForEach-Object { $_.Trim().Trim('"') }
# Ensure unique header names
$cols = @()
$seen = @{}
for ($i = 0; $i -lt $rawCols.Count; $i++) {
$name = $rawCols[$i]
if ([string]::IsNullOrWhiteSpace($name)) { $name = "Column$i" }
$uniq = $name
$idx = 1
while ($seen.ContainsKey($uniq)) { $uniq = "${name}_${idx}"; $idx++ }
$seen[$uniq] = $true
$cols += $uniq
}
# Build CSV string with cleaned header and data lines
$csvString = $cols -join ','
if ($dataLines.Count -gt 0) { $csvString += "`n" + ($dataLines -join "`n") }
$csvRecords = $csvString | ConvertFrom-Csv -ErrorAction Stop
} catch {
Log "Failed to parse Files CSV into objects: $_"
# Fallback: simple manual parse - split on commas and map to generic column names
$csvRecords = @()
try {
$lines = $CSVContent | Where-Object { $_ -ne $null -and $_.Trim() -ne '' }
if ($lines.Count -ge 1) {
$first = $lines[0]
$colCount = ($first -split ',').Count
$hdrs = 1..$colCount | ForEach-Object { "Col$_" }
foreach ($ln in $lines) {
$parts = $ln -split ','
$obj = [pscustomobject]@{}
for ($i=0; $i -lt $hdrs.Count; $i++) {
$name = $hdrs[$i]
$val = if ($i -lt $parts.Count) { ($parts[$i].Trim('"')) } else { '' }
$obj | Add-Member -NotePropertyName $name -NotePropertyValue $val
}
$csvRecords += $obj
}
}
} catch { Log "Fallback CSV parse also failed: $_" }
}
$props = @()
if ($csvRecords -and $csvRecords.Count -gt 0) {
$props = ($csvRecords | Get-Member -MemberType NoteProperty | Select-Object -ExpandProperty Name)
}
# Prefer columns containing Name/Path/File for the display name
$nameProp = ($props | Where-Object { $_ -match 'Name|Path|File' } | Select-Object -First 1)
if (-not $nameProp) { $nameProp = $props | Select-Object -First 1 }
$sizeProp = ($props | Where-Object { $_ -match 'Size|Bytes' } | Select-Object -First 1)
if (-not $sizeProp) { $sizeProp = $props | Select-Object -Index 1 }
$TopFiles = New-Object System.Collections.ArrayList
if ($ExportFull) { $FullFiles = New-Object System.Collections.ArrayList }
foreach ($rec in $csvRecords) {
$raw = ''
try { $raw = ($rec.$sizeProp -as [string]) -replace '[^\d]', '' } catch { $raw = '0' }
$bytes = 0
try { $bytes = [int64]$raw } catch { $bytes = 0 }
$name = ''
try { $name = ($rec.$nameProp -as [string]) } catch { $name = '' }
$entry = [pscustomobject]@{ Name = $name; SizeBytes = $bytes }
if ($TopFiles.Count -eq 0) { [void]$TopFiles.Add($entry) }
else {
$inserted = $false
for ($i = 0; $i -lt $TopFiles.Count; $i++) {
if ($bytes -gt $TopFiles[$i].SizeBytes) { [void]$TopFiles.Insert($i, $entry); $inserted = $true; break }
}
if (-not $inserted) { [void]$TopFiles.Add($entry) }
if ($TopFiles.Count -gt $TopN) { $TopFiles.RemoveAt($TopFiles.Count - 1) }
}
if ($ExportFull) { [void]$FullFiles.Add($entry) }
}
}
if ($ExportFull -and $FullFiles.Count -gt 0) {
$sortedFull = $FullFiles | Sort-Object -Property SizeBytes -Descending
$sw = [System.IO.StreamWriter]::new((Join-Path $TempLocation $ExportedFilesCSV), $false, [System.Text.Encoding]::UTF8)
try {
$sw.WriteLine("Name,Size (Bytes)")
foreach ($f in $sortedFull) { $sw.WriteLine('"{0}",{1}' -f ($f.Name -replace '"','""'), $f.SizeBytes) }
} finally { $sw.Close() }
}
if ($TopFiles.Count -gt 0) {
$sb = New-Object System.Text.StringBuilder
$sb.AppendLine('<table class="w3-table w3-striped">') | Out-Null
$sb.AppendLine('<tr><th>Name</th><th>Size (Bytes)</th><th>Size (KB)</th><th>Size (MB)</th><th>Size (GB)</th></tr>') | Out-Null
$count = 0
foreach ($f in $TopFiles) {
$count++
if ($count -gt $TopN) { break }
if ($KB -and $KB -ne 0) { $kb = [math]::Round(($f.SizeBytes / $KB),2) } else { $kb = 0 }
if ($MB -and $MB -ne 0) { $mb = [math]::Round(($f.SizeBytes / $MB),2) } else { $mb = 0 }
if ($GB -and $GB -ne 0) { $gb = [math]::Round(($f.SizeBytes / $GB),2) } else { $gb = 0 }
$nameEsc = ($f.Name -replace '"','&quot;')
if ([string]::IsNullOrWhiteSpace($nameEsc)) { $nameEsc = '&lt;Unnamed&gt;' }
$sb.AppendLine("<tr><td>$nameEsc</td><td>$([string]::Format('{0:N0}',$f.SizeBytes))</td><td>$kb</td><td>$mb</td><td>$gb</td></tr>") | Out-Null
}
$sb.AppendLine('</table>') | Out-Null
if ($DryRun) {
Log "DRYRUN: Would generate files HTML (Top $TopN) at: $(Join-Path $TempLocation $ExportedFilesHTML)"
} else {
$htmlBody = "<h2>Top $TopN largest files on $env:COMPUTERNAME</h2>`n" + $sb.ToString()
$htmlFull = ConvertTo-Html -Head $Style -Body $htmlBody -CssUri 'http://www.w3schools.com/lib/w3.css'
$htmlFull | Out-String | Out-File (Join-Path $TempLocation $ExportedFilesHTML)
}
} else { Log "No top files collected." }
}
#endregion
#region Folders
# Robust read of Folders CSV (header detection & temporary cleaned CSV like Files)
$FoldersCsvPathClean = $null
if (Test-Path -Path $FoldersCsvPath) {
try {
$rawLines = Get-Content -Path $FoldersCsvPath -ReadCount 0
if ($rawLines.Count -eq 0) { $CSVContent = @() }
else {
$headerPattern = '(Dateiname|Datei|Name|Path|File|Größe|Groesse|Size|Bytes|Ordner|Folder)'
$headerIndex = -1
for ($i = 0; $i -lt $rawLines.Count; $i++) {
if ($rawLines[$i] -match ',') {
if ($rawLines[$i] -match $headerPattern) { $headerIndex = $i; break }
}
}
if ($headerIndex -lt 0) {
for ($i = 0; $i -lt $rawLines.Count; $i++) { if ($rawLines[$i] -match ',') { $headerIndex = $i; break } }
}
if ($headerIndex -lt 0) {
Log "No header-like line found in Folders CSV"
$CSVContent = @()
} else {
Trace-Log "Folders CSV header detected at line $headerIndex"
$cleanLines = $rawLines[$headerIndex..($rawLines.Count - 1)]
$tmp = [System.IO.Path]::GetTempFileName() + '.csv'
Set-Content -Path $tmp -Value $cleanLines -Encoding UTF8
$FoldersCsvPathClean = $tmp
$CSVContent = Get-Content -Path $FoldersCsvPathClean -ReadCount 0
Log "Using cleaned Folders CSV: $FoldersCsvPathClean"
}
}
} catch { Log "Failed to read Folders CSV: $_"; $CSVContent = @() }
} else {
Log "Folders CSV not found: $(Join-Path $TempLocation $FoldersCSV) - skipping folders section"
$CSVContent = @()
}
# Process folders CSV using ConvertFrom-Csv and keep only TopN in memory
if ($CSVContent.Count -eq 0) {
Log "No folder CSV content to process."
$TopFolders = @()
} else {
if ($UseTextFieldParser -and (Test-Path $FoldersCsvPath)) {
Trace-Log "Using TextFieldParser for folders CSV"
try {
[void][Reflection.Assembly]::LoadWithPartialName('Microsoft.VisualBasic')
$parserPath = if ($FoldersCsvPathClean) { $FoldersCsvPathClean } else { $FoldersCsvPath }
$parser = New-Object Microsoft.VisualBasic.FileIO.TextFieldParser($parserPath)
$parser.TextFieldType = 'Delimited'
$parser.SetDelimiters(',')
$parser.HasFieldsEnclosedInQuotes = $true
$headers = $parser.ReadFields()
$headerIndex = @{}
for ($h=0;$h -lt $headers.Length;$h++) { $headerIndex[$headers[$h]] = $h }
# Detect columns using German/English tokens
$nameProp = ($headers | Where-Object { $_ -match 'Dateiname|Datei|Name|Path|File' } | Select-Object -First 1)
if (-not $nameProp) { $nameProp = $headers[0] }
$sizeProp = ($headers | Where-Object { $_ -match 'Größe|Groesse|Belegt|Size|Bytes' } | Select-Object -First 1)
if (-not $sizeProp) { $sizeProp = ($headers | Where-Object { $_ -match 'Size|Bytes' } | Select-Object -First 1) }
if (-not $sizeProp) { $sizeProp = $headers[1] }
$filesProp = ($headers | Where-Object { $_ -match 'Dateien|Files|File' } | Select-Object -First 1)
$foldersProp = ($headers | Where-Object { $_ -match 'Ordner|Folders|Fold' } | Select-Object -First 1)
$TopFolders = New-Object System.Collections.ArrayList
if ($ExportFull) { $FullFolders = New-Object System.Collections.ArrayList }
while (-not $parser.EndOfData) {
$fields = $parser.ReadFields()
$raw = ''
try { $raw = ($fields[$headerIndex[$sizeProp]] -as [string]) -replace '[^\d-]', '' } catch { $raw = '0' }
$bytes = 0
try { $bytes = [int64]$raw } catch { $bytes = 0 }
$name = ''
try { $name = ($fields[$headerIndex[$nameProp]] -as [string]) } catch { $name = '' }
$filesCount = '' ; $foldersCount = ''
try { if ($filesProp) { $filesCount = ($fields[$headerIndex[$filesProp]] -as [string]) } } catch {}
try { if ($foldersProp) { $foldersCount = ($fields[$headerIndex[$foldersProp]] -as [string]) } } catch {}
$entry = [pscustomobject]@{ Name = $name; SizeBytes = $bytes; Files = $filesCount; Folders = $foldersCount }
if ($TopFolders.Count -eq 0) { [void]$TopFolders.Add($entry) }
else {
$inserted = $false
for ($i = 0; $i -lt $TopFolders.Count; $i++) {
if ($bytes -gt $TopFolders[$i].SizeBytes) { [void]$TopFolders.Insert($i, $entry); $inserted = $true; break }
}
if (-not $inserted) { [void]$TopFolders.Add($entry) }
if ($TopFolders.Count -gt $TopN) { $TopFolders.RemoveAt($TopFolders.Count - 1) }
}
if ($ExportFull) { [void]$FullFolders.Add($entry) }
}
} finally { if ($parser) { $parser.Close() } }
} else {
try {
$csvRecords = $CSVContent -join "`n" | ConvertFrom-Csv -ErrorAction Stop
} catch {
Log "Failed to parse Folders CSV into objects: $_"
$csvRecords = @()
}
$props = ($csvRecords | Get-Member -MemberType NoteProperty | Select-Object -ExpandProperty Name)
$nameProp = ($props | Where-Object { $_ -match 'Dateiname|Datei|Name|Path|File' } | Select-Object -First 1)
if (-not $nameProp) { $nameProp = $props | Select-Object -First 1 }
$sizeProp = ($props | Where-Object { $_ -match 'Größe|Groesse|Belegt|Size|Bytes' } | Select-Object -First 1)
if (-not $sizeProp) { $sizeProp = ($props | Where-Object { $_ -match 'Size|Bytes' } | Select-Object -First 1) }
if (-not $sizeProp) { $sizeProp = $props[1] }
$filesProp = ($props | Where-Object { $_ -match 'Dateien|Files|File' } | Select-Object -First 1)
$foldersProp = ($props | Where-Object { $_ -match 'Ordner|Folders|Fold' } | Select-Object -First 1)
$TopFolders = New-Object System.Collections.ArrayList
if ($ExportFull) { $FullFolders = New-Object System.Collections.ArrayList }
foreach ($rec in $csvRecords) {
$raw = ''
try { $raw = ($rec.$sizeProp -as [string]) -replace '[^\d-]', '' } catch { $raw = '0' }
$bytes = 0
try { $bytes = [int64]$raw } catch { $bytes = 0 }
$name = ''
try { $name = ($rec.$nameProp -as [string]) } catch { $name = '' }
$filesCount = '' ; $foldersCount = ''
try { if ($filesProp) { $filesCount = ($rec.$filesProp -as [string]) } } catch {}
try { if ($foldersProp) { $foldersCount = ($rec.$foldersProp -as [string]) } } catch {}
$entry = [pscustomobject]@{ Name = $name; SizeBytes = $bytes; Files = $filesCount; Folders = $foldersCount }
if ($TopFolders.Count -eq 0) { [void]$TopFolders.Add($entry) }
else {
$inserted = $false
for ($i = 0; $i -lt $TopFolders.Count; $i++) {
if ($bytes -gt $TopFolders[$i].SizeBytes) { [void]$TopFolders.Insert($i, $entry); $inserted = $true; break }
}
if (-not $inserted) { [void]$TopFolders.Add($entry) }
if ($TopFolders.Count -gt $TopN) { $TopFolders.RemoveAt($TopFolders.Count - 1) }
}
if ($ExportFull) { [void]$FullFolders.Add($entry) }
}
}
if ($ExportFull -and $FullFolders.Count -gt 0) {
$sortedFull = $FullFolders | Sort-Object -Property SizeBytes -Descending
$sw = [System.IO.StreamWriter]::new((Join-Path $TempLocation $ExportedFoldersCSV), $false, [System.Text.Encoding]::UTF8)
try {
$sw.WriteLine("Name,Size (Bytes),Files,Folders")
foreach ($f in $sortedFull) { $sw.WriteLine('"{0}",{1},"{2}","{3}"' -f ($f.Name -replace '"','""'), $f.SizeBytes, ($f.Files -replace '"','""'), ($f.Folders -replace '"','""')) }
} finally { $sw.Close() }
}
if ($TopFolders.Count -gt 0) {
$sb = New-Object System.Text.StringBuilder
$sb.AppendLine('<table class="w3-table w3-striped">') | Out-Null
$sb.AppendLine('<tr><th>Name</th><th>Size (Bytes)</th><th>Size (KB)</th><th>Size (MB)</th><th>Size (GB)</th><th>Files</th><th>Folders</th></tr>') | Out-Null
$count = 0
foreach ($f in $TopFolders) {
$count++
if ($count -gt $TopN) { break }
if ($KB -and $KB -ne 0) { $kb = [math]::Round(($f.SizeBytes / $KB),2) } else { $kb = 0 }
if ($MB -and $MB -ne 0) { $mb = [math]::Round(($f.SizeBytes / $MB),2) } else { $mb = 0 }
if ($GB -and $GB -ne 0) { $gb = [math]::Round(($f.SizeBytes / $GB),2) } else { $gb = 0 }
$nameEsc = ($f.Name -replace '"','&quot;')
$sb.AppendLine("<tr><td>$nameEsc</td><td>$($f.SizeBytes)</td><td>$kb</td><td>$mb</td><td>$gb</td><td>$($f.Files)</td><td>$($f.Folders)</td></tr>") | Out-Null
}
$sb.AppendLine('</table>') | Out-Null
if ($DryRun) {
Log "DRYRUN: Would generate folders HTML (Top $TopN) at: $(Join-Path $TempLocation $ExportedFoldersHTML)"
} else {
$htmlBody = "<h2>Top $TopN largest directories on $env:COMPUTERNAME</h2>`n" + $sb.ToString()
$htmlFull = ConvertTo-Html -Head $Style -Body $htmlBody -CssUri 'http://www.w3schools.com/lib/w3.css'
$htmlFull | Out-String | Out-File (Join-Path $TempLocation $ExportedFoldersHTML)
}
} else { Log "No top folders collected." }
}
#endregion
#region Create HTML disk usage summary report
# Get system drive data
$WMIDiskInfo = Get-CimInstance -ClassName Win32_Volume -Property Capacity,FreeSpace,DriveLetter |
Where-Object { $_.DriveLetter -eq $env:SystemDrive } |
Select-Object Capacity,FreeSpace,DriveLetter
$DiskInfo = [pscustomobject]@{
DriveLetter = $WMIDiskInfo.DriveLetter
'Capacity (GB)' = [math]::Round(($WMIDiskInfo.Capacity / 1GB),2)
'FreeSpace (GB)' = [math]::Round(($WMIDiskInfo.FreeSpace / 1GB),2)
'UsedSpace (GB)' = [math]::Round((($WMIDiskInfo.Capacity / 1GB) - ($WMIDiskInfo.FreeSpace / 1GB)),2)
'Percent Free' = [math]::Round(($WMIDiskInfo.FreeSpace * 100 / $WMIDiskInfo.Capacity),2)
'Percent Used' = [math]::Round((($WMIDiskInfo.Capacity - $WMIDiskInfo.FreeSpace) * 100 / $WMIDiskInfo.Capacity),2)
}
# Create html header
$html = @"
<!DOCTYPE html>
<html>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="http://www.w3schools.com/lib/w3.css">
<body>
"@
# Set html
$html = $html + @"
<h2>Disk Space Usage for Drive $($DiskInfo.DriveLetter) on $env:COMPUTERNAME</h2>
<table cellpadding="0" cellspacing="0" width="700">
<tr>
<td style="background-color:$(Set-PercentageColour -Value $($DiskInfo.'Percent Used'));padding:10px;color:#ffffff;" width="$($DiskInfo.'Percent Used')%">
$($DiskInfo.'UsedSpace (GB)') GB ($($DiskInfo.'Percent Used') %)
</td>
<td style="background-color:#eeeeee;padding-top:10px;padding-bottom:10px;color:#333333;" width="$($DiskInfo.'Percent Used')%">
</td>
</tr>
</table>
<table cellpadding="0" cellspacing="0" width="700">
<tr>
<td style="padding:5px;" width="80%">
Capacity: $($DiskInfo.'Capacity (GB)') GB
</td>
</tr>
<tr>
<td style="padding:5px;" width="80%">
FreeSpace: $($DiskInfo.'FreeSpace (GB)') GB
</td>
</tr>
<tr>
<td style="padding:5px;" width="80%">
Percent Free: $($DiskInfo.'Percent Free') %
</td>
</tr>
</table>
"@
If ($DiskInfo.'FreeSpace (GB)' -lt 20)
{
$html = $html + @"
<table cellpadding="0" cellspacing="0" width="700">
<tr>
<td style="padding:5px;color:red;font-weight:bold" width="80%">
You need to free $(20 - $DiskInfo.'FreeSpace (GB)') GB on this disk to pass the W10 readiness check!
</td>
</tr>
</table>
"@
}
# Close html document
$html = $html + @"
</body>
</html>
"@
# Export to file
if ($DryRun) {
Log "DRYRUN: Would export summary HTML to: $(Join-Path $TempLocation $SummaryHTMLReport)"
} else {
$html | Out-string | Out-File (Join-Path $TempLocation $SummaryHTMLReport)
}
#endregion
#region Copy files to share
# Create a subfolder with computername if doesn't exist
If (!(Test-Path $TargetRoot\$env:COMPUTERNAME))
{
if ($DryRun) {
Log "DRYRUN: Would create share subfolder: $(Join-Path $TargetRoot $env:COMPUTERNAME)"
} else {
$null = New-Item -Path $TargetRoot -Name $env:COMPUTERNAME -ItemType Directory
}
}
# Create a subdirectory with current date-time
$DateString = ((Get-Date).ToUniversalTime() | Get-Date -Format "yyyy-MM-dd_HH-mm-ss").ToString()
If (!(Test-Path $TargetRoot\$env:COMPUTERNAME\$DateString))
{
if ($DryRun) {
Log "DRYRUN: Would create share date subfolder: $(Join-Path $TargetRoot $env:COMPUTERNAME)\$DateString"
} else {
$null = New-Item -Path $TargetRoot\$env:COMPUTERNAME -Name $DateString -ItemType Directory
}
}
# Set final target location
$TargetLocation = "$TargetRoot\$env:COMPUTERNAME\$DateString"
# Copy files
$Files = @(
$ExportedFilesCSV
$ExportedFoldersCSV
$ExportedFilesHTML
$ExportedFoldersHTML
$SummaryHTMLReport
)
Try {
$existingFiles = $Files | Where-Object { Test-Path -Path (Join-Path $TempLocation $_) }
if ($existingFiles.Count -gt 0) {
if ($DryRun) {
Log "DRYRUN: Would copy files to share ${TargetLocation}: $($existingFiles -join ', ')"
} else {
Robocopy $TempLocation $TargetLocation $existingFiles /R:10 /W:5 /NP | Out-Null
}
} else {
Log "No exported report files present to copy to share."
}
} Catch { Log "Robocopy failed: $_" }
#endregion
# Cleanup temp files
$Files = @(
$FilesCSV
$FoldersCSV
$ExportedFilesCSV
$ExportedFoldersCSV
$ExportedFilesHTML
$ExportedFoldersHTML
$SummaryHTMLReport
)
Foreach ($file in $files) {
$full = Join-Path $TempLocation $file
if (Test-Path -Path $full) {
if ($DryRun) {
Log "DRYRUN: Would remove file: $full"
} else {
try { Remove-Item -Path $full -Force -ErrorAction Stop } catch { Log "Failed to remove ${full}: $_" }
}
}
}
# Remove any temporary cleaned CSV we created
if ($FilesCsvPathClean) {
if (Test-Path -Path $FilesCsvPathClean) {
if ($DryRun) { Log "DRYRUN: Would remove temp cleaned CSV: $FilesCsvPathClean" } else { try { Remove-Item -Path $FilesCsvPathClean -Force -ErrorAction Stop } catch { Log "Failed to remove temp cleaned CSV: $_" } }
}
}
# Remove temp cleaned Folders CSV if created
if ($FoldersCsvPathClean) {
if (Test-Path -Path $FoldersCsvPathClean) {
if ($DryRun) { Log "DRYRUN: Would remove temp cleaned CSV: $FoldersCsvPathClean" } else { try { Remove-Item -Path $FoldersCsvPathClean -Force -ErrorAction Stop } catch { Log "Failed to remove temp cleaned Folders CSV: $_" } }
}
}
# End of script - do not forcibly close the host/console. Return 0 to caller.
return 0
# Force a code 0 on exit, in case of some non-terminating error.
ExitWithCode 0
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment