Private/Expand-CabArchive.ps1
|
function Expand-CabArchive { [CmdletBinding(SupportsShouldProcess = $false)] param ( [Parameter(Mandatory = $true, Position = 0)] [ValidateNotNullOrEmpty()] [ValidateScript( { if (Test-Path -Path $_ -PathType "Leaf") { $true } else { throw "Cannot find path $_." } })] [System.String] $Path, [Parameter(Mandatory = $false, Position = 1)] [ValidateNotNullOrEmpty()] [ValidateScript( { if (Test-Path -Path $(Split-Path -Path $_ -Parent) -PathType "Container") { $true } else { throw "Cannot find path $(Split-Path -Path $_ -Parent)." } })] [System.String] $DestinationPath ) if (Test-IsWindows) { $StageRoot = Join-Path -Path $DestinationPath -ChildPath ".Expand-CabArchive-$([System.Guid]::NewGuid().ToString())" $ShellStage = Join-Path -Path $StageRoot -ChildPath "Shell" $FallbackStage = Join-Path -Path $StageRoot -ChildPath "Expand" $ExpectedNames = @() try { $null = New-Item -Path $DestinationPath -ItemType "Directory" -Force $ShellSucceeded = $false $StagePath = $null try { $Shell = New-Object -ComObject "Shell.Application" $SourceCab = $Shell.NameSpace($Path) if ($null -eq $SourceCab) { throw "Shell.Application could not open CAB file '$Path'." } $CabItems = @($SourceCab.Items()) $ExpectedNames = @($CabItems | Where-Object { $null -ne $_ } | ForEach-Object { [System.String]$_.Name } | Where-Object { -not [System.String]::IsNullOrWhiteSpace($_) }) if ($ExpectedNames.Count -eq 0) { throw "Shell.Application found no items in CAB file '$Path'." } foreach ($Name in $ExpectedNames) { Write-Verbose -Message "$($MyInvocation.MyCommand): CAB file contains: '$Name'." } $null = New-Item -Path $ShellStage -ItemType "Directory" -Force $DestinationFolder = $Shell.NameSpace($ShellStage) if ($null -eq $DestinationFolder) { throw "Shell.Application could not open staging directory '$ShellStage'." } Write-Verbose -Message "$($MyInvocation.MyCommand): Expanding CAB file '$Path' to staging directory '$ShellStage'." $DestinationFolder.CopyHere($SourceCab.Items(), 0x1014) $Timer = [System.Diagnostics.Stopwatch]::StartNew() do { $MissingNames = @($ExpectedNames | Where-Object { -not (Test-Path -LiteralPath (Join-Path -Path $ShellStage -ChildPath $_) -PathType "Leaf") }) if ($MissingNames.Count -eq 0) { break } Start-Sleep -Milliseconds 100 } while ($Timer.Elapsed.TotalSeconds -lt 30) $Timer.Stop() if ($MissingNames.Count -gt 0) { throw "Shell.Application.CopyHere did not extract all CAB items before the timeout. Missing: $($MissingNames -join ', ')." } $ShellSucceeded = $true $StagePath = $ShellStage } catch { Write-Verbose -Message "$($MyInvocation.MyCommand): $($_.Exception.Message)" Write-Verbose -Message "$($MyInvocation.MyCommand): Falling back to expand.exe." $ShellError = $_.Exception.Message } if (-not $ShellSucceeded) { $ExpandExe = Join-Path -Path $Env:SystemRoot -ChildPath "System32\expand.exe" if (-not (Test-Path -LiteralPath $ExpandExe -PathType "Leaf")) { throw "$($MyInvocation.MyCommand): Shell extraction failed: $ShellError expand.exe was not found at '$ExpandExe'." } try { $null = New-Item -Path $FallbackStage -ItemType "Directory" -Force Write-Verbose -Message "$($MyInvocation.MyCommand): Expanding CAB file '$Path' with expand.exe to staging directory '$FallbackStage'." $LASTEXITCODE = 0 & $ExpandExe $Path $FallbackStage | Out-Null if ($LASTEXITCODE -ne 0) { throw "expand.exe exited with code $LASTEXITCODE." } } catch { throw "$($MyInvocation.MyCommand): Shell extraction failed: $ShellError expand.exe fallback failed: $($_.Exception.Message)" } $ExtractedFiles = @(Get-ChildItem -LiteralPath $FallbackStage -File -Recurse) if ($ExtractedFiles.Count -eq 0) { throw "$($MyInvocation.MyCommand): Shell extraction failed: $ShellError expand.exe produced no files." } if ($ExpectedNames.Count -gt 0) { $ExtractedNames = @($ExtractedFiles | Select-Object -ExpandProperty "Name") $MissingNames = @($ExpectedNames | Where-Object { $_ -notin $ExtractedNames }) if ($MissingNames.Count -gt 0) { throw "$($MyInvocation.MyCommand): expand.exe did not extract all CAB items. Missing: $($MissingNames -join ', ')." } } $StagePath = $FallbackStage } $ExtractedFiles = @(Get-ChildItem -LiteralPath $StagePath -File -Recurse) $Items = @() foreach ($File in $ExtractedFiles) { $RelativePath = $File.FullName.Substring($StagePath.Length).TrimStart([System.Char[]]@('\', '/')) $DestinationFile = Join-Path -Path $DestinationPath -ChildPath $RelativePath $DestinationParent = Split-Path -Path $DestinationFile -Parent $null = New-Item -Path $DestinationParent -ItemType "Directory" -Force Move-Item -LiteralPath $File.FullName -Destination $DestinationFile -Force -ErrorAction "Stop" $Items += $DestinationFile Write-Verbose -Message "$($MyInvocation.MyCommand): Expanded item '$DestinationFile' to '$DestinationPath'." } return $Items } finally { Remove-Item -LiteralPath $StageRoot -Recurse -Force -ErrorAction "SilentlyContinue" } } else { # Future update for cross-platform throw "$($MyInvocation.MyCommand): Expand-CabArchive is only supported on Windows." } } |