DSCResources/Grani_SymbolicLink/Grani_SymbolicLink.psm1

#region Initialize

function Initialize {
    # Enum for Ensure
    Add-Type -TypeDefinition @"
        public enum EnsureType
        {
            Present,
            Absent
        }
"@
 -ErrorAction SilentlyContinue

    # GetSymbolicLink Class
    $GetSymLink = @'
        private const int FILE_SHARE_READ = 1;
        private const int FILE_SHARE_WRITE = 2;
        private const int CREATION_DISPOSITION_OPEN_EXISTING = 3;
        private const int FILE_FLAG_BACKUP_SEMANTICS = 0x02000000;
 
        [DllImport("kernel32.dll", EntryPoint = "GetFinalPathNameByHandleW", CharSet = CharSet.Unicode, SetLastError = true)]
        public static extern int GetFinalPathNameByHandle(IntPtr handle, [In, Out] StringBuilder path, int bufLen, int flags);
 
        [DllImport("kernel32.dll", EntryPoint = "CreateFileW", CharSet = CharSet.Unicode, SetLastError = true)]
        public static extern SafeFileHandle CreateFile(string lpFileName, int dwDesiredAccess, int dwShareMode, IntPtr SecurityAttributes, int dwCreationDisposition, int dwFlagsAndAttributes, IntPtr hTemplateFile);
 
        public static string GetSymbolicLinkTarget(System.IO.DirectoryInfo symlink)
        {
            SafeFileHandle directoryHandle = CreateFile(symlink.FullName, 0, 2, System.IntPtr.Zero, CREATION_DISPOSITION_OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, System.IntPtr.Zero);
 
            if (directoryHandle.IsInvalid)
                throw new Win32Exception(Marshal.GetLastWin32Error());
            StringBuilder path = new StringBuilder(512);
            int size = GetFinalPathNameByHandle(directoryHandle.DangerousGetHandle(), path, path.Capacity, 0);
            if (size < 0) throw new Win32Exception(Marshal.GetLastWin32Error()); // The remarks section of GetFinalPathNameByHandle mentions the return being prefixed with "\\?\" // More information about "\\?\" here -> http://msdn.microsoft.com/en-us/library/aa365247(v=VS.85).aspx
            if (path[0] == '\\' && path[1] == '\\' && path[2] == '?' && path[3] == '\\') return path.ToString().Substring(4);
            else return path.ToString();
        }
'@


    $getMemberParam = @{
        Name             = "SymbolicLinkGet"
        Namespace        = "GraniResource"
        UsingNameSpace   = "System.Text", "Microsoft.Win32.SafeHandles", "System.ComponentModel"
        MemberDefinition = $GetSymLink
        ErrorAction      = "SilentlyContinue"
        PassThru         = $true
    }
    $SymbolicLinkGet = Add-Type @getMemberParam | where Name -eq $getMemberParam.Name | % {$_ -as [Type]}

    # SetSymbolicLink Class
    $SetSymLink = @'
        internal static class Win32
        {
            [DllImport("kernel32.dll", SetLastError = true)]
            [return: MarshalAs(UnmanagedType.I1)]
            public static extern bool CreateSymbolicLink(string lpSymlinkFileName, string lpTargetFileName, SymLinkFlag dwFlags);
 
            internal enum SymLinkFlag
            {
                File = 0,
                Directory = 1
            }
        }
        public static void CreateSymLink(string name, string target, bool isDirectory = false)
        {
            if (!Win32.CreateSymbolicLink(name, target, isDirectory ? Win32.SymLinkFlag.Directory : Win32.SymLinkFlag.File))
            {
                throw new System.ComponentModel.Win32Exception();
            }
        }
'@

    $setMemberParam = @{
        Name             = "SymbolicLinkSet"
        Namespace        = "GraniResource"
        MemberDefinition = $SetSymLink
        ErrorAction      = "SilentlyContinue"
        PassThru         = $true
    }
    $SymbolicLinkSet = Add-Type @setMemberParam | where Name -eq $setMemberParam.Name | % {$_ -as [Type]}
}

. Initialize

#endregion

#region Message Definition

$verboseMessages = Data {
    ConvertFrom-StringData -StringData @"
        CreateSymbolicLink = DestinationPath : '{0}', SourcePath : '{1}', IsDirectory : '{2}'
        RemovingDestinationPath = Removing reparse point (Symbolic Link) '{0}'.
"@

}

$debugMessages = Data {
    ConvertFrom-StringData -StringData @"
        SourceAttributeDetectReparsePoint = Attribute detected as ReparsePoint. : {0}
        SourceAttributeNotDetectReparsePoint = Attribute detected as NOT ReparsePoint. : {0}
        SourceDirectoryDetected = Input object : '{0}' detected as Directory.
        SourceFileDetected = Input object : '{0}' detected as File.
        DestinationFileAttribute = Attribute detected as File Archive. : {0}
        DestinationNotFileAttribute = Attribute detected as NOT File Archive. : {0}
        DestinationDirectoryAttribute = Attribute detected as Directory. : {0}
        DestinationNotDirectoryAttribute = Attribute detected as NOT Directory. : {0}
        DestinationDetectedAsFile = Destination '{0}' detected as File. Checking reparse point (Symbolic Link) or not.
        DestinationDetectedAsDirectory = Destination '{0}' detected as File. Checking reparse point (Symbolic Link) or not.
        DestinationNotFound = Destination '{0}' not found.
        DestinationNotReparsePoint = Destination was not reparse point (Symbolic Link).
        DestinationReparsePoint = Destination detected as reparse point (Symbolic Link).
        DestinationSourcePathDesired = DEstination '{0}' detected as reparse point (Symbolic Link), and SourcePath desired.
        DestinationSourcePathNotDesired = DEstination '{0}' detected as reparse point (Symbolic Link), but SourcePath not desired.
"@

}

$errorMessages = Data {
    ConvertFrom-StringData -StringData @"
"@

}

#endregion

#region *-TargetResource

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

        [parameter(Mandatory = $true)]
        [System.String]$SourcePath,

        [parameter(Mandatory = $true)]
        [ValidateSet("Present", "Absent")]
        [System.String]$Ensure
    )

    $returnValue = @{
        SourcePath      = $SourcePath
        DestinationPath = $DestinationPath
    }

    try {
        if ($target = IsFile -Path $DestinationPath) {
            Write-Debug -Message ($debugMessages.DestinationDetectedAsFile -f $DestinationPath)
            if (IsFileReparsePoint -Path $target.FullName) {
                Write-Debug -Message $debugMessages.DestinationReparsePoint
                $symTarget = $SymbolicLinkGet::GetSymbolicLinkTarget($target.FullName)
                if (Test-Path $SourcePath) {
                    Add-Member -InputObject $target -MemberType NoteProperty -Name SymbolicPath -Value $symTarget -Force
                }
            }
        }
        elseif ($target = IsDirectory -Path $DestinationPath) {
            Write-Debug -Message ($debugMessages.DestinationDetectedAsDirectory -f $DestinationPath)
            if (IsDirectoryReparsePoint -Path $target.FullName) {
                Write-Debug -Message $debugMessages.DestinationReparsePoint
                $symTarget = $SymbolicLinkGet::GetSymbolicLinkTarget($target.FullName)
                if (Test-Path $SourcePath) {
                    Add-Member -InputObject $target -MemberType NoteProperty -Name SymbolicPath -Value $symTarget -Force
                }
            }
        }

        if ([string]::IsNullOrEmpty($symTarget)) {
            Write-Debug -Message ($debugMessages.DestinationNotFound -f $DestinationPath)
            $returnValue.Ensure = [EnsureType]::Absent
        }
        elseif ($symTarget -eq $SourcePath) {
            Write-Debug -Message ($debugMessages.DestinationSourcePathDesired -f $DestinationPath)
            $returnValue.Ensure = [EnsureType]::Present
        }
        else {
            Write-Debug -Message ($debugMessages.DestinationSourcePathNotDesired -f $DestinationPath)
            $returnValue.Ensure = [EnsureType]::Absent
        }
    }
    catch {
        $returnValue.Ensure = [EnsureType]::Absent
        Write-Verbose $_
    }

    return $returnValue
}


function Set-TargetResource {
    [CmdletBinding()]
    param
    (
        [parameter(Mandatory = $true)]
        [System.String]$DestinationPath,

        [parameter(Mandatory = $true)]
        [System.String]$SourcePath,

        [parameter(Mandatory = $true)]
        [ValidateSet("Present", "Absent")]
        [System.String]$Ensure
    )

    # Absent
    if ($Ensure -eq [EnsureType]::Absent.ToString()) {
        Write-Verbose -Message ($verboseMessages.RemovingDestinationPath -f $DestinationPath)
        RemoveSymbolicLink -Path $DestinationPath
        return;
    }

    # Present
    if ($file = IsFile -Path $SourcePath) {
        # Check File Type
        if (IsFileAttribute -Path $file) {
            Write-Verbose ($verboseMessages.CreateSymbolicLink -f $DestinationPath, $file.fullname, $false)
            $SymbolicLinkSet::CreateSymLink($DestinationPath, $file.fullname, $false)
        }
    }
    elseif ($directory = IsDirectory -Path $SourcePath) {
        # Check Directory Type
        if (IsDirectoryAttribute -Path $directory) {
            Write-Verbose ($verboseMessages.CreateSymbolicLink -f $DestinationPath, $directory.fullname, $false)
            $SymbolicLinkSet::CreateSymLink($DestinationPath, $directory.fullname, $true)
        }
    }
}


function Test-TargetResource {
    [CmdletBinding()]
    [OutputType([System.Boolean])]
    param
    (
        [parameter(Mandatory = $true)]
        [System.String]$DestinationPath,

        [parameter(Mandatory = $true)]
        [System.String]$SourcePath,

        [parameter(Mandatory = $true)]
        [ValidateSet("Present", "Absent")]
        [System.String]$Ensure
    )
    return (Get-TargetResource -DestinationPath $DestinationPath -SourcePath $SourcePath -Ensure $Ensure).Ensure -eq $Ensure
}

#endregion

#region Get helper

function IsFile ([string]$Path) {
    if ([System.IO.File]::Exists($Path)) {
        Write-Debug ($debugMessages.SourceFileDetected -f $Path)
        return [System.IO.FileInfo]($Path)
    }
}

function IsDirectory ([string]$Path) {
    if ([System.IO.Directory]::Exists($Path)) {
        Write-Debug ($debugMessages.SourceDirectoryDetected -f $Path)
        return [System.IO.DirectoryInfo] ($Path)
    }
}

function IsFileReparsePoint ([System.IO.FileInfo]$Path) {
    $fileAttributes = [System.IO.FileAttributes]::Archive, [System.IO.FileAttributes]::ReparsePoint -join ', '
    $attribute = [System.IO.File]::GetAttributes($Path)
    $result = $attribute -eq $fileAttributes
    if ($result) {
        Write-Debug ($debugMessages.SourceAttributeDetectReparsePoint -f $attribute)
        return $result
    }
    else {
        Write-Debug ($debugMessages.SourceAttributeNotDetectReparsePoint -f $attribute)
        return $result
    }
}

function IsDirectoryReparsePoint ([System.IO.DirectoryInfo]$Path) {
    $directoryAttributes = [System.IO.FileAttributes]::Directory, [System.IO.FileAttributes]::ReparsePoint -join ', '
    $result = $Path.Attributes -eq $directoryAttributes
    if ($result) {
        Write-Debug ($debugMessages.SourceAttributeDetectReparsePoint -f $Path.Attributes)
        return $result
    }
    else {
        Write-Debug ($debugMessages.SourceAttributeNotDetectReparsePoint -f $Path.Attributes)
        return $result
    }
}

#endregion

#region Remove helper

function RemoveSymbolicLink {
    [OutputType([Void])]
    [cmdletBinding()]
    param
    (
        [parameter(Mandatory = 1, Position = 0, ValueFromPipeline = 1, ValueFromPipelineByPropertyName = 1)]
        [Alias('FullName')]
        [String]$Path
    )

    function RemoveFileReparsePoint ([System.IO.FileInfo]$Path) {
        [System.IO.File]::Delete($Path.FullName)
    }

    function RemoveDirectoryReparsePoint ([System.IO.DirectoryInfo]$Path) {
        [System.IO.Directory]::Delete($Path.FullName)
    }

    try {
        if ($file = IsFile -Path $Path) {
            if (IsFileReparsePoint -Path $file) {
                RemoveFileReparsePoint -Path $file
            }
        }
        elseif ($directory = IsDirectory -Path $Path) {
            if (IsDirectoryReparsePoint -Path $directory) {
                RemoveDirectoryReparsePoint -Path $directory
            }
        }
    }
    catch {
        throw $_
    }
}

#endregion

#region Set helper

function IsFileAttribute ([System.IO.FileInfo]$Path) {
    $fileAttributes = [System.IO.FileAttributes]::Archive
    $attribute = [System.IO.File]::GetAttributes($Path.fullname)
    $result = $attribute -eq $fileAttributes
    if ($result) {
        Write-Debug ($debugMessages.DestinationFileAttribute -f $attribute)
    }
    else {
        Write-Debug ($debugMessages.DestinationNotFileAttribute -f $attribute)
    }
    return $result
}

function IsDirectoryAttribute ([System.IO.DirectoryInfo]$Path) {
    $directoryAttributes = [System.IO.FileAttributes]::Directory
    $result = $Path.Attributes -eq $directoryAttributes
    if ($result) {
        Write-Debug ($debugMessages.DestinationDirectoryAttribute -f $attribute)
    }
    else {
        Write-Debug ($debugMessages.DestinationNotDirectoryAttribute -f $attribute)
    }
    return $result
}

#endregion

Export-ModuleMember -Function *-TargetResource