Hyper-V-Backup.ps1

<#PSScriptInfo
 
.VERSION 23.02.18
 
.GUID c7fb05cc-1e20-4277-9986-523020060668
 
.AUTHOR Mike Galvin Contact: mike@gal.vin / twitter.com/mikegalvin_ / discord.gg/5ZsnJ5k
 
.COMPANYNAME Mike Galvin
 
.COPYRIGHT (C) Mike Galvin. All rights reserved.
 
.TAGS Hyper-V Virtual Machines Full Backup Export Permissions Zip History 7-Zip
 
.LICENSEURI
 
.PROJECTURI https://gal.vin/utils/hyperv-backup-utility/
 
.ICONURI
 
.EXTERNALMODULEDEPENDENCIES
 
.REQUIREDSCRIPTS
 
.EXTERNALSCRIPTDEPENDENCIES
 
.RELEASENOTES
 
#>


<#
    .SYNOPSIS
    Hyper-V Backup Utility - Flexible backup of Hyper-V Virtual Machines.
 
    .DESCRIPTION
    Creates a full backup of virtual machines.
    Run with -help or no arguments for usage.
#>


## Set up command line switches.
[CmdletBinding()]
Param(
    [alias("BackupTo")]
    $BackupUsr,
    [alias("Keep")]
    $History,
    [alias("List")]
    [ValidateScript({Test-Path -Path $_ -PathType Leaf})]
    $VmList,
    [alias("Wd")]
    $WorkDirUsr,
    [alias("SzOptions")]
    $SzSwitches,
    [alias("L")]
    $LogPathUsr,
    [alias("LogRotate")]
    $LogHistory,
    [alias("Subject")]
    $MailSubject,
    [alias("SendTo")]
    $MailTo,
    [alias("From")]
    $MailFrom,
    [alias("Smtp")]
    $SmtpServer,
    [alias("Port")]
    $SmtpPort,
    [alias("User")]
    $SmtpUser,
    [alias("Pwd")]
    [ValidateScript({Test-Path -Path $_ -PathType Leaf})]
    $SmtpPwd,
    [Alias("Webhook")]
    [ValidateScript({Test-Path -Path $_ -PathType Leaf})]
    [string]$Webh,
    [switch]$UseSsl,
    [switch]$NoPerms,
    [switch]$Compress,
    [switch]$Sz,
    [switch]$ShortDate,
    [switch]$Help,
    [switch]$LowDisk,
    [switch]$NoBanner)

If ($NoBanner -eq $False)
{
    Write-Host -ForegroundColor Yellow -BackgroundColor Black -Object "
     _ _ __ __ ____ _ _ _ _ _ _ _ _
    | | | | \ \ / / | _ \ | | | | | | | (_) (_) |
    | |__| |_ _ _ __ ___ _ _\ \ / / | |_) | __ _ ___| | ___ _ _ __ | | | | |_ _| |_| |_ _ _
    | __ | | | | '_ \ / _ \ '__\ \/ / | _ < / _ |/ __| |/ / | | | '_ \ | | | | __| | | | __| | | |
    | | | | |_| | |_) | __/ | \ / | |_) | (_| | (__| <| |_| | |_) | | |__| | |_| | | | |_| |_| |
    |_| |_|\__, | .__/ \___|_| \/ |____/ \__,_|\___|_|\_\\__,_| .__/ \____/ \__|_|_|_|\__|\__, |
             __/ | | | | __/ |
            |___/|_| |_| |___/
                              Mike Galvin https://gal.vin Version 23.02.18
                         Donate: https://www.paypal.me/digressive See -help for usage
"

}

If ($PSBoundParameters.Values.Count -eq 0 -or $Help)
{
    Write-Host -Object "Usage:
    From a terminal run: [path\]Hyper-V-Backup.ps1 -BackupTo [path\]
    This will backup all the VMs running to the backup location specified.
 
    Use -List [path\]vms.txt to specify a list of vm names to backup.
    Use -Wd [path\] to configure a working directory for the backup process.
    Use -Keep [number] to specify how many days worth of backup to keep.
    Use -ShortDate to use only the Year, Month and Day in backup filenames.
    Use -LowDisk to remove old backups before new ones are created. For low disk space situations.
 
    -NoPerms should only be used when a regular backup cannot be performed.
    Please note: this will cause the VMs to shutdown during the backup process.
 
    Use -Compress to compress the VM backups in a zip file using Windows compression.
    Use -Sz to use 7-zip
    Use -SzOptions ""'-t7z,-v2g,-ppassword'"" to specify 7-zip options like file type, split files or password.
 
    To output a log: -L [path\].
    To remove logs produced by the utility older than X days: -LogRotate [number].
    Run with no ASCII banner: -NoBanner
 
    To send the log to a webhook on job completion:
    Specify a txt file containing the webhook URI with -Webhook [path\]webhook.txt
 
    To use the 'email log' function:
    Specify the subject line with -Subject ""'[subject line]'"" If you leave this blank a default subject will be used
    Make sure to encapsulate it with double & single quotes as per the example for Powershell to read it correctly.
 
    Specify the 'to' address with -SendTo [example@contoso.com]
    For multiple address, separate with a comma.
 
    Specify the 'from' address with -From [example@contoso.com]
    Specify the SMTP server with -Smtp [smtp server name]
 
    Specify the port to use with the SMTP server with -Port [port number].
    If none is specified then the default of 25 will be used.
 
    Specify the user to access SMTP with -User [example@contoso.com]
    Specify the password file to use with -Pwd [path\]ps-script-pwd.txt.
    Use SSL for SMTP server connection with -UseSsl.
 
    To generate an encrypted password file run the following commands
    on the computer and the user that will run the script:
"

    Write-Host -Object ' $creds = Get-Credential
    $creds.Password | ConvertFrom-SecureString | Set-Content [path\]ps-script-pwd.txt'

}

else {
    ## If logging is configured, start logging.
    ## If the log file already exists, clear it.
    If ($LogPathUsr)
    {
        ## Clean User entered string
        $LogPath = $LogPathUsr.trimend('\')

        ## Make sure the log directory exists.
        If ((Test-Path -Path $LogPath) -eq $False)
        {
            New-Item $LogPath -ItemType Directory -Force | Out-Null
        }

        $LogFile = ("Hyper-V-Backup_{0:yyyy-MM-dd_HH-mm-ss}.log" -f (Get-Date))
        $Log = "$LogPath\$LogFile"

        If (Test-Path -Path $Log)
        {
            Clear-Content -Path $Log
        }
    }

    ## Function to get date in specific format.
    Function Get-DateFormat()
    {
        Get-Date -Format "yyyy-MM-dd HH:mm:ss"
    }

    Function Get-DateShort()
    {
        Get-Date -Format "yyyy-MM-dd"
    }

    Function Get-DateLong()
    {
        Get-Date -Format "yyyy-MM-dd_HH-mm-ss"
    }

    ## Function for logging.
    Function Write-Log($Type,$Evt)
    {
        If ($Type -eq "Info")
        {
            If ($LogPathUsr)
            {
                Add-Content -Path $Log -Encoding ASCII -Value "$(Get-DateFormat) [INFO] $Evt"
            }

            Write-Host -Object "$(Get-DateFormat) [INFO] $Evt"
        }

        If ($Type -eq "Succ")
        {
            If ($LogPathUsr)
            {
                Add-Content -Path $Log -Encoding ASCII -Value "$(Get-DateFormat) [SUCCESS] $Evt"
            }

            Write-Host -ForegroundColor Green -Object "$(Get-DateFormat) [SUCCESS] $Evt"
        }

        If ($Type -eq "Err")
        {
            If ($LogPathUsr)
            {
                Add-Content -Path $Log -Encoding ASCII -Value "$(Get-DateFormat) [ERROR] $Evt"
            }

            Write-Host -ForegroundColor Red -BackgroundColor Black -Object "$(Get-DateFormat) [ERROR] $Evt"
        }

        If ($Type -eq "Conf")
        {
            If ($LogPathUsr)
            {
                Add-Content -Path $Log -Encoding ASCII -Value "$Evt"
            }

            Write-Host -ForegroundColor Cyan -Object "$Evt"
        }
    }

    Function UpdateCheck()
    {
        $ScriptVersion = "23.02.18"
        $RawSource = "https://raw.githubusercontent.com/Digressive/HyperV-Backup-Utility/master/Hyper-V-Backup.ps1"
        $SourceCheck = Invoke-RestMethod -uri "$RawSource"
        $VerCheck = Select-String -Pattern ".VERSION $ScriptVersion" -InputObject $SourceCheck
        If ($null -eq $VerCheck)
        {
            Write-Log -Type Conf -Evt "*** There is an update available. ***"
        }
    }
    ##
    ## Start of backup Options functions
    ##

    Function CompressFiles7zip($CompressDateFormat,$CompressDir,$CompressFileName)
    {
        $CompressFileNameSet = $CompressFileName+$CompressDateFormat
        ## 7-zip compression with shortdate
        try {
            & "$env:programfiles\7-Zip\7z.exe" $SzSwSplit -bso0 a ("$CompressDir\$CompressFileNameSet") "$CompressDir\$Vm\*"
            $BackupSucc = $true
        }
        catch {
            $_.Exception.Message | Write-Log -Type Err -Evt "(VM:$Vm) $_"
            $BackupSucc = $false
        }

        $BackupSucc | Out-Null
    }

    Function CompressFilesWin($CompressDateFormat,$CompressDir,$CompressFileName)
    {
        Add-Type -AssemblyName "system.io.compression.filesystem"

        $CompressFileNameSet = $CompressFileName+$CompressDateFormat
        ## Windows compression with shortdate
        try {
            [io.compression.zipfile]::CreateFromDirectory("$CompressDir\$Vm", ("$CompressDir\$CompressFileNameSet.zip"))
            $BackupSucc = $true
        }
        catch {
            $_.Exception.Message | Write-Log -Type Err -Evt "(VM:$Vm) $_"
            $BackupSucc = $false
        }

        $BackupSucc | Out-Null
    }

    Function ShortDateFileNo($ShortDateDir,$ShortDateFilePat)
    {
        Write-Log -Type Info -Evt "(VM:$Vm) Backup $VmFixed-$(Get-DateShort) already exists, appending number"
        $i = 1
        $ShortDateNN = ("$VmFixed-$(Get-DateShort)-{0:D3}" -f $i++)+$ShortDateFilePat
        $ShortDateExistT = Test-Path -Path $ShortDateDir\$ShortDateNN

        If ($ShortDateExistT)
        {
            do {
                $ShortDateNN = ("$VmFixed-$(Get-DateShort)-{0:D3}" -f $i++)+$ShortDateFilePat
                $ShortDateExistT = Test-Path -Path $ShortDateDir\$ShortDateNN
            } until ($ShortDateExistT -eq $false)
        }

        If ($Compress)
        {
            If ($Sz -eq $True -AND $7zT -eq $True)
            {
                If ($SzSwSplit -like "-v*")
                {
                    ## 7-zip compression with shortdate configured and a number appended.
                    try {
                        $ShortDateNN7zFix = $ShortDateNN -replace '[.*]'
                        CompressFiles7zip -CompressDir $ShortDateDir -CompressFileName $ShortDateNN7zFix
                        $BackupSucc = $true
                    }
                    catch {
                        $_.Exception.Message | Write-Log -Type Err -Evt "(VM:$Vm) $_"
                        $BackupSucc = $false
                    }
                }
                
                else {
                    ## 7-zip compression with shortdate configured and a number appended.
                    try {
                        $ShortDateNN7zFix = $ShortDateNN -replace '[.*]'
                        CompressFiles7zip -CompressDir $ShortDateDir -CompressFileName $ShortDateNN7zFix
                        $BackupSucc = $true
                    }
                    catch {
                        $_.Exception.Message | Write-Log -Type Err -Evt "(VM:$Vm) $_"
                        $BackupSucc = $false
                    }
                }
            }

            else {
                ## Windows compression with shortdate configured and a number appended.
                try {
                    $ShortDateNNWinFix = $ShortDateNN.TrimEnd(".zip")
                    CompressFilesWin -CompressDir $ShortDateDir -CompressFileName $ShortDateNNWinFix
                    $BackupSucc = $true
                }
                catch {
                    $_.Exception.Message | Write-Log -Type Err -Evt "(VM:$Vm) $_"
                    $BackupSucc = $false
                }
            }
        }

        else {
            try {
                Get-ChildItem -Path $ShortDateDir -Filter $Vm -Directory | Rename-Item -NewName ("$ShortDateDir\$ShortDateNN")
            }
            catch {
                $_.Exception.Message | Write-Log -Type Err -Evt "(VM:$Vm) $_"
            }

            If ($WorkDir -ne $Backup)
            {
                ## Moving backup folder with shortdate and renaming with number appended.
                try {
                    Get-ChildItem -Path $WorkDir -Filter "$VmFixed-*-*-*" -Directory | Move-Item -Destination $ShortDateDir\$ShortDateNN -ErrorAction 'Stop'
                }
                catch {
                    $_.Exception.Message | Write-Log -Type Err -Evt "(VM:$Vm) $_"
                }
            }
        }
        $BackupSucc | Out-Null
    }

    Function ReportRemove($RemoveDir,$RemoveFilePat,$RemoveDirOpt,$RemoveHistory)
    {
        If ($RemoveDirOpt)
        {
            $RemoveDirOptSet = @{Directory = $true}
        }

        else {
            $RemoveDirOptSet = @{Directory = $false}
        }

        $RemoveFullPath = $VmFixed+$RemoveFilePat

        ## report old files to remove
        If ($LogPathUsr)
        {
            Get-ChildItem -Path $RemoveDir -Filter $RemoveFullPath @RemoveDirOptSet | Where-Object CreationTime -lt (Get-Date).AddDays(-$RemoveHistory) | Select-Object -Property Name, CreationTime | Format-Table -HideTableHeaders | Out-File -Append $Log -Encoding ASCII
        }

        ## remove old files
        Get-ChildItem -Path $RemoveDir -Filter $RemoveFullPath @RemoveDirOptSet | Where-Object CreationTime -lt (Get-Date).AddDays(-$RemoveHistory) | Remove-Item -Recurse -Force
    }

    Function RemoveOld()
    {
        ## Remove previous backup folders. -Keep switch and -Compress switch are NOT configured.
        If ($Null -eq $History -And $Compress -eq $False)
        {
            Write-Log -Type Info -Evt "(VM:$Vm) Removing previous backups"
            ## Remove all previous backup folders
            If ($ShortDate)
            {
                ReportRemove -RemoveDir $WorkDir -RemoveFilePat "-*-*-*" -RemoveDirOpt $true -RemoveHistory $null
            }

            else {
                ReportRemove -RemoveDir $WorkDir -RemoveFilePat "-*-*-*_*-*-*" -RemoveDirOpt $true -RemoveHistory $null
            }

            ## If working directory is configured by user, remove all previous backup folders
            If ($WorkDir -ne $Backup)
            {
                ## Make sure the backup directory exists.

                If (Test-Path -Path $Backup)
                {
                    If ($ShortDate)
                    {
                        ReportRemove -RemoveDir $Backup -RemoveFilePat "-*-*-*" -RemoveDirOpt $true -RemoveHistory $null
                    }

                    else {
                        ReportRemove -RemoveDir $Backup -RemoveFilePat "-*-*-*_*-*-*" -RemoveDirOpt $true -RemoveHistory $null
                    }
                }
            }
        }

        ## Remove previous backup folders older than X configured days. -Keep switch is configured and -Compress switch is NOT.
        else {
            If ($Compress -eq $False)
            {
                Write-Log -Type Info -Evt "(VM:$Vm) Removing backup folders older than: $History days"

                ## Remove previous backup folders older than the configured number of days.
                If ($ShortDate)
                {
                    ReportRemove -RemoveDir $WorkDir -RemoveFilePat "-*-*-*" -RemoveDirOpt $true -RemoveHistory $History
                }

                else {
                    ReportRemove -RemoveDir $WorkDir -RemoveFilePat "-*-*-*_*-*-*" -RemoveDirOpt $true -RemoveHistory $History
                }

                ## If working directory is configured by user, remove all previous backup folders older than X configured days.
                If ($WorkDir -ne $Backup)
                {
                    ## Make sure the backup directory exists.
                    If (Test-Path -Path $Backup)
                    {
                        If ($ShortDate)
                        {
                            ReportRemove -RemoveDir $Backup -RemoveFilePat "-*-*-*" -RemoveDirOpt $true -RemoveHistory $History
                        }

                        else {
                            ReportRemove -RemoveDir $Backup -RemoveFilePat "-*-*-*_*-*-*" -RemoveDirOpt $true -RemoveHistory $History
                        }
                    }
                }
            }
        }

        ## Remove ALL previous backup files. -Keep switch is NOT configured and -Compress switch IS.
        If ($Compress)
        {
            If ($Null -eq $History)
            {
                Write-Log -Type Info -Evt "(VM:$Vm) Removing all previous compressed backups"

                ## Remove all previous compressed backups
                If ($ShortDate)
                {
                    Remove-Item "$WorkDir\$VmFixed-*-*-*.*" -Force
                }

                else {
                    Remove-Item "$WorkDir\$VmFixed-*-*-*_*-*-*.*" -Force
                }

                ## If working directory is configured by user, remove all previous backup files.
                If ($WorkDir -ne $Backup)
                {
                    ## Make sure the backup directory exists.
                    If (Test-Path -Path $Backup)
                    {
                        If ($ShortDate)
                        {
                            Remove-Item "$Backup\$VmFixed-*-*-*.*" -Force
                        }

                        else {
                            Remove-Item "$Backup\$VmFixed-*-*-*_*-*-*.*" -Force
                        }
                    }
                }
            }

            ## Remove previous backup files older than X days. -Keep and -Compress switch are configured.
            else {
                Write-Log -Type Info -Evt "(VM:$Vm) Removing compressed backups older than: $History days"

                ## Remove previous compressed backups older than the configured number of days.
                If ($ShortDate)
                {
                    ReportRemove -RemoveDir $WorkDir -RemoveFilePat "-*-*-*.*" -RemoveDirOpt $false -RemoveHistory $History
                }

                else {
                    ReportRemove -RemoveDir $WorkDir -RemoveFilePat "-*-*-*_*-*-*.*" -RemoveDirOpt $false -RemoveHistory $History
                }

                ## If working directory is configured by user, remove previous backup files older than X days.
                If ($WorkDir -ne $Backup)
                {
                    ## Make sure the backup directory exists.
                    If (Test-Path -Path $Backup)
                    {
                        If ($ShortDate)
                        {
                            ReportRemove -RemoveDir $Backup -RemoveFilePat "-*-*-*.*" -RemoveDirOpt $false -RemoveHistory $History
                        }

                        else {
                            ReportRemove -RemoveDir $Backup -RemoveFilePat "-*-*-*_*-*-*.*" -RemoveDirOpt $false -RemoveHistory $History
                        }
                    }
                }
            }
        }
    }

    Function OptionsRun()
    {
        If ($Compress)
        {
            ## If -Compress and -Sz are configured AND 7-zip is installed - compress the backup folder, if it isn't fallback to Windows compression.
            If ($Sz -eq $True -AND $7zT -eq $True)
            {
                Write-Log -Type Info -Evt "(VM:$Vm) Compressing backup using 7-Zip compression"

                ## If -Shortdate is configured, test for an old backup file, if true append a number (and increase the number if file still exists) before the file extension.
                If ($ShortDate)
                {
                    ## If using 7zip's split file feature with short dates, we need to handle the files a little differently.
                    If ($SzSwSplit -like "-v*")
                    {
                        $ShortDateT = Test-Path -Path ("$WorkDir\$VmFixed-$(Get-DateShort).*.*")

                        If ($ShortDateT)
                        {
                            ShortDateFileNo -ShortDateDir $WorkDir -ShortDateFilePat ".*.*"
                        }

                        else {
                            CompressFiles7zip(Get-DateShort) -CompressDir $WorkDir -CompressFileName "$VmFixed-$CompressDateFormat"
                        }
                    }

                    else
                    {
                        $ShortDateT = Test-Path -Path ("$WorkDir\$VmFixed-$(Get-DateShort).*")

                        If ($ShortDateT)
                        {
                            ShortDateFileNo -ShortDateDir $WorkDir -ShortDateFilePat ".*"
                        }

                        CompressFiles7zip(Get-DateShort) -CompressDir $WorkDir -CompressFileName "$VmFixed-$CompressDateFormat"
                    }
                }

                else {
                    CompressFiles7zip(Get-DateLong) -CompressDir $WorkDir -CompressFileName "$VmFixed-$CompressDateFormat"
                }
            }

            ## Compress the backup folder using Windows compression. -Compress is configured, -Sz switch is not, or it is and 7-zip isn't detected.
            ## This is also the "fallback" windows compression code.
            else {
                Write-Log -Type Info -Evt "(VM:$Vm) Compressing backup using Windows compression"

                If ($ShortDate)
                {
                    $ShortDateT = Test-Path -Path ("$WorkDir\$VmFixed-$(Get-DateShort).zip")

                    If ($ShortDateT)
                    {
                        ShortDateFileNo -ShortDateDir $WorkDir -ShortDateFilePat ".zip"
                    }

                    else {
                        CompressFilesWin(Get-DateShort) -CompressDir $WorkDir -CompressFileName "$VmFixed-$CompressDateFormat"
                    }
                }

                else {
                    CompressFilesWin(Get-DateLong) -CompressDir $WorkDir -CompressFileName "$VmFixed-$CompressDateFormat"
                }
            }

            ## After being compressed, if success remove the VMs export folder.
            If ($BackupSucc)
            {
                Get-ChildItem -Path $WorkDir -Filter "$Vm" -Directory | Remove-Item -Recurse -Force
            }

            else {
                Write-Log -Type Err -Evt "(VM:$Vm) Compressing backup failed."
            }

            ## If working directory has been configured by the user, move the compressed backup to the backup folder and rename to include the date.
            If ($WorkDir -ne $Backup)
            {
                ## Make sure the backup directory exists.
                If ((Test-Path -Path $Backup) -eq $False)
                {
                    Write-Log -Type Info -Evt "Backup directory $Backup doesn't exist. Creating it."
                    New-Item $Backup -ItemType Directory -Force | Out-Null
                }

                ## Get the exact name of the backup file and append numbers onto the filename, keeping the extension intact.
                ## This contains special code to do the shortDate renaming with any 7-zip split files.
                If ($ShortDate)
                {
                    If ($SzSwSplit -like "-v*")
                    {
                        $SzSplitFiles = Get-ChildItem -Path ("$WorkDir\$VmFixed-$(Get-DateShort).*.*") -File
                        
                        ForEach ($SplitFile in $SzSplitFiles) {
                            $ShortDateT = Test-Path -Path "$Backup\$($SplitFile.name)"

                            If ($ShortDateT)
                            {
                                Write-Log -Type Info -Evt "(VM:$Vm) File: $($SplitFile.name) already exists, appending number"
                                $FileExist = Get-ChildItem -Path "$Backup\$($SplitFile.name)" -File
                                $i = 1

                                $ShortDateNN = ("$VmFixed-$(Get-DateShort)-{0:D3}" -f $i++ + $FileExist.Extension)
                                $ShortDateExistT = Test-Path -Path $Backup\$ShortDateNN

                                If ($ShortDateExistT)
                                {
                                    do {
                                        $ShortDateNN = ("$VmFixed-$(Get-DateShort)-{0:D3}" -f $i++ + $FileExist.Extension)
                                        $ShortDateExistT = Test-Path -Path $Backup\$ShortDateNN
                                    } until ($ShortDateExistT -eq $false)
                                }

                                try {
                                    Get-ChildItem -Path $SplitFile | Move-Item -Destination $Backup\$ShortDateNN -ErrorAction 'Stop'
                                }
                                catch {
                                    $_.Exception.Message | Write-Log -Type Err -Evt "(VM:$Vm) $_"
                                }
                            }

                            else {
                                try {
                                    Get-ChildItem -Path $SplitFile | Move-Item -Destination $Backup\$ShortDateNN -ErrorAction 'Stop'
                                }
                                catch {
                                    $_.Exception.Message | Write-Log -Type Err -Evt "(VM:$Vm) $_"
                                }
                            }
                        }
                    }

                    else {
                        $BackupFile = Get-ChildItem -Path ("$WorkDir\$VmFixed-$(Get-DateShort).*") -File
                        $BackupFileN = $BackupFile.name
                        $BackupFileNSplit = $BackupFileN.split(".")

                        $ShortDateT = Test-Path -Path $Backup\$BackupFileN

                        If ($ShortDateT)
                        {
                            Write-Log -Type Info -Evt "(VM:$Vm) File: $BackupFileN already exists, appending number"
                            $FileExist = Get-ChildItem -Path $BackupFile -File
                            $i = 1
                            
                            If ($Null -eq $BackupFileNSplit[2])
                            {
                                $ShortDateNN = ("$VmFixed-$(Get-DateShort)-{0:D3}" -f $i++ + $FileExist.Extension)
                                $ShortDateExistT = Test-Path -Path $Backup\$ShortDateNN
                            }
                            else {
                                $ShortDateNN = ("$VmFixed-$(Get-DateShort)-{0:D3}" -f $i++ + "." + $BackupFileNSplit[1] + $FileExist.Extension)
                                $ShortDateExistT = Test-Path -Path $Backup\$ShortDateNN
                            }

                            If ($ShortDateExistT)
                            {
                                If ($Null -eq $BackupFileNSplit[2])
                                {
                                    do {
                                        $ShortDateNN = ("$VmFixed-$(Get-DateShort)-{0:D3}" -f $i++ + $FileExist.Extension)
                                        $ShortDateExistT = Test-Path -Path $Backup\$ShortDateNN
                                    } until ($ShortDateExistT -eq $false)
                                }
                                else {
                                    do {
                                        $ShortDateNN = ("$VmFixed-$(Get-DateShort)-{0:D3}" -f $i++ + "." + $BackupFileNSplit[1] + $FileExist.Extension)
                                        $ShortDateExistT = Test-Path -Path $Backup\$ShortDateNN
                                    } until ($ShortDateExistT -eq $false)
                                }
                            }

                            ## Move with shortdate and appended number
                            try {
                                Get-ChildItem -Path $BackupFile | Move-Item -Destination $Backup\$ShortDateNN -ErrorAction 'Stop'
                            }
                            catch {
                                $_.Exception.Message | Write-Log -Type Err -Evt "(VM:$Vm) $_"
                            }
                        }

                        ## Move with shortdate
                        try {
                            Get-ChildItem -Path $WorkDir -Filter "$VmFixed-*-*-*.*" | Move-Item -Destination $Backup -ErrorAction 'Stop'
                        }
                        catch {
                            $_.Exception.Message | Write-Log -Type Err -Evt "(VM:$Vm) $_"
                        }
                    }
                }

                ## Move with long date
                else {
                    try {
                        Get-ChildItem -Path $WorkDir -Filter "$VmFixed-*-*-*_*-*-*.*" | Move-Item -Destination $Backup -ErrorAction 'Stop'
                    }
                    catch {
                        $_.Exception.Message | Write-Log -Type Err -Evt "(VM:$Vm) $_"
                    }
                }
            }
        }

        ## -Compress switch is NOT configured and the -Keep switch is configured.
        ## Rename the export of each VM to include the date.
        else {
            If ($ShortDate)
            {
                $ShortDateT = Test-Path -Path ("$WorkDir\$VmFixed-$(Get-DateShort)")

                If ($ShortDateT)
                {
                    ShortDateFileNo -ShortDateDir $WorkDir -ShortDateFilePat $null
                }

                try {
                    Get-ChildItem -Path $WorkDir -Filter $Vm -Directory | Rename-Item -NewName ("$WorkDir\$VmFixed-$(Get-DateShort)")
                }
                catch {
                    $_.Exception.Message | Write-Log -Type Err -Evt "(VM:$Vm) $_"
                }
            }

            else {
                try {
                    Get-ChildItem -Path $WorkDir -Filter $Vm -Directory | Rename-Item -NewName ("$WorkDir\$VmFixed-$(Get-DateLong)")
                }
                catch {
                    $_.Exception.Message | Write-Log -Type Err -Evt "(VM:$Vm) $_"
                }
            }

            ## If working directory has been configured by the user, move the backup to the backup folder and rename to include the date.
            If ($WorkDir -ne $Backup)
            {
                ## Make sure the backup directory exists.
                If ((Test-Path -Path $Backup) -eq $False)
                {
                    Write-Log -Type Info -Evt "Backup directory $Backup doesn't exist. Creating it."
                    New-Item $Backup -ItemType Directory -Force | Out-Null
                }

                If ($ShortDate)
                {
                    $ShortDateT = Test-Path -Path ("$Backup\$VmFixed-$(Get-DateShort)")

                    If ($ShortDateT)
                    {
                        ShortDateFileNo -ShortDateDir $Backup -ShortDateFilePat $null
                    }

                    ## Moving backup folder with shortdate
                    try {
                        Get-ChildItem -Path $WorkDir -Filter "$VmFixed-*-*-*" -Directory | Move-Item -Destination ("$Backup\$VmFixed-$(Get-DateShort)") -ErrorAction 'Stop'
                    }
                    catch {
                        $_.Exception.Message | Write-Log -Type Err -Evt "(VM:$Vm) $_"
                    }
                }

                ## Moving backup folder with longdate
                else {
                    try {
                        Get-ChildItem -Path $WorkDir -Filter "$VmFixed-*-*-*_*-*-*" -Directory | Move-Item -Destination ("$Backup\$VmFixed-$(Get-DateLong)") -ErrorAction 'Stop'
                    }
                    catch {
                        $_.Exception.Message | Write-Log -Type Err -Evt "(VM:$Vm) $_"
                    }
                }
            }
        }
    }
    ##
    ## End of backup Options functions
    ##

    ## getting Windows Version info
    $OSVMaj = [environment]::OSVersion.Version | Select-Object -expand major
    $OSVMin = [environment]::OSVersion.Version | Select-Object -expand minor
    $OSVBui = [environment]::OSVersion.Version | Select-Object -expand build
    $OSV = "$OSVMaj" + "." + "$OSVMin" + "." + "$OSVBui"

    If ($Null -eq $BackupUsr)
    {
        Write-Log -Type Err -Evt "You must specify -BackupTo [path\]."
        Exit
    }

    else {
        ## Test for Hyper-V feature installed on local machine.
        ## Old version of Win Serv have a different service name.
        try {
            If ($OSV -eq "6.3.9600")
            {
                Get-Service vmms -ErrorAction Stop | Out-Null
            }

            else {
                Get-Service vmcompute -ErrorAction Stop | Out-Null
            }
        }

        catch {
            Write-Log -Type Err -Evt "Hyper-V is not installed on this local machine."
            Exit
        }

        If ($Compress -eq $false -And $Sz -eq $true)
        {
            Write-Log -Type Err -Evt "You must specify -Compress to use -Sz."
            Exit
        }

        If ($Sz -eq $false -And $Null -ne $SzSwitches)
        {
            Write-Log -Type Err -Evt "You must specify -Sz to use -SzOptions."
            Exit
        }

        If ($Null -eq $LogPathUsr -And $Null -ne $LogHistory)
        {
            Write-Log -Type Err -Evt "You must specify -L [path\] to use -LogRotate [number]."
            Exit
        }

        If ($Null -eq $LogPathUsr -And $SmtpServer)
        {
            Write-Log -Type Err -Evt "You must specify -L [path\] to use the email log function."
            Exit
        }

        If ($Null -eq $LogPathUsr -And $Webh)
        {
            Write-Log -Type Err -Evt "You must specify -L [path\] to use send the log to a webhook."
            Exit
        }

        ## Clean User entered string
        If ($BackupUsr)
        {
            $Backup = $BackupUsr.trimend('\')
        }

        If ($WorkDirUsr)
        {
            $WorkDir = $WorkDirUsr.trimend('\')
        }
    }

    ## Setting an easier to use variable for computer name of the Hyper-V server.
    $Vs = $Env:ComputerName

    ## If a VM list file is configured, get the content of the file, otherwise just get the running VMs.
    ## Clean list if it has empty lines.
    If ($VmList)
    {
        $Vms = Get-Content $VmList | Where-Object {$_.trim() -ne ""}
    }

    else {
        $Vms = Get-VM | Where-Object {$_.State -eq 'Running'} | Select-Object -ExpandProperty Name
    }

    ## Check to see if there are any VMs to process.
    If ($Vms.count -ne 0)
    {
        ## If the user has not configured the working directory, set it as the backup directory.
        If ($Null -eq $WorkDir)
        {
            $WorkDir = "$Backup"
        }

        If ($Null -eq $ShortDate)
        {
            $ShortDate = "$LongDate"
        }

        If ($SzSwitches)
        {
            $SzSwSplit = $SzSwitches.split(",")
        }

        If ($Sz -eq $True)
        {
            $7zT = Test-Path -Path "$env:programfiles\7-Zip\7z.exe"
        }

        ##
        ## Display the current config and log if configured.
        ##

        Write-Log -Type Conf -Evt "--- Running with the following config ---"
        Write-Log -Type Conf -Evt "Utility Version: 23.02.18"
        UpdateCheck ## Run Update checker function
        Write-Log -Type Conf -Evt "Hostname: $Vs."
        Write-Log -Type Conf -Evt "Windows Version: $OSV."

        If ($Vms)
        {
            Write-Log -Type Conf -Evt "No. of VMs: $($Vms.count)."
            Write-Log -Type Conf -Evt "VMs to backup:"
            ForEach ($Vm in $Vms)
            {
                Write-Log -Type Conf -Evt "*** $Vm ***"
            }
        }

        If ($BackupUsr)
        {
            Write-Log -Type Conf -Evt "Backup directory: $BackupUsr."
        }

        If ($WorkDirUsr)
        {
            Write-Log -Type Conf -Evt "Working directory: $WorkDirUsr."
        }

        If ($NoPerms)
        {
            Write-Log -Type Conf -Evt "-NoPerms switch: $NoPerms."
        }

        If ($ShortDate)
        {
            Write-Log -Type Conf -Evt "-ShortDate switch: $ShortDate."
        }

        If ($LowDisk)
        {
            Write-Log -Type Conf -Evt "-LowDisk switch: $LowDisk."
        }

        If ($Compress)
        {
            Write-Log -Type Conf -Evt "-Compress switch: $Compress."
        }

        If ($Sz)
        {
            Write-Log -Type Conf -Evt "-Sz switch: $Sz."
        }

        If ($Sz)
        {
            Write-Log -Type Conf -Evt "7-zip installed: $7zT."
        }

        If ($SzSwitches)
        {
            Write-Log -Type Conf -Evt "7-zip Options: $SzSwitches."
        }

        If ($Null -ne $History)
        {
            Write-Log -Type Conf -Evt "Backups to keep: $History days"
        }

        If ($LogPathUsr)
        {
            Write-Log -Type Conf -Evt "Logs directory: $LogPathUsr."
        }

        If ($Webh)
        {
            Write-Log -Type Conf -Evt "Webhook file: $Webh."
        }

        If ($MailTo)
        {
            Write-Log -Type Conf -Evt "E-mail log to: $MailTo."
        }

        If ($MailFrom)
        {
            Write-Log -Type Conf -Evt "E-mail log from: $MailFrom."
        }

        If ($MailSubject)
        {
            Write-Log -Type Conf -Evt "E-mail subject: $MailSubject."
        }

        If ($SmtpServer)
        {
            Write-Log -Type Conf -Evt "SMTP server: Configured"
        }

        If ($SmtpUser)
        {
            Write-Log -Type Conf -Evt "SMTP auth: Configured"
        }
        Write-Log -Type Conf -Evt "---"
        Write-Log -Type Info -Evt "Process started"
        ##
        ## Display current config ends here.
        ##

        ## For Success/Fail stats
        $Succi = 0
        $Faili = 0

        ##
        ## -NoPerms process starts here.
        ##
        ## If the -NoPerms switch is set, start a custom process to copy all the VM data.
        If ($NoPerms)
        {
            ForEach ($Vm in $Vms)
            {
                $VmFixed = $Vm.replace(".","-")
                $VmInfo = Get-VM -Name $Vm
                $BackupSucc = $false

                ## Remove old backups if -LowDisk is configured
                If ($LowDisk)
                {
                    RemoveOld
                }

                ## Test for the existence of a previous VM export. If it exists, delete it.
                If (Test-Path -Path "$WorkDir\$Vm")
                {
                    Remove-Item "$WorkDir\$Vm" -Recurse -Force
                }

                ## Create directories for the VM export.
                try {
                    New-Item "$WorkDir\$Vm" -ItemType Directory -Force | Out-Null
                    New-Item "$WorkDir\$Vm\Virtual Machines" -ItemType Directory -Force | Out-Null
                    New-Item "$WorkDir\$Vm\Virtual Hard Disks" -ItemType Directory -Force | Out-Null
                    New-Item "$WorkDir\$Vm\Snapshots" -ItemType Directory -Force | Out-Null
                    $BackupSucc = $true
                }
                catch {
                    $_.Exception.Message | Write-Log -Type Err -Evt "(VM:$Vm) $_"
                    $BackupSucc = $false
                }

                ## Check for VM running
                If (Get-VM | Where-Object {$VmInfo.State -eq 'Running'})
                {
                    $VMwasRunning = $true
                    Write-Log -Type Info -Evt "(VM:$Vm) VM is running, saving state"
                    Stop-VM -Name $Vm -Save
                }

                else {
                    $VMwasRunning = $false
                    Write-Log -Type Info -Evt "(VM:$Vm) VM not running"
                }

                ##
                ## Copy the VM config files and log if there is an error.
                ##

                ## Check for VM being in the correct state before continuing
                $VmState = Get-Vm -Name $Vm

                If ($VmState.State -ne 'Off' -OR $VmState.State -ne 'Saved' -AND $VmState.Status -ne 'Operating normally')
                {
                    do {
                        Write-Log -Type Err -Evt "(VM:$Vm) VM not in the desired state. Waiting 60 seconds..."
                        Start-Sleep -S 60
                    } until ($VmState.State -eq 'Off' -OR $VmState.State -eq 'Saved' -AND $VmState.Status -eq 'Operating normally')
                }

                try {
                    $BackupSucc = $false
                    Write-Log -Type Info -Evt "(VM:$Vm) Copying config files"
                    Copy-Item "$($VmInfo.ConfigurationLocation)\Virtual Machines\$($VmInfo.id)" "$WorkDir\$Vm\Virtual Machines\" -Recurse -Force
                    Copy-Item "$($VmInfo.ConfigurationLocation)\Virtual Machines\$($VmInfo.id).*" "$WorkDir\$Vm\Virtual Machines\" -Recurse -Force
                    $BackupSucc = $true
                }
                catch {
                    $_.Exception.Message | Write-Log -Type Err -Evt "(VM:$Vm) $_"
                    $BackupSucc = $false
                }
                ##
                ## End of VM config files.
                ##

                ##
                ## Copy the VHDs and log if there is an error.
                ##
                try {
                    $BackupSucc = $false
                    Write-Log -Type Info -Evt "(VM:$Vm) Copying VHD files"
                    Copy-Item $VmInfo.HardDrives.Path -Destination "$WorkDir\$Vm\Virtual Hard Disks\" -Recurse -Force
                    $BackupSucc = $true
                }
                catch {
                    $_.Exception.Message | Write-Log -Type Err -Evt "(VM:$Vm) $_"
                    $BackupSucc = $false
                }
                ##
                ## End of VHDs.
                ##

                ## Get the VM snapshots/checkpoints.
                $Snaps = Get-VMSnapshot $Vm

                ForEach ($Snap in $Snaps)
                {
                    ##
                    ## Copy the snapshot config files and log if there is an error.
                    ##
                    try {
                        $BackupSucc = $false
                        Write-Log -Type Info -Evt "(VM:$Vm) Copying Snapshot config files"
                        Copy-Item "$($VmInfo.ConfigurationLocation)\Snapshots\$($Snap.id)" "$WorkDir\$Vm\Snapshots\" -Recurse -Force
                        Copy-Item "$($VmInfo.ConfigurationLocation)\Snapshots\$($Snap.id).*" "$WorkDir\$Vm\Snapshots\" -Recurse -Force
                        $BackupSucc = $true
                    }
                    catch {
                        $_.Exception.Message | Write-Log -Type Err -Evt "(VM:$Vm) $_"
                        $BackupSucc = $false
                    }
                    ##
                    ## End of snapshot config.
                    ##

                    ## Copy the snapshot root VHD.
                    try {
                        $BackupSucc = $false
                        Write-Log -Type Info -Evt "(VM:$Vm) Copying Snapshot root VHD files"
                        Copy-Item $Snap.HardDrives.Path -Destination "$WorkDir\$Vm\Virtual Hard Disks\" -Recurse -Force -ErrorAction 'Stop'
                        $BackupSucc = $true
                    }
                    catch {
                        $_.Exception.Message | Write-Log -Type Err -Evt "(VM:$Vm) $_"
                        $BackupSucc = $false
                    }
                }

                If ($VMwasRunning)
                {
                    Write-Log -Type Info -Evt "(VM:$Vm) Starting VM"
                    Start-VM $Vm
                    Write-Log -Type Info -Evt "(VM:$Vm) Waiting 60 seconds..."
                    Start-Sleep -S 60
                }

                If ($BackupSucc)
                {
                    ## Remove old backups if -LowDisk is NOT configured
                    If ($LowDisk -eq $false)
                    {
                        RemoveOld
                    }

                    OptionsRun
                    Write-Log -Type Succ -Evt "(VM:$Vm) Backup Successful"
                    $Succi = $Succi+1
                }

                else {
                    Write-Log -Type Err -Evt "(VM:$Vm) Backup failed, VM skipped"
                    $Faili = $Faili+1
                }
            }
        }
        ##
        ## -NoPerms process ends here.
        ##

        ##
        ## Standard export process starts here.
        ##

        ## If the -NoPerms switch is NOT set, for each VM check for the existence of a previous export.
        ## If it exists then delete it, otherwise the export will fail.
        else {
            ForEach ($Vm in $Vms)
            {
                If (Test-Path -Path "$WorkDir\$Vm")
                {
                    Remove-Item "$WorkDir\$Vm" -Recurse -Force
                }

                If ($WorkDir -ne $Backup)
                {
                    If (Test-Path -Path "$Backup\$Vm")
                    {
                        Remove-Item "$Backup\$Vm" -Recurse -Force
                    }
                }
            }

            ## If default key is already null, then disable VSS Legacy Tracing on Windows Server 2016 to prevent possible BSOD on Hyper-V Host.
            ## Don't want to mess up anyone's config. :)
            If ($OSV -eq "10.0.14393")
            {
                If ($null -eq (get-ItemProperty -literalPath HKLM:\System\CurrentControlSet\Services\VSS\Diag\).'(Default)')
                {
                    $RegVSSFix = $True
                    Set-ItemProperty -Path HKLM:\System\CurrentControlSet\Services\VSS\Diag -Name "(default)" -Value "Disabled"
                    Write-Log -Type Info -Evt "Disabling VSS Legacy Tracing on Windows Server 2016 to prevent possible BSOD on Hyper-V Host."
                }
            }

            ## Do a regular export of the VMs.
            ForEach ($Vm in $Vms)
            {
                $VmFixed = $Vm.replace(".","-")
                $BackupSucc = $false

                ## Remove old backups if -LowDisk is configured
                If ($LowDisk)
                {
                    RemoveOld
                }

                try {
                    Write-Log -Type Info -Evt "(VM:$Vm) Attempting to export VM"
                    $Vm | Export-VM -Path "$WorkDir" -ErrorAction 'Stop'
                    $BackupSucc = $true
                }
                catch {
                    $_.Exception.Message | Write-Log -Type Err -Evt "(VM:$Vm) $_"
                    $BackupSucc = $false
                }

                If ($BackupSucc)
                {
                    ## Remove old backups if -LowDisk is NOT configured
                    If ($LowDisk -eq $false)
                    {
                        RemoveOld
                    }

                    OptionsRun
                    Write-Log -Type Succ -Evt "(VM:$Vm) Backup Successful"
                    $Succi = $Succi+1
                }

                else {
                    Write-Log -Type Err -Evt "(VM:$Vm) Export failed, VM skipped"
                    $Faili = $Faili+1
                }
            }

            ## If the VSS fix was run, return regkey back to original state.
            If ($OSV -eq "10.0.14393")
            {
                If ($RegVSSFix)
                {
                    REG DELETE "HKEY_LOCAL_MACHINE\System\CurrentControlSet\Services\VSS\Diag" /ve /f
                    Write-Log -Type Info -Evt "Returning VSS Legacy Tracing config to default."
                }
            }
        }
        ##
        ## End of standard export block
        ##
    }

    ## If there are no VMs, then do nothing.
    else {
        Write-Log -Type Err -Evt "There are no VMs running to backup"
    }

    Write-Log -Type Info -Evt "Process finished."
    Write-Log -Type Info -Evt "Number of VMs to Backup:$($Vms.count)"
    Write-Log -Type Info -Evt "Backups Successful:$Succi"
    Write-Log -Type Info -Evt "Backups Failed:$Faili"

    If ($Null -ne $LogHistory)
    {
        ## Clean up logs.
        Write-Log -Type Info -Evt "Deleting logs older than: $LogHistory days"
        Get-ChildItem -Path "$LogPath\Hyper-V-Backup_*" -File | Where-Object CreationTime -lt (Get-Date).AddDays(-$LogHistory) | Remove-Item -Recurse
    }

    ## This whole block is for e-mail, if it is configured.
    If ($SmtpServer)
    {
        If (Test-Path -Path $Log)
        {
            ## Default e-mail subject if none is configured.
            If ($Null -eq $MailSubject)
            {
                $MailSubject = "Hyper-V Backup Utility Log"
            }

            ## Default Smtp Port if none is configured.
            If ($Null -eq $SmtpPort)
            {
                $SmtpPort = "25"
            }

            ## Setting the contents of the log to be the e-mail body.
            $MailBody = Get-Content -Path $Log | Out-String

            ForEach ($MailAddress in $MailTo)
            {
                ## If an smtp password is configured, get the username and password together for authentication.
                ## If an smtp password is not provided then send the e-mail without authentication and obviously no SSL.
                If ($SmtpPwd)
                {
                    $SmtpPwdEncrypt = Get-Content $SmtpPwd | ConvertTo-SecureString
                    $SmtpCreds = New-Object System.Management.Automation.PSCredential -ArgumentList ($SmtpUser, $SmtpPwdEncrypt)

                    ## If -ssl switch is used, send the email with SSL.
                    ## If it isn't then don't use SSL, but still authenticate with the credentials.
                    If ($UseSsl)
                    {
                        Send-MailMessage -To $MailAddress -From $MailFrom -Subject "$MailSubject $Succi/$($Vms.count) VMs Successful" -Body $MailBody -SmtpServer $SmtpServer -Port $SmtpPort -UseSsl -Credential $SmtpCreds
                    }

                    else {
                        Send-MailMessage -To $MailAddress -From $MailFrom -Subject "$MailSubject $Succi/$($Vms.count) VMs Successful" -Body $MailBody -SmtpServer $SmtpServer -Port $SmtpPort -Credential $SmtpCreds
                    }
                }

                else {
                    Send-MailMessage -To $MailAddress -From $MailFrom -Subject "$MailSubject $Succi/$($Vms.count) VMs Successful" -Body $MailBody -SmtpServer $SmtpServer -Port $SmtpPort
                }
            }
        }

        else {
            Write-Host -ForegroundColor Red -BackgroundColor Black -Object "There's no log file to email."
        }
    }
    ## End of Email block

    ## Webhook block
    If ($Webh)
    {
        $WebHookUri = Get-Content $Webh
        $WebHookArr = @()

        $title       = "Hyper-V Backup Utility $Succi/$($Vms.count) VMs Successful"
        $description = Get-Content -Path $Log | Out-String

        $WebHookObj = [PSCustomObject]@{
            title = $title
            description = $description
        }

        $WebHookArr += $WebHookObj
        $payload = [PSCustomObject]@{
            embeds = $WebHookArr
        }

        Invoke-RestMethod -Uri $WebHookUri -Body ($payload | ConvertTo-Json -Depth 2) -Method Post -ContentType 'application/json'
    }
}
## End