InstanceCreationPlugins/symlink-plugin.psm1

#requires -version 4

function ModuleRoot
{
    $MyInvocation.ScriptName | Split-Path -Parent
}
$PrivPath = $(Join-Path $(ModuleRoot | Split-Path -Parent) "\Private")

<#
.SYNOPSIS
    Plugin for creating symlinks as defined in the ISPSInstance.config
.DESCRIPTION
    This plugin only acts on nodes with the Name attribute of "Symlink".
    This element is an instruction to create a symlink to either a directory or file.
    The symlink will be placed in %progdata%\IntelliSearch\<InstanceName>\<LinkName> and targets a location in relation to the root of the package.
    The LinkName can contain directories, so "\cfg\aSymLink" is valid.
    If the Force parameter is true, the cmdlet will overwrite the existing file at the path from LinkName.
 
    Example from ISPSInstance.config:
    <Symlink
        Path="\someFolderInTheComponentPackage"
        LinkName="\lib"
        Force="true" />
     
.EXAMPLE
    $Splat = @{
        Node = @{
            LinkName = "Symlink"
            Path = "\bin\someexecutablefile.exe"
            Force = $false
        }
        InstanceName = "This is ignored"
        InstanceDirectory = "C:\ProgramData\IntelliSearch\MyInstance\"
        ComponentDirectory "C:\ProgramFiles\IntelliSearch\IntelliSearch.Server.IndexManager.4.0.2\"
    }
    synlink.plugin @Splat
.PARAMETER Node
    The XML node currently being iterated over
.PARAMETER InstanceName
    A string with the name of the instance. This plugin ignores this parameter.
.PARAMETER InstanceDirectory
    The root directory of the new instance
.PARAMETER ComponentDirectory
    The root directory of the installed component
.INPUTS
    System.String
.OUTPUTS
    System.File.SymbolicLink
#>

function symlink-plugin
{
    param (
        # XML node from ISPSInstance.config
        [Parameter(
            Mandatory = $true,
            Position = 0,
            ValueFromPipelineByPropertyName = $true,
            HelpMessage = "XML node from ISPSInstance.config")]
        $Node,
    
        # The name representing the new instance
        [Parameter(
            Mandatory = $true,
            Position = 1,
            ValueFromPipelineByPropertyName = $true,
            HelpMessage = "Instance name")]
        [ValidateNotNullOrEmpty()]
        [string] $InstanceName,
        
        # Path to the directory of the new instance
        [Parameter(
            Mandatory = $true,
            Position = 2,
            ValueFromPipelineByPropertyName = $true,
            HelpMessage = "Path to the directory of the new instance")]
        [ValidateNotNullOrEmpty()]
        [ValidateScript( {Test-Path -Path $_ -PathType Container})]
        [string] $InstanceDirectory,
    
        # Path to the component nuget package directory
        [Parameter(
            Mandatory = $true,
            Position = 3,
            ValueFromPipelineByPropertyName = $true,
            HelpMessage = "Path to the component nuget package directory")]
        [ValidateNotNullOrEmpty()]
        [ValidateScript( {Test-Path -Path $_ -PathType Container})]
        [string] $ComponentDirectory
    )

    if ($Node.Name -ne "Symlink")
    {
        return
    }

    Write-Verbose "Creating new symlink"

    $Path = (Join-Path $ComponentDirectory $Node.Path)
    $SymName = (Join-Path $InstanceDirectory $Node.LinkName)

    if (-not (Test-Path $Path -IsValid))
    {
        Throw "The symlink path is not valid. Got $Path"
    }

    if (-not (Test-Path $Path))
    {
        Throw "The symlink path does not exist. Got $Path"
    }

    if (-not (Test-Path $SymName -IsValid))
    {
        Throw "The symlink name is not valid. Got $SymName"
    }
    
    if ([System.Convert]::ToBoolean($Node.Force) -and
        (Test-Path $SymName) )
    {
        Write-Verbose "Symlink path already exists. Overwriting."
        Remove-Item -Path $SymName -Force
    }
    elseif (([System.Convert]::ToBoolean($Node.Force) -eq $false) -and
        (Test-Path $SymName))
    {
        Throw "File already exists and overwrite is not specified."   
    }
    
    # Check if the target is a directory or a file
    [int]$TargetType = Get-Item $Path | Select-Object -ExpandProperty PSIsContainer
    $TargetTypeString = if($TargetType) {"directory"} else {"file"}
    Write-Verbose ("Symlink target type is {0}." -f $TargetTypeString)
    
    Try
    {
        $return = [mklink.symlink]::CreateSymbolicLinkFix($SymName, $Path, $TargetType)
        If (-not $return)
        {
            $object = New-Object PSObject -Property @{
                SymLink = $SymName
                Target  = $Path
                Type    = $TargetTypeString
            }
            $object.pstypenames.insert(0, 'System.File.SymbolicLink')
            $object
        }
        elseif ($return -eq 1314) {
            Throw "Unable to create symbolic link! Plugin is not running as administrator."
        }
        Else
        {
            Throw "Unable to create symbolic link! Return code: $Return"
        }
    }
    Catch
    {
        Write-warning ("{0}: {1}" -f $path, $_.Exception.Message)
    }
}


#region Load dll for CreateSymbolicLink method
Try
{
    $null = [mklink.symlink]
}
Catch
{
    Add-Type @"
            using System;
            using System.Runtime.InteropServices;
  
            namespace mklink
            {
                public enum SYMBOLIC_LINK_FLAG
                {
                    File = 0,
                    Directory = 1
                }
 
                public class symlink
                {
                    [DllImport("kernel32.dll", EntryPoint = "CreateSymbolicLinkW", CharSet = CharSet.Unicode, SetLastError = true)]
                    static extern int CreateSymbolicLink(string lpSymlinkFileName, string lpTargetFileName, SYMBOLIC_LINK_FLAG dwFlags);
 
                    public static int CreateSymbolicLinkFix(string lpSymlinkFileName, string lpTargetFileName, SYMBOLIC_LINK_FLAG dwFlags) {
                        var result = CreateSymbolicLink(lpSymlinkFileName, lpTargetFileName, dwFlags);
                        if (result == 1) return 0; // Success
                        return Marshal.GetLastWin32Error();
                    }
                }
            }
"@

}
#endregion Load dll for CreateSymbolicLink method