Forked from SMSAgentSoftware/New-WizTreeDiskUsageReport.ps1
Last active
November 8, 2025 21:40
-
-
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # 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 '"','"') | |
| if ([string]::IsNullOrWhiteSpace($nameEsc)) { $nameEsc = '<Unnamed>' } | |
| $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 '"','"') | |
| $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