refactor(scripts): optimize system remediation and M365 export scripts
- teams: harden Classic Teams removal, protect New Teams, and clean HKU run keys - dotnet: add bootstrapper EXE uninstall support and remove orphaned 3.1.* folders - teams: add MSIX package targeting and version parsing for vulnerable New Teams builds - m365: improve Graph SDK export speed, reuse sessions, and add null-safe handling
This commit is contained in:
1 parent
4fd95b76d6
commit
5ef82842f2
19 files changed
+1651
-1268
No files matched your search
Executable
+296
@@ -0,0 +1,296 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Generates interactive HTML and plain-text directory tree reports.
|
||||
|
||||
.DESCRIPTION
|
||||
Recursively scans a target directory and produces two report files:
|
||||
1. A plain-text ASCII directory tree file (.txt).
|
||||
2. An interactive HTML tree file (.html) with search filtering and collapse/expand controls.
|
||||
|
||||
.PARAMETER RootPath
|
||||
The target root folder path to scan.
|
||||
|
||||
.PARAMETER OutputDirectory
|
||||
The destination directory where reports will be saved. Defaults to the current working directory.
|
||||
|
||||
.PARAMETER ReportNamePrefix
|
||||
Optional custom prefix for generated filenames. Defaults to the sanitized root folder name.
|
||||
|
||||
.PARAMETER OpenInBrowser
|
||||
Switch parameter to automatically open the generated HTML report upon completion.
|
||||
|
||||
.EXAMPLE
|
||||
.\Export-DirectoryTree.ps1 -RootPath "E:\Sync Method\Corporate\Documents" -OutputDirectory "C:\Reports" -OpenInBrowser
|
||||
|
||||
.NOTES
|
||||
Created: 2025-03-02
|
||||
Modified: 2026-10-04
|
||||
#>
|
||||
|
||||
[CmdletBinding()]
|
||||
param (
|
||||
[Parameter(Mandatory = $true, Position = 0, ValueFromPipeline =$true)]
|
||||
[ValidateNotNullOrEmpty()]
|
||||
[string]$RootPath,
|
||||
|
||||
[Parameter(Mandatory = $false, Position = 1)] [string]$OutputDirectory = (Get-Location).Path,
|
||||
|
||||
[Parameter(Mandatory = $false)]
|
||||
[string]$ReportNamePrefix,
|
||||
|
||||
[switch]$OpenInBrowser
|
||||
)
|
||||
|
||||
# 1. Validate & Resolve Paths
|
||||
if (-not (Test-Path -LiteralPath $RootPath -PathType Container)) {
|
||||
Write-Error "Target root directory not found or unreadable: '$RootPath'"
|
||||
return
|
||||
}
|
||||
|
||||
$ResolvedRoot = (Get-Item -LiteralPath$RootPath).FullName
|
||||
$RootLeafName = Split-Path -Path$ResolvedRoot -Leaf
|
||||
|
||||
# Auto-generate report prefix if not explicitly provided
|
||||
if ([string]::IsNullOrWhiteSpace($ReportNamePrefix)) {
|
||||
$ReportNamePrefix =$RootLeafName -replace '[\/\\:\*\?"<>\|]', '-'
|
||||
}
|
||||
|
||||
# Ensure output directory exists
|
||||
if (-not (Test-Path -LiteralPath $OutputDirectory)) {
|
||||
New-Item -ItemType Directory -Path $OutputDirectory -Force | Out-Null
|
||||
}
|
||||
|
||||
$TextFile = Join-Path -Path $OutputDirectory -ChildPath "$ReportNamePrefix-tree.txt"
|
||||
$HtmlFile = Join-Path -Path $OutputDirectory -ChildPath "$ReportNamePrefix-tree.html"
|
||||
|
||||
# 2. State Tracking Setup
|
||||
$script:textLines = [System.Collections.Generic.List[string]]::new()
|
||||
$script:html = [System.Text.StringBuilder]::new()$script:scanErrors = 0
|
||||
$script:totalFiles = 0$script:totalFolders = 0
|
||||
|
||||
function ConvertTo-HtmlEncoded {
|
||||
param([string]$Text)
|
||||
return [System.Net.WebUtility]::HtmlEncode($Text)
|
||||
}
|
||||
|
||||
function Add-TreeItems {
|
||||
param(
|
||||
[string]$Path,
|
||||
[string]$Prefix
|
||||
)
|
||||
|
||||
try {
|
||||
$items = @(
|
||||
Get-ChildItem -LiteralPath $Path -Force -ErrorAction Stop |
|
||||
Sort-Object @{ Expression = { if ($_.PSIsContainer) { 0 } else { 1 } } }, Name
|
||||
)
|
||||
}
|
||||
catch {
|
||||
$script:scanErrors++
|
||||
[void]$script:textLines.Add("$Prefix[Unable to read: $($_.Exception.Message)]")
|
||||
[void]$script:html.AppendLine(
|
||||
"<li class='error'>Unable to read: $(ConvertTo-HtmlEncoded$_.Exception.Message)</li>"
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
for ($index = 0; $index -lt $items.Count; $index++) {
|
||||
$item =$items[$index]$isLast = $index -eq ($items.Count - 1)
|
||||
$connector = if ($isLast) { '\-- ' } else { '|-- ' }
|
||||
$nextPrefix =$Prefix + $(if ($isLast) { ' ' } else { '| ' })
|
||||
|
||||
$nameHtml = ConvertTo-HtmlEncoded$item.Name
|
||||
$pathHtml = ConvertTo-HtmlEncoded$item.FullName
|
||||
$isLink = -not [string]::IsNullOrWhiteSpace([string]$item.LinkType)
|
||||
|
||||
if ($item.PSIsContainer -and -not $isLink) {$script:totalFolders++
|
||||
[void]$script:textLines.Add("$Prefix$connector$($item.Name)")
|
||||
[void]$script:html.AppendLine(
|
||||
"<li><details data-path='$pathHtml'><summary>📁 $nameHtml</summary><ul>"
|
||||
)
|
||||
|
||||
# Recurse subdirectories
|
||||
Add-TreeItems -Path $item.FullName -Prefix$nextPrefix
|
||||
|
||||
[void]$script:html.AppendLine('</ul></details></li>')
|
||||
}
|
||||
elseif ($item.PSIsContainer -and $isLink) {$script:totalFolders++
|
||||
$target = @($item.Target) -join ', '
|
||||
[void]$script:textLines.Add("$Prefix$connector$($item.Name) [Link → $target]")
|
||||
[void]$script:html.AppendLine(
|
||||
"<li class='link' data-path='$pathHtml'>🔗 $nameHtml <small>→ $(ConvertTo-HtmlEncoded$target)</small></li>"
|
||||
)
|
||||
}
|
||||
else {
|
||||
$script:totalFiles++
|
||||
[void]$script:textLines.Add("$Prefix$connector$($item.Name)")
|
||||
[void]$script:html.AppendLine(
|
||||
"<li class='file' data-path='$pathHtml'>📄 $nameHtml</li>"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# 3. Document Building
|
||||
Write-Host "Scanning directory structure: $ResolvedRoot" -ForegroundColor Cyan
|
||||
|
||||
$rootNameHtml = ConvertTo-HtmlEncoded $RootLeafName$rootPathHtml = ConvertTo-HtmlEncoded $ResolvedRoot$generated = Get-Date -Format 'yyyy-MM-dd HH:mm:ss'
|
||||
|
||||
[void]$script:textLines.Add($ResolvedRoot)
|
||||
|
||||
[void]$script:html.AppendLine(@"
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>Directory Tree - $rootNameHtml</title>
|
||||
<style>
|
||||
:root {
|
||||
--bg-color: #ffffff;
|
||||
--text-color: #1f2328;
|
||||
--border-color: #d0d7de;
|
||||
--link-color: #8250df;
|
||||
--error-color: #cf222e;
|
||||
--subtext-color: #57606a;
|
||||
}
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||
margin: 24px;
|
||||
color: var(--text-color);
|
||||
background: var(--bg-color);
|
||||
}
|
||||
h1 { margin-bottom: 6px; font-size: 1.5rem; }
|
||||
.meta { color: var(--subtext-color); margin-bottom: 18px; font-size: 0.9rem; line-height: 1.4; }
|
||||
.controls {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
background: var(--bg-color);
|
||||
padding: 10px 0;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
z-index: 10;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
input[type="search"] {
|
||||
width: min(500px, 60vw);
|
||||
padding: 6px 10px;
|
||||
font-size: 14px;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 6px;
|
||||
}
|
||||
button {
|
||||
padding: 6px 12px;
|
||||
font-size: 14px;
|
||||
background-color: #f6f8fa;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
}
|
||||
button:hover { background-color: #f3f4f6; }
|
||||
ul {
|
||||
list-style: none;
|
||||
margin: 4px 0 4px 16px;
|
||||
padding-left: 12px;
|
||||
border-left: 1px solid #d8dee4;
|
||||
}
|
||||
.tree { border-left: 0; margin-left: 0; padding-left: 0; }
|
||||
summary { cursor: pointer; padding: 3px; font-weight: 500; }
|
||||
summary:hover { background-color: #f6f8fa; border-radius: 4px; }
|
||||
.file, .link { padding: 3px; }
|
||||
.link { color: var(--link-color); }
|
||||
.error { color: var(--error-color); }
|
||||
small { color: var(--subtext-color); }
|
||||
.hidden { display: none !important; }
|
||||
code { background: #f6f8fa; padding: 2px 6px; border-radius: 4px; font-size: 0.85em; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Directory Tree Report</h1>
|
||||
<div class="meta">
|
||||
<strong>Root:</strong> <code>$rootPathHtml</code><br>
|
||||
<strong>Generated:</strong> $generated
|
||||
</div>
|
||||
<div class="controls">
|
||||
<input id="search" type="search" placeholder="Search files and folders...">
|
||||
<button type="button" onclick="setAll(true)">Expand All</button>
|
||||
<button type="button" onclick="setAll(false)">Collapse All</button>
|
||||
</div>
|
||||
<ul class="tree">
|
||||
<li>
|
||||
<details open data-path="$rootPathHtml">
|
||||
<summary>📁 $rootNameHtml</summary>
|
||||
<ul>
|
||||
"@)
|
||||
|
||||
# Run recursive scan
|
||||
Add-TreeItems -Path $ResolvedRoot -Prefix ''
|
||||
|
||||
[void]$script:html.AppendLine(@"
|
||||
</ul>
|
||||
</details>
|
||||
</li>
|
||||
</ul>
|
||||
<script>
|
||||
function setAll(state) {
|
||||
document.querySelectorAll('details').forEach(function (item) {
|
||||
item.open = state;
|
||||
});
|
||||
}
|
||||
|
||||
document.getElementById('search').addEventListener('input', function () {
|
||||
const term = this.value.trim().toLowerCase();
|
||||
const nodes = Array.from(document.querySelectorAll('[data-path]'));
|
||||
|
||||
nodes.forEach(function (node) {
|
||||
node.classList.remove('hidden');
|
||||
});
|
||||
|
||||
if (!term) return;
|
||||
|
||||
nodes.forEach(function (node) {
|
||||
if (!node.dataset.path.toLowerCase().includes(term)) {
|
||||
node.classList.add('hidden');
|
||||
}
|
||||
});
|
||||
|
||||
nodes.filter(function (node) {
|
||||
return node.dataset.path.toLowerCase().includes(term);
|
||||
}).forEach(function (node) {
|
||||
node.classList.remove('hidden');
|
||||
let parent = node.parentElement;
|
||||
|
||||
while (parent) {
|
||||
if (parent.tagName === 'DETAILS') {
|
||||
parent.open = true;
|
||||
parent.classList.remove('hidden');
|
||||
}
|
||||
if (parent.tagName === 'LI') {
|
||||
parent.classList.remove('hidden');
|
||||
}
|
||||
parent = parent.parentElement;
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
"@)
|
||||
|
||||
# 4. Save Output Files
|
||||
$script:textLines \vert{} Set-Content -LiteralPath$TextFile -Encoding UTF8
|
||||
$script:html.ToString() \vert{} Set-Content -LiteralPath$HtmlFile -Encoding UTF8
|
||||
|
||||
# Output Summary
|
||||
Write-Host "`nScan Completed Successfully:" -ForegroundColor Green
|
||||
Write-Host " Folders Scanned : $script:totalFolders"
|
||||
Write-Host " Files Cataloged : $script:totalFiles"
|
||||
Write-Host " Unreadable Errors: $script:scanErrors"
|
||||
Write-Host " Text Report : $TextFile"
|
||||
Write-Host " HTML Report : $HtmlFile`n"
|
||||
|
||||
if ($OpenInBrowser) {
|
||||
Start-Process $HtmlFile
|
||||
}
|
||||
Reference in new issue
Block a user