Private/LinkTools.ps1

function Assert-ValidPath4LinkTools{
    [CmdletBinding()]
    [OutputType([System.Boolean])]
    param(
        [FormattedFileSystemPath]$Path,
        [switch]$File
    )
    if($File){
        if(!$Path.IsFile){
            throw "The $Path should be a file."
        }
    }else{
        if(!$Path.IsDir){
            throw "The $Path should be a directory."
        }
    }

    if($Path.IsSymbolicLink -or $Path.IsJunction){
        throw "The $Path should not be a symbolic link or junction point."
    }
    return $true
}


function Merge-DirectoryWithBackup{
<#
.DESCRIPTION
    Backup $Source to a path based on $Backuppath
    Backup $Destination to a path based on $Backuppath
    Then, merge items from $Source to $Destination
    Record logs
#>

    [CmdletBinding(SupportsShouldProcess)]
    param(
        [Parameter(Mandatory)]
        [ValidateScript({Assert-ValidPath4LinkTools $_})]
        [FormattedFileSystemPath]$Source,
        [Parameter(Mandatory)]
        [ValidateScript({Assert-ValidPath4LinkTools $_})]
        [FormattedFileSystemPath]$Destination,
        [Parameter(Mandatory)]
        [ValidateScript({Assert-ValidPath4LinkTools $_})]
        [FormattedFileSystemPath]$Backuppath
    )

    $guid = [guid]::NewGuid()
    $source_name = $Source.ToShortName()
    $backup_source = "$Backuppath/$guid-$source_name"
    $destination_name = $Destination.ToShortName()
    $backup_destination = "$Backuppath/$guid-$destination_name"
    $log_file = Get-LogFileName "Robocopy Merge-DirectoryWithBackup"
    if($PSCmdlet.ShouldProcess(
        "Backup $Source to $backup_source"+[Environment]::NewLine+
        "Backup $Destination to $backup_destination"+[Environment]::NewLine+
        "Then, merge items from $Source to $Destination"+[Environment]::NewLine+
        "Record logs to $log_file",'',''))
    {
        Assert-AdminRobocopyAvailable
        Robocopy $Source $backup_source /E /copyall /DCOPY:DATE /LOG:"$log_file"
        Robocopy $Destination $backup_destination /E /copyall /DCOPY:DATE /LOG:"$log_file"
        Robocopy $Source $Destination /E /copyall /DCOPY:DATE /LOG:"$log_file"
    }

}
function Move-FileWithBackup{
<#
.DESCRIPTION
    Backup $Source to a path based on $Backuppath
    Backup $Destination to a path based on $Backuppath
    Then, move $Source to $Destination
    Record logs
#>

    [CmdletBinding(SupportsShouldProcess)]
    param(
        [Parameter(Mandatory)]
        [ValidateScript({Assert-ValidPath4LinkTools $_ -File})]
        [FormattedFileSystemPath]$Source,
        [Parameter(Mandatory)]
        [ValidateScript({Assert-ValidPath4LinkTools $_ -File})]
        [FormattedFileSystemPath]$Destination,
        [Parameter(Mandatory)]
        [ValidateScript({Assert-ValidPath4LinkTools $_})]
        [FormattedFileSystemPath]$Backuppath
    )
    $guid = [guid]::NewGuid()
    $source_name = $Source.ToShortName()
    $backup_source = "$Backuppath/$guid-$source_name"
    $destination_name = $Destination.ToShortName()
    $backup_destination = "$Backuppath/$guid-$destination_name"
    $log_file = Get-LogFileName
    if($PSCmdlet.ShouldProcess(
        "Backup $Source to $backup_source"+[Environment]::NewLine+
        "Backup $Destination to $backup_destination"+[Environment]::NewLine+
        "Then, move $Source to $Destination"+[Environment]::NewLine+
        "Record logs to $log_file",'',''))
    {
        Write-Logs (Copy-Item $Source $backup_source)
        Write-Logs (Copy-Item $Destination $backup_destination)
        Write-Logs (Copy-Item $Source $Destination)
    }
}
function Merge-BeforeSetDirLink{
<#
.DESCRIPTION
    Before setting a directory link (Symbolic Link or Junction)
    from $Target2 to $Target1, this function should be used to
    merge the content in $Target1 to $Target2.

    Merge form $Target1 to $Target2 by the following rules:
        $Target1------------------------| $Target2----------------------| Opeartion
        non-existent | non-existent | pass(do nothing)
        non-existent | dir-symbolic-or-hardlink | throw error
        non-existent | dir-not-symbolic-or-hardlink | pass(do nothing)
        dir-symbolic-or-hardlink | non-existent | throw error
        dir-symbolic-or-hardlink | dir-symbolic-or-hardlink | throw error
        dir-symbolic-or-hardlink | dir-not-symbolic-or-hardlink | del $Target1
        dir-not-symbolic-or-hardlink | non-existent | copy $Target1 to $Target2, del $Target1
        dir-not-symbolic-or-hardlink | dir-symbolic-or-hardlink | throw error
        dir-not-symbolic-or-hardlink | dir-not-symbolic-or-hardlink | backup $Target1 and $Target2 to $Backuppath, then merge $Target1 to $Target2, then del $Target1
#>

    [CmdletBinding(SupportsShouldProcess)]
    param(
        [Parameter(Mandatory)]
        [string]$Target1,
        [Parameter(Mandatory)]
        [string]$Target2,
        [Parameter(Mandatory)]
        [ValidateScript({Assert-ValidPath4LinkTools $_})]
        [FormattedFileSystemPath]$Backuppath
    )
    try {
        $_target1 = [FormattedFileSystemPath]::new($Target1)
        $_target1_exist = $true
    }
    catch [System.Management.Automation.ItemNotFoundException]{
        $_target1_exist = $false
    }
    catch {
        Write-Logs  "Exception caught: $_"
    }
    if ($_target1_exist){
        if (!$_target1.IsDir){
            throw "The $_target1 should be a directory"
        }
    }

    try {
        $_target2 = [FormattedFileSystemPath]::new($Target2)
        $_target2_exist = $true
    }
    catch [System.Management.Automation.ItemNotFoundException]{
        $_target2_exist = $false
    }
    catch {
        Write-Logs  "Exception caught: $_"
    }
    if ($_target2_exist){
        if (!$_target2.IsDir){
            throw "The $_target2 should be a directory."
        }
    }

    if($PSCmdlet.ShouldProcess("Merge the content in $_target1 to $_target2, and backup essential items in $Backuppath",'','')){
        if ($_target1_exist){
            if ($_target1.IsSymbolicLink -or $_target1.IsJunction){
                if ($_target2_exist){
                    if ($_target2.IsSymbolicLink -or $_target2.IsJunction){
                        # dir-symbolic-or-hardlink | dir-symbolic-or-hardlink | throw error
                        throw "Cannot merge $_target1 to $_target2, because $_target1 and $_target2 are both symbolic link or junction point."
                    }else{
                        # dir-symbolic-or-hardlink | dir-not-symbolic-or-hardlink | del $Target1
                        Write-Logs  "Remove-Item $_target1 -Force -Recurse"
                        Remove-Item $_target1 -Force -Recurse
                    }
                }else{
                    # dir-symbolic-or-hardlink | non-existent | throw error
                    throw "Cannot merge $_target1 to $_target2, because $_target2 does not exist."
                }
            }else{
                if ($_target2_exist){
                    if ($_target2.IsSymbolicLink -or $_target2.IsJunction){
                        # dir-not-symbolic-or-hardlink | dir-symbolic-or-hardlink | throw error
                        throw "Cannot merge $_target1 to $_target2, because $_target1 is not symbolic link or junction point, but $_target2 is."
                    }else{
                        #dir-not-symbolic-or-hardlink | dir-not-symbolic-or-hardlink | backup $Target1 and $Target2 to $Backuppath, then merge $Target1 to $Target2, then del $Target1
                        Merge-DirectoryWithBackup -Source $_target1 -Destination $_target2 -Backuppath $Backuppath
                        Write-Logs  "Remove-Item $_target1 -Force -Recurse"
                        Remove-Item $_target1 -Force -Recurse
                    }
                }else{
                    Assert-AdminRobocopyAvailable
                    # dir-not-symbolic-or-hardlink | non-existent | copy $Target1 to $Target2, del $Target1
                    Write-Logs  "Robocopy $_target1 $_target2"
                    $log_file = Get-LogFileName "Robocopy Merge-BeforeSetDirLink"
                    Robocopy $_target1 $_target2  /E /copyall /DCOPY:DATE /LOG:"$log_file"
                    Write-Logs  "Remove-Item $_target1 -Force -Recurse"
                    Remove-Item $_target1 -Force -Recurse
                }
            }
        }else{
            if ($_target2_exist){
                if ($_target2.IsSymbolicLink -or $_target2.IsJunction){
                    # non-existent | dir-symbolic-or-hardlink | throw error
                    throw "Cannot merge $_target1 to $_target2, because $_target1 does not exist and $_target2 is symbolic link or junction point."
                }else{
                    # non-existent | dir-not-symbolic-or-hardlink | pass(do nothing)
                    Write-Logs  "Do nothing."
                }
            }else{
                # non-existent | non-existent | pass(do nothing)
                Write-Logs  "Do nothing."
            }
        }
    }
}
function Move-BeforeSetFileLink{
<#
.DESCRIPTION
    Before setting a file link (Symbolic Link or HardLink)
    from $Target2 to $Target1, this function should be used to
    move the file $Target1 to $Target2.

    Move the file $Target1 to $Target2 by the following rules:
        $Target1------------------------| $Target2----------------------| Opeartion
        non-existent | non-existent | pass(do nothing)
        non-existent | file-symbolic-or-hardlink | throw error
        non-existent | file-non-symbolic-or-hardlink | pass(do nothing)
        file-symbolic-or-hardlink | non-existent | throw error
        file-symbolic-or-hardlink | file-symbolic-or-hardlink | throw error
        file-symbolic-or-hardlink | file-non-symbolic-or-hardlink | del $Target1
        file-non-symbolic-or-hardlink | non-existent | copy $Target1 to $Target2, del $Target1
        file-non-symbolic-or-hardlink | file-symbolic-or-hardlink | throw error
        file-non-symbolic-or-hardlink | file-non-symbolic-or-hardlink | backup $Target1 and $Target2 to $Backuppath, then del $Target1
#>

    [CmdletBinding(SupportsShouldProcess)]
    param(
        [Parameter(Mandatory)]
        [string]$Target1,
        [Parameter(Mandatory)]
        [string]$Target2,
        [Parameter(Mandatory)]
        [ValidateScript({Assert-ValidPath4LinkTools $_})]
        [FormattedFileSystemPath]$Backuppath
    )

    try {
        $_target1 = [FormattedFileSystemPath]::new($Target1)
        $_target1_exist = $true
    }
    catch [System.Management.Automation.ItemNotFoundException]{
        $_target1_exist = $false
    }
    catch {
        Write-Logs  "Exception caught: $_"
    }
    if ($_target1_exist){
        if (!$_target1.IsFile){
            throw "The $_target1 should be a file."
        }
    }

    try {
        $_target2 = [FormattedFileSystemPath]::new($Target2)
        $_target2_exist = $true
    }
    catch [System.Management.Automation.ItemNotFoundException]{
        $_target2_exist = $false
    }
    catch {
        Write-Logs  "Exception caught: $_"
    }
    if ($_target2_exist){
        if (!$_target2.IsFile){
            throw "The $_target2 should be a file."
        }
    }

    if($PSCmdlet.ShouldProcess("Move the content in $_target1 to $_target2, and backup essential items in $Backuppath",'','')){
        if ($_target1_exist){
            if ($_target1.IsSymbolicLink -or $_target1.IsHardLink){
                if ($_target2_exist){
                    if ($_target2.IsSymbolicLink -or $_target2.IsHardLink){
                        # file-symbolic-or-hardlink | file-symbolic-or-hardlink | throw error
                        throw "Cannot move $_target1 to $_target2, because $_target1 and $_target2 are both symbolic link or hard link."
                    }else{
                        # file-symbolic-or-hardlink | file-non-symbolic-or-hardlink | del $Target1
                        Write-Logs  "Remove-Item $_target1 -Force -Recurse"
                        Remove-Item $_target1 -Force -Recurse
                    }
                }else{
                    # file-symbolic-or-hardlink | non-existent | throw error
                    throw "Cannot move $_target1 to $_target2, because $_target1 is symbolic link or hard link while $_target2 does not exist."
                }
            }else{
                if ($_target2_exist){
                    if ($_target2.IsSymbolicLink -or $_target2.IsHardLink){
                        # file-non-symbolic-or-hardlink | file-symbolic-or-hardlink | throw error
                        throw "Cannot move $_target1 to $_target2, because $_target1 is not symbolic link or hard link while $_target2 is."
                    }else{
                        # file-non-symbolic-or-hardlink | file-non-symbolic-or-hardlink | backup $Target1 and $Target2 to $Backuppath, then del $Target1
                        Move-FileWithBackup -Source $_target1 -Destination $_target2 -Backuppath $Backuppath
                        Write-Logs  "Remove-Item $_target1 -Force -Recurse"
                        Remove-Item $_target1 -Force -Recurse
                    }
                }else{
                    Assert-AdminRobocopyAvailable
                    # file-non-symbolic-or-hardlink | non-existent | copy $Target1 to $Target2, del $Target1
                    Write-Logs  "Robocopy $_target1 $_target2"
                    $log_file = Get-LogFileName "Robocopy Move-BeforeSetFileLink"
                    Robocopy $_target1 $_target2  /copyall /DCOPY:DATE /LOG:"$log_file"
                    Write-Logs  "Remove-Item $_target1 -Force -Recurse"
                    Remove-Item $_target1 -Force -Recurse
                }
            }
        }else{
            if ($_target2_exist){
                if ($_target2.IsSymbolicLink -or $_target2.IsHardLink){
                    # non-existent | file-symbolic-or-hardlink | throw error
                    throw "Cannot move $_target1 to $_target2, because $_target1 does not exist and $_target2 is symbolic link or hard link."
                }else{
                    # non-existent | file-non-symbolic-or-hardlink | pass(do nothing)
                    Write-Logs  "Do nothing."
                }
            }else{
                # non-existent | non-existent | pass(do nothing)
                Write-Logs  "Do nothing."
            }
        }
    }
}