DSCResources/MSFT_xVhdFileDirectory/MSFT_xVhdFileDirectory.psm1


$script:dscResourceCommonModulePath = Join-Path -Path $PSScriptRoot -ChildPath '../../Modules/DscResource.Common'

Import-Module -Name $script:dscResourceCommonModulePath

$script:localizedData = Get-LocalizedData -DefaultUICulture 'en-US'

<#
# Get the current configuration of the machine
# This function is called when you do Get-DscConfiguration after the configuration is set.
#>

function Get-TargetResource
{
    [CmdletBinding()]
    [OutputType([System.Collections.Hashtable])]
    param
    (
        [Parameter(Mandatory = $true)]
        [System.String]
        $VhdPath,

        [Parameter(Mandatory = $true)]
        [Microsoft.Management.Infrastructure.CimInstance[]]
        $FileDirectory,

        [Parameter()]
        [ValidateSet('ModifiedDate', 'SHA-1', 'SHA-256', 'SHA-512')]
        [System.String]
        $CheckSum = 'ModifiedDate'
    )

    if ( -not (Test-path $VhdPath))
    {
        $item = New-CimInstance -ClassName MSFT_FileDirectoryConfiguration -Property @{
            DestinationPath = $VhdPath
            Ensure          = 'Absent'
        } -Namespace 'root/microsoft/windows/desiredstateconfiguration' -ClientOnly

        return @{
            VhdPath       = $VhdPath
            FileDirectory = $item
        }
    }

    # Mount VHD.
    $mountVHD = EnsureVHDState -Mounted -vhdPath $vhdPath

    $itemsFound = foreach ($Item in $FileDirectory)
    {
        $item = GetItemToCopy -item $item

        $mountedDrive = $mountVHD |
            Get-Disk |
                Get-Partition |
                    Where-Object -FilterScript {
                        $_.Type -ne 'Recovery'
                    } |
                        Get-Volume

        $letterDrive = (-join $mountedDrive.DriveLetter) + ':\'

        # show the drive letters.
        Get-PSDrive | Write-Verbose

        $finalPath = Join-Path $letterDrive $item.DestinationPath

        Write-Verbose "Getting the current value at $finalPath ..."

        if (Test-Path $finalPath)
        {
            New-CimInstance -ClassName MSFT_FileDirectoryConfiguration -Property @{
                DestinationPath = $finalPath
                Ensure = 'Present'
            } -Namespace 'root/microsoft/windows/desiredstateconfiguration' -ClientOnly
        }
        else
        {
            New-CimInstance -ClassName MSFT_FileDirectoryConfiguration -Property @{
                DestinationPath = $finalPath
                Ensure = 'Absent'
            } -Namespace 'root/microsoft/windows/desiredstateconfiguration' -ClientOnly
        }
    }

    # Dismount VHD.
    EnsureVHDState -Dismounted -vhdPath $VhdPath

    # Return the result.
    return @{
        VhdPath       = $VhdPath
        FileDirectory = $itemsFound
    }
}


# This is a resource method that gets called if the Test-TargetResource returns false.
function Set-TargetResource
{
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory = $true)]
        [System.String]
        $VhdPath,

        [Parameter(Mandatory = $true)]
        [Microsoft.Management.Infrastructure.CimInstance[]]
        $FileDirectory,

        [Parameter()]
        [ValidateSet('ModifiedDate', 'SHA-1', 'SHA-256', 'SHA-512')]
        [System.String]
        $CheckSum = 'ModifiedDate'
    )

    if (-not (Test-Path $VhdPath))
    {
        throw "Specified destination path $VhdPath does not exist!"
    }

    # mount the VHD.
    $mountedVHD = EnsureVHDState -Mounted -vhdPath $VhdPath

    try
    {
        # show the drive letters.
        Get-PSDrive | Write-Verbose

        $mountedDrive = $mountedVHD | Get-Disk | Get-Partition | Where-Object -FilterScript { $_.Type -ne 'Recovery' } | Get-Volume

        foreach ($item in $FileDirectory)
        {
            $itemToCopy = GetItemToCopy -item $item
            $letterDrive = (-join $mountedDrive.DriveLetter) + ':\'
            $finalDestinationPath = $letterDrive
            $finalDestinationPath = Join-Path  $letterDrive  $itemToCopy.DestinationPath

            # if the destination should be removed
            if (-not($itemToCopy.Ensure))
            {
                if (Test-Path $finalDestinationPath)
                {
                    SetVHDFile -destinationPath $finalDestinationPath -ensure:$false -recurse:($itemToCopy.Recurse)
                }
            }
            else
            {
                # Copy Scenario
                if ($itemToCopy.SourcePath)
                {
                    SetVHDFile -sourcePath $itemToCopy.SourcePath  -destinationPath $finalDestinationPath -recurse:($itemToCopy.Recurse) -force:($itemToCopy.Force)
                }
                elseif ($itemToCopy.Content)
                {
                    # if the type is not specified assume it is a file.
                    if (-not ($itemToCopy.Type))
                    {
                        $itemToCopy.Type = 'File'
                    }

                    # Create file/folder scenario
                    SetVHDFile -destinationPath $finalDestinationPath -type $itemToCopy.Type -force:($itemToCopy.Force)  -content $itemToCopy.Content
                }

                # Set Attribute scenario
                if ($itemToCopy.Attributes)
                {
                    SetVHDFile -destinationPath $finalDestinationPath -attribute $itemToCopy.Attributes -force:($itemToCopy.Force)
                }
            }

        }
    }
    finally
    {
        EnsureVHDState -Dismounted -vhdPath $VhdPath
    }
}

# This function returns if the current configuration of the machine is the same as the desired configration for this resource.
function Test-TargetResource
{
    [CmdletBinding()]
    [OutputType([System.Boolean])]
    param
    (
        [Parameter(Mandatory = $true)]
        [System.String]
        $VhdPath,

        [Parameter(Mandatory = $true)]
        [Microsoft.Management.Infrastructure.CimInstance[]]
        $FileDirectory,

        [Parameter()]
        [ValidateSet('ModifiedDate', 'SHA-1', 'SHA-256', 'SHA-512')]
        [System.String]
        $CheckSum = 'ModifiedDate'
    )

    # If the VHD path does not exist throw an error and stop.
    if ( -not (Test-Path $VhdPath))
    {
        throw "VHD does not exist in the specified path $VhdPath"
    }

    # mount the vhd.
    $mountedVHD = EnsureVHDState -Mounted -vhdPath $VhdPath

    try
    {
        # Show the drive letters after mount
        Get-PSDrive | Write-Verbose

        $mountedDrive = $mountedVHD | Get-Disk | Get-Partition | Where-Object -FilterScript { $_.Type -ne 'Recovery' } | Get-Volume
        $letterDrive = (-join $mountedDrive.DriveLetter) + ':\'
        Write-Verbose $letterDrive

        # return test result equal to true unless one of the tests in the loop below fails.
        $result = $true

        foreach ($item in $FileDirectory)
        {
            $itemToCopy = GetItemToCopy -item $item
            $destination = $itemToCopy.DestinationPath
            Write-Verbose ("Testing the file with relative VHD destination $destination")
            $destination = $itemToCopy.DestinationPath
            $finalDestinationPath = $letterDrive
            $finalDestinationPath = Join-Path $letterDrive $destination

            if (Test-Path $finalDestinationPath)
            {
                if ( -not ($itemToCopy.Ensure))
                {
                    $result = $false
                    break
                }
                else
                {
                    $itemToCopyIsFile = Test-Path $itemToCopy.SourcePath -PathType Leaf
                    $destinationIsFolder = Test-Path $finalDestinationPath -PathType Container

                    if ($itemToCopyIsFile -and $destinationIsFolder)
                    {
                        # Verify if the file exist inside the folder
                        $fileName = Split-Path $itemToCopy.SourcePath -Leaf
                        Write-Verbose "Checking if $fileName exist under $finalDestinationPath"
                        $fileExistInDestination = Test-Path (Join-Path $finalDestinationPath $fileName)

                        # Report if the file exist on the destination folder.
                        Write-Verbose "File exist on the destination under $finalDestinationPath :- $fileExistInDestination"
                        $result = $fileExistInDestination
                        $result = $result -and -not(ItemHasChanged -sourcePath $itemToCopy.SourcePath -destinationPath (Join-Path $finalDestinationPath $fileName) -CheckSum $CheckSum)
                    }

                    if (($itemToCopy.Type -eq 'Directory') -and ($itemToCopy.Recurse))
                    {
                        $result = $result -and -not(ItemHasChanged -sourcePath $itemToCopy.SourcePath -destinationPath $finalDestinationPath -CheckSum $CheckSum)

                        if (-not ($result))
                        {
                            break
                        }
                    }
                }
            }
            else
            {
                # If Ensure is specified as Present or if Ensure is not specified at all.
                if (($itemToCopy.Ensure))
                {
                    $result = $false
                    break
                }
            }

            # Check the attribute
            if ($itemToCopy.Attributes)
            {
                $currentAttribute = @(Get-ItemProperty -Path $finalDestinationPath | ForEach-Object -MemberName Attributes)
                $result = $currentAttribute.Contains($itemToCopy.Attributes)
            }
        }
    }
    finally
    {
        EnsureVHDState -Dismounted -vhdPath $VhdPath
    }


    Write-Verbose "Test returned $result"
    return $result
}

# Assert the state of the VHD.
function EnsureVHDState
{
    [CmdletBinding(DefaultParameterSetName = 'Mounted')]
    param
    (
        [Parameter(ParameterSetName = 'Mounted')]
        [switch]$Mounted,

        [Parameter(ParameterSetName = 'Dismounted')]
        [switch]$Dismounted,

        [Parameter(Mandatory = $true)]
        $vhdPath
    )

    if (-not (Get-Module -ListAvailable 'Hyper-V'))
    {
        throw 'Hyper-v-Powershell Windows Feature is required to run this resource. Please install Hyper-v feature and try again'
    }

    if ($PSCmdlet.ParameterSetName -eq 'Mounted')
    {
        # Try mounting the VHD.
        $mountedVHD = Mount-VHD -Path $vhdPath -Passthru -ErrorAction SilentlyContinue -ErrorVariable var

        # If mounting the VHD failed. Dismount the VHD and mount it again.
        if ($var)
        {
            Write-Verbose 'Mounting Failed. Attempting to dismount and mount it back'
            Dismount-VHD $vhdPath
            $mountedVHD = Mount-VHD -Path $vhdPath -Passthru -ErrorAction SilentlyContinue

            return $mountedVHD
        }
        else
        {
            return $mountedVHD
        }
    }
    else
    {
        Dismount-VHD $vhdPath -ea SilentlyContinue

    }
}

# Change the Cim Instance objects in to a hash table containing property value pair.
function GetItemToCopy
{
    param
    (
        [Parameter()]
        [Microsoft.Management.Infrastructure.CimInstance]
        $item
    )

    #Initialize Return Object
    $returnValue = @{ }

    #Define Default Values

    $DesiredProperties = [ordered]@{
        'SourcePath'      = $null
        'DestinationPath' = $null
        'Ensure'          = 'Present'
        'Recurse'         = 'True'
        'Force'           = 'True'
        'Content'         = $null
        'Attributes'      = $null
        'Type'            = 'Directory'
    }

    [System.String[]] ($DesiredProperties.Keys) | Foreach-Object -Process {
        #Get Property Value
        $thisItem = $item.CimInstanceProperties[$_].Value

        if (-not $thisItem -and $_ -in $DefaultValues.Keys)
        {
            #If unset and a default value is defined enter here
            if ($_ -eq 'Type')
            {
                #Special behavior for the Type property based on SourcePath
                #This relies on SourcePath preceeding Type in the list of keys (the reason for using OrderedDictionary)
                if (Test-Path $returnValue.SourcePath -PathType Leaf )
                {
                    #If the sourcepath resolves to a file, set the default to File instad of Directory
                    $DefaultValues.Type = 'File'
                }
            }

            $returnValue[$_] = $DefaultValues[$_]
        }
        else
        {
            #If value present or no default value enter here
            $returnValue[$_] = $item.CimInstanceProperties[$_].Value
        }
    }

    #Relies on default values in the $DesiredProperties object being the $True equivalent values
    $PropertyValuesToBoolean = @(
        'Force',
        'Recurse',
        'Ensure'
    )

    # Convert string values to boolean for ease of programming.
    $PropertyValuesToBoolean | ForEach-Object -Process {
        $returnValue[$_] = $returnValue[$_] -eq $DesiredProperties[$_]
    }


    $returnValue.Keys | ForEach-Object -Process {
        Write-Verbose "$_ => $($returnValue[$_])"
    }

    return $returnValue
}


# This is the main function that gets called after the file is mounted to perform copy, set or new operations on the mounted drive.
function SetVHDFile
{
    [CmdletBinding(DefaultParameterSetName = 'Copy')]
    param
    (
        [Parameter(Mandatory = $true, ParameterSetName = 'Copy')]
        $sourcePath,

        [Parameter()]
        [switch]
        $recurse,

        [Parameter()]
        [switch]
        $force,

        [Parameter(ParameterSetName = 'New')]
        $type,

        [Parameter(ParameterSetName = 'New')]
        $content,

        [Parameter(Mandatory = $true)]
        $destinationPath,

        [Parameter(Mandatory = $true, ParameterSetName = 'Set')]
        $attribute,

        [Parameter(Mandatory = $true, ParameterSetName = 'Delete')]
        [switch]
        $ensure
    )

    Write-Verbose "Setting the VHD file $($PSCmdlet.ParameterSetName)"
    if ($PSCmdlet.ParameterSetName -eq 'Copy')
    {
        New-Item -Path (Split-Path $destinationPath) -ItemType Directory -ErrorAction SilentlyContinue
        Copy-Item -Path $sourcePath -Destination $destinationPath -Force:$force -Recurse:$recurse -ErrorAction SilentlyContinue
    }
    elseif ($PSCmdlet.ParameterSetName -eq 'New')
    {
        if ($type -eq 'Directory')
        {
            New-Item -Path $destinationPath -ItemType $type
        }
        else
        {
            New-Item -Path $destinationPath -ItemType $type
            $content | Out-File $destinationPath
        }

    }
    elseif ($PSCmdlet.ParameterSetName -eq 'Set')
    {
        Write-Verbose "Attempting to change the attribute of the file $destinationPath to value $attribute"
        Set-ItemProperty -Path $destinationPath -Name Attributes -Value $attribute
    }
    elseif (!($ensure))
    {
        Remove-Item -Path $destinationPath -Force:$force -Recurse:$recurse
    }
}

# Detect if the item to be copied is modified version of the orginal.
function ItemHasChanged
{
    param
    (
        [Parameter(Mandatory = $true)]
        [ValidateScript( { Test-Path $_ })]
        $sourcePath,

        [Parameter(Mandatory = $true)]
        [ValidateScript( { Test-Path $_ })]
        $destinationPath,

        [Parameter()]
        [ValidateSet('ModifiedDate', 'SHA-1', 'SHA-256', 'SHA-512')]
        $CheckSum = 'ModifiedDate'
    )

    $itemIsFolder = Test-Path $sourcePath -Type Container
    $sourceItems = $null
    $destinationItems = $null

    if ($itemIsFolder)
    {
        $sourceItems = Get-ChildItem "$sourcePath\*.*" -Recurse
        $destinationItems = Get-ChildItem "$destinationPath\*.*" -Recurse

    }
    else
    {
        $sourceItems = Get-ChildItem $sourcePath
        $destinationItems = Get-ChildItem $destinationPath

    }

    if (-not ($destinationItems))
    {
        return $true
    }

    # Compute the difference using the algorithem specified.
    $difference = $null

    switch ($CheckSum)
    {
        'ModifiedDate'
        {
            $difference = Compare-Object -ReferenceObject $sourceItems -DifferenceObject $destinationItems -Property LastWriteTime
        }

        'SHA-1'
        {
            $difference = Compare-Object -ReferenceObject ($sourceItems | Get-FileHash -Algorithm SHA1) -DifferenceObject ($destinationItems | Get-FileHash -Algorithm SHA1) -Property Hash
        }

        'SHA-256'
        {
            $difference = Compare-Object -ReferenceObject ($sourceItems | Get-FileHash -Algorithm SHA256) -DifferenceObject ($destinationItems | Get-FileHash -Algorithm SHA256) -Property Hash
        }

        'SHA-512'
        {
            $difference = Compare-Object -ReferenceObject ($sourceItems | Get-FileHash -Algorithm SHA512) -DifferenceObject ($destinationItems | Get-FileHash -Algorithm SHA512) -Property Hash
        }
    }

    # If there are object difference between the item at the source and Items at the distenation.
    return ($null -ne $difference)

}

Export-ModuleMember -Function *-TargetResource