MSIPatches.psm1
function Get-MsiPatches { <# .SYNOPSIS Scans the "C:\Windows\Installer" directory for all installed and orpaned msp files. .DESCRIPTION Office installations can leave behind a large amount of orphaned patches which can take up many GBs of disk space. Using the "Get-MSIPatchInfo" cmdlet from the "MSI" module we can determine which msp files are currently installed. From there we can calculate the amount and size of the orpaned msp files. .EXAMPLE Get-MsiPatches This will display a PsCustomObject of all msp files and size, which are installed and size, and which are orpaned and their total size. .EXAMPLE Get-MsiPatches -Verbose Same as above only verbose information is displayed for each msp detailing whether they are installed or orphaned. .EXAMPLE Get-MsiPatches | FT Simply changes the format to a table view of the object. .NOTES Author: Mark Kerry Date: 08/01/2018 #> [CmdletBinding()] Param() # Begin by checking the user in running the function from elevated priviledges if (-not ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] "Administrator")) { Write-Warning "You need to run this script from an elevated PowerShell prompt!`nPlease start PowerShell as an Administrator..." break } if ($PSVersionTable.PSEdition -eq 'Core') { Write-Warning "This function is not compatible with PowerShell Core as it reiles on the MSI module binaries." break } # Get each .msp in the Installer directory and calculate the size. $allMsp = Get-Item -Path C:\Windows\Installer\*.msp | Measure-Object -Property Length -Sum $allMspSum = "{0:N2}" -f ($allMsp.sum / 1GB) + " GB" # List all msp files in %windir%\Installer and convert to string $strAllMsp = Get-ChildItem -Path C:\Windows\Installer\*.msp | Select-Object Name -ExpandProperty Name # List all currently installed msp files and convert to string $strInstalledMsps = Get-MSIPatchInfo | Select-Object LocalPackage -ExpandProperty LocalPackage # Create a new array of all the installed msp files but run a regex query to strip the path from the string so the format is the same as the strAllMsp variable $array = @() foreach ($Msp in $strInstalledMsps) { $a = $Msp -creplace '(?s)^.*\\', '' $array += $a } # Remove any duplicate values from the array $array = $array | Select-Object -uniq # Now list both installed and Obsolete msps $inCount=0 $obCount=0 $inst = @() $obinst = @() foreach ($x in $strAllMsp) { if ($array.Contains($x)) { Write-Verbose "$x is installed." $inst += $x $inCount++ } else { Write-Verbose "$x is obsolete and can be moved/deleted." $obinst += $x $obCount++ } } # Scan for each installed msp and calculate the total size of them all. Write-Progress -Activity 'Retrieving installed msp files...' -Status '33% Complete.' -PercentComplete 33 $size = $null foreach ($i in $inst) { $instSize = Get-ChildItem -Path C:\Windows\Installer | Where-Object {$_.Name -like $i} | Select-Object Length -ExpandProperty Length $size += $instSize } $TotalInst = "{0:N2}" -f ($size / 1GB) + " GB" # Scan for each obsolete msp and calculate the total size of them all. Write-Progress -Activity 'Retrieving orphaned msp files...' -Status '66% Complete.' -PercentComplete 66 $obsize = $null foreach ($o in $obinst) { $obinstSize = Get-ChildItem -Path C:\Windows\Installer | Where-Object {$_.Name -like $o} | Select-Object Length -ExpandProperty Length $obsize += $obinstSize } $TotalOb = "{0:N2}" -f ($obsize / 1GB) + " GB" # Display them all in a PsCustomObject [PsCustomObject]@{ TotalPatchCount = $allMsp.Count TotalPatchSize = $allMspSum InstalledPatchCount = $inCount InstalledPatchSize = $TotalInst OrphanedPatchCount = $obCount OrphanedPatchSize = $TotalOb } } function Get-OrphanedPatches { <# .SYNOPSIS Scans the "C:\Windows\Installer" directory for all orpaned msp files. .DESCRIPTION Office installations can leave behind a large amount of orphaned patches which can take up many GBs of disk space. Using the "Get-MSIPatchInfo" cmdlet from the "MSI" module we can determine which msp files are currently installed. From there we can calculate the amount and size of the orpaned msp files. This can be run on it's own to list the orphaned patches, or piped to Move-OrphanedPatches or Remove-OrphanedPatches. .EXAMPLE Get-OrphanedPatches Simply lists orphaned msp files in "C:\Windows\Installer" .EXAMPLE Get-OrphanedPatches | Move-OrphanedPatches -Destination <String> If you want to free up space I recommend moving the orphaned msp files to another location so they can easily be restored. Supports the "-Whatif" and "Verbose" paramters. .EXAMPLE Get-OrphanedPatches | Remove-OrphanedPatches This will permanently delete the orphaned msp files from "C:\Windows\OInstaller". Supports the "-Whatif" and "Verbose" paramters. .NOTES Author: Mark Kerry Date: 08/01/2018 #> [CmdletBinding()] [OutputType([System.IO.FileInfo])] Param() # Begin by checking the user in running the function from elevated priviledges if (-not ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] "Administrator")) { Write-Warning "You need to run this script from an elevated PowerShell prompt!`nPlease start PowerShell as an Administrator..." break } if ($PSVersionTable.PSEdition -eq 'Core') { Write-Warning "This function is not compatible with PowerShell Core as it reiles on the MSI module binaries." break } # List all msp files in %windir%\Installer and convert to string $strAllMsp = Get-ChildItem -Path C:\Windows\Installer\*.msp | Select-Object Name -ExpandProperty Name # List all currently installed msp files and convert to string $strInstalledMsps = Get-MSIPatchInfo | Select-Object LocalPackage -ExpandProperty LocalPackage # Create a new array of all the installed msp files but run a regex query to strip the path from the string so the format is the same as the strAllMsp variable $array = @() foreach ($Msp in $strInstalledMsps) { $a = $Msp -creplace '(?s)^.*\\', '' $array += $a } # Remove any duplicate values from the array $array = $array | Select-Object -uniq $obinst = @() foreach ($x in $strAllMsp) { if (!($array.Contains($x))) { $obinst += $x } } $obinst | ForEach-Object {Get-ChildItem -Path C:\Windows\Installer\$_} -ov item } function Move-OrphanedPatches { <# .SYNOPSIS Moves the orphaned patches to a specified backup location. .DESCRIPTION This function recieves the [System.IO.FileInfo] objects through the pipeline from Get-OrphanedPatches. It will move each object to a specified backup location. The location can be created if it doesn't exist. .EXAMPLE Get-OrphanedPatches | Move-OrphanedPatches -Destination <String> This will move the orphaned patches to a different location so they can be restored to "C:\Windows\Installer" directory if needed. Supports the "-Whatif" and "Verbose" paramters. .NOTES Author: Mark Kerry Date: 08/01/2018 #> [CmdletBinding(SupportsShouldProcess=$true)] Param( [Parameter( Mandatory=$true, ValueFromPipeline=$true)] [System.IO.FileInfo]$item, [Parameter( Mandatory=$true)] [String]$Destination ) begin { if (!(Test-Path -Path $Destination -PathType Container)) { $confirmation = Read-Host "Destination: $Destination does not exist. Create it? [y/n]" while ($confirmation -ne 'y') { if ($confirmation -eq 'n') { break } $confirmation = Read-Host "Destination: $Destination does not exist. Create it? [y/n]" } if ($confirmation -eq 'y') { try { New-Item -ItemType Directory $Destination } catch { Write-Warning $_.exception.message } } else { break } } } process { try { Move-Item -Path $item -Destination $Destination -Verbose } catch { Write-Warning $_.exception.message break } } } function Restore-OrphanedPatches { <# .SYNOPSIS Restores the previously backed up msp files .DESCRIPTION This function can be run in the event of needing to restore the moved msp files. .EXAMPLE Restore-OrphanedPatches -BackupLocation D:\Backup .NOTES Author: Mark Kerry Date: 18/01/2018 #> [CmdletBinding(SupportsShouldProcess=$true)] Param( [Parameter( Mandatory=$true)] [String]$BackupLocation ) # Begin by checking the user in running the function from elevated priviledges if (-not ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] "Administrator")) { Write-Warning "You need to run this script from an elevated PowerShell prompt!`nPlease start PowerShell as an Administrator..." break } # Check the backup location is exists or is accessible if (Test-Path -Path $BackupLocation) { # If so check there are msp files in the location. Then attempt to move them back to the installer directory. if (Get-ChildItem -Path "$($BackupLocation)\*.msp") { try { Move-Item -Path "$($BackupLocation)\*.msp" -Destination 'C:\Windows\Installer' -Verbose } catch { Write-Warning $_.exception.message break } } else { Write-Warning "Unable to find any msp files in $BackupLocation" } } else { Write-Warning "Unable to find $BackupLocation" } } |