My PowerShell function Compress-Vhdx allows you to compress multiple VHDX files with a single command to reclaim space from a folder (and its nested subfolders). The function uses the Optimize-VHD PowerShell cmdlet, which comes with the Hyper-V role. You no longer need to provide the full syntax separately for each VHD. When the operation is complete, you get a nice summary of the amount of space saved for each separate VHDX.

Prerequisites ^

You'll need a supported version of Windows with the Hyper-V role installed, including the Client editions. Yes, you can also use it with a Windows 10/11 machine.

Compress-Vhdx uses Optimize-VHD, which comes with the Hyper-V management module. This means you need the Hyper-V management module installed on the computer where you plan to run the command. Normally, this should already be installed. If it isn't, you can quickly add it using the following command:

Install-WindowsFeature -Name RSAT-Hyper-V-Tools -IncludeManagementTools
Installing Hyper V management module PowerShell in progress

Installing Hyper V management module PowerShell in progress

Wait for a few seconds for the installation to complete …

Installing Hyper V management module PowerShell—completed no restart needed.png

Installing Hyper V management module PowerShell—completed no restart needed

… and you're done. You do not need to restart your computer when the installation has completed.

Usage examples ^

Now it's time to see Compress-Vhdx in action. I find it very easy to learn by example, so let's put our function to work.

Basic usage

The basic usage is just Compress-Vhdx with the path to the folder where your VHDX files are located.

Compress-Vhdx C:\VMs 
Compress Vhdx with no parameters other than the path to our VHDx folder

Compress Vhdx with no parameters other than the path to our VHDx folder

If you know for sure that there are more VHDX files in your folder, you may wonder why they don't appear in your list. Well, the answer is that if a VHDX file is in use (the VM is running), that virtual disk cannot be compressed. You need to turn off your VM before you can do that. The example below shows you the additional information you get in Verbose mode.

Using the Verbose switch

Let's use the same command as above but switch to Verbose mode.

Compress-Vhdx -Path C:\VMs\ -Verbose 
You get more information in Verbose mode

You get more information in Verbose mode

In Verbose mode, you see more information about what's happening under the hood:

  • You see how many files will be processed.
  • You see which VHDX files have been skipped.
  • At the end, you may also see how much time the command took to complete. This may be useful if you're dealing with larger files.

Note: You may notice that I have explicitly included the "Path" switch before providing the path to my target folder. It is not mandatory, but it sure is nice to use it, particularly in a script that other admins might use.

Include subfolders

Another useful switch is IncludeSubfolders, which allows the command to work recursively through all the subfolders in the path you provide. If you have 20 VMs and each VM has its dedicated folder—including the VHDX file(s)—you only need to run the command once, at the level of the root folder.

Compress-Vhdx -Path C:\VMs\ -IncludeSubfolders -Verbose 
Process the VHDX files in all the subfolders by using the switch IncludeSubfolders

Process the VHDX files in all the subfolders by using the switch IncludeSubfolders

In this last example, an extra VHDX file (highlighted in yellow) was processed. It was located in a subfolder, which was not scanned in the previous examples.

Note: For this last example, I shut down one of my VMs (named WSUS_Export) to show the difference from the previous example. Before shutdown, I deleted some files from it. You may see the space gains in the column Saved [GB] (highlighted in green).

The Compress-Vhdx function ^

The complete code for the function is shown below. You may include it in your PowerShell profile script or in a module for easy access.

Subscribe to 4sysops newsletter!

function Compress-Vhdx {
<#
.Synopsis
   Compresses all the VHDX files from a specified location
.DESCRIPTION
   Compress-Vhdx retrieves each VHDX files from a specified location and compacts each using the native command Optimize-VHD
.EXAMPLE
   Compress-Vhdx -Path "C:\MyVMs" -Recurse
   Compacts all the VHDX files from the specified path, including subfolders
.NOTES
   Last updated on 2021.11.05
#>

    [CmdletBinding()]
    Param
    (
        # Path to the VHDX files
        [Parameter(ValueFromPipelineByPropertyName,Position=0)]$Path = (Get-Location).Path,

        # Go through subfolders, if selected
        [Parameter(Position=1)][switch]$IncludeSubfolders
    )

    Begin {
        $StartTime = Get-Date
 
        # Search for VHDX files in subfolders?
        if ($IncludeSubfolders) {
            $AllVhdx = Get-ChildItem *.vhdx -Path $Path -Recurse -ErrorAction SilentlyContinue
            }
        else {
            $AllVhdx = Get-ChildItem *.vhdx -Path $Path -ErrorAction SilentlyContinue
            }

        # Are there any VHDX files in the location?
        if ($AllVhdx.Count -lt 1) {
            Write-Warning "There is no VHDX file to compress in `"$Path`". Make sure that the path is correct and it contains VHDX files"
            break
            }
        #Clear-Host
        Write-Verbose "Compacting $($AllVhdx.Count) VHDX files, please wait"
        } #Begin
    
    Process {

        $Stats = foreach ($v in $AllVhdx) {
            
            $OldSize = $v.Length

            try {
                Optimize-VHD -Path $v.FullName -Mode Full -ErrorAction Stop
                Write-Verbose "Compressing $($v.Name)"
                $NewSize = (Get-ChildItem -Path $v.FullName).Length                
                $Saved = $OldSize - $NewSize
                          
                [PSCustomObject] @{
                    #Name = $v.Name
                    Path = $v.FullName
                    "Initial Size [GB]" = [math]::round($OldSize /1Gb, 2)
                    "Current Size [GB]" = [math]::round($NewSize /1Gb, 2)
                    "Saved [GB]" = [math]::round($Saved /1Gb, 2)
                    }
                 }

             catch {
                Write-Verbose "Skipping $($v.Name). File may be in use "
                }   
            
            $TotalSaved += $Saved  

            } #$Stats

        } #Process

    End {
        $Duration = New-TimeSpan -Start $StartTime -End (Get-Date)
        $DurationPretty = $($Duration.Hours).ToString() + "h:" + $($Duration.Minutes).ToString() + "m:" + $($Duration.Seconds).ToString() + "s"
        $Stats | FT -Wrap -AutoSize

        Write-Verbose "The operation completed in $DurationPretty"
        Write-Verbose "Disk space saved: $([math]::round($TotalSaved /1Gb, 2)) GB"
        } #End

} #function 
0 Comments

Leave a reply

Please enclose code in pre tags

Your email address will not be published. Required fields are marked *

*

© 4sysops 2006 - 2021

CONTACT US

Please ask IT administration questions in the forums. Any other messages are welcome.

Sending

Log in with your credentials

or    

Forgot your details?

Create Account