Behind the PowerShell Pipeline logo

Behind the PowerShell Pipeline

Archives
Log in
Subscribe
July 28, 2026

A VHD Backup Solution

In this issue:

  • Creating a VHDX
    • Sizing
    • Creating the VHD
    • Partitions and Formatting
  • Copy Files
  • Optional Compression
  • DocBackup-to-VHD.ps1
  • Summary

We're almost to the end of the month which means it is time to look at a possible solution to last month's PowerShell scripting challenge. I hope at least a few of you were brave enough to at least start the exercise. Challenging yourself with something novel is a terrific way to improve your PowerShell scripting skills and extend your knowledge. As always with these challenges, the real value is the process you undertook, not necessarily the result.

As a refresher, this is what I tasked you with last month:

Your challenge is to create a PowerShell solution that will create a VHD file that contains the contents of one or more folders. In other words, we're using the VHD file is a backup device. How you create the VHD is up to you. There are cmdlets you can use. Or you might turn to command-line tools and wrap them in PowerShell. To accomplish this in Linux will most likely require third-party tools. You are welcome to try, but this is really a Windows platform challenge.

Here are some other elements you might tackle:

- By default, backup your documents folder.

- Sized the VHD to match the size requirements.

- Format as NTFS.

- Allow the option to specify a partition drive letter.

- Create a dynamic VHD file.

- Add an option to move the VHD file to a ZIP or other archive format.

This challenge will not only test your PowerShell experience, but also your knowledge on managing storage. For today's solution, I am going to stick with native PowerShell commands. Although you will need the Hyper-V module, which means enabling the Hyper-V feature.

Creating a VHDX

The reason I opt for the Hyper-V module, is that it includes a command specific to the task at hand, New-VHD. When you create a VHD, you have several options:

  • Fixed size
  • Dynamically sized
  • A differencing disk

A differencing disk doesn't make sense for this challenge since it is creating something new. Although, I can see where you could create a backup solution using differencing disks as incremental backups. But I'm not going off in that tangent today.

For the purpose of the challenge, it may not make much difference between using a fixed or dynamic disk since we're creating in essence a one-time disk. The code I'm using is always creating a new disk. It is certainly possible to re-use the VHD as a backup device in which case a dynamic disk might make more sense.

I decided to use a dynamic VHD. I am also using the modern VHDX format instead of the legacy VHD format. For the usage I have in mind there is no reason to use the legacy format. My script needs to know the name and location of new VHD. I can also add parameter validation.

[Parameter(HelpMessage = "What is the file name and path to the VHDX file?")]
[ValidatePattern('\.vhdx$',ErrorMessage = "The value {0} does not appear to be a .vhdx file.")]
[ValidateScript({ Split-Path $_ | Test-Path},ErrorMessage = "Failed to find or validate the parent part of the path.")]
[string]$VHDPath = "c:\temp\$env:username.vhdx",

Sizing

In order to properly size the VHD, I need to know the file size of the specified paths. I'm using Documents as a default value.

[Parameter(Position = 0,HelpMessage = "What is the path to be backed up?")]
[ValidateScript({Test-Path -path $_},ErrorMessage = "Failed to find or validate {0}.")]
[ValidateNotNullOrEmpty()]
[string[]]$Path = "$env:UserProfile\documents",

Notice that I am specifying that the parameter can accept multiple paths. This will allow me to backup multiple locations to the same VHD.

I can then get an idea of how much space I will need.

#measure the file sizes to size the VHD accordingly
$files = Get-ChildItem -Path $path -Recurse -file -FollowSymlink
$size = $files | Measure-Object -Property Length -Sum

One issue I encountered during development was properly measuring files, especially in my Documents folder where I have several folders that contain files with symbolic links. By default, Get-ChildItem doesn't follow the link so measuring things like file size return 0. This leads to an inaccurate total file size which makes the VHD too small when it comes time to copy files. Copying does follow the link. Fortunately, PowerShell 7 has a parameter for Get-ChildItem that will follow the symbolic link thus ensuring I get a valid total size.

And just to be safe, and in case I want to manually add anything else, I'll pad the total by 25%.

$vhdSize = $size.sum * 1.25

Creating the VHD

At this point, I know where to create the VHD file, how big to make it, and that I want a dynamic disk. I have no problem with the other defaults with New-VHD. One bit of code I had to add was handling situations where the VHD file already exists. New-VHD won't overwrite an existing file, so I need to handle it. I added a -Force switch parameter to my script.

if ((Test-Path -path $VHDPath) -AND (-Not $Force)) {
    Write-Warning "$VHDPath already exists. Delete the file or re-run $($MyInvocation.MyCommand) with -Force"
    #quit
    Return
}
elseif ((Test-Path -path $VHDPath) -AND $Force) {
    Write-Host "Removing existing file $VHDPath" -ForegroundColor magenta
    Remove-Item $VHDPath -Force
}

The parameters to use with New-VHD seem pretty straightforward.

New-VHD -Path $vhdPath -Dynamic -SizeBytes $vhdSize

Unfortunately, there is an odd quirk. I don't think it is technically a bug, but it is certainly unexpected.

You would expect the SizeBytes parameter to accept a numeric value. It even looks like it does according to the help.

PS C:\>  help New-Vhd -Parameter SizeBytes

-SizeBytes <uint64>
    The maximum size, in bytes, of the virtual hard disk to be created.

    Required?                    true
    Position?                    1
    Default value                None
    Accept pipeline input?       False
    Aliases                      none
    Accept wildcard characters?  false

However you have to specify the value using one of PowerShell's size shortcuts like MB or GB:

New-VHD -Path $vhdPath -Dynamic -SizeBytes 100MB

This is odd and unfortunate as it means I need to perform a little data transformation in my script.

New-VHD -Path $vhdPath -Dynamic -SizeBytes "$($vhdSize/1mb -as [int])MB"

There might be some rounding of the value, but that hasn't been an issue.

Want to read the full issue?
Already a paid subscriber? Click here to log in.
GitHub
Bluesky
LinkedIn
Mastodon
jdhitsolutions.github.io
Powered by Buttondown, the easiest way to start and grow your newsletter.