dbaclone.psm1

$script:ModuleRoot = $PSScriptRoot
$script:ModuleVersion = (Import-PowerShellDataFile -Path "$($script:ModuleRoot)\dbaclone.psd1").ModuleVersion

# Detect whether at some level dotsourcing was enforced
$script:doDotSource = Get-PSFConfigValue -FullName dbaclone.Import.DoDotSource -Fallback $false
if ($dbaclone_dotsourcemodule) { $script:doDotSource = $true }

<#
Note on Resolve-Path:
All paths are sent through Resolve-Path/Resolve-PSFPath in order to convert them to the correct path separator.
This allows ignoring path separators throughout the import sequence, which could otherwise cause trouble depending on OS.
Resolve-Path can only be used for paths that already exist, Resolve-PSFPath can accept that the last leaf my not exist.
This is important when testing for paths.
#>


# Detect whether at some level loading individual module files, rather than the compiled module was enforced
$importIndividualFiles = Get-PSFConfigValue -FullName dbaclone.Import.IndividualFiles -Fallback $false
if ($dbaclone_importIndividualFiles) { $importIndividualFiles = $true }
if (Test-Path (Resolve-PSFPath -Path "$($script:ModuleRoot)\..\.git" -SingleItem -NewChild)) { $importIndividualFiles = $true }
if ("<was compiled>" -eq '<was not compiled>') { $importIndividualFiles = $true }
    
function Import-ModuleFile
{
    <#
        .SYNOPSIS
            Loads files into the module on module import.
        
        .DESCRIPTION
            This helper function is used during module initialization.
            It should always be dotsourced itself, in order to proper function.
            
            This provides a central location to react to files being imported, if later desired
        
        .PARAMETER Path
            The path to the file to load
        
        .EXAMPLE
            PS C:\> . Import-ModuleFile -File $function.FullName
    
            Imports the file stored in $function according to import policy
    #>

    [CmdletBinding()]
    Param (
        [string]
        $Path
    )
    
    $resolvedPath = $ExecutionContext.SessionState.Path.GetResolvedPSPathFromPSPath($Path).ProviderPath
    if ($doDotSource) { . $resolvedPath }
    else { $ExecutionContext.InvokeCommand.InvokeScript($false, ([scriptblock]::Create([io.file]::ReadAllText($resolvedPath))), $null, $null) }
}

#region Load individual files
if ($importIndividualFiles)
{
    # Execute Preimport actions
    . Import-ModuleFile -Path "$ModuleRoot\internal\scripts\preimport.ps1"
    
    # Import all internal functions
    foreach ($function in (Get-ChildItem "$ModuleRoot\internal\functions" -Filter "*.ps1" -Recurse -ErrorAction Ignore))
    {
        . Import-ModuleFile -Path $function.FullName
    }
    
    # Import all public functions
    foreach ($function in (Get-ChildItem "$ModuleRoot\functions" -Filter "*.ps1" -Recurse -ErrorAction Ignore))
    {
        . Import-ModuleFile -Path $function.FullName
    }
    
    # Execute Postimport actions
    . Import-ModuleFile -Path "$ModuleRoot\internal\scripts\postimport.ps1"
    
    # End it here, do not load compiled code below
    return
}
#endregion Load individual files

#region Load compiled code
function Test-DcnConfiguration {
    <#
    .SYNOPSIS
        Test the configuration of the module

    .DESCRIPTION
        The configuration of the module is vital to let it function.
        This function checks several configurations

    .PARAMETER SqlInstance
        The instance that represents the dbaclone instance that holds the database

    .PARAMETER SqlCredential
        Allows you to login to servers using SQL Logins as opposed to Windows Auth/Integrated/Trusted. To use:

        $scred = Get-Credential, then pass $scred object to the -SourceSqlCredential parameter.

        Windows Authentication will be used if SqlCredential is not specified. SQL Server does not accept Windows credentials being passed as credentials.
        To connect as a different Windows user, run PowerShell as that user.

    .PARAMETER Database
        The database that holds all the information for the dbaclone module

    .PARAMETER EnableException
        By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message.
        This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting.
        Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch.

    .PARAMETER WhatIf
        If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run.

    .PARAMETER Confirm
        If this switch is enabled, you will be prompted for confirmation before executing any operations that change state.

    .NOTES
        Author: Sander Stad (@sqlstad, sqlstad.nl)

        Website: https://dbaclone.org
        Copyright: (C) Sander Stad, sander@sqlstad.nl
        License: MIT https://opensource.org/licenses/MIT

    .LINK
        https://dbaclone.org/

    .EXAMPLE
        Test-DcnConfiguration

        Test the configuration of the module retrieving the set configurations

    .EXAMPLE
        Test-DcnConfiguration -SqlInstance SQLDB1 -Database dbaclone

        Test the configuration with the instance and database set

    #>


    [CmdLetBinding()]

    param(
        [DbaInstanceParameter]$SqlInstance,
        [System.Management.Automation.PSCredential]
        $SqlCredential,
        [string]$Database,
        [switch]$EnableException
    )

    Write-PSFMessage -Message "SqlInstance: $SqlInstance, Database: $Database" -Level Debug

    # Check if the values for the dbaclone database are set
    if (($null -eq $SqlInstance) -or ($null -eq $Database) -or ($null -eq $SqlCredential)) {
        # Get the configurations for the program database
        $Database = Get-PSFConfigValue -FullName dbaclone.database.name -Fallback "NotConfigured"
        $SqlInstance = Get-PSFConfigValue -FullName dbaclone.database.server -Fallback "NotConfigured"
        $SqlCredential = Get-PSFConfigValue -FullName dbaclone.informationstore.credential -Fallback $null
    }

    Write-PSFMessage -Message "Checking configurations" -Level Verbose

    # Check the module database server and database name configurations
    if ($SqlInstance -eq 'NotConfigured') {
        Stop-PSFFunction -Message "The dbaclone database server is not yet configured. Please run Set-DcnConfiguration" -Target $SqlInstance -Continue
    }

    if ($Database -eq 'NotConfigured') {
        Stop-PSFFunction -Message "The dbaclone database is not yet configured. Please run Set-DcnConfiguration" -Target $Database -Continue
    }

    Write-PSFMessage -Message "Attempting to connect to dbaclone database server $SqlInstance.." -Level Verbose
    try {
        $pdcServer = Connect-DbaInstance -SqlInstance $SqlInstance -SqlCredential $SqlCredential -NonPooledConnection
    }
    catch {
        Stop-PSFFunction -Message "Could not connect to Sql Server instance $SqlInstance" -ErrorRecord $_ -Target $pdcServer -Continue
    }

    # Check if the dbaclone database is present
    if ($pdcServer.Databases.Name -notcontains $Database) {
        Stop-PSFFunction -Message "dbaclone database $Database is not present on $SqlInstance" -Target $pdcServer -Continue
    }

    Write-PSFMessage -Message "Finished checking configurations" -Level Verbose

}


function Get-DcnClone {
    <#
    .SYNOPSIS
        Get-DcnClone get on or more clones

    .DESCRIPTION
        Get-DcnClone will retrieve the clones and apply filters if needed.
        By default all the clones are returned

    .PARAMETER SqlCredential
        Allows you to login to servers using SQL Logins as opposed to Windows Auth/Integrated/Trusted. To use:

        $scred = Get-Credential, then pass $scred object to the -SqlCredential parameter.

        Windows Authentication will be used if SqlCredential is not specified. SQL Server does not accept Windows credentials being passed as credentials.
        To connect as a different Windows user, run PowerShell as that user.

    .PARAMETER DcnSqlCredential
        Allows you to login to servers using SQL Logins as opposed to Windows Auth/Integrated/Trusted.
        This works similar as SqlCredential but is only meant for authentication to the PSDatabaseClone database server and database.

    .PARAMETER Credential
        Allows you to login to servers or use authentication to access files and folder/shares

        $scred = Get-Credential, then pass $scred object to the -Credential parameter.

    .PARAMETER HostName
        Filter based on the hostname

    .PARAMETER Database
        Filter based on the database

    .PARAMETER ImageID
        Filter based on the image id

    .PARAMETER ImageName
        Filter based on the image name

    .PARAMETER ImageLocation
        Filter based on the image location

    .PARAMETER EnableException
        By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message.
        This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting.
        Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch.

    .PARAMETER WhatIf
        If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run.

    .PARAMETER Confirm
        If this switch is enabled, you will be prompted for confirmation before executing any operations that change state.

    .NOTES
        Author: Sander Stad (@sqlstad, sqlstad.nl)

        Website: https://psdatabaseclone.org
        Copyright: (C) Sander Stad, sander@sqlstad.nl
        License: MIT https://opensource.org/licenses/MIT

    .LINK
        https://psdatabaseclone.org/

    .EXAMPLE
        Get-DcnClone

        Get all the clones

    .EXAMPLE
        Get-DcnClone -HostName host1, host2

        Retrieve the clones for host1 and host2

    .EXAMPLE
        Get-DcnClone -Database DB1

        Get all the clones that have the name DB1

    .EXAMPLE
        Get-DcnClone -ImageName DB1_20180703085917

        Get all the clones that were made with image "DB1_20180703085917"
    #>


    [CmdLetBinding()]

    param(
        [PSCredential]$SqlCredential,
        [PSCredential]$DcnSqlCredential,
        [PSCredential]$Credential,
        [string[]]$HostName,
        [string[]]$Database,
        [int[]]$ImageID,
        [string[]]$ImageName,
        [string[]]$ImageLocation,
        [switch]$EnableException
    )

    begin {
        # Check if the setup has ran
        if (-not (Test-DcnModule -SetupStatus)) {
            Stop-PSFFunction -Message "The module setup has NOT yet successfully run. Please run 'Set-DcnConfiguration'" -Continue
        }

        # Get the information store
        $informationStore = Get-PSFConfigValue -FullName psdatabaseclone.informationstore.mode

        if ($informationStore -eq 'SQL') {

            # Get the module configurations
            $pdcSqlInstance = Get-PSFConfigValue -FullName psdatabaseclone.database.server
            $pdcDatabase = Get-PSFConfigValue -FullName psdatabaseclone.database.name
            if (-not $DcnSqlCredential) {
                $pdcCredential = Get-PSFConfigValue -FullName psdatabaseclone.informationstore.credential -Fallback $null
            }
            else {
                $pdcCredential = $DcnSqlCredential
            }

            # Test the module database setup
            try {
                Test-DcnConfiguration -SqlCredential $pdcCredential -EnableException
            }
            catch {
                Stop-PSFFunction -Message "Something is wrong in the module configuration" -ErrorRecord $_ -Continue
            }

            $query = "
                SELECT c.CloneID,
                    c.CloneLocation,
                    c.AccessPath,
                    c.SqlInstance,
                    c.DatabaseName,
                    c.IsEnabled,
                    i.ImageID,
                    i.ImageName,
                    i.ImageLocation,
                    h.HostName
                FROM dbo.Clone AS c
                    INNER JOIN dbo.Host AS h
                        ON h.HostID = c.HostID
                    INNER JOIN dbo.Image AS i
                        ON i.ImageID = c.ImageID;
            "


            try {
                $results = @()
                $results = Invoke-DbaQuery -SqlInstance $pdcSqlInstance -SqlCredential $pdcCredential -Database $pdcDatabase -Query $query -As PSObject
            }
            catch {
                Stop-PSFFunction -Message "Could not execute query" -ErrorRecord $_ -Target $query
            }
        }
        elseif ($informationStore -eq 'File') {
            # Create the PS Drive and get the results
            try {
                if (Test-Path -Path "DCNJSONFolder:\") {
                    # Get the clones
                    $results = Get-ChildItem -Path "DCNJSONFolder:\" -Filter "*clones.json" | ForEach-Object { Get-Content $_.FullName | ConvertFrom-Json }
                }
                else {
                    Stop-PSFFunction -Message "Could not reach clone information location 'DCNJSONFolder:\'" -ErrorRecord $_ -Target "DCNJSONFolder:\"
                    return
                }
            }
            catch {
                Stop-PSFFunction -Message "Couldn't get results from JSN folder" -ErrorRecord $_ -Target "DCNJSONFolder:\"
                return
            }
        }

        # Filter host name
        if ($HostName) {
            $results = $results | Where-Object { $_.HostName -in $HostName }
        }

        # Filter image id
        if ($Database) {
            $results = $results | Where-Object { $_.DatabaseName -in $Database }
        }

        # Filter image id
        if ($ImageID) {
            $results = $results | Where-Object { $_.ImageID -in $ImageID }
        }

        # Filter image name
        if ($ImageName) {
            $results = $results | Where-Object { $_.ImageName -in $ImageName }
        }

        # Filter image location
        if ($ImageLocation) {
            $results = $results | Where-Object { $_.ImageLocation -in $ImageLocation }
        }

    }

    process {

        # Test if there are any errors
        if (Test-PSFFunctionInterrupt) { return }

        # Convert the results to the DCLClone data type
        foreach ($result in $results) {

            [pscustomobject]@{
                CloneID       = $result.CloneID
                CloneLocation = $result.CloneLocation
                AccessPath    = $result.AccessPath
                SqlInstance   = $result.SqlInstance
                DatabaseName  = $result.DatabaseName
                IsEnabled     = $result.IsEnabled
                ImageID       = $result.ImageID
                ImageName     = $result.ImageName
                ImageLocation = $result.ImageLocation
                HostName      = $result.HostName
            }
        }
    }

    end {

        # Test if there are any errors
        if (Test-PSFFunctionInterrupt) { return }

        Write-PSFMessage -Message "Finished retrieving clone(s)" -Level Verbose

    }
}


function Invoke-DcnRepairClone {
    <#
    .SYNOPSIS
        Invoke-DcnRepairClone repairs the clones

    .DESCRIPTION
        Invoke-DcnRepairClone has the ability to repair the clones when they have gotten disconnected from the image.
        In such a case the clone is no longer available for the database server and the database will either not show
        any information or the database will have the status (Recovery Pending).

        By running this command all the clones will be retrieved from the database for a certain host.

    .PARAMETER HostName
        Set on or more hostnames to retrieve the configurations for

    .PARAMETER SqlCredential
        Allows you to login to servers using SQL Logins as opposed to Windows Auth/Integrated/Trusted. To use:

        $scred = Get-Credential, then pass $scred object to the -SqlCredential parameter.

        Windows Authentication will be used if SqlCredential is not specified. SQL Server does not accept Windows credentials being passed as credentials.
        To connect as a different Windows user, run PowerShell as that user.

    .PARAMETER Credential
        Allows you to login to servers using SQL Logins as opposed to Windows Auth/Integrated/Trusted.
        This works similar as SqlCredential but is only meant for authentication to the the host

    .PARAMETER DcnSqlCredential
        Allows you to login to servers using SQL Logins as opposed to Windows Auth/Integrated/Trusted.
        This works similar as SqlCredential but is only meant for authentication to the PSDatabaseClone database server and database.

    .PARAMETER EnableException
        By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message.
        This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting.
        Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch.

    .PARAMETER WhatIf
        If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run.

    .PARAMETER Confirm
        If this switch is enabled, you will be prompted for confirmation before executing any operations that change state.

    .PARAMETER WhatIf
        If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run.

    .PARAMETER Confirm
        If this switch is enabled, you will be prompted for confirmation before executing any operations that change state.

    .NOTES
        Author: Sander Stad (@sqlstad, sqlstad.nl)

        Website: https://psdatabaseclone.org
        Copyright: (C) Sander Stad, sander@sqlstad.nl
        License: MIT https://opensource.org/licenses/MIT

    .LINK
        https://psdatabaseclone.org/

    .EXAMPLE
        Invoke-DcnRepairClone -Hostname Host1

        Repair the clones for Host1

    #>


    [CmdLetBinding(SupportsShouldProcess = $true)]

    param(
        [Parameter(Mandatory = $true)]
        [string[]]$HostName,
        [PSCredential]$SqlCredential,
        [PSCredential]$Credential,
        [PSCredential]$DcnSqlCredential,
        [switch]$EnableException
    )

    begin {
        # Check if the console is run in Administrator mode
        if ( -not (Test-PSFPowerShell -Elevated) ) {
            Stop-PSFFunction -Message "Module requires elevation. Please run the console in Administrator mode" -Continue
        }

        if (-not (Test-DcnModule -SetupStatus)) {
            Stop-PSFFunction -Message "The module setup has NOT yet successfully run. Please run 'Set-DcnConfiguration'" -Continue
        }
    }

    process {
        # Test if there are any errors
        if (Test-PSFFunctionInterrupt) { return }

        # Loop through each of the hosts
        foreach ($hst in $HostName) {

            # Setup the computer object
            $computer = [PSFComputer]$hst

            if (-not $computer.IsLocalhost) {
                # Get the result for the remote test
                $resultPSRemote = Test-DcnRemoting -ComputerName $hst -Credential $Credential

                # Check the result
                if ($resultPSRemote.Result) {

                    $command = [scriptblock]::Create("Import-Module dbaclone")

                    try {
                        Invoke-PSFCommand -ComputerName $computer -ScriptBlock $command -Credential $Credential
                    }
                    catch {
                        Stop-PSFFunction -Message "Couldn't import module remotely" -Target $command
                        return
                    }
                }
                else {
                    Stop-PSFFunction -Message "Couldn't connect to host remotely.`nVerify that the specified computer name is valid, that the computer is accessible over the network, and that a firewall exception for the WinRM service is enabled and allows access from this computer" -Target $resultPSRemote -Continue
                }
            }

            # Get the clones
            [array]$results = Get-DcnClone -HostName $hst

            # Loop through the results
            foreach ($result in $results) {

                $server = Connect-DbaInstance -SqlInstance $result.SqlInstance -SqlCredential $SqlCredential

                # Get the databases
                Write-PSFMessage -Message "Retrieve the databases for $($result.SqlInstance)" -Level Verbose
                $databases = $server.Databases

                $image = Get-DcnImage -ImageID $result.ImageID

                # Check if the parent of the clone can be reached
                try {
                    $null = New-PSDrive -Name ImagePath -Root (Split-Path $image.ImageLocation) -Credential $Credential -PSProvider FileSystem
                }
                catch {
                    Stop-PSFFunction -Message "Could not create drive for image path '$($image.ImageLocation)'" -ErrorRecord $_ -Continue
                }

                # Test if the image still exists
                if (Test-Path -Path "ImagePath:\$($image.ImageName).vhdx") {
                    # Mount the clone
                    try {
                        Write-PSFMessage -Message "Mounting vhd $($result.CloneLocation)" -Level Verbose

                        if (Test-Path -Path $result.CloneLocation) {
                            $disk = Get-Disk | Where-Object Location -eq $result.CloneLocation

                            if (-not $disk) {
                                # Check if computer is local
                                if ($PSCmdlet.ShouldProcess("Mounting $($result.CloneLocation)")) {
                                    if ($computer.IsLocalhost) {
                                        $null = Mount-DiskImage -ImagePath $result.CloneLocation -NoDriveLetter
                                    }
                                    else {
                                        $command = [ScriptBlock]::Create("Mount-DiskImage -ImagePath '$($result.CloneLocation)' -NoDriveLetter -ErrorAction SilentlyContinue")
                                        $null = Invoke-PSFCommand -ComputerName $computer -ScriptBlock $command -Credential $Credential
                                    }
                                }
                            }
                        }
                        else {
                            Stop-PSFFunction -Message "Couldn't find clone file '$($result.CloneLocation)'" -Target $result -Continue
                        }
                    }
                    catch {
                        Stop-PSFFunction -Message "Couldn't mount vhd" -Target $result -ErrorRecord $_ -Continue
                    }
                }
                else {
                    Stop-PSFFunction -Message "Vhd $($result.CloneLocation) cannot be mounted because image path cannot be reached" -Target $image -Continue
                }

                # Remove the PS Drive
                try {
                    $null = Remove-PSDrive -Name ImagePath
                }
                catch {
                    Stop-PSFFunction -Message "Could not remove drive 'ImagePath'" -ErrorRecord $_ -Continue
                }

                # Check if the database is already attached
                if ($result.DatabaseName -in $databases.Name) {
                    $db = $databases | Where-Object Name -eq $result.DatabaseName

                    if ($db.Status -eq 'RecoveryPending') {
                        try {
                            Write-PSFMessage -Message "Setting database offline" -Level Verbose
                            $db.SetOffline()

                            Write-PSFMessage -Message "Setting database online" -Level Verbose
                            $db.SetOnline()
                        }
                        catch {
                            Stop-PSFFunction -Message "Could not detach database [$($result.DatabaseName)]" -ErrorRecord $_ -Continue
                        }
                    }
                    else {
                        try {
                            $null = Detach-DbaDatabase -SqlInstance $result.SQLInstance -SqlCredential $SqlCredential -Database $result.DatabaseName
                        }
                        catch {
                            Stop-PSFFunction -Message "Could not detach database [$($result.DatabaseName)]" -ErrorRecord $_ -Continue
                        }
                    }
                }
                else {
                    # Get all the files of the database
                    if ($PSCmdlet.ShouldProcess("Retrieving database files from $($result.AccessPath)")) {
                        # Check if computer is local
                        if ($computer.IsLocalhost) {
                            $databaseFiles = Get-ChildItem -Path $result.AccessPath -Recurse | Where-Object { -not $_.PSIsContainer }
                        }
                        else {
                            $commandText = "Get-ChildItem -Path $($result.AccessPath) -Recurse | " + 'Where-Object {-not $_.PSIsContainer}'
                            $command = [ScriptBlock]::Create($commandText)
                            $databaseFiles = Invoke-PSFCommand -ComputerName $computer -ScriptBlock $command -Credential $Credential
                        }
                    }

                    # Setup the database filestructure
                    $dbFileStructure = New-Object System.Collections.Specialized.StringCollection

                    # Loop through each of the database files and add them to the file structure
                    foreach ($dbFile in $databaseFiles) {
                        $dbFileStructure.Add($dbFile.FullName) | Out-Null
                    }

                    Write-PSFMessage -Message "Mounting database from clone" -Level Verbose

                    # Mount the database using the config file
                    if ($PSCmdlet.ShouldProcess("Mounting database $($result.DatabaseName) to $($result.SQLInstance)")) {
                        try {
                            $null = Mount-DbaDatabase -SqlInstance $result.SQLInstance -Database $result.DatabaseName -FileStructure $dbFileStructure
                        }
                        catch {
                            Stop-PSFFunction -Message "Couldn't mount database $($result.DatabaseName)" -Target $result.DatabaseName -Continue
                        }
                    }
                }
            } # End for ech result
        } # End for each host
    } # End process

    end {
        # Test if there are any errors
        if (Test-PSFFunctionInterrupt) { return }

        Write-PSFMessage -Message "Finished repairing clones" -Level Verbose
    }

}


function New-DcnClone {
    <#
    .SYNOPSIS
        New-DcnClone creates a new clone

    .DESCRIPTION
        New-DcnClone willcreate a new clone based on an image.
        The clone will be created in a certain directory, mounted and attached to a database server.

    .PARAMETER SqlInstance
        SQL Server name or SMO object representing the SQL Server to connect to

    .PARAMETER SqlCredential
        Allows you to login to servers using SQL Logins as opposed to Windows Auth/Integrated/Trusted. To use:

        $scred = Get-Credential, then pass $scred object to the -SqlCredential parameter.

        Windows Authentication will be used if SqlCredential is not specified. SQL Server does not accept Windows credentials being passed as credentials.
        To connect as a different Windows user, run PowerShell as that user.

    .PARAMETER DcnSqlCredential
        Allows you to login to servers using SQL Logins as opposed to Windows Auth/Integrated/Trusted.
        This works similar as SqlCredential but is only meant for authentication to the PSDatabaseClone database server and database.

    .PARAMETER Credential
        Allows you to login to servers using Windows Auth/Integrated/Trusted. To use:

        $scred = Get-Credential, then pass $scred object to the -Credential parameter.

    .PARAMETER ParentVhd
        Points to the parent VHD to create the clone from

    .PARAMETER Destination
        Destination directory to save the clone to

    .PARAMETER CloneName
        Name of the clone

    .PARAMETER Database
        Database name for the clone

    .PARAMETER LatestImage
        Automatically get the last image ever created for an specific database

    .PARAMETER Disabled
        Registers the clone in the configuration as disabled.
        If this setting is used the clone will not be recovered when the repair command is run

    .PARAMETER SkipDatabaseMount
        If this parameter is used, the database will not be mounted.

    .PARAMETER Force
        Forcefully create items when needed

    .PARAMETER EnableException
        By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message.
        This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting.
        Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch.

    .PARAMETER WhatIf
        If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run.

    .PARAMETER Confirm
        If this switch is enabled, you will be prompted for confirmation before executing any operations that change state.

    .NOTES
        Author: Sander Stad (@sqlstad, sqlstad.nl)

        Website: https://psdatabaseclone.org
        Copyright: (C) Sander Stad, sander@sqlstad.nl
        License: MIT https://opensource.org/licenses/MIT

    .LINK
        https://psdatabaseclone.org/

    .EXAMPLE
        New-DcnClone -SqlInstance SQLDB1 -ParentVhd C:\Temp\images\DB1_20180623203204.vhdx -Destination C:\Temp\clones\ -CloneName DB1_Clone1

        Create a new clone based on the image DB1_20180623203204.vhdx and attach the database to SQLDB1 as DB1_Clone1

    .EXAMPLE
        New-DcnClone -SqlInstance SQLDB1 -Database DB1, DB2 -LatestImage

        Create a new clone on SQLDB1 for the databases DB1 and DB2 with the latest image for those databases

    .EXAMPLE
        New-DcnClone -SqlInstance SQLDB1, SQLDB2 -Database DB1 -LatestImage

        Create a new clone on SQLDB1 and SQLDB2 for the databases DB1 with the latest image
    #>

    [CmdLetBinding(DefaultParameterSetName = 'ByLatest', SupportsShouldProcess = $true)]

    param(
        [DbaInstanceParameter]$SqlInstance,
        [PSCredential]$SqlCredential,
        [PSCredential]$DcnSqlCredential,
        [PSCredential]$Credential,
        [parameter(Mandatory = $true, ParameterSetName = "ByParent")]
        [string]$ParentVhd,
        [string]$Destination,
        [string]$CloneName,
        [parameter(Mandatory = $true, ParameterSetName = "ByLatest")]
        [string[]]$Database,
        [parameter(Mandatory = $true, ParameterSetName = "ByLatest")]
        [switch]$LatestImage,
        [switch]$Disabled,
        [switch]$SkipDatabaseMount,
        [switch]$Force,
        [switch]$EnableException
    )

    begin {
        # Check if the console is run in Administrator mode
        if ( -not (Test-PSFPowerShell -Elevated) ) {
            Stop-PSFFunction -Message "Module requires elevation. Please run the console in Administrator mode" -Continue
        }

        if (-not (Test-DcnModule -SetupStatus)) {
            Stop-PSFFunction -Message "The module setup has NOT yet successfully run. Please run 'Set-DcnConfiguration'" -Continue
        }

        if (-not $SqlInstance) {
            $SkipDatabaseMount = $true
        }

        if (-not $Destination -and -not $SqlInstance) {
            Stop-PSFFunction -Message "Please enter a destination or enter a SQL Server instance" -Continue
        }

        if (-not $Destination -and $SkipDatabaseMount) {
            Stop-PSFFunction -Message "Please enter a destination when using -SkipDatabaseMount" -Continue
        }

        # Check the available images
        $images = Get-DcnImage

        if ($Database -notin $images.DatabaseName) {
            Stop-PSFFunction -Message "There is no image for database '$Database'" -Continue
        }

        # Get the information store
        $informationStore = Get-PSFConfigValue -FullName psdatabaseclone.informationstore.mode

        if ($informationStore -eq 'SQL') {
            # Get the module configurations
            $pdcSqlInstance = Get-PSFConfigValue -FullName psdatabaseclone.database.Server
            $pdcDatabase = Get-PSFConfigValue -FullName psdatabaseclone.database.name
            if (-not $DcnSqlCredential) {
                $pdcCredential = Get-PSFConfigValue -FullName psdatabaseclone.informationstore.credential -Fallback $null
            }
            else {
                $pdcCredential = $DcnSqlCredential
            }

            # Test the module database setup
            if ($PSCmdlet.ShouldProcess("Test-DcnConfiguration", "Testing module setup")) {
                try {
                    Test-DcnConfiguration -SqlCredential $pdcCredential -EnableException
                }
                catch {
                    Stop-PSFFunction -Message "Something is wrong in the module configuration" -ErrorRecord $_ -Continue
                }
            }
        }

        Write-PSFMessage -Message "Started clone creation" -Level Verbose

        # Check the disabled parameter
        $active = 1
        if ($Disabled) {
            $active = 0
        }

        # Set the location where to save the diskpart command
        $diskpartScriptFile = Get-PSFConfigValue -FullName psdatabaseclone.diskpart.scriptfile -Fallback "$env:APPDATA\psdatabaseclone\diskpartcommand.txt"

        if (-not (Test-Path -Path $diskpartScriptFile)) {
            try {
                $null = New-Item -Path $diskpartScriptFile -ItemType File
            }
            catch {
                Stop-PSFFunction -Message "Could not create diskpart script file" -ErrorRecord $_ -Continue
            }
        }
    }

    process {
        # Test if there are any errors
        if (Test-PSFFunctionInterrupt) { return }

        # Loop through all the instances
        #foreach ($instance in $SqlInstance) {
        if ($SqlInstance) {
            if (-not $SkipDatabaseMount) {
                # Try connecting to the instance
                Write-PSFMessage -Message "Attempting to connect to Sql Server $SqlInstance.." -Level Verbose
                try {
                    $server = Connect-DbaInstance -SqlInstance $SqlInstance -SqlCredential $SqlCredential
                }
                catch {
                    Stop-PSFFunction -Message "Could not connect to Sql Server instance $instance" -ErrorRecord $_ -Target $instance
                    return
                }
            }

            # Setup the computer object
            $computer = [PsfComputer]$server.ComputerName
        }
        else {
            $computer = [PsfComputer]"$($env:COMPUTERNAME)"
        }

        if (-not $computer.IsLocalhost) {
            # Get the result for the remote test
            $resultPSRemote = Test-DcnRemoting -ComputerName $computer.ComputerName -Credential $Credential

            # Check the result
            if ($resultPSRemote.Result) {
                $command = [scriptblock]::Create("Import-Module dbaclone")

                try {
                    Invoke-PSFCommand -ComputerName $computer -ScriptBlock $command -Credential $Credential
                }
                catch {
                    Stop-PSFFunction -Message "Couldn't import module remotely" -Target $command
                    return
                }
            }
            else {
                Stop-PSFFunction -Message "Couldn't connect to host remotely.`nVerify that the specified computer name is valid, that the computer is accessible over the network, and that a firewall exception for the WinRM service is enabled and allows access from this computer" -Target $resultPSRemote -Continue
            }
        }


        # Check destination
        if (-not $Destination) {
            $Destination = Join-PSFPath -Path $server.DefaultFile -Child "clone"
        }
        else {
            # If the destination is a network path
            if ($Destination.StartsWith("\\")) {
                Write-PSFMessage -Message "The destination cannot be an UNC path. Trying to convert to local path" -Level Verbose

                if ($PSCmdlet.ShouldProcess($Destination, "Converting UNC path '$Destination' to local path")) {
                    try {
                        # Check if computer is local
                        if ($computer.IsLocalhost) {
                            $Destination = Convert-DcnUncPathToLocalPath -UncPath $Destination
                        }
                        else {
                            $command = [ScriptBlock]::Create("Convert-DcnUncPathToLocalPath -UncPath `"$Destination`"")
                            $Destination = Invoke-PSFCommand -ComputerName $computer -ScriptBlock $command -Credential $Credential
                        }
                    }
                    catch {
                        Stop-PSFFunction -Message "Something went wrong getting the local image path" -Target $Destination
                        return
                    }
                }
            }

            # Remove the last "\" from the path it would mess up the mount of the VHD
            if ($Destination.EndsWith("\")) {
                $Destination = $Destination.Substring(0, $Destination.Length - 1)
            }

            # Test if the destination can be reached
            # Check if computer is local
            if ($computer.IsLocalhost) {
                if (-not (Test-Path -Path $Destination)) {
                    Stop-PSFFunction -Message "Could not find destination path $Destination" -Target $SqlInstance
                }
            }
            else {
                $command = [ScriptBlock]::Create("Test-Path -Path '$Destination'")
                $result = Invoke-PSFCommand -ComputerName $computer -ScriptBlock $command -Credential $Credential
                if (-not $result) {
                    Stop-PSFFunction -Message "Could not find destination path $Destination" -Target $SqlInstance
                }
            }

        }

        # Loopt through all the databases
        foreach ($db in $Database) {

            if ($LatestImage) {
                $images = Get-DcnImage -Database $db
                $result = $images[-1] | Sort-Object CreatedOn
            }

            # Check the results
            if ($null -eq $result) {
                Stop-PSFFunction -Message "No image could be found for database $db" -Target $pdcSqlInstance -Continue
            }
            else {
                $ParentVhd = $result.ImageLocation
            }

            # Take apart the vhd directory
            if ($PSCmdlet.ShouldProcess($ParentVhd, "Setting up parent VHD variables")) {
                $uri = new-object System.Uri($ParentVhd)
                $vhdComputer = [PsfComputer]$uri.Host

                if ($vhdComputer.IsLocalhost) {
                    if ((Test-Path -Path $ParentVhd)) {
                        $parentVhdFileName = $ParentVhd.Split("\")[-1]
                        $parentVhdFile = $parentVhdFileName.Split(".")[0]
                    }
                    else {
                        Stop-PSFFunction -Message "Parent vhd could not be found" -Target $SqlInstance -Continue
                    }
                }
                else {
                    $command = [scriptblock]::Create("Test-Path -Path '$ParentVhd'")
                    $result = Invoke-PSFCommand -ComputerName $vhdComputer -ScriptBlock $command -Credential $Credential
                    if ($result) {
                        $parentVhdFileName = $ParentVhd.Split("\")[-1]
                        $parentVhdFile = $parentVhdFileName.Split(".")[0]
                    }
                    else {
                        Stop-PSFFunction -Message "Parent vhd could not be found" -Target $SqlInstance -Continue
                    }
                }
            }

            # Check clone name parameter
            if ($PSCmdlet.ShouldProcess($ParentVhd, "Setting up clone variables")) {
                if (-not $CloneName) {
                    $cloneDatabase = $parentVhdFile
                    $CloneName = $parentVhdFile
                    $mountDirectory = "$($parentVhdFile)"
                }
                elseif ($CloneName) {
                    $cloneDatabase = $CloneName
                    $mountDirectory = "$($CloneName)"
                }
            }

            # Check if the database is already present
            if (-not $SkipDatabaseMount) {
                if ($PSCmdlet.ShouldProcess($cloneDatabase, "Verifying database existence")) {
                    if ($server.Databases.Name -contains $cloneDatabase) {
                        Stop-PSFFunction -Message "Database $cloneDatabase is already present on $SqlInstance" -Target $SqlInstance
                    }
                }
            }

            # Setup access path location
            $accessPath = Join-PSFPath -Path $Destination -Child $mountDirectory

            # Check if access path is already present
            if ($PSCmdlet.ShouldProcess($accessPath, "Testing existence access path $accessPath and create it")) {
                if ($computer.IsLocalhost) {
                    if (-not (Test-Path -Path $accessPath)) {
                        try {
                            $null = New-Item -Path $accessPath -ItemType Directory -Force
                        }
                        catch {
                            Stop-PSFFunction -Message "Couldn't create access path directory" -ErrorRecord $_ -Target $accessPath -Continue
                        }
                    }
                }
                else {
                    $command = [ScriptBlock]::Create("Test-Path -Path '$accessPath'")
                    $result = Invoke-PSFCommand -ComputerName $computer -ScriptBlock $command -Credential $Credential
                    if (-not $result) {
                        try {
                            $command = [ScriptBlock]::Create("New-Item -Path '$accessPath' -ItemType Directory -Force")
                            $null = Invoke-PSFCommand -ComputerName $computer -ScriptBlock $command -Credential $Credential
                        }
                        catch {
                            Stop-PSFFunction -Message "Couldn't create access path directory" -ErrorRecord $_ -Target $accessPath -Continue
                        }
                    }
                }
            }

            # Check if the clone vhd does not yet exist
            $clonePath = Join-PSFPath -Path $Destination -Child "$($CloneName).vhdx"
            if ($computer.IsLocalhost) {
                if (Test-Path -Path "$($clonePath)" -Credential $DestinationCredential) {
                    Stop-PSFFunction -Message "Clone $CloneName already exists" -Target $accessPath -Continue
                }
            }
            else {
                $command = [ScriptBlock]::Create("Test-Path -Path `"$($clonePath)`"")
                $result = Invoke-PSFCommand -ComputerName $computer -ScriptBlock $command -Credential $Credential
                if ($result) {
                    Stop-PSFFunction -Message "Clone $CloneName already exists" -Target $accessPath -Continue
                }
            }

            # Create the new child vhd
            if ($PSCmdlet.ShouldProcess($ParentVhd, "Creating clone")) {
                try {
                    Write-PSFMessage -Message "Creating clone from $ParentVhd" -Level Verbose

                    $command = "create vdisk file='$($clonePath)' parent='$ParentVhd'"

                    # Check if computer is local
                    if ($computer.IsLocalhost) {
                        # Set the content of the diskpart script file
                        Set-Content -Path $diskpartScriptFile -Value $command -Force

                        $script = [ScriptBlock]::Create("diskpart /s $diskpartScriptFile")
                        $null = Invoke-PSFCommand -ScriptBlock $script
                    }
                    else {
                        $command = [ScriptBlock]::Create("New-VHD -ParentPath $ParentVhd -Path `"$($clonePath)`" -Differencing")
                        $vhd = Invoke-PSFCommand -ComputerName $computer -ScriptBlock $command -Credential $Credential

                        if (-not $vhd) {
                            return
                        }
                    }

                }
                catch {
                    Stop-PSFFunction -Message "Could not create clone" -Target $vhd -Continue -ErrorRecord $_
                }
            }

            # Mount the vhd
            if ($PSCmdlet.ShouldProcess("$($clonePath)", "Mounting clone clone")) {
                try {
                    Write-PSFMessage -Message "Mounting clone" -Level Verbose

                    # Check if computer is local
                    if ($computer.IsLocalhost) {
                        # Mount the disk
                        $null = Mount-DiskImage -ImagePath "$($clonePath)"

                        # Get the disk based on the name of the vhd
                        $diskImage = Get-DiskImage -ImagePath $clonePath
                        $disk = Get-Disk | Where-Object Number -eq $diskImage.Number
                    }
                    else {
                        # Mount the disk
                        $command = [ScriptBlock]::Create("Mount-DiskImage -ImagePath `"$($clonePath)`"")
                        $null = Invoke-PSFCommand -ComputerName $computer -ScriptBlock $command -Credential $Credential

                        # Get the disk based on the name of the vhd
                        $command = [ScriptBlock]::Create("
                                `$diskImage = Get-DiskImage -ImagePath $($clonePath)
                                Get-Disk | Where-Object Number -eq $($diskImage.Number)
                            "
)
                        $disk = Invoke-PSFCommand -ComputerName $computer -ScriptBlock $command -Credential $Credential
                    }
                }
                catch {
                    Stop-PSFFunction -Message "Couldn't mount vhd $vhdPath" -ErrorRecord $_ -Target $disk -Continue
                }
            }

            # Check if the disk is offline
            if ($PSCmdlet.ShouldProcess($disk.Number, "Initializing disk")) {
                # Check if computer is local
                if ($computer.IsLocalhost) {
                    $null = Initialize-Disk -Number $disk.Number -PartitionStyle GPT -ErrorAction SilentlyContinue
                }
                else {
                    $command = [ScriptBlock]::Create("Initialize-Disk -Number $($disk.Number) -PartitionStyle GPT -ErrorAction SilentlyContinue")

                    $null = Invoke-PSFCommand -ComputerName $computer -ScriptBlock $command -Credential $Credential
                }
            }

            # Mounting disk to access path
            if ($PSCmdlet.ShouldProcess($disk.Number, "Mounting volume to accesspath")) {
                try {
                    # Check if computer is local
                    if ($computer.IsLocalhost) {
                        # Get the partition based on the disk
                        $partition = Get-Partition -Disk $disk | Where-Object { $_.Type -ne "Reserved" } | Select-Object -First 1

                        # Create an access path for the disk
                        $null = Add-PartitionAccessPath -DiskNumber $disk.Number -PartitionNumber $partition.PartitionNumber -AccessPath $accessPath -ErrorAction SilentlyContinue
                    }
                    else {
                        $command = [ScriptBlock]::Create("Get-Partition -DiskNumber $($disk.Number)")
                        $partition = Invoke-PSFCommand -ComputerName $computer -ScriptBlock $command -Credential $Credential | Where-Object { $_.Type -ne "Reserved" } | Select-Object -First 1

                        $command = [ScriptBlock]::Create("Add-PartitionAccessPath -DiskNumber $($disk.Number) -PartitionNumber $($partition.PartitionNumber) -AccessPath '$accessPath' -ErrorAction Ignore")

                        $null = Invoke-PSFCommand -ComputerName $computer -ScriptBlock $command -Credential $Credential
                    }

                }
                catch {
                    Stop-PSFFunction -Message "Couldn't create access path for partition" -ErrorRecord $_ -Target $partition -Continue
                }
            }

            if (-not $SkipDatabaseMount) {
                # Get all the files of the database
                if ($computer.IsLocalhost) {
                    $databaseFiles = Get-ChildItem -Path $accessPath -Filter *.*df -Recurse
                }
                else {
                    $commandText = "Get-ChildItem -Path '$accessPath' -Filter *.*df -Recurse"
                    $command = [ScriptBlock]::Create($commandText)
                    $databaseFiles = Invoke-PSFCommand -ComputerName $computer -ScriptBlock $command -Credential $Credential
                }

                # Setup the database filestructure
                $dbFileStructure = New-Object System.Collections.Specialized.StringCollection

                # Loop through each of the database files and add them to the file structure
                foreach ($dbFile in $databaseFiles) {
                    $null = $dbFileStructure.Add($dbFile.FullName)
                }

                # Mount the database
                if ($PSCmdlet.ShouldProcess($cloneDatabase, "Mounting database $cloneDatabase")) {
                    try {
                        Write-PSFMessage -Message "Mounting database from clone" -Level Verbose
                        $null = Mount-DbaDatabase -SqlInstance $server -SqlCredential $SqlCredential -Database $cloneDatabase -FileStructure $dbFileStructure
                    }
                    catch {
                        Stop-PSFFunction -Message "Couldn't mount database $cloneDatabase" -ErrorRecord $_ -Target $instance -Continue
                    }
                }
            }

            # Write the data to the database
            try {
                # Get the data of the host
                if ($computer.IsLocalhost) {
                    $computerinfo = [System.Net.Dns]::GetHostByName(($env:computerName))

                    $hostname = $computerinfo.HostName
                    $ipAddress = $computerinfo.AddressList[0]
                    $fqdn = $computerinfo.HostName
                }
                else {
                    $command = [scriptblock]::Create('[System.Net.Dns]::GetHostByName(($env:computerName))')
                    $computerinfo = Invoke-PSFCommand -ComputerName $computer -ScriptBlock $command -Credential $Credential

                    $command = [scriptblock]::Create('$env:COMPUTERNAME')
                    $result = Invoke-PSFCommand -ComputerName $computer -ScriptBlock $command -Credential $Credential

                    $hostname = $result.ToString()
                    $ipAddress = $computerinfo.AddressList[0]
                    $fqdn = $computerinfo.HostName
                }

                if ($informationStore -eq 'SQL') {
                    # Setup the query to check of the host is already added
                    $query = "
                            IF EXISTS (SELECT HostName FROM Host WHERE HostName ='$hostname')
                            BEGIN
                                SELECT CAST(1 AS BIT) AS HostKnown;
                            END;
                            ELSE
                            BEGIN
                                SELECT CAST(0 AS BIT) AS HostKnown;
                            END;
                        "


                    # Execute the query
                    $hostKnown = (Invoke-DbaQuery -SqlInstance $pdcSqlInstance -SqlCredential $pdcCredential -Database $pdcDatabase -Query $query -EnableException).HostKnown
                }
                elseif ($informationStore -eq 'File') {
                    $hosts = Get-ChildItem -Path DCNJSONFolder:\ -Filter *hosts.json | ForEach-Object { Get-Content $_.FullName | ConvertFrom-Json }

                    $hostKnown = [bool]($hostname -in $hosts.HostName)
                }
            }
            catch {
                Stop-PSFFunction -Message "Couldnt execute query to see if host was known" -Target $query -ErrorRecord $_ -Continue
            }

            # Add the host if the host is known
            if (-not $hostKnown) {
                if ($PSCmdlet.ShouldProcess($hostname, "Adding hostname to database")) {

                    if ($informationStore -eq 'SQL') {

                        Write-PSFMessage -Message "Adding host $hostname to database" -Level Verbose

                        $query = "
                                DECLARE @HostID INT;
                                EXECUTE dbo.Host_New @HostID = @HostID OUTPUT, -- int
                                                    @HostName = '$hostname', -- varchar(100)
                                                    @IPAddress = '$ipAddress', -- varchar(20)
                                                    @FQDN = '$fqdn' -- varchar(255)

                                SELECT @HostID AS HostID
                            "


                        try {
                            $hostID = (Invoke-DbaQuery -SqlInstance $pdcSqlInstance -SqlCredential $pdcCredential -Database $pdcDatabase -Query $query -EnableException).HostID
                        }
                        catch {
                            Stop-PSFFunction -Message "Couldnt execute query for adding host" -Target $query -ErrorRecord $_ -Continue
                        }
                    }
                    elseif ($informationStore -eq 'File') {
                        [array]$hosts = $null

                        # Get all the images
                        $hosts = Get-ChildItem -Path DCNJSONFolder:\ -Filter *hosts.json | ForEach-Object { Get-Content $_.FullName | ConvertFrom-Json }

                        # Setup the new host id
                        if ($hosts.Count -ge 1) {
                            $hostID = ($hosts[-1].HostID | Sort-Object HostID) + 1
                        }
                        else {
                            $hostID = 1
                        }

                        # Add the new information to the array
                        $hosts += [PSCustomObject]@{
                            HostID    = $hostID
                            HostName  = $hostname
                            IPAddress = $ipAddress.IPAddressToString
                            FQDN      = $fqdn
                        }

                        # Test if the JSON folder can be reached
                        if (-not (Test-Path -Path "DCNJSONFolder:\")) {
                            $command = [scriptblock]::Create("Import-Module dbaclone -Force")

                            try {
                                Invoke-PSFCommand -ComputerName $computer -ScriptBlock $command -Credential $Credential
                            }
                            catch {
                                Stop-PSFFunction -Message "Couldn't import module remotely" -Target $command
                                return
                            }
                        }

                        # Setup the json file
                        $jsonHostFile = "DCNJSONFolder:\hosts.json"

                        # Convert the data back to JSON
                        $hosts | ConvertTo-Json | Set-Content $jsonHostFile
                    }
                }
            }
            else {
                if ($informationStore -eq 'SQL') {
                    Write-PSFMessage -Message "Selecting host $hostname from database" -Level Verbose
                    $query = "SELECT HostID FROM Host WHERE HostName = '$hostname'"

                    try {
                        $hostID = (Invoke-DbaQuery -SqlInstance $pdcSqlInstance -SqlCredential $pdcCredential -Database $pdcDatabase -Query $query -EnableException).HostID
                    }
                    catch {
                        Stop-PSFFunction -Message "Couldnt execute query for retrieving host id" -Target $query -ErrorRecord $_ -Continue
                    }
                }
                elseif ($informationStore -eq 'File') {
                    $hostID = ($hosts | Where-Object { $_.Hostname -eq $hostname } | Select-Object HostID -Unique).HostID
                }
            }

            # Set privileges for access path
            try {
                # Check if computer is local
                if ($computer.IsLocalhost) {
                    Set-DcnPermission -Path $accessPath
                }
                else {
                    [string]$commandText = "Set-DcnPermission -Path '$($accessPath)'"

                    $command = [scriptblock]::Create($commandText)

                    $null = Invoke-PSFCommand -ComputerName $computer -ScriptBlock $command -Credential $DestinationCredential
                }
            }
            catch {
                Stop-PSFFunction -Message "Couldn't create access path directory" -ErrorRecord $_ -Target $accessPath -Continue
            }

            # Setup the clone location
            $cloneLocation = "$($clonePath)"

            if ($informationStore -eq 'SQL') {
                # Get the image id from the database
                Write-PSFMessage -Message "Selecting image from database" -Level Verbose
                try {
                    $query = "SELECT ImageID, ImageName FROM dbo.Image WHERE ImageLocation = '$ParentVhd'"
                    $image = Invoke-DbaQuery -SqlInstance $pdcSqlInstance -SqlCredential $pdcCredential -Database $pdcDatabase -Query $query -EnableException
                }
                catch {
                    Stop-PSFFunction -Message "Couldnt execute query for retrieving image id" -Target $query -ErrorRecord $_ -Continue
                }

                if ($PSCmdlet.ShouldProcess("$($clonePath)", "Adding clone to database")) {
                    if ($null -ne $image.ImageID) {
                        # Setup the query to add the clone to the database
                        Write-PSFMessage -Message "Adding clone $cloneLocation to database" -Level Verbose
                        $query = "
                                DECLARE @CloneID INT;
                                EXECUTE dbo.Clone_New @CloneID = @CloneID OUTPUT, -- int
                                                    @ImageID = $($image.ImageID), -- int
                                                    @HostID = $hostId, -- int
                                                    @CloneLocation = '$cloneLocation', -- varchar(255)
                                                    @AccessPath = '$accessPath', -- varchar(255)
                                                    @SqlInstance = '$($server.DomainInstanceName)', -- varchar(50)
                                                    @DatabaseName = '$cloneDatabase', -- varchar(100)
                                                    @IsEnabled = $active -- bit

                                SELECT @CloneID AS CloneID
                            "


                        Write-PSFMessage -Message "Query New Clone`n$query" -Level Debug

                        # execute the query
                        try {
                            $result = Invoke-DbaQuery -SqlInstance $pdcSqlInstance -SqlCredential $pdcCredential -Database $pdcDatabase -Query $query -EnableException
                            $cloneID = $result.CloneID
                        }
                        catch {
                            Stop-PSFFunction -Message "Couldnt execute query for adding clone" -Target $query -ErrorRecord $_ -Continue
                        }

                    }
                    else {
                        Stop-PSFFunction -Message "Image couldn't be found" -Target $imageName -Continue
                    }
                }
            }
            elseif ($informationStore -eq 'File') {
                # Get the image
                $image = Get-DcnImage -ImageLocation $ParentVhd

                [array]$clones = $null

                # Get all the images
                $clones = Get-DcnClone

                # Setup the new image id
                if ($clones.Count -ge 1) {
                    $cloneID = ($clones[-1].CloneID | Sort-Object CloneID) + 1
                }
                else {
                    $cloneID = 1
                }

                # Add the new information to the array
                $clones += [PSCustomObject]@{
                    CloneID       = $cloneID
                    ImageID       = $image.ImageID
                    ImageName     = $image.ImageName
                    ImageLocation = $ParentVhd
                    HostID        = $hostId
                    HostName      = $hostname
                    CloneLocation = $cloneLocation
                    AccessPath    = $accessPath
                    SqlInstance   = $($server.DomainInstanceName)
                    DatabaseName  = $cloneDatabase
                    IsEnabled     = $active
                }

                # Test if the JSON folder can be reached
                if (-not (Test-Path -Path "DCNJSONFolder:\")) {
                    $command = [scriptblock]::Create("Import-Module dbaclone -Force")

                    try {
                        Invoke-PSFCommand -ComputerName $computer -ScriptBlock $command -Credential $Credential
                    }
                    catch {
                        Stop-PSFFunction -Message "Couldn't import module remotely" -Target $command
                        return
                    }
                }

                # Set the clone file
                $jsonCloneFile = "DCNJSONFolder:\clones.json"

                # Convert the data back to JSON
                $clones | ConvertTo-Json | Set-Content $jsonCloneFile
            }

            if (-not $SkipDatabaseMount) {
                $cloneInstance = $server.DomainInstanceName
            }
            else {
                $cloneInstance = $null
            }

            # Add the results to the custom object
            [PSCustomObject]@{
                CloneID       = $cloneID
                CloneLocation = $cloneLocation
                AccessPath    = $accessPath
                SqlInstance   = $cloneInstance
                DatabaseName  = $cloneDatabase
                IsEnabled     = $active
                ImageID       = $image.ImageID
                ImageName     = $image.ImageName
                ImageLocation = $ParentVhd
                HostName      = $hostname
            }
        } # End for each database
        #} # End for each sql instance
    } # End process

    end {
        # Test if there are any errors
        if (Test-PSFFunctionInterrupt) { return }

        Write-PSFMessage -Message "Finished creating database clone" -Level Verbose
    }
}


function Remove-DcnClone {
    <#
    .SYNOPSIS
        Remove-DcnClone removes one or more clones from a host

    .DESCRIPTION
        Remove-DcnClone is able to remove one or more clones from a host.
        The command looks up all the records dor a particular hostname.
        It will remove the database from the database server and all related files.

        The filter parameters Database and ExcludeDatabase work like wildcards.
        There is no need to include the asterisk (*). See the examples for more details

    .PARAMETER HostName
        The hostname to filter on

    .PARAMETER SqlCredential
        Allows you to login to servers using SQL Logins as opposed to Windows Auth/Integrated/Trusted. To use:

        $scred = Get-Credential, then pass $scred object to the -SqlCredential parameter.

        Windows Authentication will be used if SqlCredential is not specified. SQL Server does not accept Windows credentials being passed as credentials.
        To connect as a different Windows user, run PowerShell as that user.

    .PARAMETER DcnSqlCredential
        Allows you to login to servers using SQL Logins as opposed to Windows Auth/Integrated/Trusted.
        This works similar as SqlCredential but is only meant for authentication to the PSDatabaseClone database server and database.

    .PARAMETER Credential
        Allows you to login to systems using a credential. To use:

        $scred = Get-Credential, then pass $scred object to the -Credential parameter.

    .PARAMETER Database
        Allows to filter to include specific databases

    .PARAMETER ExcludeDatabase
        Allows to filter to exclude specific databases

    .PARAMETER All
        Remove all the clones

    .PARAMETER InputObject
        The input object that is used for pipeline use

    .PARAMETER EnableException
        By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message.
        This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting.
        Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch.

    .PARAMETER WhatIf
        If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run.

    .PARAMETER Confirm
        If this switch is enabled, you will be prompted for confirmation before executing any operations that change state.

    .NOTES
        Author: Sander Stad (@sqlstad, sqlstad.nl)

        Website: https://psdatabaseclone.org
        Copyright: (C) Sander Stad, sander@sqlstad.nl
        License: MIT https://opensource.org/licenses/MIT

    .LINK
        https://psdatabaseclone.org/

    .EXAMPLE
        Remove-DcnClone -HostName Host1 -Database Clone1

        Removes the clones that are registered at Host1 and have the text "Clone1"

    .EXAMPLE
        Remove-DcnClone -HostName Host1, Host2, Host3 -Database Clone

        Removes the clones that are registered at multiple hosts and have the text "Clone"

    .EXAMPLE
        Remove-DcnClone -HostName Host1

        Removes all clones from Host1

    #>


    [CmdLetBinding(DefaultParameterSetName = "HostName", SupportsShouldProcess = $true, ConfirmImpact = 'High')]

    param(
        [parameter(ParameterSetName = "HostName")]
        [string[]]$HostName,
        [PSCredential]$SqlCredential,
        [PSCredential]$DcnSqlCredential,
        [PSCredential]$Credential,
        [string[]]$Database,
        [string[]]$ExcludeDatabase,
        [switch]$All,
        [parameter(ValueFromPipeline = $true, ParameterSetName = "Clone")]
        [object[]]$InputObject,
        [switch]$EnableException
    )

    begin {
        # Check if the console is run in Administrator mode
        if ( -not (Test-PSFPowerShell -Elevated) ) {
            Stop-PSFFunction -Message "Module requires elevation. Please run the console in Administrator mode" -Continue
        }

        if (-not (Test-DcnModule -SetupStatus)) {
            Stop-PSFFunction -Message "The module setup has NOT yet successfully run. Please run 'Set-DcnConfiguration'" -Continue
        }

        # Get the information store
        $informationStore = Get-PSFConfigValue -FullName psdatabaseclone.informationstore.mode

        if ($informationStore -eq 'SQL') {
            # Get the module configurations
            [DbaInstanceParameter]$pdcSqlInstance = Get-PSFConfigValue -FullName psdatabaseclone.database.Server
            $pdcDatabase = Get-PSFConfigValue -FullName psdatabaseclone.database.name
            if (-not $DcnSqlCredential) {
                $pdcCredential = Get-PSFConfigValue -FullName psdatabaseclone.informationstore.credential -Fallback $null
            }
            else {
                $pdcCredential = $DcnSqlCredential
            }

            # Test the module database setup
            if ($PSCmdlet.ShouldProcess("Test-DcnConfiguration", "Testing module setup")) {
                try {
                    Test-DcnConfiguration -SqlCredential $pdcCredential -EnableException
                }
                catch {
                    Stop-PSFFunction -Message "Something is wrong in the module configuration" -ErrorRecord $_ -Continue
                }
            }
        }

        # Get all the items
        $items = @()
        $items += Get-DcnClone

        if (-not $All) {
            if ($HostName) {
                Write-PSFMessage -Message "Filtering hostnames" -Level Verbose
                $items = $items | Where-Object { $_.HostName -in $HostName }
            }

            if ($Database) {
                Write-PSFMessage -Message "Filtering included databases" -Level Verbose
                $items = $items | Where-Object { $_.DatabaseName -in $Database }
            }

            if ($ExcludeDatabase) {
                Write-PSFMessage -Message "Filtering excluded databases" -Level Verbose
                $items = $items | Where-Object { $_.DatabaseName -notin $Database }
            }
        }

        # Append the items
        $InputObject += $items

    }

    process {
        # Test if there are any errors
        if (Test-PSFFunctionInterrupt) { return }

        Write-PSFMessage -Message "Started removing database clones" -Level Verbose

        # Group the objects to make it easier to go through
        [array]$clones = $InputObject | Group-Object SqlInstance

        # Loop through each of the host names
        foreach ($clone in $clones) {

            # Connect to the instance
            if (-not $null -eq $clone.Name) {
                Write-PSFMessage -Message "Attempting to connect to clone database server $($clone.Name).." -Level Verbose
                try {
                    $server = Connect-DbaInstance -SqlInstance $clone.Name -SqlCredential $SqlCredential -SqlConnectionOnly
                }
                catch {
                    Stop-PSFFunction -Message "Could not connect to Sql Server instance $($clone.Name)" -ErrorRecord $_ -Target $clone.Name -Continue
                }
            }

            # Loop through each of the results
            foreach ($item in $clone.Group) {

                # Setup the computer object
                $computer = [PsfComputer]$item.HostName

                if (-not $computer.IsLocalhost) {
                    # Get the result for the remote test
                    try {
                        $resultPSRemote = Test-DcnRemoting -ComputerName $item.HostName -Credential $Credential -EnableException

                        # Check the result
                        if ($resultPSRemote.Result) {

                            $command = [scriptblock]::Create("Import-Module dbaclone")

                            try {
                                Invoke-PSFCommand -ComputerName $computer -ScriptBlock $command -Credential $Credential
                            }
                            catch {
                                Stop-PSFFunction -Message "Couldn't import module remotely" -Target $command
                                return
                            }
                        }
                        else {
                            Stop-PSFFunction -Message "Couldn't connect to host remotely.`nVerify that the specified computer name is valid, that the computer is accessible over the network, and that a firewall exception for the WinRM service is enabled and allows access from this computer" -Target $resultPSRemote -Continue
                        }
                    }
                    catch {
                        Stop-PSFFunction -Message "Something went wrong testing if the host is remote" -Target $item -ErrordRecord $_
                        return
                    }
                }

                if (-not $null -eq $item.SqlInstance) {
                    $server = Connect-DbaInstance -SqlInstance $item.SqlInstance -SqlCredential $SqlCredential

                    if ($item.DatabaseName -in $server.Databases.Name) {
                        if ($PSCmdlet.ShouldProcess($item.DatabaseName, "Removing database $($item.DatabaseName)")) {
                            # Remove the database
                            try {
                                Write-PSFMessage -Message "Removing database $($item.DatabaseName) from $($item.SqlInstance)" -Level Verbose

                                $null = Remove-DbaDatabase -SqlInstance $item.SqlInstance -SqlCredential $SqlCredential -Database $item.DatabaseName -Confirm:$false -EnableException
                            }
                            catch {
                                Stop-PSFFunction -Message "Could not remove database $($item.DatabaseName) from $server" -ErrorRecord $_ -Target $server -Continue
                            }
                        }
                    }
                    else {
                        Write-PSFMessage -Level Verbose -Message "Could not find database [$($item.DatabaseName)] on $($item.SqlInstance)"
                    }
                }

                if ($PSCmdlet.ShouldProcess($item.CloneLocation, "Dismounting the vhd")) {
                    # Dismounting the vhd
                    try {
                        if (Test-Path -Path $item.CloneLocation) {
                            if ($computer.IsLocalhost) {
                                Write-PSFMessage -Message "Dismounting disk '$($item.CloneLocation)' from $($item.HostName)" -Level Verbose
                                $null = Dismount-DiskImage -ImagePath $item.CloneLocation
                            }
                            else {
                                $command = [ScriptBlock]::Create("Test-Path -Path '$($item.CloneLocation)'")
                                Write-PSFMessage -Message "Dismounting disk '$($item.CloneLocation)' from $($item.HostName)" -Level Verbose
                                $result = Invoke-PSFCommand -ComputerName $item.HostName -ScriptBlock $command -Credential $Credential
                                $command = [scriptblock]::Create("Dismount-DiskImage -ImagePath '$($item.CloneLocation)'")
                                $null = Invoke-PSFCommand -ComputerName $item.HostName -ScriptBlock $command -Credential $Credential
                            }
                        }
                        else {
                            Write-PSFMessage -Level Verbose -Message "Could not find clone file '$($item.CloneLocation)'"
                        }
                    }
                    catch {
                        Stop-PSFFunction -Message "Could not dismount vhd $($item.CloneLocation)" -ErrorRecord $_ -Target $result -Continue
                    }
                }

                if ($PSCmdlet.ShouldProcess($item.CloneLocation, "Removing clone files and folders")) {
                    # Remove clone file and related access path
                    try {
                        if ($computer.IsLocalhost) {
                            if (Test-Path -Path $item.AccessPath) {
                                Write-PSFMessage -Message "Removing vhd access path" -Level Verbose
                                $null = Remove-Item -Path "$($item.AccessPath)" -Credential $Credential -Force
                            }

                            if (Test-Path -Path $item.CloneLocation) {
                                Write-PSFMessage -Message "Removing vhd" -Level Verbose
                                $null = Remove-Item -Path "$($item.CloneLocation)" -Credential $Credential -Force
                            }
                        }
                        else {
                            $command = [scriptblock]::Create("Test-Path -Path '$($item.AccessPath)'")
                            $result = Invoke-PSFCommand -ComputerName $item.HostName -ScriptBlock $command -Credential $Credential
                            if ($result) {
                                Write-PSFMessage -Message "Removing vhd access path" -Level Verbose
                                $command = [scriptblock]::Create("Remove-Item -Path '$($item.AccessPath)' -Force")
                                $null = Invoke-PSFCommand -ComputerName $item.HostName -ScriptBlock $command -Credential $Credential
                            }

                            $command = [scriptblock]::Create("Test-Path -Path '$($item.CloneLocation)'")
                            $result = Invoke-PSFCommand -ComputerName $item.HostName -ScriptBlock $command -Credential $Credential
                            if ($result) {
                                Write-PSFMessage -Message "Removing vhd" -Level Verbose
                                $command = [scriptblock]::Create("Remove-Item -Path '$($item.CloneLocation)' -Force")
                                $null = Invoke-PSFCommand -ComputerName $item.HostName -ScriptBlock $command -Credential $Credential
                            }
                        }
                    }
                    catch {
                        Stop-PSFFunction -Message "Could not remove clone files" -ErrorRecord $_ -Target $result -Continue
                    }
                }

                if ($PSCmdlet.ShouldProcess("Clone ID: $($item.CloneID)", "Deleting clone from database")) {
                    if ($informationStore -eq 'SQL') {
                        # Removing records from database
                        try {
                            $query = "DELETE FROM dbo.Clone WHERE CloneID = $($item.CloneID);"

                            $null = Invoke-DbaQuery -SqlInstance $pdcSqlInstance -SqlCredential $pdcCredential -Database $pdcDatabase -Query $query -EnableException
                        }
                        catch {
                            Stop-PSFFunction -Message "Could not remove clone record from database" -ErrorRecord $_ -Target $query -Continue
                        }
                    }
                    elseif ($informationStore -eq 'File') {
                        [array]$cloneData = $null
                        [array]$newCloneData = $null

                        $cloneData = Get-DcnClone

                        $newCloneData = $cloneData | Where-Object { $_.CloneID -ne $item.CloneID }

                        # Set the clone file
                        $jsonCloneFile = "DCNJSONFolder:\clones.json"

                        # Convert the data back to JSON
                        if ($newCloneData.Count -ge 1) {
                            $newCloneData | ConvertTo-Json | Set-Content $jsonCloneFile
                        }
                        else {
                            Clear-Content -Path $jsonCloneFile
                        }
                    }
                }
            } # End for each group item
        } # End for each clone
    } # End process

    end {
        # Test if there are any errors
        if (Test-PSFFunctionInterrupt) { return }

        Write-PSFMessage -Message "Finished removing database clone(s)" -Level Verbose
    }
}


function Initialize-DcnVhdDisk {
    <#
    .SYNOPSIS
        Initialize-DcnVhdDisk initialized the VHD

    .DESCRIPTION
        Initialize-DcnVhdDisk will initialize the VHD.
        It mounts the disk, creates a volume, creates the partition and sets it to active

    .PARAMETER Path
        The path to the VHD

    .PARAMETER Credential
        Allows you to use credentials for creating items in other locations To use:

        $scred = Get-Credential, then pass $scred object to the -Credential parameter.

    .PARAMETER PartitionStyle
        A partition can either be initialized as MBR or as GPT. GPT is the default.

    .PARAMETER AllocationUnitSize
        Set the allocation unit size for the disk.
        By default it's 64 KB because that's what SQL Server tends to write most of the time.

    .PARAMETER EnableException
        By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message.
        This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting.
        Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch.

    .PARAMETER WhatIf
        If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run.

    .PARAMETER Confirm
        If this switch is enabled, you will be prompted for confirmation before executing any operations that change state.

    .NOTES
        Author: Sander Stad (@sqlstad, sqlstad.nl)

        Website: https://psdatabaseclone.org
        Copyright: (C) Sander Stad, sander@sqlstad.nl
        License: MIT https://opensource.org/licenses/MIT

    .LINK
        https://psdatabaseclone.org/

    .EXAMPLE
        Initialize-DcnVhdDisk -Path $path

        Initialize the disk pointing to the path with all default settings

    .EXAMPLE
        Initialize-DcnVhdDisk -Path $path -AllocationUnitSize 4KB

        Initialize the disk and format the partition with a 4Kb allocation unit size

    #>


    [CmdLetBinding(SupportsShouldProcess = $true)]
    [OutputType('System.String')]
    [OutputType('PSCustomObject')]

    Param(
        [Parameter(Mandatory = $true)]
        [string]$Path,
        [PSCredential]$Credential,
        [ValidateSet('GPT', 'MBR')]
        [string]$PartitionStyle,
        [int]$AllocationUnitSize = 64KB,
        [switch]$EnableException
    )

    begin {

        # Check if the console is run in Administrator mode
        if ( -not (Test-PSFPowerShell -Elevated) ) {
            Stop-PSFFunction -Message "Module requires elevation. Please run the console in Administrator mode" -Continue
        }

        # Check the path to the vhd
        if (-not (Test-Path -Path $Path -Credential $Credential)) {
            Stop-PSFFunction -Message "Vhd path $Path cannot be found" -Target $Path -Continue
        }

        # Check the partition style
        if (-not $PartitionStyle) {
            Write-PSFMessage -Message "Setting partition style to 'GPT'" -Level Verbose
            $PartitionStyle = 'GPT'
        }
    }

    process {
        # Test if there are any errors
        if (Test-PSFFunctionInterrupt) { return }

        # Get all the disks
        $disks = Get-Disk

        # Check if disk is already mounted
        if ($disks.Location -contains $Path) {
            Write-PSFMessage -Message "Vhd is already mounted" -Level Warning

            # retrieve the specific disk
            $disk = $disks | Where-Object Location -eq $Path
        }
        else {
            if ($PSCmdlet.ShouldProcess("Mounting disk")) {
                # Mount the vhd
                try {
                    Write-PSFMessage -Message "Mounting disk $disk" -Level Verbose

                    # Mount the disk
                    Mount-DiskImage -ImagePath $Path

                    # Get the disk
                    $diskImage = Get-DiskImage -ImagePath $Path
                    $disk = Get-Disk | Where-Object Number -eq $diskImage.Number
                }
                catch {
                    Stop-PSFFunction -Message "Couldn't mount vhd" -Target $Path -ErrorRecord $_ -Continue
                }
            }
        }

        if ($PSCmdlet.ShouldProcess("Initializing disk")) {
            # Check if the disk is already initialized
            if ($disk.PartitionStyle -eq 'RAW') {
                try {
                    Write-PSFMessage -Message "Initializing disk $disk" -Level Verbose
                    $disk | Initialize-Disk -PartitionStyle $PartitionStyle -Confirm:$false
                }
                catch {
                    Stop-PSFFunction -Message "Couldn't initialize disk" -Target $disk -ErrorRecord $_ -Continue
                }
            }
        }

        if ($PSCmdlet.ShouldProcess("Partitioning volume")) {
            # Create the partition, set the drive letter and format the volume
            try {
                $params = @{
                    FileSystem         = "NTFS"
                    NewFileSystemLabel = "PSDatabaseClone"
                    AllocationUnitSize = $AllocationUnitSize
                    Confirm            = $false
                }

                $volume = $disk | New-Partition -UseMaximumSize | Format-Volume @params
            }
            catch {
                # Dismount the drive
                Dismount-DiskImage -ImagePath $Path

                Stop-PSFFunction -Message "Couldn't create the partition" -Target $disk -ErrorRecord $_ -Continue
            }
        }

        # Add the results to the custom object
        [PSCustomObject]@{
            Disk      = $disk
            Partition = (Get-Partition -Disk $disk)
            Volume    = $volume
        }
    }

    end {
        # Test if there are any errors
        if (Test-PSFFunctionInterrupt) { return }

        Write-PSFMessage -Message "Finished initializing disk(s)" -Level Verbose
    }

}


function New-DcnVhdDisk {
    <#
    .SYNOPSIS
        New-DcnVhdDisk creates a new VHD

    .DESCRIPTION
        New-DcnVhdDisk will create a new VHD.

    .PARAMETER Destination
        The destination path of the VHD

    .PARAMETER Name
        The name of the VHD

    .PARAMETER FileName
        The file name of the VHD

    .PARAMETER VhdType
        The type of the harddisk. This can either by VHD (version 1) or VHDX (version 2)
        The default is VHDX.

    .PARAMETER Size
        The size of the VHD in MB.
        If no size is used the default will be set to the type of VHD.
        The default for VHD is 2 TB and for VHDX 64TB

    .PARAMETER FixedSize
        Set the VHD to have a fixed size or not.
        Be careful using this parameter. Fixed will make the VHD use the space assigned in -Size

    .PARAMETER ReadOnly
        Set the VHD to readonly

    .PARAMETER Force
        Forcefully create the neccesary items

    .PARAMETER EnableException
        By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message.
        This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting.
        Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch.

    .PARAMETER WhatIf
        If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run.

    .PARAMETER Confirm
        If this switch is enabled, you will be prompted for confirmation before executing any operations that change state.

    .NOTES
        Author: Sander Stad (@sqlstad, sqlstad.nl)

        Website: https://psdatabaseclone.org
        Copyright: (C) Sander Stad, sander@sqlstad.nl
        License: MIT https://opensource.org/licenses/MIT

    .LINK
        https://psdatabaseclone.org/

    .EXAMPLE
        New-DcnVhdDisk -Destination C:\temp -Name Database1 -Size 1GB

        Creates a dynamic VHD in C:\Temp named Database1.vhdx with a size of 1GB

    #>


    [CmdLetBinding(SupportsShouldProcess = $true)]
    [OutputType('System.String')]

    param(
        [parameter(Mandatory = $true)]
        [string]$Destination,
        [string]$Name,
        [string]$FileName,
        [ValidateSet('VHD', 'VHDX', 'vhd', 'vhdx')]
        [string]$VhdType,
        [uint64]$Size,
        [switch]$FixedSize,
        [switch]$ReadOnly,
        [switch]$Force,
        [switch]$EnableException
    )

    begin {
        # Check if the console is run in Administrator mode
        if ( -not (Test-PSFPowerShell -Elevated) ) {
            Stop-PSFFunction -Message "Module requires elevation. Please run the console in Administrator mode" -Continue
        }

        # Check the destination path
        if (-not (Test-Path $Destination)) {
            if ($PSCmdlet.ShouldProcess($Destination, "Creating destination directory")) {
                try {
                    Write-PSFMessage -Message "Creating destination directory $Destination" -Level Verbose
                    $null = New-Item -Path $Destination -ItemType Directory -Force
                }
                catch {
                    Stop-PSFFunction -Message "Couldn't create directory $Destination" -ErrorRecord $_ -Target $Destination -Continue
                }
            }
        }

        # Check the vhd type
        if (-not $VhdType) {
            Write-PSFMessage -Message "Setting vhd type to 'VHDX'" -Level Verbose
            $VhdType = 'VHDX'
        }

        # Make sure thevalue is in uppercase all th time
        $VhdType = $VhdType.ToUpper()

        # Check the size of the file
        if (-not $Size) {
            switch ($VhdType) {
                'VHD' { $Size = 2048MB }
                'VHDX' { $Size = 64TB }
            }
        }
        else {
            if ($VhdType -eq 'VHD' -and $Size -gt 2TB) {
                Stop-PSFFunction -Message "Size cannot exceed 2TB when using VHD type."
            }
            elseif ($VhdType -eq 'VHDX' -and $Size -gt 64TB) {
                Stop-PSFFunction -Message "Size cannot exceed 64TB when using VHDX type."
            }

            if ($Size -lt 3MB) {
                Stop-PSFFunction -Message "The size of the vhd cannot be smaller than 3MB" -Continue
            }
        }

        # Make sure the size in MB instead of some other version
        $Size = $Size / 1MB

        # Check the name and file name parameters
        if (-not $Name -and -not $FileName) {
            Stop-PSFFunction -Message "Either set the Name or FileName parameter"
        }
        else {
            if (-not $FileName) {
                $FileName = "$Name.$($VhdType.ToLower())"
                Write-PSFMessage -Message "Setting file name to $FileName" -Level Verbose
            }
            elseif ($FileName) {
                if (($FileName -notlike "*.vhd") -and ($FileName -notlike "*.vhdx")) {
                    Stop-PSFFunction -Message "The filename needs to have the .vhd or .vhdx extension" -Target $FileName -Continue
                }
            }
        }

        # Set the vhd path
        if ($Destination.EndsWith("\")) {
            $vhdPath = "$Destination$FileName"
        }
        else {
            $vhdPath = "$Destination\$FileName"
        }

        Write-PSFMessage -Message "Vhd path set to $vhdPath" -Level Verbose

        # Check if the file does not yet exist
        if (Test-Path $vhdPath) {
            if (-not $Force) {
                Stop-PSFFunction -Message "The vhd file already exists" -Continue
            }
            else {
                try {
                    Remove-Item -Path $vhdPath -Force:$Force
                }
                catch {
                    Stop-PSFFunction -Message "Could not remove VHD '$vhdPath'" -Continue -ErrorRecord $_
                }
            }
        }

        # Set the location where to save the diskpart command
        $diskpartScriptFile = Get-PSFConfigValue -FullName psdatabaseclone.diskpart.scriptfile -Fallback "$env:APPDATA\psdatabaseclone\diskpartcommand.txt"

        if (-not (Test-Path -Path $diskpartScriptFile)) {
            if ($PSCmdlet.ShouldProcess($diskpartScriptFile, "Creating dispart file")) {
                try {
                    $null = New-Item -Path $diskpartScriptFile -ItemType File
                }
                catch {
                    Stop-PSFFunction -Message "Could not create diskpart script file" -ErrorRecord $_ -Continue
                }
            }
        }
    }

    process {
        # Test if there are any errors
        if (Test-PSFFunctionInterrupt) { return }

        if ($PSCmdlet.ShouldProcess($vhdPath, "Creating VHD")) {
            # Check if the file needs to have a fixed size
            try {
                if ($FixedSize) {
                    $command = "create vdisk file='$vhdPath' maximum=$Size type=fixed"
                }
                else {
                    $command = "create vdisk file='$vhdPath' maximum=$Size type=expandable"
                }

                # Set the content of the diskpart script file
                Set-Content -Path $diskpartScriptFile -Value $command -Force

                $script = [ScriptBlock]::Create("diskpart /s $diskpartScriptFile")
                $null = Invoke-PSFCommand -ScriptBlock $script

            }
            catch {
                Stop-PSFFunction -Message "Something went wrong creating the vhd" -ErrorRecord $_ -Continue
            }
        }
    }

    end {
        # Clean up the script file for diskpart
        Remove-Item $diskpartScriptFile -Force -Confirm:$false

        # Test if there are any errors
        if (Test-PSFFunctionInterrupt) { return }

        Write-PSFMessage "Finished creating vhd file" -Level Verbose
    }
}







function Get-DcnHost {
    <#
    .SYNOPSIS
        Get-DcnHost get all the hosts that have clones

    .DESCRIPTION
        Get-DcnHost will retrieve the hosts that have clones
        By default all the hosts are returned

    .PARAMETER SqlCredential
        Allows you to login to servers using SQL Logins as opposed to Windows Auth/Integrated/Trusted. To use:

        $scred = Get-Credential, then pass $scred object to the -SqlCredential parameter.

        Windows Authentication will be used if SqlCredential is not specified. SQL Server does not accept Windows credentials being passed as credentials.
        To connect as a different Windows user, run PowerShell as that user.

    .PARAMETER DcnSqlCredential
        Allows you to login to servers using SQL Logins as opposed to Windows Auth/Integrated/Trusted.
        This works similar as SqlCredential but is only meant for authentication to the PSDatabaseClone database server and database.

    .PARAMETER Credential
        Allows you to login to servers or use authentication to access files and folder/shares

        $scred = Get-Credential, then pass $scred object to the -Credential parameter.

    .PARAMETER EnableException
        By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message.
        This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting.
        Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch.

    .PARAMETER WhatIf
        If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run.

    .PARAMETER Confirm
        If this switch is enabled, you will be prompted for confirmation before executing any operations that change state.

    .NOTES
        Author: Sander Stad (@sqlstad, sqlstad.nl)

        Website: https://psdatabaseclone.org
        Copyright: (C) Sander Stad, sander@sqlstad.nl
        License: MIT https://opensource.org/licenses/MIT

    .LINK
        https://psdatabaseclone.org/

    .EXAMPLE
        Get-DcnHost

        Get all the hosts

    #>


    [CmdLetBinding()]

    param(
        [PSCredential]$SqlCredential,
        [PSCredential]$DcnSqlCredential,
        [PSCredential]$Credential,
        [switch]$EnableException
    )

    begin {
        # Check if the setup has ran
        if (-not (Test-DcnModule -SetupStatus)) {
            Stop-PSFFunction -Message "The module setup has NOT yet successfully run. Please run 'Set-DcnConfiguration'" -Continue
        }

        # Get the information store
        $informationStore = Get-PSFConfigValue -FullName psdatabaseclone.informationstore.mode

        if ($informationStore -eq 'SQL') {

            # Get the module configurations
            [DbaInstanceParameter]$pdcSqlInstance = Get-PSFConfigValue -FullName psdatabaseclone.database.Server
            $pdcDatabase = Get-PSFConfigValue -FullName psdatabaseclone.database.name
            if (-not $DcnSqlCredential) {
                $pdcCredential = Get-PSFConfigValue -FullName psdatabaseclone.informationstore.credential -Fallback $null
            }
            else {
                $pdcCredential = $DcnSqlCredential
            }

            # Test the module database setup
            try {
                Test-DcnConfiguration -SqlCredential $pdcCredential -EnableException
            }
            catch {
                Stop-PSFFunction -Message "Something is wrong in the module configuration" -ErrorRecord $_ -Continue
            }

            $query = "SELECT DISTINCT h.HostName FROM dbo.Host AS h"

            try {
                $results = @()
                $results += Invoke-DbaQuery -SqlInstance $pdcSqlInstance -SqlCredential $DcnSqlCredential -Database $pdcDatabase -Query $query -As PSObject
            }
            catch {
                Stop-PSFFunction -Message "Could retrieve images from database $pdcDatabase" -ErrorRecord $_ -Target $query
            }
        }
        elseif ($informationStore -eq 'File') {
            try {
                if (Test-Path -Path "DCNJSONFolder:\") {
                    # Get the clones
                    $results = Get-ChildItem -Path "DCNJSONFolder:\" -Filter "*clones.json" | ForEach-Object { Get-Content $_.FullName | ConvertFrom-Json }
                }
                else {
                    Stop-PSFFunction -Message "Could not reach image information location 'DCNJSONFolder:\'" -ErrorRecord $_ -Target "DCNJSONFolder:\"
                    return
                }
            }
            catch {
                Stop-PSFFunction -Message "Couldn't get results from JSN folder" -Target "DCNJSONFolder:\" -ErrorRecord $_
            }
        }
    }

    process {
        # Test if there are any errors
        if (Test-PSFFunctionInterrupt) { return }

        foreach ($result in $results) {

            [pscustomobject]@{
                HostName = $result.HostName
            }
        }
    }

    end {
        # Test if there are any errors
        if (Test-PSFFunctionInterrupt) { return }

        Write-PSFMessage -Message "Finished retrieving host(s)" -Level Verbose
    }
}


function Get-DcnImage {
    <#
    .SYNOPSIS
        Get-DcnImage get on or more clones

    .DESCRIPTION
        Get-DcnImage will retrieve the clones and apply filters if needed.
        By default all the clones are returned

    .PARAMETER SqlCredential
        Allows you to login to servers using SQL Logins as opposed to Windows Auth/Integrated/Trusted. To use:

        $scred = Get-Credential, then pass $scred object to the -SqlCredential parameter.

        Windows Authentication will be used if SqlCredential is not specified. SQL Server does not accept Windows credentials being passed as credentials.
        To connect as a different Windows user, run PowerShell as that user.

    .PARAMETER DcnSqlCredential
        Allows you to login to servers using SQL Logins as opposed to Windows Auth/Integrated/Trusted.
        This works similar as SqlCredential but is only meant for authentication to the PSDatabaseClone database server and database.

    .PARAMETER Credential
        Allows you to login to servers or use authentication to access files and folder/shares

        $scred = Get-Credential, then pass $scred object to the -Credential parameter.

    .PARAMETER ImageID
        Filter based on the image id

    .PARAMETER ImageName
        Filter based on the image name

    .PARAMETER ImageLocation
        Filter based on the image location

    .PARAMETER Database
        Filter based on the database

    .PARAMETER EnableException
        By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message.
        This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting.
        Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch.

    .PARAMETER WhatIf
        If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run.

    .PARAMETER Confirm
        If this switch is enabled, you will be prompted for confirmation before executing any operations that change state.

    .NOTES
        Author: Sander Stad (@sqlstad, sqlstad.nl)

        Website: https://psdatabaseclone.org
        Copyright: (C) Sander Stad, sander@sqlstad.nl
        License: MIT https://opensource.org/licenses/MIT

    .LINK
        https://psdatabaseclone.org/

    .EXAMPLE
        Get-DcnImage

        Get all the images

    .EXAMPLE
        Get-DcnImage -ImageName DB1_20180704220944, DB2_20180704221144

        Retrieve the images for DB1_20180704220944, DB2_20180704221144

    .EXAMPLE
        Get-DcnImage -ImageLocation "\\fileserver1\psdatabaseclone\images\DB1_20180704220944.vhdx"

        Get all the images that are the same as the image location

    .EXAMPLE
        Get-DcnImage -Database DB1, DB2

        Get all the images that were made for databases DB1 and DB2
    #>


    [CmdLetBinding()]

    param(
        [PSCredential]$SqlCredential,
        [PSCredential]$DcnSqlCredential,
        [PSCredential]$Credential,
        [int[]]$ImageID,
        [string[]]$ImageName,
        [string[]]$ImageLocation,
        [string[]]$Database,
        [switch]$EnableException
    )

    begin {
        # Check if the setup has ran
        if (-not (Test-DcnModule -SetupStatus)) {
            Stop-PSFFunction -Message "The module setup has NOT yet successfully run. Please run 'Set-DcnConfiguration'" -Continue
        }

        # Get the information store
        $informationStore = Get-PSFConfigValue -FullName psdatabaseclone.informationstore.mode

        if ($informationStore -eq 'SQL') {

            # Get the module configurations
            [DbaInstanceParameter]$pdcSqlInstance = Get-PSFConfigValue -FullName psdatabaseclone.database.Server
            $pdcDatabase = Get-PSFConfigValue -FullName psdatabaseclone.database.name
            if (-not $DcnSqlCredential) {
                $pdcCredential = Get-PSFConfigValue -FullName psdatabaseclone.informationstore.credential -Fallback $null
            }
            else {
                $pdcCredential = $DcnSqlCredential
            }

            # Test the module database setup
            try {
                Test-DcnConfiguration -SqlCredential $pdcCredential -EnableException
            }
            catch {
                Stop-PSFFunction -Message "Something is wrong in the module configuration" -ErrorRecord $_ -Continue
            }

            $query = "
                SELECT ImageID,
                    ImageName,
                    ImageLocation,
                    SizeMB,
                    DatabaseName,
                    DatabaseTimestamp,
                    CreatedOn
                FROM dbo.Image;
            "


            try {
                $results = @()
                $results += Invoke-DbaQuery -SqlInstance $pdcSqlInstance -SqlCredential $DcnSqlCredential -Database $pdcDatabase -Query $query -As PSObject
            }
            catch {
                Stop-PSFFunction -Message "Could retrieve images from database $pdcDatabase" -ErrorRecord $_ -Target $query
            }
        }
        elseif ($informationStore -eq 'File') {
            try {
                if (Test-Path -Path "DCNJSONFolder:\") {
                    # Get the clones
                    $results = Get-ChildItem -Path "DCNJSONFolder:\" -Filter "*images.json" | ForEach-Object { Get-Content $_.FullName | ConvertFrom-Json }
                }
                else {
                    Stop-PSFFunction -Message "Could not reach image information location 'DCNJSONFolder:\'" -ErrorRecord $_ -Target "DCNJSONFolder:\"
                    return
                }
            }
            catch {
                Stop-PSFFunction -Message "Couldn't get results from JSN folder" -Target "DCNJSONFolder:\" -ErrorRecord $_
            }
        }

        # Filter image id
        if ($ImageID) {
            $results = $results | Where-Object { $_.ImageID -in $ImageID }
        }

        # Filter image name
        if ($ImageName) {
            $results = $results | Where-Object { $_.ImageName -in $ImageName }
        }

        # Filter image location
        if ($ImageLocation) {
            $results = $results | Where-Object { $_.ImageLocation -in $ImageLocation }
        }

        # Filter database
        if ($Database) {
            $results = $results | Where-Object { $_.DatabaseName -in $Database }
        }
    }

    process {
        # Test if there are any errors
        if (Test-PSFFunctionInterrupt) { return }

        foreach ($result in $results) {

            [pscustomobject]@{
                ImageID           = $result.ImageID
                ImageName         = $result.ImageName
                ImageLocation     = $result.ImageLocation
                SizeMB            = $result.SizeMB
                DatabaseName      = $result.DatabaseName
                DatabaseTimestamp = $result.DatabaseTimestamp
                CreatedOn         = $result.CreatedOn
            }
        }
    }

    end {
        # Test if there are any errors
        if (Test-PSFFunctionInterrupt) { return }

        Write-PSFMessage -Message "Finished retrieving image(s)" -Level Verbose
    }
}


function New-DcnImage {
    <#
    .SYNOPSIS
        New-DcnImage creates a new image

    .DESCRIPTION
        New-DcnImage will create a new image based on a SQL Server database

        The command will either create a full backup or use the last full backup to create the image.

        Every image is created with the name of the database and a time stamp yyyyMMddHHmmss i.e "DB1_20180622171819.vhdx"

    .PARAMETER SourceSqlInstance
        Source SQL Server name or SMO object representing the SQL Server to connect to.
        This will be where the database is currently located

    .PARAMETER SourceSqlCredential
        Allows you to login to servers using SQL Logins as opposed to Windows Auth/Integrated/Trusted. To use:

        $scred = Get-Credential, then pass $scred object to the -SourceSqlCredential parameter.

        Windows Authentication will be used if SqlCredential is not specified. SQL Server does not accept Windows credentials being passed as credentials.
        To connect as a different Windows user, run PowerShell as that user.

    .PARAMETER SourceCredential
        Allows you to login to other parts of a system like folders. To use:

        $scred = Get-Credential, then pass $scred object to the -SourceCredential parameter.

    .PARAMETER DestinationSqlInstance
        SQL Server name or SMO object representing the SQL Server to connect to.
        This is the server to use to temporarily restore the database to create the image.

    .PARAMETER DestinationSqlCredential
        Allows you to login to servers using SQL Logins as opposed to Windows Auth/Integrated/Trusted. To use:

        $scred = Get-Credential, then pass $scred object to the -DestinationSqlCredential parameter.

        Windows Authentication will be used if SqlCredential is not specified. SQL Server does not accept Windows credentials being passed as credentials.
        To connect as a different Windows user, run PowerShell as that user.

    .PARAMETER DestinationCredential
        Allows you to login to other parts of a system like folders. To use:

        $scred = Get-Credential, then pass $scred object to the -DestinationCredential parameter.

    .PARAMETER DcnSqlCredential
        Allows you to login to servers using SQL Logins as opposed to Windows Auth/Integrated/Trusted.
        This works similar as SqlCredential but is only meant for authentication to the PSDatabaseClone database server and database.

        By default the script will try to retrieve the configuration value "psdatabaseclone.informationstore.credential"

    .PARAMETER ImageNetworkPath
        Network path where to save the image. This has to be a UNC path

    .PARAMETER ImageLocalPath
        Local path where to save the image

    .PARAMETER Database
        Databases to create an image of

    .PARAMETER VhdType
        The type of the harddisk. This can either by VHD (version 1) or VHDX (version 2)
        The default is VHDX.

    .PARAMETER CreateFullBackup
        Create a new full backup of the database. The backup will be saved in the default backup directory

    .PARAMETER UseLastFullBackup
        Use the last full backup created for the database

    .PARAMETER BackupFilePath
        Use a specific backup file to create the image

    .PARAMETER CopyOnlyBackup
        Create a backup as COPY_ONLY

    .PARAMETER Force
        Forcefully execute commands when needed

    .PARAMETER EnableException
        By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message.
        This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting.
        Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch.

    .PARAMETER WhatIf
        If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run.

    .PARAMETER Confirm
        If this switch is enabled, you will be prompted for confirmation before executing any operations that change state.

    .NOTES
        Author: Sander Stad (@sqlstad, sqlstad.nl)

        Website: https://psdatabaseclone.org
        Copyright: (C) Sander Stad, sander@sqlstad.nl
        License: MIT https://opensource.org/licenses/MIT

    .LINK
        https://psdatabaseclone.org/

    .EXAMPLE
        New-DcnImage -SourceSqlInstance SQLDB1 -DestinationSqlInstance SQLDB2 -ImageLocalPath C:\Temp\images\ -Database DB1 -CreateFullBackup

        Create an image for databas DB1 from SQL Server SQLDB1. The temporary destination will be SQLDB2.
        The image will be saved in C:\Temp\images.
    .EXAMPLE
        New-DcnImage -SourceSqlInstance SQLDB1 -DestinationSqlInstance SQLDB2 -ImageLocalPath C:\Temp\images\ -Database DB1 -UseLastFullBackup

        Create an image from the database DB1 on SQLDB1 using the last full backup and use SQLDB2 as the temporary database server.
        The image is written to c:\Temp\images
    #>

    [CmdLetBinding(SupportsShouldProcess = $true)]

    param(
        [parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [DbaInstanceParameter]$SourceSqlInstance,
        [PSCredential]$SourceSqlCredential,
        [PSCredential]$SourceCredential,
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [DbaInstanceParameter]$DestinationSqlInstance,
        [PSCredential]$DestinationSqlCredential,
        [PSCredential]$DestinationCredential,
        [PSCredential]$DcnSqlCredential,
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [object[]]$Database,
        [string]$ImageNetworkPath,
        [string]$ImageLocalPath,
        [ValidateSet('VHD', 'VHDX', 'vhd', 'vhdx')]
        [string]$VhdType,
        [switch]$CreateFullBackup,
        [switch]$UseLastFullBackup,
        [string]$BackupFilePath,
        [switch]$CopyOnlyBackup,
        [Alias('MaskingConfigFile', 'MaskingConfigFilePath')]
        [switch]$Force,
        [switch]$EnableException
    )

    begin {
        # Check if the console is run in Administrator mode
        if ( -not (Test-PSFPowerShell -Elevated) ) {
            Stop-PSFFunction -Message "Module requires elevation. Please run the console in Administrator mode" -Continue
        }

        # Check if the setup has ran
        if (-not (Test-DcnModule -SetupStatus)) {
            Stop-PSFFunction -Message "The module setup has NOT yet successfully run. Please run 'Set-DcnConfiguration'" -Continue
        }

        if(-not $CreateFullBackup -and -not $UseLastFullBackup -and -not $BackupFilePath){
            Stop-PSFFunction -Message "Unable to get last backup file. Please use -CreateFullBackup, -UseLastFullBackup or -BackupFile" -Continue
        }

        # Checking parameters
        if (-not $ImageNetworkPath) {
            Stop-PSFFunction -Message "Please enter the network path where to save the images" -Continue
        }

        # Check the vhd type
        if (-not $VhdType) {
            Write-PSFMessage -Message "Setting vhd type to 'VHDX'" -Level Verbose
            $VhdType = 'VHDX'
        }

        # Get the information store
        $informationStore = Get-PSFConfigValue -FullName psdatabaseclone.informationstore.mode

        if ($informationStore -eq 'SQL') {
            # Get the module configurations
            $pdcSqlInstance = Get-PSFConfigValue -FullName psdatabaseclone.database.Server
            $pdcDatabase = Get-PSFConfigValue -FullName psdatabaseclone.database.name
            if (-not $DcnSqlCredential) {
                $pdcCredential = Get-PSFConfigValue -FullName psdatabaseclone.informationstore.credential -Fallback $null
            }
            else {
                $pdcCredential = $DcnSqlCredential
            }

            # Test the module database setup
            if ($PSCmdlet.ShouldProcess("Test-DcnConfiguration", "Testing module setup")) {
                try {
                    Test-DcnConfiguration -SqlCredential $pdcCredential -EnableException
                }
                catch {
                    Stop-PSFFunction -Message "Something is wrong in the module configuration" -ErrorRecord $_ -Continue
                }
            }
        }

        Write-PSFMessage -Message "Started image creation" -Level Verbose

        # Try connecting to the instance
        Write-PSFMessage -Message "Attempting to connect to Sql Server $SourceSqlInstance.." -Level Verbose
        try {
            $sourceServer = Connect-DbaInstance -SqlInstance $SourceSqlInstance -SqlCredential $SourceSqlCredential
        }
        catch {
            Stop-PSFFunction -Message "Could not connect to Sql Server instance $SourceSqlInstance" -ErrorRecord $_ -Target $SourceSqlInstance
            return
        }

        # Cleanup the values in the network path
        if ($ImageNetworkPath.EndsWith("\")) {
            $ImageNetworkPath = $ImageNetworkPath.Substring(0, $ImageNetworkPath.Length - 1)
        }

        # Make up the data from the network path
        try {
            [uri]$uri = New-Object System.Uri($ImageNetworkPath)
            $uriHost = $uri.Host
        }
        catch {
            Stop-PSFFunction -Message "The image network path $ImageNetworkPath is not valid" -ErrorRecord $_ -Target $ImageNetworkPath
            return
        }

        # Setup the computer object
        $computer = [PsfComputer]$uriHost

        if (-not $computer.IsLocalhost) {
            # Get the result for the remote test
            $resultPSRemote = Test-DcnRemoting -ComputerName $computer -Credential $Credential

            # Check the result
            if ($resultPSRemote.Result) {
                $command = [scriptblock]::Create("Import-Module dbaclone -Force")

                try {
                    Invoke-PSFCommand -ComputerName $computer -ScriptBlock $command -Credential $Credential
                }
                catch {
                    Stop-PSFFunction -Message "Couldn't import module remotely" -Target $command
                    return
                }
            }
            else {
                Stop-PSFFunction -Message "Couldn't connect to host remotely.`nVerify that the specified computer name is valid, that the computer is accessible over the network, and that a firewall exception for the WinRM service is enabled and allows access from this computer" -Target $resultPSRemote -Continue
            }
        }

        # Get the local path from the network path
        if (-not $ImageLocalPath) {
            if ($PSCmdlet.ShouldProcess($ImageNetworkPath, "Converting UNC path to local path")) {
                try {
                    # Check if computer is local
                    if ($computer.IsLocalhost) {
                        $ImageLocalPath = Convert-DcnUncPathToLocalPath -UncPath $ImageNetworkPath -EnableException
                    }
                    else {
                        $command = "Convert-DcnUncPathToLocalPath -UncPath `"$ImageNetworkPath`" -EnableException"
                        $commandGetLocalPath = [ScriptBlock]::Create($command)
                        $ImageLocalPath = Invoke-PSFCommand -ComputerName $computer -ScriptBlock $commandGetLocalPath -Credential $DestinationCredential

                        if (-not $ImageLocalPath) {
                            Stop-PSFFunction -Message "Could not convert network path to local path" -Target $ImageLocalPath
                            return
                        }
                    }

                    Write-PSFMessage -Message "Converted '$ImageNetworkPath' to '$ImageLocalPath'" -Level Verbose

                }
                catch {
                    Stop-PSFFunction -Message "Something went wrong getting the local image path" -Target $ImageNetworkPath
                    return
                }
            }
        }
        else {
            # Cleanup the values in the network path
            if ($ImageLocalPath.EndsWith("\")) {
                $ImageLocalPath = $ImageLocalPath.Substring(0, $ImageLocalPath.Length - 1)
            }

            # Check if the assigned value in the local path corresponds to the one retrieved
            try {
                # Check if computer is local
                if ($computer.IsLocalhost) {
                    $convertedLocalPath = Convert-DcnUncPathToLocalPath -UncPath $ImageNetworkPath -EnableException
                }
                else {
                    $command = "Convert-DcnUncPathToLocalPath -UncPath `"$ImageNetworkPath`" -EnableException"
                    $commandGetLocalPath = [ScriptBlock]::Create($command)
                    $convertedLocalPath = Invoke-PSFCommand -ComputerName $computer -ScriptBlock $commandGetLocalPath -Credential $DestinationCredential
                }

                Write-PSFMessage -Message "Converted '$ImageNetworkPath' to '$ImageLocalPath'" -Level Verbose

                # Check if the ImageLocalPath and convertedLocalPath are the same
                if ($ImageLocalPath -ne $convertedLocalPath) {
                    Stop-PSFFunction -Message "The local path '$ImageLocalPath' is not the same location as the network path '$ImageNetworkPath'" -Target $ImageNetworkPath
                    return
                }

            }
            catch {
                Stop-PSFFunction -Message "Something went wrong getting the local image path" -Target $ImageNetworkPath
                return
            }
        }

        # Check the image local path
        if ($PSCmdlet.ShouldProcess("Verifying image local path")) {
            if ((Test-DbaPath -Path $ImageLocalPath -SqlInstance $SourceSqlInstance -SqlCredential $DestinationCredential) -ne $true) {
                Stop-PSFFunction -Message "Image local path $ImageLocalPath is not valid directory or can't be reached." -Target $SourceSqlInstance
                return
            }

            # Clean up the paths
            if ($ImageLocalPath.EndsWith("\")) {
                $ImageLocalPath = $ImageLocalPath.Substring(0, $ImageLocalPath.Length - 1)
            }

            $imagePath = $ImageLocalPath
        }

        # Check the database parameter
        if ($Database) {
            foreach ($db in $Database) {
                if ($db -notin $sourceServer.Databases.Name) {
                    Stop-PSFFunction -Message "Database $db cannot be found on instance $SourceSqlInstance" -Target $SourceSqlInstance
                }
            }

            $DatabaseCollection = $sourceServer.Databases | Where-Object { $_.Name -in $Database }
        }
        else {
            Stop-PSFFunction -Message "Please supply a database to create an image for" -Target $SourceSqlInstance -Continue
        }

        if($BackupFilePath){
            if(-not (Test-Path -Path $BackupFilePath)){
                Stop-PSFFunction -Message "Could not find backup file '$($BackupFilePath)'"
            }

            if($Database.Count -gt 1){
                Stop-PSFFunction -Message "You cannot enter multiple databases for the same backup file. Please just enter one"
            }
        }



        # Set time stamp
        $timestamp = Get-Date -format "yyyyMMddHHmmss"

    }

    process {
        # Test if there are any errors
        if (Test-PSFFunctionInterrupt) { return }

        # Loop through each of the databases
        foreach ($db in $DatabaseCollection) {
            Write-PSFMessage -Message "Creating image for database $db from $SourceSqlInstance" -Level Verbose

            if ($PSCmdlet.ShouldProcess($db, "Checking available disk space for database")) {
                # Check the database size to the available disk space
                if ($computer.IsLocalhost) {
                    $availableMB = (Get-PSDrive -Name $ImageLocalPath.Substring(0, 1)).Free / 1MB
                }
                else {
                    $command = [ScriptBlock]::Create("(Get-PSDrive -Name $($ImageLocalPath.Substring(0, 1)) ).Free / 1MB")
                    $availableMB = Invoke-PSFCommand -ComputerName $computer -ScriptBlock $commandGetLocalPath -Credential $DestinationCredential
                }

                $dbSizeMB = $db.Size

                if ($availableMB -lt $dbSizeMB) {
                    Stop-PSFFunction -Message "Size of database $($db.Name) does not fit within the image local path" -Target $db -Continue
                }
            }

            # Setup the image variables
            $imageName = "$($db.Name)_$timestamp"

            # Setup the access path
            $accessPath = Join-PSFPath -Path $ImageLocalPath -Child $imageName

            # Setup the vhd path
            $vhdPath = "$($accessPath).$($VhdType.ToLower())"

            if ($CreateFullBackup) {
                if ($PSCmdlet.ShouldProcess($db, "Creating full backup for database $db")) {

                    # Create the backup
                    Write-PSFMessage -Message "Creating new full backup for database $db" -Level Verbose
                    $lastFullBackup = Backup-DbaDatabase -SqlInstance $SourceSqlInstance -SqlCredential $SourceSqlCredential -Database $db.Name -CopyOnly:$CopyOnlyBackup
                }
            }
            elseif ($BackupFilePath) {
                [pscustomobject]$lastFullBackup = @{
                    Path = $BackupFilePath
                }
            }
            else {
                Write-PSFMessage -Message "Trying to retrieve the last full backup for $db" -Level Verbose

                # Get the last full backup
                $lastFullBackup = Get-DbaDbBackupHistory -SqlInstance $SourceSqlInstance -SqlCredential $SourceSqlCredential -Database $db.Name -LastFull
            }

            if (-not $lastFullBackup.Path) {
                Stop-PSFFunction -Message "No full backup could be found. Please use -CreateFullBackup or create a full backup manually" -Target $lastFullBackup
                return
            }
            elseif (-not (Test-Path -Path $lastFullBackup.Path)) {
                Stop-PSFFunction -Message "Could not access the full backup file. Check if it exists or that you have enough privileges to access it" -Target $lastFullBackup
                return
            }

            if ($PSCmdlet.ShouldProcess("$imageName", "Creating the vhd")) {
                # try to create the new VHD
                try {
                    Write-PSFMessage -Message "Create the vhd $imageName" -Level Verbose

                    # Check if computer is local
                    if ($computer.IsLocalhost) {
                        $null = New-DcnVhdDisk -Destination $imagePath -Name $imageName -VhdType $VhdType -EnableException
                    }
                    else {
                        $command = [ScriptBlock]::Create("New-DcnVhdDisk -Destination '$imagePath' -Name $imageName -VhdType $VhdType -EnableException")
                        $null = Invoke-PSFCommand -ComputerName $computer -ScriptBlock $command -Credential $DestinationCredential
                    }

                }
                catch {
                    Stop-PSFFunction -Message "Couldn't create vhd(x) $imageName" -Target $imageName -ErrorRecord $_ -Continue
                }
            }


            if ($PSCmdlet.ShouldProcess("$imageName", "Initializing the vhd")) {
                # Try to initialize the vhd
                try {
                    Write-PSFMessage -Message "Initializing the vhd $imageName" -Level Verbose

                    # Check if computer is local
                    if ($computer.IsLocalhost) {
                        $diskResult = Initialize-DcnVhdDisk -Path $vhdPath -Credential $DestinationCredential -EnableException
                    }
                    else {
                        $command = [ScriptBlock]::Create("Initialize-DcnVhdDisk -Path $vhdPath -EnableException")
                        $diskResult = Invoke-PSFCommand -ComputerName $computer -ScriptBlock $command -Credential $DestinationCredential
                    }
                }
                catch {
                    Stop-PSFFunction -Message "Couldn't initialize vhd $vhdPath" -Target $imageName -ErrorRecord $_
                }
            }

            # Create folder structure for image
            $imageDataFolder = Join-PSFPath -Path $imagePath -Child "$($imageName)\Data"
            $imageLogFolder = Join-PSFPath -Path $imagePath -Child "$($imageName)\Log"

            # try to create access path
            try {
                # Check if access path is already present
                if (-not (Test-Path -Path $accessPath)) {
                    if ($PSCmdlet.ShouldProcess($accessPath, "Creating access path $accessPath")) {
                        try {
                            # Check if computer is local
                            if ($computer.IsLocalhost) {
                                $null = New-Item -Path $accessPath -ItemType Directory -Force
                            }
                            else {
                                $command = [ScriptBlock]::Create("New-Item -Path $accessPath -ItemType Directory -Force")
                                $null = Invoke-PSFCommand -ComputerName $computer -ScriptBlock $command -Credential $DestinationCredential

                                # Set the permissions
                                $script = "
                                    `$permission = 'Everyone', 'FullControl', 'ContainerInherit,ObjectInherit', 'None', 'Allow'
                                    `$accessRule = New-Object System.Security.AccessControl.FileSystemAccessRule `$permission
                                    `$acl = Get-Acl -Path '$accessPath'
                                    `$acl.SetAccessRule(`$accessRule)
                                    `$acl | Set-Acl '$accessPath'
                                "


                                $command = [ScriptBlock]::Create($script)
                                $null = Invoke-PSFCommand -ComputerName $computer -ScriptBlock $command -Credential $DestinationCredential
                            }
                        }
                        catch {
                            Stop-PSFFunction -Message "Couldn't create access path directory" -ErrorRecord $_ -Target $accessPath -Continue
                        }
                    }

                    # Set the permissions
                    #$permission = "Everyone", "FullControl", "ContainerInherit,ObjectInherit", "None", "Allow"
                    $accessRule = New-Object System.Security.AccessControl.FileSystemAccessRule("Everyone", "FullControl", "ContainerInherit,Objectinherit", "None", "Allow")
                    $acl = Get-Acl -Path $accessPath
                    $acl.SetAccessRule($accessRule)
                    Set-Acl -Path $accessPath -AclObject $acl
                }

                # Get the properties of the disk and partition
                $disk = $diskResult.Disk
                $partition = Get-Partition -DiskNumber $disk.Number | Where-Object { $_.Type -ne "Reserved" } | Select-Object -First 1

                if ($PSCmdlet.ShouldProcess($accessPath, "Adding access path '$accessPath' to mounted disk")) {
                    # Add the access path to the mounted disk
                    if ($computer.IsLocalhost) {
                        $null = Add-PartitionAccessPath -DiskNumber $disk.Number -PartitionNumber $partition.PartitionNumber -AccessPath $accessPath -ErrorAction SilentlyContinue
                    }
                    else {
                        $command = [ScriptBlock]::Create("Add-PartitionAccessPath -DiskNumber $($disk.Number) -PartitionNumber $($partition.PartitionNumber) -AccessPath $accessPath -ErrorAction SilentlyContinue")
                        $null = Invoke-PSFCommand -ComputerName $computer -ScriptBlock $command -Credential $DestinationCredential
                    }
                }

            }
            catch {
                Stop-PSFFunction -Message "Couldn't create access path for partition" -ErrorRecord $_ -Target $diskResult.partition
            }

            # Check if image data folder exist
            if (-not (Test-Path -Path $imageDataFolder)) {
                if ($PSCmdlet.ShouldProcess($accessPath, "Creating data folder in vhd")) {
                    try {
                        Write-PSFMessage -Message "Creating data folder for image" -Level Verbose

                        # Check if computer is local
                        if ($computer.IsLocalhost) {
                            $null = New-Item -Path $imageDataFolder -ItemType Directory

                            $acl = Get-ACL -Path $imageDataFolder
                            $acl.SetAccessRule($accessRule)
                            Set-Acl -Path $imageDataFolder -AclObject $acl
                        }
                        else {
                            $command = [ScriptBlock]::Create("New-Item -Path $imageDataFolder -ItemType Directory")
                            $null = Invoke-PSFCommand -ComputerName $computer -ScriptBlock $command -Credential $DestinationCredential
                        }
                    }
                    catch {
                        Stop-PSFFunction -Message "Couldn't create image data folder" -Target $imageName -ErrorRecord $_ -Continue
                    }
                }
            }

            # Test if the image log folder exists
            if (-not (Test-Path -Path $imageLogFolder)) {
                if ($PSCmdlet.ShouldProcess($accessPath, "Creating log folder in vhd")) {
                    try {
                        Write-PSFMessage -Message "Creating transaction log folder for image" -Level Verbose

                        # Check if computer is local
                        if ($computer.IsLocalhost) {
                            $null = New-Item -Path $imageLogFolder -ItemType Directory

                            $acl = Get-ACL -Path $imageLogFolder
                            $acl.SetAccessRule($accessRule)
                            Set-Acl -Path $imageLogFolder -AclObject $acl
                        }
                        else {
                            $command = [ScriptBlock]::Create("New-Item -Path $imageLogFolder -ItemType Directory")
                            $null = Invoke-PSFCommand -ComputerName $computer -ScriptBlock $command -Credential $DestinationCredential
                        }

                    }
                    catch {
                        Stop-PSFFunction -Message "Couldn't create image data folder" -Target $imageName -ErrorRecord $_ -Continue
                    }
                }
            }

            # Setup the temporary database name
            $tempDbName = "$($db.Name)-PSDatabaseClone"

            if ($PSCmdlet.ShouldProcess($tempDbName, "Restoring database")) {
                # Restore database to image folder
                try {
                    Write-PSFMessage -Message "Restoring database $db on $DestinationSqlInstance" -Level Verbose

                    $params = @{
                        SqlInstance              = $DestinationSqlInstance
                        SqlCredential            = $DestinationSqlCredential
                        DatabaseName             = $tempDbName
                        Path                     = $lastFullBackup.Path
                        DestinationDataDirectory = $imageDataFolder
                        DestinationLogDirectory  = $imageLogFolder
                        WithReplace              = $true
                        EnableException          = $true
                    }

                    $restore = Restore-DbaDatabase @params
                }
                catch {
                    Stop-PSFFunction -Message "Couldn't restore database $db as $tempDbName on $DestinationSqlInstance.`n$($_)" -Target $restore -ErrorRecord $_ -Continue
                }
            }

            # Detach database
            if ($PSCmdlet.ShouldProcess($tempDbName, "Detaching database")) {
                try {
                    Write-PSFMessage -Message "Detaching database $tempDbName on $DestinationSqlInstance" -Level Verbose
                    $null = Dismount-DbaDatabase -SqlInstance $DestinationSqlInstance -Database $tempDbName -SqlCredential $DestinationSqlCredential -Force
                }
                catch {
                    Stop-PSFFunction -Message "Couldn't detach database $db as $tempDbName on $DestinationSqlInstance" -Target $db -ErrorRecord $_ -Continue
                }
            }

            if ($PSCmdlet.ShouldProcess($vhdPath, "Dismounting the vhd")) {
                # Dismount the vhd
                try {
                    Write-PSFMessage -Message "Dismounting vhd" -Level Verbose

                    # Check if computer is local
                    if ($computer.IsLocalhost) {
                        # Dismount the VHD
                        $null = Dismount-DiskImage -ImagePath $vhdPath

                        # Remove the access path
                        $null = Remove-Item -Path $accessPath -Recurse -Force
                    }
                    else {
                        $command = [ScriptBlock]::Create("Dismount-DiskImage -ImagePath $vhdPath")
                        $null = Invoke-PSFCommand -ComputerName $computer -ScriptBlock $command -Credential $DestinationCredential

                        $command = [ScriptBlock]::Create("Remove-Item -Path $accessPath -Force")
                        $null = Invoke-PSFCommand -ComputerName $computer -ScriptBlock $command -Credential $DestinationCredential
                    }
                }
                catch {
                    Stop-PSFFunction -Message "Couldn't dismount vhd" -Target $imageName -ErrorRecord $_ -Continue
                }
            }

            # Write the data to the database
            $imageLocation = Join-PSFPath $uri.LocalPath -Child "$($imageName).vhdx"
            $sizeMB = $dbSizeMB
            $databaseName = $db.Name
            $databaseTS = $lastFullBackup.Start

            if ($informationStore -eq 'SQL') {
                $query = "
                DECLARE @ImageID INT;
                EXECUTE dbo.Image_New @ImageID = @ImageID OUTPUT, -- int
                                    @ImageName = '$($imageName)', -- varchar(100)
                                    @ImageLocation = '$($imageLocation)', -- varchar(255)
                                    @SizeMB = $($sizeMB), -- int
                                    @DatabaseName = '$($databaseName)', -- varchar(100)
                                    @DatabaseTimestamp = '$($databaseTS)' -- datetime

                SELECT @ImageID as ImageID
            "


                # Add image to database
                if ($PSCmdlet.ShouldProcess($imageName, "Adding image to database")) {
                    try {
                        Write-PSFMessage -Message "Saving image information in database" -Level Verbose

                        $result += Invoke-DbaQuery -SqlInstance $pdcSqlInstance -SqlCredential $pdcCredential -Database $pdcDatabase -Query $query -EnableException
                        $imageID = $result.ImageID
                    }
                    catch {
                        Stop-PSFFunction -Message "Couldn't add image to database" -Target $imageName -ErrorRecord $_
                    }
                }
            }
            elseif ($informationStore -eq 'File') {
                [array]$images = $null

                # Get all the images
                try {
                    $images = Get-DcnImage
                }
                catch {
                    Stop-PSFFunction -Message "Couldn't get images" -Target $imageName -ErrorRecord $_
                    return
                }


                # Setup the new image id
                if ($images.Count -ge 1) {
                    $imageID = ($images[-1].ImageID | Sort-Object ImageID) + 1
                }
                else {
                    $imageID = 1
                }

                # Add the new information to the array
                $images += [PSCustomObject]@{
                    ImageID           = $imageID
                    ImageName         = $imageName
                    ImageLocation     = $imageLocation
                    SizeMB            = $sizeMB
                    DatabaseName      = $databaseName
                    DatabaseTimestamp = $databaseTS
                    CreatedOn         = (Get-Date -format "yyyyMMddHHmmss")
                }

                # Test if the JSON folder can be reached
                if (-not (Test-Path -Path "DCNJSONFolder:\")) {
                    $command = [scriptblock]::Create("Import-Module dbaclone -Force")

                    try {
                        Invoke-PSFCommand -ComputerName $computer -ScriptBlock $command -Credential $Credential
                    }
                    catch {
                        Stop-PSFFunction -Message "Couldn't import module remotely" -Target $command
                        return
                    }
                }

                # Set the image file
                $jsonImageFile = "DCNJSONFolder:\images.json"

                # Convert the data back to JSON
                $images | ConvertTo-Json | Set-Content $jsonImageFile
            }

            # Add the results to the custom object
            [PSCustomObject]@{
                ImageID           = $imageID
                ImageName         = $imageName
                ImageLocation     = $imageLocation
                SizeMB            = $sizeMB
                DatabaseName      = $databaseName
                DatabaseTimestamp = $databaseTS
            }
        } # for each database
    } # end process

    end {
        # Test if there are any errors
        if (Test-PSFFunctionInterrupt) { return }

        Write-PSFMessage -Message "Finished creating database image" -Level Verbose
    }

}


function Remove-DcnImage {
    <#
    .SYNOPSIS
        Remove-DcnImage removes one or more images

    .DESCRIPTION
        The command will remove an image from PSDatabaseClone.
        It will also remove all the clones associated with it on the hosts.

    .PARAMETER ImageID
        Remove images based on the image id

    .PARAMETER ImageName
        Remove images based on the image name

    .PARAMETER ImageLocation
        Location of the image as it's saved in the database or can be seen on the file system.

    .PARAMETER Database
        Remove images based on the database

    .PARAMETER ExcludeDatabase
        Filter the images based on the excluded database

    .PARAMETER Unused
        Remove images not used by any clones

    .PARAMETER Keep
        When used with the Unused parameter, sets the number of most recent images to keep

    .PARAMETER DcnSqlCredential
        Allows you to login to servers using SQL Logins as opposed to Windows Auth/Integrated/Trusted.
        This works similar as SqlCredential but is only meant for authentication to the PSDatabaseClone database server and database.

    .PARAMETER Credential
        Allows you to login to servers using Windows Auth/Integrated/Trusted. To use:

        $scred = Get-Credential, then pass $scred object to the -Credential parameter.

    .PARAMETER Force
        Forcefully remove the items.

    .PARAMETER InputObject
        The input object that is used for pipeline use

    .PARAMETER EnableException
        By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message.
        This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting.
        Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch.

    .PARAMETER WhatIf
        If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run.

    .PARAMETER Confirm
        If this switch is enabled, you will be prompted for confirmation before executing any operations that change state.

    .NOTES
        Author: Sander Stad (@sqlstad, sqlstad.nl)

        Website: https://psdatabaseclone.org
        Copyright: (C) Sander Stad, sander@sqlstad.nl
        License: MIT https://opensource.org/licenses/MIT

    .LINK
        https://psdatabaseclone.org/

    .EXAMPLE
        Remove-DcnImage -ImageLocation "\\server1\images\DB1_20180703193345.vhdx"

        Remove an image

    .EXAMPLE
        Get-DcnImage -Database DB1 | Remove-DcnImage

        Remove all images and clones based on database DB1
    #>

    [CmdLetBinding(DefaultParameterSetName = "ImageLocation", SupportsShouldProcess = $true,
        ConfirmImpact = 'High')]

    param(
        [int[]]$ImageID,
        [string[]]$ImageName,
        [parameter(ParameterSetName = "ImageLocation")]
        [string[]]$ImageLocation,
        [string[]]$Database,
        [string[]]$ExcludeDatabase,
        [switch]$Unused,
        [int]$Keep = 0,
        [PSCredential]$DcnSqlCredential,
        [PSCredential]$Credential,
        [switch]$Force,
        [parameter(ValueFromPipeline = $true, ParameterSetName = "Image")]
        [object[]]$InputObject,
        [switch]$EnableException
    )

    begin {
        # Check if the console is run in Administrator mode
        if ( -not (Test-PSFPowerShell -Elevated) ) {
            Stop-PSFFunction -Message "Module requires elevation. Please run the console in Administrator mode" -Continue
        }

        # Check if the setup has ran
        if (-not (Test-DcnModule -SetupStatus)) {
            Stop-PSFFunction -Message "The module setup has NOT yet successfully run. Please run 'Set-DcnConfiguration'" -Continue
        }

        # Get the information store
        $informationStore = Get-PSFConfigValue -FullName psdatabaseclone.informationstore.mode

        if ($informationStore -eq 'SQL') {
            # Get the module configurations
            $pdcSqlInstance = Get-PSFConfigValue -FullName psdatabaseclone.database.Server
            $pdcDatabase = Get-PSFConfigValue -FullName psdatabaseclone.database.name
            if (-not $DcnSqlCredential) {
                $pdcCredential = Get-PSFConfigValue -FullName psdatabaseclone.informationstore.credential -Fallback $null
            }
            else {
                $pdcCredential = $DcnSqlCredential
            }

            # Test the module database setup
            try {
                Test-DcnConfiguration -SqlCredential $pdcCredential -EnableException
            }
            catch {
                Stop-PSFFunction -Message "Something is wrong in the module configuration" -ErrorRecord $_ -Continue
            }
        }

        # Get all the items
        $items = @()
        $clones = @()
        $items += Get-DcnImage
        $clones += Get-DcnClone

        if ($ImageID) {
            Write-PSFMessage -Message "Filtering image ids" -Level Verbose
            $items = $items | Where-Object { $_.ImageID -in $ImageID }
        }

        if ($ImageName) {
            Write-PSFMessage -Message "Filtering image name" -Level Verbose
            $items = $items | Where-Object { $_.ImageName -in $ImageName }
        }

        if ($ImageLocation) {
            Write-PSFMessage -Message "Filtering image locations" -Level Verbose
            $items = $items | Where-Object { $_.ImageLocation -in $ImageLocation }
        }

        if ($Database) {
            Write-PSFMessage -Message "Filtering databases" -Level Verbose
            $items = $items | Where-Object { $_.DatabaseName -in $Database }
        }

        if ($ExcludeDatabase) {
            Write-PSFMessage -Message "Filtering excluded databases" -Level Verbose
            $items = $items | Where-Object { $_.DatabaseName -notin $Database }
        }

        if ($Unused) {
            Write-PSFMessage -Message "Filtering images with associated clones, keeping latest $Keep images." -Level Verbose
            $images = $items
            foreach ($item in $items) {
                if ($clones.ImageID -contains $item.ImageID) {
                    $images = $images | Where-Object { $_.ImageID -ne $item.ImageID }
                }
            }

            $items = $images | Sort-Object -Property CreatedOn -Descending | Select-Object -Skip $Keep
        }

        # Append the items
        $InputObject += $items
    }

    process {
        # Test if there are any errors
        if (Test-PSFFunctionInterrupt) { return }

        Write-PSFMessage -Message "Started removing database images" -Level Verbose

        # Group the objects to make it easier to go through
        $images = $InputObject | Group-Object ImageID

        foreach ($image in $images) {

            # Loop through each of the results
            foreach ($item in $image.Group) {

                # Make up the data from the network path
                try {
                    [uri]$uri = New-Object System.Uri($item.ImageLocation)
                    $uriHost = $uri.Host
                }
                catch {
                    Stop-PSFFunction -Message "The image location $ImageNetworkPath is not valid" -ErrorRecord $_ -Target $ImageNetworkPath
                    return
                }

                # Setup the computer object
                $computer = [PsfComputer]$uriHost

                if (-not $computer.IsLocalhost) {
                    # Get the result for the remote test
                    $resultPSRemote = Test-DcnRemoting -ComputerName $uriHost -Credential $Credential

                    # Check the result
                    if ($resultPSRemote.Result) {

                        $command = [scriptblock]::Create("Import-Module dbaclone -Force")

                        try {
                            Invoke-PSFCommand -ComputerName $computer -ScriptBlock $command -Credential $Credential
                        }
                        catch {
                            Stop-PSFFunction -Message "Couldn't import module remotely" -Target $command
                            return
                        }
                    }
                    else {
                        Stop-PSFFunction -Message "Couldn't connect to host remotely.`nVerify that the specified computer name is valid, that the computer is accessible over the network, and that a firewall exception for the WinRM service is enabled and allows access from this computer" -Target $resultPSRemote -Continue
                    }
                }

                # Get the clones associated with the image
                $results = @()
                $results = Get-DcnClone -ImageID $item.ImageID

                # Check the results
                if ($results.Count -ge 1) {
                    # Loop through the results
                    foreach ($result in $results) {
                        if ($PSCmdlet.ShouldProcess($item.CloneID, "Removing clone $($result.CloneLocation) from $($result.HostName)")) {
                            # Remove the clones for the host
                            try {
                                Write-PSFMessage -Message "Removing clones for host $($result.HostName) and database $($result.DatabaseName)" -Level Verbose
                                Remove-DcnClone -HostName $result.HostName -Database $result.DatabaseName -DcnSqlCredential $pdcCredential -Credential $Credential -Confirm:$false
                            }
                            catch {
                                Stop-PSFFunction -Message "Couldn't remove clones from host $($result.HostName)" -ErrorRecord $_ -Target $result -Continue
                            }
                        }
                    }
                }
                else {
                    Write-PSFMessage -Message "No clones were found created with image $($image.Name)" -Level Verbose
                }

                if ($PSCmdlet.ShouldProcess($item.ImageLocation, "Removing image from system")) {
                    # Remove the image from the file system
                    try {
                        if ($computer.IsLocalhost) {
                            if (Test-Path -Path $item.ImageLocation -Credential $Credential) {
                                Write-PSFMessage -Message "Removing image '$($item.ImageLocation)' from file system" -Level Verbose

                                $null = Remove-Item -Path $item.ImageLocation -Credential $Credential -Force:$Force
                            }
                            else {
                                Write-PSFMessage -Message "Couldn't find image $($item.ImageLocation)" -Level Verbose
                            }
                        }
                        else {
                            $command = [scriptblock]::Create("Test-Path -Path '$($item.ImageLocation)'")
                            $result = Invoke-PSFCommand -ComputerName $computer -ScriptBlock $command -Credential $Credential

                            if ($result) {
                                $command = [scriptblock]::Create("Remove-Item -Path '$($item.ImageLocation)' -Force")
                                $result = Invoke-PSFCommand -ComputerName $computer -ScriptBlock $command -Credential $Credential
                            }
                            else {
                                Write-PSFMessage -Message "Couldn't find image '$($item.ImageLocation)'" -Level Verbose
                            }
                        }
                    }
                    catch {
                        Stop-PSFFunction -Message "Couldn't remove image '$($item.ImageLocation)' from file system" -ErrorRecord $_ -Target $result
                    }
                }

                if ($PSCmdlet.ShouldProcess($item.ImageLocation, "Removing image from database")) {
                    if ($informationStore -eq 'SQL') {
                        # Remove the image from the database
                        try {
                            $query = "DELETE FROM dbo.Image WHERE ImageID = $($item.ImageID)"

                            $null = Invoke-DbaQuery -SqlInstance $pdcSqlInstance -SqlCredential $pdcCredential -Database $pdcDatabase -Query $query
                        }
                        catch {
                            Stop-PSFFunction -Message "Couldn't remove image '$($item.ImageLocation)' from database" -ErrorRecord $_ -Target $query
                        }
                    }
                    elseif ($informationStore -eq 'File') {
                        $imageData = Get-DcnImage | Where-Object { $_.ImageID -ne $item.ImageID }

                        # Set the image file
                        $jsonImageFile = "DCNJSONFolder:\images.json"

                        # Convert the data back to JSON
                        if ($imageData) {
                            $imageData | ConvertTo-Json | Set-Content $jsonImageFile
                        }
                        else {
                            Clear-Content -Path $jsonImageFile
                        }
                    }
                }
            } # End for each item in group
        } # End for each image
    } # End process

    end {
        # Test if there are any errors
        if (Test-PSFFunctionInterrupt) { return }

        Write-PSFMessage -Message "Finished removing database image(s)" -Level Verbose
    }

}


function Convert-DcnUncPathToLocalPath {
    <#
    .SYNOPSIS
        Convert a UNC path on a computer to a local path.

    .DESCRIPTION
        In some cases you want to convert a UNC path to a local path.
        This function will look up which path belongs to the UNC path by supplying only the UNC path

    .PARAMETER UncPath
        UNC path to convert to local path

    .PARAMETER EnableException
        By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message.
        This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting.
        Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch.

    .PARAMETER WhatIf
        If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run.

    .PARAMETER Confirm
        If this switch is enabled, you will be prompted for confirmation before executing any operations that change state.

    .NOTES
        Author: Sander Stad (@sqlstad, sqlstad.nl)

        Website: https://psdatabaseclone.org
        Copyright: (C) Sander Stad, sander@sqlstad.nl
        License: MIT https://opensource.org/licenses/MIT

    .LINK
        https://psdatabaseclone.org/

    .EXAMPLE
        Convert-DcnUncPathToLocalPath -UncPath "\\server1\share1"

        Convert path "\\server1\share1" to a local path from server1
    #>


    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]$UncPath,
        [switch]$EnableException
    )

    # Create the object
    try {
        $uri = New-Object System.Uri($UncPath)
    }
    catch {
        Stop-PSFFunction -Message "Something went wrong converting the UncPath $UncPath to URI" -Target $UncPath -ErrorRecord $_
    }

    # Check if the path is a valid UNC path
    if (-not $uri.IsUnc) {
        Stop-PSFFunction -Message "The path $UncPath is not a valid UNC path" -Target $UncPath
    }

    # Get the local shares from the computer
    if (Get-Command "Get-SmbShare" -ErrorAction SilentlyContinue) {
        $localShares = Get-SmbShare
    }
    else {
        $localShares = Invoke-PSFCommand -ScriptBlock { Get-CimInstance -Class Win32_Share } | Select-Object Name, Path, Description
    }


    # Split up the unc path
    $uncArray = $uri.AbsolutePath -split '/'

    # Check if the
    if (($uncArray.Length -lt 2) -or (-not $uncArray[1])) {
        Stop-PSFFunction -Message "Could not map un path $UncPath. Make sure it consists of at least two segments i.e \\server\directory or \\server\c$)" -Target $uri
    }

    # Get the share
    $share = $localShares | Where-Object { $_.Name -eq $uncArray[1] }

    # Check if something returned
    if (!$share) {
        Stop-PSFFunction -Message "The unc path could not be mapped to a share" -Target $localShares
    }

    # Rebuild the array so we have a the same construction with folders
    $uncArray[1] = $share.Path
    $uncArray = $uncArray[1..($uncArray.Length - 1)]

    return ($uncArray -join '\') -replace '\\\\', '\'
}


function Set-DcnConfiguration {
    <#
    .SYNOPSIS
        Set-DcnConfiguration sets up the module

    .DESCRIPTION
        For the module to work properly the module needs a couple of settings.
        The most important settings are the ones for the database to store the information
        about the images and the clones.

        The configurations will be saved in the registry of Windows for all users.

        If the database does not yet exist on the server it will try to create the database.
        After that the objects for the database will be created.

    .PARAMETER InformationStore
        Where is the information going to be stored.
        This can be either a SQL Server database or files formatted as JSON.

        SQL Server has the advantage of being more reliable and have all the information available
        JSON files have a small footprint, are fast and don't require a database server

        The best way to save the JSON files is in a network share to make is possible for other
        clients to get the information.

        The default is to save the data in JSON files.

    .PARAMETER SqlInstance
        SQL Server name or SMO object representing the SQL Server to connect to

    .PARAMETER SqlCredential
        Allows you to login to servers using SQL Logins as opposed to Windows Auth/Integrated/Trusted. To use:

        $scred = Get-Credential, then pass $scred object to the -SqlCredential parameter.

        Windows Authentication will be used if SqlCredential is not specified. SQL Server does not accept Windows credentials being passed as credentials.
        To connect as a different Windows user, run PowerShell as that user.

    .PARAMETER Credential
        Allows you to login to servers or use authentication to access files and folder/shares

        $scred = Get-Credential, then pass $scred object to the -Credential parameter.

    .PARAMETER Database
        Database to use to save all the information in

    .PARAMETER Path
        Path where the JSON files will be created

    .PARAMETER InputPrompt
        Use this parameter to get a question to put in values using user input

    .PARAMETER EnableException
        By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message.
        This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting.
        Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch.

    .PARAMETER WhatIf
        If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run.

    .PARAMETER Confirm
        If this switch is enabled, you will be prompted for confirmation before executing any operations that change state.

    .PARAMETER Force
        Forcefully create items when needed

    .NOTES
        Author: Sander Stad (@sqlstad, sqlstad.nl)

        Website: https://dbaclone.org
        Copyright: (C) Sander Stad, sander@sqlstad.nl
        License: MIT https://opensource.org/licenses/MIT

    .LINK
        https://dbaclone.org/

    .EXAMPLE
        Set-DcnConfiguration -SqlInstance SQLDB1 -Database dbaclone

        Set up the module to use SQLDB1 as the database servers and dbaclone to save the values in

    .EXAMPLE
        Set-DcnConfiguration

        The user will be prompted to enter the information to configure the module
    #>

    [CmdLetBinding(SupportsShouldProcess = $true, DefaultParameterSetName = "Prompt")]

    param(
        [ValidateSet('SQL', 'File')]
        [string]$InformationStore,
        [parameter(ParameterSetName = "SQL", Mandatory = $true)]
        [DbaInstanceParameter]$SqlInstance,
        [parameter(ParameterSetName = "SQL")]
        [PSCredential]$SqlCredential,
        [parameter(ParameterSetName = "SQL")]
        [string]$Database,
        [parameter(ParameterSetName = "File", Mandatory = $true)]
        [string]$Path,
        [parameter(ParameterSetName = "File")]
        [PSCredential]$Credential,
        [switch]$EnableException,
        [parameter(ParameterSetName = "Prompt")]
        [switch]$InputPrompt,
        [switch]$Force
    )

    begin {
        if ( -not (Test-PSFPowerShell -Elevated) ) {
            Stop-PSFFunction -Message "Please run the module in elevated/administrator mode" -Continue
        }

        Write-PSFMessage -Message "Started dbaclone Setup" -Level Verbose

        # Check if the user needs to be asked for user input
        if ($InputPrompt -or (-not $InformationStore) -and (-not $SqlInstance -and -not $SqlCredential -and -not $Credential -and -not $Database)) {
            # Setup the choices for the user
            $choiceDatabase = New-Object System.Management.Automation.Host.ChoiceDescription '&Database', 'Save the information in a database'
            $choiceDatabase.HelpMessage = "Choose to have the information saved in a database. This is reliable and is the default choice"
            $choiceJSON = New-Object System.Management.Automation.Host.ChoiceDescription '&JSON', 'Save the information in JSON files'
            $choiceJSON.HelpMessage = "If you don't want to rely on a database you can choose JSON to save your information. "

            # Create the options
            $options = [System.Management.Automation.Host.ChoiceDescription[]]($choiceDatabase, $choiceJSON)

            # Set extra information for the prompt
            $title = 'Choose your system'
            $message = 'Where do you want your data to be saved?'

            # Present the user with the choices
            $resultSystem = $host.ui.PromptForChoice($title, $message, $options, 0)

            if ($resultSystem -eq 0) {
                # Database
                $SqlInstance = Read-Host ' - Please enter the SQL Server instance'
                $Database = Read-Host ' - Database Name [dbaclone]'
                $databaseUser = Read-Host ' - Database Login'
                $databasePass = Read-Host ' - Password' -AsSecureString

                # If the credentials are entered create the credential object
                if (($DatabaseUser -ne '') -and (($databasePass -ne '') -or ($null -ne $databasePass))) {
                    $SqlCredential = New-Object PSCredential ($databaseUser, $databasePass)
                }

                # Set the flag for the new database
                [bool]$newDatabase = $false

                # Set the variable for the information store
                $InformationStore = 'SQL'
            }
            elseif ($resultSystem -eq 1) {
                # Make sure other variables are not set
                $SqlInstance = $null
                $Database = $null
                $SqlCredential = $null

                # Database
                $filePath = Read-Host ' - Please enter the path to save the files to'
                $fileUser = Read-Host ' - Login (Optional)'
                $filePass = Read-Host ' - Password (Optional)' -AsSecureString

                # If the credentials are entered create the credential object
                if (($fileUser -ne '') -and (($filePass -ne '') -or ($null -ne $filePass))) {
                    $Credential = New-Object PSCredential ($fileUser, $filePass)
                }

                # Clean up the file path
                if ($filePath.EndsWith("\")) {
                    $filePath = $filePath.Substring(0, $filePath.Length - 1)
                }

                # Check the file path
                if ($filePath.StartsWith("\\")) {
                    # Make up the data from the network path
                    try {
                        # Convert to uri
                        [uri]$uri = New-Object System.Uri($filePath)
                        $uriHost = $uri.Host

                        # Setup the computer object
                        $computer = [PsfComputer]$uriHost

                        # Check if the path is reachable
                        $command = [scriptblock]::Create("Test-Path -Path $filePath")

                        $resultTestFilePath = Invoke-PSFCommand -ComputerName $computer -ScriptBlock $command -Credential $Credential
                    }
                    catch {
                        Stop-PSFFunction -Message "The file path $filePath is not valid" -ErrorRecord $_ -Target $filePath
                        return
                    }
                }
                else {
                    $resultTestFilePath = Test-Path -Path $filePath
                }

                # Check the result
                if (-not $resultTestFilePath) {
                    Stop-PSFFunction -Message "Could not access the path $filePath" -Target $filePath
                    return
                }

                # Set the variable for the information store
                $InformationStore = 'File'
            }

        }

        # Unregister any configurations
        try {
            Unregister-PSFConfig -Scope SystemDefault -Module dbaclone
        }
        catch {
            Stop-PSFFunction -Message "Something went wrong unregistering the configurations" -ErrorRecord $_ -Target $SqlInstance
            return
        }

        if ($Path -and -not (Test-Path -Path $Path)) {
            try {
                $null = New-Item -Path $Path -ItemType Directory -Confirm:$false -Force
            }
            catch {
                Stop-PSFFunction -Message "Could not create working directory" -ErrorRecord $_ -Target $SqlInstance
            }
        }
    }

    process {

        # Test if there are any errors
        if (Test-PSFFunctionInterrupt) { return }


        if ($InformationStore -eq 'SQL') {
            # Setup the database name
            if (-not $Database -or $Database -eq '') {
                $Database = "dbaclone"
            }

            # Try connecting to the instance
            if ($SqlInstance) {
                Write-PSFMessage -Message "Attempting to connect to Sql Server $SqlInstance.." -Level Verbose
                try {
                    $server = Connect-DbaInstance -SqlInstance $SqlInstance -SqlCredential $SqlCredential
                }
                catch {
                    Stop-PSFFunction -Message "Could not connect to Sql Server instance $SqlInstance" -ErrorRecord $_ -Target $SqlInstance
                    return
                }
            }

            # Check if the database is already present
            if (($server.Databases.Name -contains $Database) -or ($server.Databases[$Database].Tables.Count -ge 1)) {
                if ($Force) {
                    try {
                        Remove-DbaDatabase -SqlInstance $SqlInstance -SqlCredential $SqlCredential -Database $Database
                    }
                    catch {
                        Stop-PSFFunction -Message "Couldn't remove database $Database on $SqlInstance" -ErrorRecord $_ -Target $SqlInstance
                        return
                    }
                }
                else {
                    Write-PSFMessage -Message "Database $Database already exists" -Level Verbose
                }
            }

            # Check if the database exists
            if ($server.Databases.Name -notcontains $Database) {

                # Set the flag
                $newDatabase = $true

                try {
                    # Setup the query to create the database
                    $query = "CREATE DATABASE [$Database]"

                    Write-PSFMessage -Message "Creating database $Database on $SqlInstance" -Level Verbose

                    # Executing the query
                    Invoke-DbaQuery -SqlInstance $SqlInstance -SqlCredential $SqlCredential -Database master -Query $query
                }
                catch {
                    Stop-PSFFunction -Message "Couldn't create database $Database on $SqlInstance" -ErrorRecord $_ -Target $SqlInstance
                }
            }
            else {
                # Check if there are any user objects already in the database
                $newDatabase = ($server.Databases[$Database].Tables.Count -eq 0)
            }

            # Setup the path to the sql file
            if ($newDatabase) {
                try {
                    $path = "$($MyInvocation.MyCommand.Module.ModuleBase)\internal\resources\database\database.sql"
                    $query = [System.IO.File]::ReadAllText($path)

                    # Create the objects
                    try {
                        Write-PSFMessage -Message "Creating database objects" -Level Verbose

                        # Executing the query
                        Invoke-DbaQuery -SqlInstance $SqlInstance -SqlCredential $SqlCredential -Database $Database -Query $query
                    }
                    catch {
                        Stop-PSFFunction -Message "Couldn't create database objects" -ErrorRecord $_ -Target $SqlInstance
                    }
                }
                catch {
                    Stop-PSFFunction -Message "Couldn't find database script. Make sure you have a valid installation of the module" -ErrorRecord $_ -Target $SqlInstance
                }
            }
            else {
                Write-PSFMessage -Message "Database already contains objects" -Level Verbose
            }

            # Set the database server and database values
            Set-PSFConfig -Module dbaclone -Name database.server -Value $SqlInstance -Validation string
            Set-PSFConfig -Module dbaclone -Name database.name -Value $Database -Validation string
        }
        else {
            # Create the JSON files

            if ($null -eq $filePath) {
                $filePath = $Path
            }

            # Create the PSDrive to be able to use credentials
            try {
                $null = New-PSDrive -Name dbaclone -PSProvider FileSystem -Root $filePath -Credential $Credential
            }
            catch {
                Stop-PSFFunction -Message "Couldn not create PS-Drive to JSON files" -Target $filePath
            }

            # Create the files
            try {
                # Get the files
                $files = Get-ChildItem -Path dbaclone:\

                # Check if we have any files
                if ($files.Count -eq 0) {
                    $null = New-Item -Path dbaclone:\hosts.json -Force:$Force
                    $null = New-Item -Path dbaclone:\images.json -Force:$Force
                    $null = New-Item -Path dbaclone:\clones.json -Force:$Force
                }
                else {
                    if (-not $Force -and ("$filePath\hosts.json" -in $files.FullName)) {
                        Stop-PSFFunction -Message "File 'hosts.json' already exists" -Target $filePath
                    }
                    else {
                        $null = New-Item -Path dbaclone:\hosts.json -Force:$Force
                    }

                    if (-not $Force -and ("$filePath\images.json" -in $files.FullName)) {
                        Stop-PSFFunction -Message "File 'images.json' already exists" -Target $filePath
                    }
                    else {
                        $null = New-Item -Path dbaclone:\images.json -Force:$Force
                    }

                    if (-not $Force -and ("$filePath\clones.json" -in $files.FullName)) {
                        Stop-PSFFunction -Message "File 'clones.json' already exists" -Target $filePath
                    }
                    else {
                        $null = New-Item -Path dbaclone:\clones.json -Force:$Force
                    }
                }

                # Set the path in case it's set for file store mode
                Set-PSFConfig -Module dbaclone -Name informationstore.path -Value "$filePath" -Validation string
            }
            catch {
                Stop-PSFFunction -Message "Could not create the JSON files in path $filePath" -Target $filePath -ErrorRecord $_
                return
            }
        }

        # Set the credential for the database if needed
        if ($SqlCredential) {
            Set-PSFConfig -Module dbaclone -Name informationstore.credential -Value $SqlCredential
        }

        # Set the credential for files and folders if needed
        if ($Credential) {
            Set-PSFConfig -Module dbaclone -Name informationstore.credential -Value $Credential
        }

        # Set the information store mode
        Set-PSFConfig -Module dbaclone -Name informationstore.mode -Value $InformationStore

        # Register the configurations in the system for all users
        Get-PSFConfig -FullName dbaclone.informationstore.mode | Register-PSFConfig -Scope SystemDefault
        Get-PSFConfig -FullName dbaclone.informationstore.credential | Register-PSFConfig -Scope SystemDefault
        Get-PSFConfig -FullName dbaclone.informationstore.path | Register-PSFConfig -Scope SystemDefault

        Get-PSFConfig -FullName dbaclone.database.server | Register-PSFConfig -Scope SystemDefault
        Get-PSFConfig -FullName dbaclone.database.name | Register-PSFConfig -Scope SystemDefault

        # Set the path to the diskpart script file
        Set-PSFConfig -Module dbaclone -Name diskpart.scriptfile -Value "$env:APPDATA\dbaclone\diskpartcommand.txt" -Validation string

        # Check if the path exists and create it if neccesary
        if (Test-Path -Path "$env:APPDATA\dbaclone") {
            if (-not $Force) {
                Write-PSFMessage -Message "PSDatabaClone working directory already exists" -Level Verbose
            }
            else {
                try {
                    $null = New-Item -Path "$env:APPDATA\dbaclone" -ItemType Directory -Force:$Force
                }
                catch {
                    Stop-PSFFunction -Message "Something went wrong creating the working directory" -ErrorRecord $_ -Continue
                }
            }
        }

        # Set the configuration
        Get-PSFConfig -FullName dbaclone.diskpart.scriptfile | Register-PSFConfig -Scope SystemDefault

        # Check if all the settings have been made
        if ($InformationStore -eq 'SQL') {
            $dbServer = Get-PSFConfigValue -FullName dbaclone.database.server
            $dbName = Get-PSFConfigValue -FullName dbaclone.database.name

            if (($false -ne $dbServer) -and ($false -ne $dbName)) {
                Write-PSFMessage -Message "All mandatory configurations have been made" -Level Verbose
                Set-PSFConfig -Module dbaclone -Name setup.status -Value $true -Validation bool
            }
            else {
                Write-PSFMessage -Message "The mandatory configurations have NOT been made. Please check your settings." -Level Warning
                Set-PSFConfig -Module dbaclone -Name setup.status -Value $false -Validation bool
            }
        }
        else {
            $path = Get-PSFConfigValue -FullName dbaclone.informationstore.path

            if (($null -ne $path) -or ('' -ne $path)) {
                Write-PSFMessage -Message "All mandatory configurations have been made" -Level Verbose
                Set-PSFConfig -Module dbaclone -Name setup.status -Value $true -Validation bool
            }
            else {
                Write-PSFMessage -Message "The mandatory configurations have NOT been made. Please check your settings." -Level Warning
                Set-PSFConfig -Module dbaclone -Name setup.status -Value $false -Validation bool
            }
        }

        # Set the overall status in the configurations
        Get-PSFConfig -FullName dbaclone.setup.status | Register-PSFConfig -Scope SystemDefault
    }

    end {
        # Test if there are any errors
        if (Test-PSFFunctionInterrupt) { return }

        Write-PSFMessage -Message "Finished setting up dbaclone" -Level Verbose
    }
}


function Set-DcnPermission {
    <#
    .SYNOPSIS
        New-DcnClone creates a new clone

    .DESCRIPTION
        New-DcnClone willcreate a new clone based on an image.
        The clone will be created in a certain directory, mounted and attached to a database server.

    .PARAMETER Path
        Path to set the permissions for

    .PARAMETER EnableException
        By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message.
        This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting.
        Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch.

    .PARAMETER WhatIf
        If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run.

    .PARAMETER Confirm
        If this switch is enabled, you will be prompted for confirmation before executing any operations that change state.

    .NOTES
        Author: Sander Stad (@sqlstad, sqlstad.nl)

        Website: https://psdatabaseclone.org
        Copyright: (C) Sander Stad, sander@sqlstad.nl
        License: MIT https://opensource.org/licenses/MIT

    .LINK
        https://psdatabaseclone.org/

    .EXAMPLE
        Set-DcnPermission -Path "C:\projects\dbaclone\clone\AW2017-C1\"

        Set the permissions for the path
    #>


    [CmdLetBinding()]

    param(
        [string]$Path,
        [switch]$EnableException
    )

    begin {
        if (-not $Path) {
            Stop-PSFFunction -Message "Please enter a path"
        }
        else {
            if (-not (Test-Path -Path $Path)) {
                Stop-PSFFunction -Message "Could not enter path. Please check if the path is valid and reachable."
            }
        }
    }

    process {
        if (Test-PSFFunctionInterrupt) { return }

        $everyone = [System.Security.Principal.WellKnownSidType]::WorldSid
        $sid = New-Object System.Security.Principal.SecurityIdentifier($everyone, $Null)

        $group = New-Object System.Security.Principal.NTAccount("Everyone")

        $accessRule = New-Object System.Security.AccessControl.FilesystemAccessrule($sid, "FullControl", "Allow")
        $accessRule = New-Object System.Security.AccessControl.FilesystemAccessrule("Everyone", "FullControl", "Allow")

        foreach ($file in $(Get-ChildItem -Path "$Path" -Recurse)) {
            Write-PSFMessage -Level Verbose -Message "Setting permissions for '$($file.FullName)'"
            $acl = Get-Acl $file.Fullname

            # Add this access rule to the ACL
            $acl.SetAccessRule($accessRule)
            $acl.SetOwner($group)

            try {
                # Write the changes to the object
                Set-Acl -Path $file.Fullname -AclObject $acl
            }
            catch {
                Stop-PSFFunction -Message "Could not set permissions for '$($file.FullName)'" -Target $file -ErrorRecord $_ -Continue
            }

        }
    }
}

function Test-DcnModule {

    <#
    .SYNOPSIS
        Tests for conditions in the PSDatabaseClone module.

    .DESCRIPTION
        This helper command can evaluate various runtime conditions, such as:
        - Configuration
        - Windows version

    .PARAMETER IsLocal
        Returns if the computer given is local

    .PARAMETER SetupStatus
        Setup status should be set.

    .PARAMETER WindowsVersion
        The windows version should be in the supported version list

        Windows version should be in
            - 'Microsoft Windows 10 Pro',
            - 'Microsoft Windows 10 Enterprise',
            - 'Microsoft Windows 10 Education',
            - 'Microsoft Windows Server 2008 R2 Standard',
            - 'Microsoft Windows Server 2008 R2 Enterprise',
            - 'Microsoft Windows Server 2008 R2 Datacenter'
            - 'Microsoft Windows Server 2012 R2 Standard',
            - 'Microsoft Windows Server 2012 R2 Enterprise',
            - 'Microsoft Windows Server 2012 R2 Datacenter',
            - 'Microsoft Windows Server 2016 Standard',
            - 'Microsoft Windows Server 2016 Enterprise',
            - 'Microsoft Windows Server 2016 Datacenter'
            - 'Microsoft Windows Server 2019 Datacenter'

    .PARAMETER WhatIf
        If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run.

    .PARAMETER Confirm
        If this switch is enabled, you will be prompted for confirmation before executing any operations that change state.

    .NOTES
        Author: Sander Stad (@sqlstad, sqlstad.nl)

        Website: https://psdatabaseclone.org
        Copyright: (C) Sander Stad, sander@sqlstad.nl
        License: MIT https://opensource.org/licenses/MIT

    .LINK
        https://psdatabaseclone.org/

    .EXAMPLE
        Test-DcnModule -SetupStatus

        Return true if the status if correct, if not returns false

    .EXAMPLE
        Test-DcnModule -WindowsVersion

        Return true if the windows version is supported, if not returns false
    #>


    param(
        [PSFComputer]$IsLocal,
        [switch]$SetupStatus,
        [switch]$WindowsVersion
    )

    begin {

    }

    process {
        #region Is Local
        if ($IsLocal) {
            $computer = [PsfComputer]$IsLocal

            return $computer.IsLocalhost
        }
        # endregion Is Local

        # region Setup status
        if ($SetupStatus) {
            if (-not (Get-PSFConfigValue -FullName psdatabaseclone.setup.status)) {
                return $false
            }
            else {
                return $true
            }
        }
        # endregion Setup Status

        #region Windows Version
        if ($WindowsVersion) {
            $supportedVersions = @(
                'Microsoft Windows 10 Pro',
                'Microsoft Windows 10 Enterprise',
                'Microsoft Windows 10 Education',
                'Microsoft Windows Server 2008 R2 Standard',
                'Microsoft Windows Server 2008 R2 Enterprise',
                'Microsoft Windows Server 2008 R2 Datacenter'
                'Microsoft Windows Server 2012 R2 Standard',
                'Microsoft Windows Server 2012 R2 Enterprise',
                'Microsoft Windows Server 2012 R2 Datacenter',
                'Microsoft Windows Server 2016 Standard',
                'Microsoft Windows Server 2016 Enterprise',
                'Microsoft Windows Server 2016 Datacenter'
                'Microsoft Windows Server 2019 Datacenter'
            )

            # Get the OS details
            $osDetails = Get-CimInstance Win32_OperatingSystem | Select-Object Caption, Description, Name, OSType, Version

            $windowsEdition = ($osDetails.Caption).Replace(" Evaluation", "").Trim()

            # Check which version of windows we're dealing with
            if ($windowsEdition -notin $supportedVersions ) {
                if ($windowsEdition -like '*Windows 7*') {
                    return $false
                    #Stop-PSFFunction -Message "Module does not work on Windows 7" -Target $OSDetails -FunctionName 'Pre Import'
                }
                else {
                    #Stop-PSFFunction -Message "Unsupported version of Windows." -Target $OSDetails -FunctionName 'Pre Import'
                    return $false
                }
            }
        }
        # endregion Windows version

        return $true
    }

    end {

    }

}

function Test-DcnRemoting {

    <#
    .SYNOPSIS
        Test-DcnRemoting tests if remoting is enabled and configured

    .DESCRIPTION
        The function will test if the WSMan service is running.
        It will also test if it's able to retrieve a value from the remote host.

        The function will return an object with the results.

        The following information will be returned:
        - HostName : Which is the host entered at execution
        - IsLocalHost : If the host is a local host
        - Reachable : Was the host reachable by a ping request
        - WSManServiceRunning : Is the WSMan service running = $resultWSManService
        - CommandExecuted : Was it possible to execute a command remotely
        - Result : The overall result of the test

    .PARAMETER ComputerName
        Host to connect to

    .PARAMETER Credential
        Allows you to login to servers using Windows Auth/Integrated/Trusted. To use:

        $scred = Get-Credential, then pass $scred object to the -Credential parameter.

    .PARAMETER EnableException
        By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message.
        This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting.
        Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch.

    .PARAMETER WhatIf
        If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run.

    .PARAMETER Confirm
        If this switch is enabled, you will be prompted for confirmation before executing any operations that change state.

    .NOTES
        Author: Sander Stad (@sqlstad, sqlstad.nl)

        Website: https://psdatabaseclone.org
        Copyright: (C) Sander Stad, sander@sqlstad.nl
        License: MIT https://opensource.org/licenses/MIT

    .LINK
        https://psdatabaseclone.org/

    .EXAMPLE
        Test-DcnRemoting -HostName SQLDB1

        Test the PS remoting for one host

    .EXAMPLE
        Test-DcnRemoting -HostName SQLDB1, SQLDB2

        Test the PS remoting for multiple hosts

    .EXAMPLE

    #>


    [CmdLetBinding()]

    param(
        [Parameter(Mandatory = $true)]
        [string[]]$ComputerName,
        [PSCredential]$Credential,
        [switch]$EnableException
    )

    begin {

        if (-not $ComputerName) {
            Stop-PSFFunction -Message "Please enter one or more hostnames to test" -Target $ComputerName -Continue
        }

    }

    process {
        # Test if there are any errors
        if (Test-PSFFunctionInterrupt) { return }

        foreach ($comp in $ComputerName) {

            # Get the computer object
            $computer = [PSFComputer]$comp

            # Initialize the variables
            $connectionResult = $false
            $resultWSManService = $false
            $resultCommand = $false

            # Test if the computer is reachable
            [bool]$connectionResult = Test-Connection -ComputerName $comp -BufferSize 16 -Count 1 -ErrorAction 0 -Quiet

            if ($connectionResult) {
                # Test if the WSMan service is running
                try {
                    if ($Credential) {
                        $resultWSManService = [bool](Test-WSMan -ComputerName $comp -Credential $Credential -Authentication Default -ErrorAction SilentlyContinue)
                    }
                    else {
                        $resultWSManService = [bool](Test-WSMan -ComputerName $comp -ErrorAction SilentlyContinue)
                    }
                }
                catch {
                    Stop-PSFFunction -Message "Couldn't test WSMAN.`nVerify that the specified computer name is valid, that the computer is accessible over the network, and that a firewall exception for the WinRM service is enabled and allows access from this computer" -Target $comp -ErrorRecord $_ -Continue
                }

                if ($resultWSManService) {
                    # Reset the result
                    [string]$result = $null

                    # Setup the command
                    $command = [scriptblock]::Create('$env:COMPUTERNAME')

                    # Get the result
                    $result = Invoke-PSFCommand -ComputerName $comp -ScriptBlock $command -Credential $Credential -ErrorAction SilentlyContinue

                    if ($result) {
                        $resultCommand = $true
                    }
                }
            }

            # Return the results
            [PSCustomObject]@{
                ComputerName        = $computer.ComputerName
                IsLocalHost         = $computer.IsLocalhost
                Reachable           = $connectionResult
                WSManServiceRunning = $resultWSManService
                CommandExecuted     = $resultCommand
                Result              = (($connectionResult) -and ($resultWSManService) -and ($resultCommand))
            }
        }
    }
}

<#
This is an example configuration file

By default, it is enough to have a single one of them,
however if you have enough configuration settings to justify having multiple copies of it,
feel totally free to split them into multiple files.
#>


<#
# Example Configuration
Set-PSFConfig -Module 'dbaclone' -Name 'Example.Setting' -Value 10 -Initialize -Validation 'integer' -Handler { } -Description "Example configuration setting. Your module can then use the setting using 'Get-PSFConfigValue'"
#>


Set-PSFConfig -Module 'dbaclone' -Name 'Import.DoDotSource' -Value $false -Initialize -Validation 'bool' -Description "Whether the module files should be dotsourced on import. By default, the files of this module are read as string value and invoked, which is faster but worse on debugging."
Set-PSFConfig -Module 'dbaclone' -Name 'Import.IndividualFiles' -Value $false -Initialize -Validation 'bool' -Description "Whether the module files should be imported individually. During the module build, all module code is compiled into few files, which are imported instead by default. Loading the compiled versions is faster, using the individual files is easier for debugging and testing out adjustments."


[array]$availableClones = Get-DcnClone

$cloneIds = [scriptblock]::Create($availableClones.CloneID -join ",")
$cloneDbNames = [scriptblock]::Create("'$($availableClones.DatabaseName -join "','")'")
$cloneHostnames = [scriptblock]::Create("'$($availableClones.HostName -join "','")'")
$cloneImageIds = [scriptblock]::Create($availableClones.ImageID -join ",")
$cloneImageNames = [scriptblock]::Create("'$($availableClones.ImageName -join "','")'")

Register-PSFTeppScriptblock -Name "dbaclone.clones.id" -ScriptBlock $cloneIds
Register-PSFTeppScriptblock -Name "dbaclone.clones.databasename" -ScriptBlock $cloneDbNames
Register-PSFTeppScriptblock -Name "dbaclone.clones.hostname" -ScriptBlock $cloneHostnames
Register-PSFTeppScriptblock -Name "dbaclone.clones.imageid" -ScriptBlock $cloneImageIds
Register-PSFTeppScriptblock -Name "dbaclone.clones.imagename" -ScriptBlock $cloneImageNames



[array]$availableImages = Get-DcnImage

$imageIds = [scriptblock]::Create($availableImages.ImageID -join ",")
$imageNames = [scriptblock]::Create("'$($availableImages.ImageName -join "','")'")
$imageDbNames = [scriptblock]::Create("'$($availableImages.DatabaseName -join "','")'")
$imageLocations = [scriptblock]::Create("'$($availableImages.ImageLocation -join "','")'")

Register-PSFTeppScriptblock -Name "dbaclone.images.id" -ScriptBlock $imageIds
Register-PSFTeppScriptblock -Name "dbaclone.images.name" -ScriptBlock $imageNames
Register-PSFTeppScriptblock -Name "dbaclone.images.database" -ScriptBlock $imageDbNames
Register-PSFTeppScriptblock -Name "dbaclone.images.location" -ScriptBlock $imageLocations


<#
# Example:
Register-PSFTeppArgumentCompleter -Command Get-Alcohol -Parameter Type -Name dbaclone.alcohol
#>


# Image commands
Register-PSFTeppArgumentCompleter -Command Get-DcnImage -Parameter ImageID -Name 'dbaclone.images.id'
Register-PSFTeppArgumentCompleter -Command Get-DcnImage -Parameter ImageName -Name 'dbaclone.images.name'

Register-PSFTeppArgumentCompleter -Command Remove-DcnImage -Parameter Database -Name 'dbaclone.images.database'
Register-PSFTeppArgumentCompleter -Command Remove-DcnImage -Parameter ImageID -Name 'dbaclone.images.id'
Register-PSFTeppArgumentCompleter -Command Remove-DcnImage -Parameter ImageName -Name 'dbaclone.images.name'
Register-PSFTeppArgumentCompleter -Command Remove-DcnImage -Parameter ImageLocation -Name 'dbaclone.images.location'

# Clone commands
Register-PSFTeppArgumentCompleter -Command Get-DcnClone -Parameter Database -Name 'dbaclone.clones.databasename'
Register-PSFTeppArgumentCompleter -Command Get-DcnClone -Parameter HostName -Name 'dbaclone.clones.hostname'
Register-PSFTeppArgumentCompleter -Command Get-DcnClone -Parameter ImageID -Name 'dbaclone.clones.imageid'
Register-PSFTeppArgumentCompleter -Command Get-DcnClone -Parameter ImageName -Name 'dbaclone.clones.imagename'

Register-PSFTeppArgumentCompleter -Command Invoke-DcnRepairClone -Parameter HostName -Name 'dbaclone.clones.hostname'

Register-PSFTeppArgumentCompleter -Command New-DcnClone -Parameter Database -Name 'dbaclone.images.database'

Register-PSFTeppArgumentCompleter -Command Remove-DcnClone -Parameter Database -Name 'dbaclone.clones.databasename'
Register-PSFTeppArgumentCompleter -Command Remove-DcnClone -Parameter HostName -Name 'dbaclone.clones.hostname'





New-PSFLicense -Product 'dbaclone' -Manufacturer 'sstad' -ProductVersion $script:ModuleVersion -ProductType Module -Name MIT -Version "1.0.0.0" -Date (Get-Date "2020-02-11") -Text @"
Copyright (c) 2020 sstad

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
"@

#endregion Load compiled code