217 lines
9.5 KiB
PowerShell
217 lines
9.5 KiB
PowerShell
<#
|
|
.SYNOPSIS
|
|
Captures the layout of a Media Creator Lite cache, plus its small
|
|
control files, so the PXE import can be checked against what the
|
|
current MCL actually writes.
|
|
|
|
.DESCRIPTION
|
|
Run on the PC where Media Creator Lite just FINISHED a run. Run it once
|
|
after the Windows 11 Pro run and once after the Windows 11 LTSC run,
|
|
before starting the next MCL run (MCL deletes the cache when it starts).
|
|
|
|
.\Capture-MclCache.ps1 -Label pro
|
|
.\Capture-MclCache.ps1 -Label ltsc
|
|
|
|
It writes, under the Desktop:
|
|
mcl-capture-<label>-<timestamp>\
|
|
tree.csv every file: path, size, last write time (UTC)
|
|
summary.txt tag files and their contents, file counts, totals
|
|
files\ copies of the small text files (*.json, *.xml,
|
|
*.tag, *.txt, *.ini, *.log, *.cmd, *.ps1, *.bat)
|
|
under 5 MB, same relative paths
|
|
SHA256SUMS hash of every file above, for checking the copy
|
|
|
|
Then it robocopies that folder to -CopyTo (default Z:\mcl-capture, the
|
|
dev box pxe-images share), hashes every copied file back through the
|
|
share and compares with SHA256SUMS. No zip: archives arrive empty on
|
|
that share. The dev box can re-check with: sha256sum -c SHA256SUMS
|
|
|
|
Nothing large is copied: no WIM, driver, MSU or installer. Text copies
|
|
have any Azure SAS signature (sig=...) blanked, so the capture carries
|
|
no storage credential. Read-only on the cache; needs no admin rights.
|
|
|
|
.PARAMETER Label
|
|
Short name for this run, for example pro or ltsc.
|
|
|
|
.PARAMETER Root
|
|
The Media Creator folder. Default C:\ProgramData\GEAerospace\MediaCreator.
|
|
|
|
.PARAMETER OutDir
|
|
Where to write the capture. Default: the current user's Desktop, or
|
|
this script's folder when there is no Desktop.
|
|
|
|
.PARAMETER CopyTo
|
|
Folder to copy the capture into and verify. Default Z:\mcl-capture.
|
|
Pass -CopyTo none to keep it on this PC only.
|
|
#>
|
|
param(
|
|
[Parameter(Mandatory = $true)][ValidatePattern('^[A-Za-z0-9_-]{1,20}$')][string]$Label,
|
|
[string]$Root = "C:\ProgramData\GEAerospace\MediaCreator",
|
|
[string]$OutDir = [Environment]::GetFolderPath("Desktop"),
|
|
[string]$CopyTo = "Z:\mcl-capture"
|
|
)
|
|
$ErrorActionPreference = "Stop"
|
|
if (-not $OutDir) { $OutDir = $PSScriptRoot } # no Desktop (e.g. run as SYSTEM)
|
|
|
|
if (-not (Test-Path -LiteralPath $Root -PathType Container)) {
|
|
Write-Host "Media Creator folder not found: $Root" -ForegroundColor Red
|
|
exit 1
|
|
}
|
|
$Root = (Resolve-Path -LiteralPath $Root).ProviderPath.TrimEnd('\')
|
|
|
|
$stamp = Get-Date -Format "yyyyMMdd-HHmmss"
|
|
$dest = Join-Path $OutDir "mcl-capture-$Label-$stamp"
|
|
$filesDir = Join-Path $dest "files"
|
|
New-Item -ItemType Directory -Path $filesDir -Force | Out-Null
|
|
|
|
$textExt = @(".json", ".xml", ".tag", ".txt", ".ini", ".log", ".cmd", ".ps1", ".bat")
|
|
$maxCopy = 5MB
|
|
# Azure SAS signature in a URL or connection string.
|
|
$sasPattern = '(?i)(sig=)[^&"''\s<]+'
|
|
|
|
Write-Host "Reading $Root ..."
|
|
$all = @(Get-ChildItem -LiteralPath $Root -Recurse -Force -File -ErrorAction SilentlyContinue)
|
|
|
|
$rows = foreach ($f in $all) {
|
|
[pscustomobject]@{
|
|
Path = $f.FullName.Substring($Root.Length + 1)
|
|
Length = $f.Length
|
|
LastWriteUtc = $f.LastWriteTimeUtc.ToString("yyyy-MM-ddTHH:mm:ssZ")
|
|
}
|
|
}
|
|
$rows | Sort-Object Path | Export-Csv -LiteralPath (Join-Path $dest "tree.csv") -NoTypeInformation -Encoding UTF8
|
|
|
|
$copied = 0
|
|
$redacted = 0
|
|
$skippedBig = New-Object System.Collections.Generic.List[string]
|
|
$unreadable = New-Object System.Collections.Generic.List[string]
|
|
foreach ($f in $all) {
|
|
if ($textExt -notcontains $f.Extension.ToLower()) { continue }
|
|
$rel = $f.FullName.Substring($Root.Length + 1)
|
|
if ($f.Length -gt $maxCopy) { $skippedBig.Add("$rel ($($f.Length) bytes)"); continue }
|
|
$target = Join-Path $filesDir $rel
|
|
New-Item -ItemType Directory -Path (Split-Path $target -Parent) -Force | Out-Null
|
|
# Share mode lets this read the log MCL still has open.
|
|
try {
|
|
$fs = New-Object System.IO.FileStream($f.FullName, [System.IO.FileMode]::Open,
|
|
[System.IO.FileAccess]::Read, ([System.IO.FileShare]::ReadWrite -bor [System.IO.FileShare]::Delete))
|
|
try {
|
|
$bytes = New-Object byte[] $fs.Length
|
|
$read = 0
|
|
while ($read -lt $bytes.Length) {
|
|
$n = $fs.Read($bytes, $read, $bytes.Length - $read)
|
|
if ($n -le 0) { break }
|
|
$read += $n
|
|
}
|
|
if ($read -lt $bytes.Length) { $bytes = $bytes[0..($read - 1)] }
|
|
} finally { $fs.Dispose() }
|
|
} catch {
|
|
$unreadable.Add("$rel ($($_.Exception.InnerException.Message))")
|
|
continue
|
|
}
|
|
$text = [System.Text.Encoding]::UTF8.GetString($bytes)
|
|
if ($text -match $sasPattern) {
|
|
$text = [regex]::Replace($text, $sasPattern, '${1}REDACTED')
|
|
$bytes = [System.Text.Encoding]::UTF8.GetBytes($text)
|
|
$redacted++
|
|
}
|
|
[System.IO.File]::WriteAllBytes($target, $bytes)
|
|
$copied++
|
|
}
|
|
|
|
$tags = @($all | Where-Object { $_.Extension -eq ".tag" })
|
|
$sum = New-Object System.Collections.Generic.List[string]
|
|
$sum.Add("Media Creator Lite cache capture")
|
|
$sum.Add("Label: $Label")
|
|
$sum.Add("Captured: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss zzz')")
|
|
$sum.Add("Computer: $env:COMPUTERNAME")
|
|
$sum.Add("Root: $Root")
|
|
$sum.Add("PowerShell $($PSVersionTable.PSVersion)")
|
|
$sum.Add("")
|
|
$sum.Add("Files: $($all.Count), total $([math]::Round((($all | Measure-Object Length -Sum).Sum) / 1GB, 2)) GB")
|
|
$sum.Add("Text files copied: $copied (SAS signatures blanked in $redacted)")
|
|
$sum.Add("")
|
|
$sum.Add("Tag files ($($tags.Count)):")
|
|
foreach ($t in $tags) {
|
|
$content = (Get-Content -LiteralPath $t.FullName -Raw -ErrorAction SilentlyContinue)
|
|
if ($content) { $content = $content.Trim() }
|
|
$sum.Add(" $($t.FullName.Substring($Root.Length + 1)) [$content] written $($t.LastWriteTime)")
|
|
}
|
|
if ($tags.Count -eq 0) { $sum.Add(" NONE - the MCL run did not finish") }
|
|
$sum.Add("")
|
|
$sum.Add("Top-level folders:")
|
|
foreach ($g in ($rows | Group-Object { ($_.Path -split '\\')[0..1] -join '\' } | Sort-Object Name)) {
|
|
$gb = [math]::Round((($g.Group | Measure-Object Length -Sum).Sum) / 1GB, 2)
|
|
$sum.Add(" $($g.Name): $($g.Count) files, $gb GB")
|
|
}
|
|
if ($unreadable.Count) {
|
|
$sum.Add("")
|
|
$sum.Add("Text files that could not be read, not copied:")
|
|
foreach ($s in $unreadable) { $sum.Add(" $s") }
|
|
}
|
|
if ($skippedBig.Count) {
|
|
$sum.Add("")
|
|
$sum.Add("Text files over 5 MB, not copied:")
|
|
foreach ($s in $skippedBig) { $sum.Add(" $s") }
|
|
}
|
|
$sum | Set-Content -LiteralPath (Join-Path $dest "summary.txt") -Encoding UTF8
|
|
|
|
# Relative path with forward slashes, the way sha256sum -c reads it.
|
|
function Get-RelPath($full, $base) { $full.Substring($base.Length + 1).Replace('\', '/') }
|
|
|
|
$sumLines = foreach ($f in (Get-ChildItem -LiteralPath $dest -Recurse -File | Sort-Object FullName)) {
|
|
"$((Get-FileHash -LiteralPath $f.FullName -Algorithm SHA256).Hash.ToLower()) $(Get-RelPath $f.FullName $dest)"
|
|
}
|
|
[System.IO.File]::WriteAllText((Join-Path $dest "SHA256SUMS"), (($sumLines -join "`n") + "`n"))
|
|
|
|
Write-Host ""
|
|
Write-Host "Captured $($all.Count) files ($copied text files copied)." -ForegroundColor Green
|
|
if ($tags.Count -eq 0) {
|
|
Write-Host "WARNING: no .tag file. MCL did not finish this run." -ForegroundColor Yellow
|
|
}
|
|
if ($unreadable.Count) {
|
|
Write-Host "$($unreadable.Count) file(s) could not be read; listed in summary.txt." -ForegroundColor Yellow
|
|
}
|
|
Write-Host "Capture folder: $dest"
|
|
|
|
if (-not $CopyTo -or $CopyTo -eq "none") { exit 0 }
|
|
$drive = Split-Path -Qualifier $CopyTo -ErrorAction SilentlyContinue
|
|
if (($drive -and -not (Test-Path -LiteralPath "$drive\")) -or -not (Test-Path -LiteralPath (Split-Path $CopyTo -Parent))) {
|
|
Write-Host ""
|
|
Write-Host "$CopyTo is not reachable, so nothing was copied." -ForegroundColor Yellow
|
|
Write-Host "Map Z: to the dev box pxe-images share and run again, or copy the folder above by hand."
|
|
exit 1
|
|
}
|
|
|
|
$target = Join-Path $CopyTo (Split-Path $dest -Leaf)
|
|
Write-Host ""
|
|
Write-Host "Copying to $target ..."
|
|
& robocopy.exe $dest $target /E /COPY:DT /R:2 /W:5 /NP /NFL /NDL /NJH /NJS | Out-Null
|
|
$rc = $LASTEXITCODE
|
|
if ($rc -ge 8) {
|
|
Write-Host "Copy FAILED (robocopy exit $rc)." -ForegroundColor Red
|
|
exit 2
|
|
}
|
|
|
|
# Read every file back through the share and compare with SHA256SUMS.
|
|
$bad = New-Object System.Collections.Generic.List[string]
|
|
foreach ($line in $sumLines) {
|
|
$hash = $line.Substring(0, 64)
|
|
$rel = $line.Substring(66)
|
|
$copy = Join-Path $target ($rel.Replace('/', '\'))
|
|
if (-not (Test-Path -LiteralPath $copy -PathType Leaf)) { $bad.Add("missing $rel"); continue }
|
|
$got = (Get-FileHash -LiteralPath $copy -Algorithm SHA256).Hash.ToLower()
|
|
if ($got -ne $hash) { $bad.Add("differs $rel ($((Get-Item -LiteralPath $copy).Length) bytes)") }
|
|
}
|
|
if ((Get-FileHash -LiteralPath (Join-Path $target "SHA256SUMS") -Algorithm SHA256).Hash -ne
|
|
(Get-FileHash -LiteralPath (Join-Path $dest "SHA256SUMS") -Algorithm SHA256).Hash) {
|
|
$bad.Add("differs SHA256SUMS")
|
|
}
|
|
if ($bad.Count) {
|
|
Write-Host "Copy NOT verified: $($bad.Count) of $($sumLines.Count + 1) files wrong." -ForegroundColor Red
|
|
foreach ($b in $bad) { Write-Host " $b" }
|
|
exit 2
|
|
}
|
|
Write-Host "Copied and verified: all $($sumLines.Count + 1) files match." -ForegroundColor Green
|
|
Write-Host "On the share: $target"
|