GenXdev.FileSystem.psm1

###############################################################################

<#
Copyright 2021 René Vaessen - genXdev
 
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.
 
#>

###############################################################################

<#
.SYNOPSIS
 Finds files by searchmask
 
.DESCRIPTION
Finds files by searchmask on every disk available in the current session
 
.PARAMETER SearchMask
Partial or full filename to look for
 
.PARAMETER File
Only find files
 
.PARAMETER Directory
Only find directories
 
.EXAMPLE
Find-Item settings.json -File
 
Find-Item node_modules -Directory
#>

function Find-Item {

    [Alias("fi")]

    param (
        [parameter(
            Mandatory = $true,
            Position = 0,
            HelpMessage = "Search phrase to look for",
            ValueFromPipeline = $false
        )]
        [string] $SearchMask,

        [Parameter(
            HelpMessage = "Files only",
            Mandatory = $false,
            ValueFromPipeline = $false
        )]
        [switch] $File,

        [Parameter(
            HelpMessage = "Directory only",
            Mandatory = $false,
            ValueFromPipeline = $false
        )]
        [switch] $Directory
    )

    Get-PSDrive -ErrorAction SilentlyContinue | ForEach-Object -ThrottleLimit 8 -Parallel {

        try {
            if ($_.Provider.Name -eq "FileSystem") {

                Get-ChildItem -Path "$($_.Root)*$SearchMask*" -File:$File -Directory:$Directory -ErrorAction SilentlyContinue

                Get-ChildItem -Path "$($_.Root)" -Directory -ErrorAction SilentlyContinue  |
                ForEach-Object -ThrottleLimit 16 -Parallel {

                    try {

                        Get-ChildItem -Path "$($_.FullName)\*$SearchMask*" -File:$File -Directory:$Directory -Recurse -ErrorAction SilentlyContinue

                    }
                    catch {

                    }

                }
            }
        }
        catch {

        }
    }
}

###############################################################################

<#
.SYNOPSIS
Expands any given file reference to a full pathname
 
.DESCRIPTION
Expands any given file reference to a full pathname, with respect to the users current directory
 
.PARAMETER FilePath
Path to expand
 
.PARAMETER CreateDirectory
Will create directory if it does not exist
 
.EXAMPLE
GetFullPath .\
 
#>

function Expand-Path {

    [CmdletBinding()]
    [Alias("ep")]

    param(
        [parameter(Mandatory, Position = 0)]
        [string] $FilePath,

        [parameter(Mandatory = $false, Position = 1)]
        [switch] $CreateDirectory = $false
    )

    # root folder included?
    if ((($FilePath.Length -gt 1) -and ($FilePath.Substring(1,1) -eq ":")) -or $FilePath.StartsWith("\\")) {

        try {

            # just normalize
            $FilePath = [System.IO.Path]::GetFullPath($FilePath);
        }
        catch {

            # keep original
        }
    }
    else {

        try {
            # combine with users current directory
            $FilePath = [System.IO.Path]::GetFullPath([System.IO.Path]::Combine($pwd, $FilePath));
        }
        catch {

            # allow powershell to try to convert it
            $FilePath = Convert-Path $FilePath;
        }
    }

    # create directory?
    if ($CreateDirectory -eq $true) {

        # get directory name
        $directory = [System.IO.Path]::GetDirectoryName($FilePath);

        # does not exist?
        if (![IO.Directory]::Exists($directory)) {

            # create it
            New-Item -ItemType Directory -Path $directory -Force
        }
    }

    # remove trailing path delimiter
    while ($FilePath.EndsWith("\") -and $FilePath.Length -gt 4) {

        $FilePath = $FilePath.SubString(0, $FilePath.Length - 1)
    }

    return $FilePath;
}

###############################################################################

<#
.SYNOPSIS
Wrapper for Microsoft's Robust Copy Utility
Copies file data from one location to another.
 
.DESCRIPTION
Wrapper for Microsoft's Robust Copy Utility
Copies file data from one location to another.
 
Robocopy, for "Robust File Copy", is a command-line directory and/or file replication command for Microsoft Windows.
Robocopy functionally replaces Xcopy, with more options. Created by Kevin Allen and first released as part of the
Windows NT 4.0 Resource Kit, it has been a standard feature of Windows since Windows Vista and Windows Server 2008.
 
Key features
 
- Folder synchronization
- Support for extra long pathnames > 256 characters
- Restartable mode backups
- Support for copying and fixing security settings
- Advanced file attribute features
- Advanced symbolic link and junction support
- Monitor mode (restart copying after change threshold)
- Optimization features for LargeFiles, multithreaded copying and network compression
- Recovery mode (copy from failing disks)
 
.PARAMETER Source
The directory, filepath, or directory+searchmask
 
.PARAMETER DestinationDirectory
The destination directory to place the copied files and directories into.
If this directory does not exist yet, all missing directories will be created.
Default value = `.\`
 
.PARAMETER FileMask
Optional searchmask for selecting the files that need to be copied.
 
.PARAMETER Mirror
Synchronizes the content of specified directories, will also delete any files and directories in the destination that do not exist in the source
 
.PARAMETER Move
Will move instead of copy all files from source to destination
 
.PARAMETER IncludeSecurity
Will also copy ownership, security descriptors and auditing information of files and directories
 
.PARAMETER SkipDirectories
Copies only files from source and skips sub-directories (no recurse)
 
.PARAMETER SkipEmptyDirectories
Does not copy directories if they would be empty
 
.PARAMETER CopyOnlyDirectoryTreeStructure
Create directory tree only
 
.PARAMETER CopyOnlyDirectoryTreeStructureAndEmptyFiles
Create directory tree and zero-length files only
 
.PARAMETER SkipAllSymbolicLinks
Don't copy symbolic links, junctions or the content they point to
 
.PARAMETER CopySymbolicLinksAsLinks
Instead of copying the content where symbolic links point to, copy the links themselves
 
.PARAMETER SkipJunctions
Don't copy directory junctions (symbolic link for a folder) or the content they point to
 
.PARAMETER SkipSymbolicFileLinks
Don't copy file symbolic links but do follow directory junctions
 
.PARAMETER CopyJunctionsAsJunctons
Instead of copying the content where junctions point to, copy the junctions themselves
 
.PARAMETER Force
Will copy all files even if they are older then the ones in the destination
 
.PARAMETER SkipFilesWithoutArchiveAttribute
Copies only files that have the archive attribute set
 
.PARAMETER ResetArchiveAttributeAfterSelection
In addition of copying only files that have the archive attribute set, will then reset this attribute on the source
 
.PARAMETER FileExcludeFilter
Exclude any files that matches any of these names/paths/wildcards
 
.PARAMETER DirectoryExcludeFilter
Exclude any directories that matches any of these names/paths/wildcards
 
.PARAMETER AttributeIncludeFilter
Copy only files that have all these attributes set [RASHCNETO]
 
.PARAMETER AttributeExcludeFilter
Exclude files that have any of these attributes set [RASHCNETO]
 
.PARAMETER SetAttributesAfterCopy
Will set the given attributes to copied files [RASHCNETO]
 
.PARAMETER RemoveAttributesAfterCopy
Will remove the given attributes from copied files [RASHCNETO]
 
.PARAMETER MaxSubDirTreeLevelDepth
Only copy the top n levels of the source directory tree
 
.PARAMETER MinFileSize
Skip files that are not at least n bytes in size
 
.PARAMETER MaxFileSize
Skip files that are larger then n bytes
 
.PARAMETER MinFileAge
Skip files that are not at least: n days old OR created before n date (if n < 1900 then n = n days, else n = YYYYMMDD date)
 
.PARAMETER MaxFileAge
Skip files that are older then: n days OR created after n date (if n < 1900 then n = n days, else n = YYYYMMDD date)
 
.PARAMETER MinLastAccessAge
Skip files that are accessed within the last: n days OR before n date (if n < 1900 then n = n days, else n = YYYYMMDD date)
 
.PARAMETER MaxLastAccessAge
Skip files that have not been accessed in: n days OR after n date (if n < 1900 then n = n days, else n = YYYYMMDD date)
 
.PARAMETER RecoveryMode
Will shortly pause and retry when I/O errors occur during copying
 
.PARAMETER MonitorMode
Will stay active after copying, and copy additional changes after a a default threshold of 10 minutes
 
.PARAMETER MonitorModeThresholdMinutes
Run again in n minutes Time, if changed
 
.PARAMETER MonitorModeThresholdNrOfChanges
Run again when more then n changes seen
 
.PARAMETER MonitorModeRunHoursFrom
Run hours - times when new copies may be started, start-time, range 0000:2359
 
.PARAMETER MonitorModeRunHoursUntil
Run hours - times when new copies may be started, end-time, range 0000:2359
 
.PARAMETER LogFilePath
If specified, logging will also be done to specified file
 
.PARAMETER LogfileOverwrite
Don't append to the specified logfile, but overwrite instead
 
.PARAMETER LogDirectoryNames
Include all scanned directory names in output
 
.PARAMETER LogAllFileNames
Include all scanned file names in output, even skipped onces
 
.PARAMETER Unicode
Output status as UNICODE
 
.PARAMETER LargeFiles
Enables optimization for copying large files
 
.PARAMETER Multithreaded
Optimize performance by doing multithreaded copying
 
.PARAMETER CompressibleContent
If applicable use compression when copying files between servers to safe bandwidth and time
 
.PARAMETER Override
Overrides, Removes, or Adds any specified robocopy parameter.
 
Usage:
 
Add or replace parameter:
 
    -Override /SwitchWithValue:'SomeValue'
 
    -Override /Switch
 
Remove parameter:
 
    -Override -/Switch
 
Multiple overrides:
 
    -Override "/ReplaceThisSwitchWithValue:'SomeValue' -/RemoveThisSwitch /AddThisSwitch"
 
.PARAMETER WhatIf
Displays a message that describes the effect of the command, instead of executing the command.
 
.EXAMPLE
Start-RoboCopy c:\videos e:\backups\videos
 
Start-RoboCopy c:\users\user\onedrive\photos\screenshots e:\backups\screenshots -Move
 
Start-RoboCopy c:\users\user\onedrive e:\backups\onedrive -Mirror
 
.LINK
https://docs.microsoft.com/en-us/windows-server/administration/windows-commands/robocopy
 
.LINK
https://en.wikipedia.org/wiki/Robocopy
 
#>

function Start-RoboCopy {
    [CmdLetBinding(
        DefaultParameterSetName = "Default",
        ConfirmImpact = "Medium"
    )]
    [Alias("xc", "rc")]
    Param
    (
        ###############################################################################

        [Parameter(
            Mandatory = $true,
            Position = 0,
            ValueFromPipeline = $false,
            HelpMessage = "The directory, filepath, or directory+searchmask"
        )]
        [string]$Source,
        ###############################################################################

        [Parameter(
            Mandatory = $false,
            Position = 1,
            ValueFromPipeline = $false,
            HelpMessage = "The destination directory to place the copied files and directories into.
            If this directory does not exist yet, all missing directories will be created.
            Default value = `".\`""

        )]
        [string]$DestinationDirectory = ".\",
        ###############################################################################

        [Parameter(
            Mandatory = $false,
            ValueFromPipeline = $false,
            Position = 2,
            HelpMessage = "Optional searchmask for selecting the files that need to be copied.
            Default value = '*'"

        )] [string[]] $Files = @(),
        ###############################################################################

        ###############################################################################
        [Parameter(
            Mandatory = $false,
            ValueFromPipeline = $false,
            HelpMessage = "Synchronizes the content of specified directories, will also delete any files and directories in the destination that do not exist in the source"
        )]
        [switch] $Mirror,
        ###############################################################################

        [Parameter(
            Mandatory = $false,
            ValueFromPipeline = $false,
            HelpMessage = "Will move instead of copy all files from source to destination"
        )]
        [switch] $Move,
        ###############################################################################

        [Parameter(
            Mandatory = $false,
            ValueFromPipeline = $false,
            HelpMessage = "Will also copy ownership, security descriptors and auditing information of files and directories"
        )]
        [switch] $IncludeSecurity,
        ###############################################################################

        ###############################################################################
        [Parameter(
            ParameterSetName = "Default",
            Mandatory = $false,
            ValueFromPipeline = $false,
            HelpMessage = "Copies only files from source and skips sub-directories (no recurse)"
        )]
        [switch] $SkipDirectories,
        ###############################################################################

        [Parameter(
            ParameterSetName = "SkipDirectories",
            Mandatory = $false,
            ValueFromPipeline = $false,
            HelpMessage = "Does not copy directories if they would be empty"
        )]
        [switch] $SkipEmptyDirectories,
        ###############################################################################

        [Parameter(
            ParameterSetName = "SkipDirectories",
            Mandatory = $false,
            ValueFromPipeline = $false,
            HelpMessage = "Create directory tree only"
        )]
        [switch] $CopyOnlyDirectoryTreeStructure,
        ###############################################################################

        [Parameter(
            Mandatory = $false,
            ValueFromPipeline = $false,
            HelpMessage = "Create directory tree and zero-length files only"
        )]
        [switch] $CopyOnlyDirectoryTreeStructureAndEmptyFiles,
        ###############################################################################

        ###############################################################################
        [Parameter(
            Mandatory = $false,
            ValueFromPipeline = $false,
            HelpMessage = "Don't copy symbolic links, junctions or the content they point to"
        )]
        [switch] $SkipAllSymbolicLinks,
        ###############################################################################

        [Parameter(
            Mandatory = $false,
            ValueFromPipeline = $false,
            HelpMessage = "Don't copy file symbolic links but do follow directory junctions"
        )]
        [switch] $SkipSymbolicFileLinks,
        ###############################################################################

        [Parameter(
            Mandatory = $false,
            ValueFromPipeline = $false,
            HelpMessage = "Instead of copying the content where symbolic links point to, copy the links themselves"
        )]
        [switch] $CopySymbolicLinksAsLinks,
        ###############################################################################

        [Parameter(

            ParameterSetName = "SkipDirectories",
            Mandatory = $false,
            ValueFromPipeline = $false,
            HelpMessage = "Don't copy directory junctions (symbolic link for a folder) or the content they point to"
        )]
        [switch] $SkipJunctions,
        ###############################################################################

        [Parameter(

            ParameterSetName = "SkipDirectories",
            Mandatory = $false,
            ValueFromPipeline = $false,
            HelpMessage = "Instead of copying the content where junctions point to, copy the junctions themselves"
        )]
        [switch] $CopyJunctionsAsJunctons,
        ###############################################################################

        ###############################################################################
        [Parameter(
            Mandatory = $false,
            ValueFromPipeline = $false,
            HelpMessage = "Will copy all files even if they are older then the ones in the destination"
        )]
        [switch] $Force,
        ###############################################################################

        ###############################################################################
        [Parameter(
            Mandatory = $false,
            ValueFromPipeline = $false,
            HelpMessage = "Copies only files that have the archive attribute set"
        )]
        [switch] $SkipFilesWithoutArchiveAttribute,
        ###############################################################################

        [Parameter(
            Mandatory = $false,
            ValueFromPipeline = $false,
            HelpMessage = "In addition of copying only files that have the archive attribute set, will then reset this attribute on the source"
        )]
        [switch] $ResetArchiveAttributeAfterSelection,
        ###############################################################################

        [Parameter(
            Mandatory = $false,
            ValueFromPipeline = $false,
            HelpMessage = "Exclude any files that matches any of these names/paths/wildcards"
        )]
        [string[]] $FileExcludeFilter = @(),
        ###############################################################################

        [Parameter(
            ParameterSetName = "SkipDirectories",
            Mandatory = $false,
            ValueFromPipeline = $false,
            HelpMessage = "Exclude any directories that matches any of these names/paths/wildcards"
        )]
        [string[]] $DirectoryExcludeFilter = @(),
        ###############################################################################

        [Parameter(
            Mandatory = $false,
            ValueFromPipeline = $false,
            HelpMessage = "Copy only files that have all these attributes set [RASHCNETO]"
        )]
        [string] $AttributeIncludeFilter,
        ###############################################################################

        [Parameter(
            Mandatory = $false,
            ValueFromPipeline = $false,
            HelpMessage = "Exclude files that have any of these attributes set [RASHCNETO]"
        )]
        [string] $AttributeExcludeFilter,
        ###############################################################################

        [Parameter(
            Mandatory = $false,
            ValueFromPipeline = $false,
            HelpMessage = "Will set the given attributes to copied files [RASHCNETO]"
        )]
        [string] $SetAttributesAfterCopy,
        ###############################################################################

        [Parameter(
            Mandatory = $false,
            ValueFromPipeline = $false,
            HelpMessage = "Will remove the given attributes from copied files [RASHCNETO]"
        )]
        [string] $RemoveAttributesAfterCopy,
        ###############################################################################

        ###############################################################################
        [ValidateRange(1, 1000000)]
        [Parameter(
            ParameterSetName = "SkipDirectories",
            Mandatory = $false,
            ValueFromPipeline = $false,
            HelpMessage = "Only copy the top n levels of the source directory tree"
        )]
        [int] $MaxSubDirTreeLevelDepth = -1,
        ###############################################################################

        [ValidateRange(0, 9999999999999999)]
        [Parameter(
            Mandatory = $false,
            ValueFromPipeline = $false,
            HelpMessage = "Skip files that are not at least n bytes in size"
        )]
        [int] $MinFileSize = -1,
        ###############################################################################

        [ValidateRange(0, 9999999999999999)]
        [Parameter(
            Mandatory = $false,
            ValueFromPipeline = $false,
            HelpMessage = "Skip files that are larger then n bytes"
        )]
        [int] $MaxFileSize = -1,
        ###############################################################################

        [ValidateRange(0, 99999999)]
        [Parameter(
            Mandatory = $false,
            ValueFromPipeline = $false,
            HelpMessage = "Skip files that are not at least: n days old OR created before n date (if n < 1900 then n = n days, else n = YYYYMMDD date)"
        )]
        [int] $MinFileAge = -1,
        ###############################################################################

        [ValidateRange(0, 99999999)]
        [Parameter(
            Mandatory = $false,
            ValueFromPipeline = $false,
            HelpMessage = "Skip files that are older then: n days OR created after n date (if n < 1900 then n = n days, else n = YYYYMMDD date)"
        )]
        [int] $MaxFileAge = -1,
        ###############################################################################

        [ValidateRange(0, 99999999)]
        [Parameter(
            Mandatory = $false,
            ValueFromPipeline = $false,
            HelpMessage = "Skip files that are accessed within the last: n days OR before n date (if n < 1900 then n = n days, else n = YYYYMMDD date)"
        )]
        [int] $MinLastAccessAge = -1,
        ###############################################################################

        [ValidateRange(0, 99999999)]
        [Parameter(
            Mandatory = $false,
            ValueFromPipeline = $false,
            HelpMessage = "Skip files that have not been accessed in: n days OR after n date (if n < 1900 then n = n days, else n = YYYYMMDD date)"
        )]
        [int] $MaxLastAccessAge = -1,
        ###############################################################################

        ###############################################################################
        [Parameter(
            Mandatory = $false,
            ValueFromPipeline = $false,
            HelpMessage = "Will shortly pause and retry when I/O errors occur during copying"
        )]
        [switch] $RecoveryMode,
        ###############################################################################

        ###############################################################################
        [Parameter(
            Mandatory = $false,
            ValueFromPipeline = $false,
            HelpMessage = "Will stay active after copying, and copy additional changes after a a default threshold of 10 minutes"
        )]
        [switch] $MonitorMode,
        ###############################################################################

        [ValidateRange(1, 144000)]
        [Parameter(
            Mandatory = $false,
            ValueFromPipeline = $false,
            HelpMessage = "Run again in n minutes Time, if changed"
        )]
        [int] $MonitorModeThresholdMinutes = -1,
        ###############################################################################

        [ValidateRange(1, 1000000000)]
        [Parameter(
            Mandatory = $false,
            ValueFromPipeline = $false,
            HelpMessage = "Run again when more then n changes seen"
        )]
        [int] $MonitorModeThresholdNrOfChanges = -1,
        ###############################################################################

        [ValidateRange(0, 2359)]
        [Parameter(
            Mandatory = $false,
            ValueFromPipeline = $false,
            HelpMessage = "Run hours - times when new copies may be started, start-time, range 0000:2359"
        )]
        [int] $MonitorModeRunHoursFrom = -1,
        ###############################################################################

        [ValidateRange(0, 2359)]
        [Parameter(
            Mandatory = $false,
            ValueFromPipeline = $false,
            HelpMessage = "Run hours - times when new copies may be started, end-time, range 0000:2359"
        )]
        [int] $MonitorModeRunHoursUntil = -1,
        ###############################################################################

        ###############################################################################
        [Parameter(
            Mandatory = $false,
            ValueFromPipeline = $false,
            HelpMessage = "If specified, logging will also be done to specified file"
        )]
        [string] $LogFilePath,
        ###############################################################################

        [Parameter(
            Mandatory = $false,
            ValueFromPipeline = $false,
            HelpMessage = "Don't append to the specified logfile, but overwrite instead"
        )]
        [switch] $LogfileOverwrite,
        ###############################################################################

        [Parameter(
            Mandatory = $false,
            ValueFromPipeline = $false,
            HelpMessage = "Include all scanned directory names in output"
        )]
        [switch] $LogDirectoryNames,
        ###############################################################################

        [Parameter(
            Mandatory = $false,
            ValueFromPipeline = $false,
            HelpMessage = "Include all scanned file names in output, even skipped onces"
        )]
        [switch] $LogAllFileNames,
        ###############################################################################

        [Parameter(
            Mandatory = $false,
            ValueFromPipeline = $false,
            HelpMessage = "Output status as UNICODE"
        )]
        [switch] $Unicode,
        ###############################################################################

        ###############################################################################
        [Parameter(
            Mandatory = $false,
            ValueFromPipeline = $false,
            HelpMessage = "Enables optimization for copying large files"
        )]
        [switch] $LargeFiles,
        ###############################################################################

        [Parameter(
            Mandatory = $false,
            ValueFromPipeline = $false,
            HelpMessage = "Optimize performance by doing multithreaded copying"
        )]
        [switch] $MultiThreaded,
        ###############################################################################

        [Parameter(
            Mandatory = $false,
            ValueFromPipeline = $false,
            HelpMessage = "If applicable use compression when copying files between servers to safe bandwidth and time"
        )]
        [switch] $CompressibleContent,
        ###############################################################################

        [Parameter(
            Mandatory = $false,
            ValueFromPipeline = $false,
            ValueFromRemainingArguments = $true,
            Position = 3,
            HelpMessage = "Overrides, Removes, or Adds any specified robocopy parameter.
 
Usage:
 
Add or replace parameter:
 
    -Override /SwitchWithValue:'SomeValue'
 
    -Override /Switch
 
Remove parameter:
 
    -Override -/Switch
 
Multiple overrides:
 
    -Override `"/ReplaceThisSwitchWithValue:'SomeValue' -/RemoveThisSwitch /AddThisSwitch`"
"

        )]
        [string] $Override,
        ###############################################################################

        ###############################################################################
        [Parameter(
            Mandatory = $false,
            ValueFromPipeline = $false,
            HelpMessage = "Displays a message that describes the effect of the command, instead of executing the command."
        )]
        [switch] $WhatIf
    )

    Begin {

        ###############################################################################

        # initialize settings
        $RobocopyPath = "$env:SystemRoot\system32\robocopy.exe";

        # normalize to current directory
        $Source = Expand-Path $Source
        $DestinationDirectory = Expand-Path $DestinationDirectory

        # source is not an existing directory?
        if ([IO.Directory]::Exists($Source) -eq $false) {

            # split directory and filename
            $SourceSearchMask = [IO.Path]::GetFileName($Source);
            $SourceDirOnly = [IO.Path]::GetDirectoryName($Source);

            # does parent directory exist?
            if ([IO.Directory]::Exists($SourceDirOnly)) {

                # ..but the supplied source parameter is not an existing file?
                if ([IO.File]::Exists($Source) -eq $false) {

                    # ..and the supplied filename is not searchMask?
                    if (!$SourceSearchMask.Contains("*") -and !$SourceSearchMask.Contains("?")) {

                        throw "Could not find source: $Source"
                    }
                }

                $Mirror = $false;
            }

            # reconfigure
            $Source = $SourceDirOnly;
            if ($Files -notcontains $SourceSearchMask) {

                $Files = $Files + @($SourceSearchMask);
            }
        }

        # default value
        if ($Files.Length -eq 0) {

            $Files = @("*");
        }

        # destination directory does not exist yet?
        if ([IO.Directory]::Exists($DestinationDirectory) -eq $false) {

            # create it
            [IO.Directory]::CreateDirectory($DestinationDirectory) | Out-Null
        }

        # Turn on verbose
        $VerbosePreference = "Continue"

        ###############################################################################

        function CurrentUserHasElivatedRights() {

            $id = [System.Security.Principal.WindowsIdentity]::GetCurrent()
            $p = New-Object System.Security.Principal.WindowsPrincipal($id)

            if ($p.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator) -or
                $p.IsInRole([System.Security.Principal.WindowsBuiltInRole]::BackupOperator)) {

                return $true;
            }

            return $false;
        }

        function ConstructFileFilterSet([string[]] $FileFilterSet, [string] $CommandName) {

            $result = "";

            $FileFilterSet | ForEach-Object {

                $result = "$result '$PSItem'".Trim()
            }

            return $result;
        }

        function SanitizeAttributeSet([string] $AttributeSet, [string] $CommandName) {

            $AttributeSetNew = "";
            $AttributeSet.Replace("[", "").Replace("]", "").ToUpperInvariant().ToCharArray() | ForEach-Object {

                if (("RASHCNETO".IndexOf($PSItem) -ge 0) -and ($AttributeSetNew.IndexOf($PSItem) -lt 0)) {

                    $AttributeSetNew = "$AttributeSet$PSItem";
                }
                else {

                    throw "Could not parse parameter -$CommandName $AttributeSet - '$PSItem' is not valid
    possible attributes to combine: [RASHCNETO]
 
    R - Read only
    A - Archive
    S - System
    H - Hidden
    C - Compressed
    N - Not content indexed
    E - Encrypted
    T - Temporary
    O - Offline
"

                }
            }

            return $AttributeSetNew
        }

        function CheckAgeInteger([int] $AgeValue, [string] $CommandName) {

            if ($AgeValue -ge 1900) {

                [DateTime] $date;
                if ([DateTime]::TryParse("$MaxFileAge", [ref] $date) -eq $false) {

                    throw "Could not parse parameter '-$CommandName $AgeValue as a valid date (if n < 1900 then n = n days, else n = YYYYMMDD date)"
                }
            }
        }

        function getSwitchesDictionary([string] $Switches) {

            # initialize
            $switchesDictionary = New-Object "System.Collections.Generic.Dictionary[String, String]";

            if ([String]::IsNullOrWhiteSpace($Switches)) {

                return $switchesDictionary
            }

            $switchesCleaned = " $Switches ";

            # remove spaces
            while ($switchesCleaned.IndexOf(" /") -ge 0) {

                $switchesCleaned = $switchesCleaned.Replace(" /", " /");
            }
            while ($switchesCleaned.IndexOf(" -/") -ge 0) {

                $switchesCleaned = $switchesCleaned.Replace(" -/", " -/");
            }

            # split up
            $allSwitches = $switchesCleaned.Replace(" -/", " /-").Split([string[]]@(" /"), [System.StringSplitOptions]::RemoveEmptyEntries);

            # enumerate switches
            $allSwitches | ForEach-Object -ErrorAction SilentlyContinue {

                # add to Dictionary
                $switchesDictionary["$($PSItem.Trim().Split(" ")[0].Split(":" )[0].Trim().ToUpperInvariant())"] = $PSItem.Trim()
            }

            return $switchesDictionary;
        }

        function overrideAndCleanSwitches([string] $Switches) {

            $autoGeneratedSwitches = (getSwitchesDictionary $Switches)
            $overridenSwitches = (getSwitchesDictionary $Override)
            $newSwitches = "";

            $autoGeneratedSwitches.GetEnumerator() | ForEach-Object -ErrorAction SilentlyContinue {

                # should NOT remove it?
                if (!$overridenSwitches.ContainsKey("-$($PSItem.Key)")) {

                    # should replace it?
                    if ($overridenSwitches.ContainsKey($PSItem.Key)) {

                        $newSwitches += " /$($overridenSwitches[$PSItem.Key])"
                    }
                    else {

                        # keep the autogenerated switch
                        $newSwitches += " /$($PSItem.Value)"
                    }
                }
            }

            $overridenSwitches.GetEnumerator() | ForEach-Object -ErrorAction SilentlyContinue {

                # not already processed above?
                if (!$PSItem.Key.StartsWith("-") -and !$autoGeneratedSwitches.ContainsKey("$($PSItem.Key)")) {

                    # add it
                    $newSwitches += " /$($PSItem.Value)"
                }
            }

            return $newSwitches.Trim();
        }

        ###############################################################################

        # /B â–ˆ copy files in Backup mode.
        # /ZB â–ˆ use restartable mode; if access denied use Backup mode.
        if (CurrentUserHasElivatedRights) {

            $ParamMode = "/B"
        }
        else {
            $ParamMode = ""
        }

        # /MOV â–ˆ MOVE files AND dirs (delete from source after copying).
        $ParamMOV = "";

        # /MIR â–ˆ MIRror a directory tree (equivalent to /E plus /PURGE).
        $ParamMIR = "";

        # /SECFIX â–ˆ FIX file SECurity on all files, even skipped files.
        $ParamSECFIX = "";

        # /E â–ˆ copy subdirectories, including Empty ones.
        # /S â–ˆ copy Subdirectories, but not empty ones.
        $ParamDirs = "/E"

        # /COPY â–ˆ what to COPY for files (default is /COPY:DAT).
        $ParamCOPY = "/COPY:DAT"

        # /XO â–ˆ eXclude Older files.
        $ParamXO = "/XO";

        # /IM â–ˆ Include Modified files (differing change times).
        $ParamIM = "/IM";

        # /IT â–ˆ Include Tweaked files.
        $ParamIT = "/IT";

        # /IS â–ˆ Include Same files.
        $ParamIS = "";

        # /EFSRAW â–ˆ copy all encrypted files in EFS RAW mode.
        $ParamEFSRAW = "";

        # /NOOFFLOAD â–ˆ copy files without using the Windows Copy Offload mechanism.
        $ParamNOOFFLOAD = "";

        # /R â–ˆ number of Retries on failed copies: default 1 million.
        $ParamR = "/R:0";

        # /W â–ˆ Wait time between retries: default is 30 seconds.
        $ParamW = "/W:0";

        # /J â–ˆ copy using unbuffered I/O (recommended for large files).
        $paramJ = "";

        # /MT â–ˆ Do multi-threaded copies with n threads (default 8).
        $paramMT = "";

        # /NDL â–ˆ No Directory List - don't log directory names.
        $ParamNDL = "/NDL";

        # /X â–ˆ report all eXtra files, not just those selected.
        $ParamX = "";

        # /V â–ˆ produce Verbose output, showing skipped files.
        $ParamV = "";

        # /CREATE â–ˆ CREATE directory tree and zero-length files only.
        $ParamCREATE = "";

        # /XJ â–ˆ eXclude symbolic links (for both files and directories) and Junction points.
        $ParamXJ = "";

        # /XJD â–ˆ eXclude symbolic links for Directories and Junction points.
        $ParamXJD = "";

        # /XJF â–ˆ eXclude symbolic links for Files.
        $ParamXJF = "";

        # /SJ â–ˆ copy Junctions as junctions instead of as the junction targets.
        $ParamSJ = "";

        # /SL â–ˆ copy Symbolic Links as links instead of as the link targets.
        $ParamSL = "";

        # /A â–ˆ copy only files with the Archive attribute set.
        # /M â–ˆ copy only files with the Archive attribute and reset it.
        $ParamArchive = "";

        # /XF
        $ParamXF = "" # â–ˆ eXclude Files matching given names/paths/wildcards.

        # /XD
        $ParamXD = "" # â–ˆ eXclude Directories matching given names/paths/wildcards.

        # /IA â–ˆ Include only files with any of the given Attributes set.
        $ParamIA = "";

        # /XA â–ˆ eXclude files with any of the given Attributes set.
        $ParamXA = "";

        # /A+ â–ˆ add the given Attributes to copied files
        $ParamAttrSet = "";

        # /A- â–ˆ remove the given Attributes from copied files.
        $ParamAttrRemove = "";

        # /LEV â–ˆ only copy the top n LEVels of the source directory tree.
        $ParamLEV = "";

        # /MIN â–ˆ MINimum file size - exclude files smaller than n bytes.
        $ParamMIN = "";

        # /MAX â–ˆ MAXimum file size - exclude files bigger than n bytes.
        $ParamMAX = "";

        # /MINAGE â–ˆ MINimum file AGE - exclude files newer than n days/date.
        $ParamMINAGE = "";

        # /MAXAGE â–ˆ MAXimum file AGE - exclude files older than n days/date.
        $ParamMaxAGE = "";

        # /LOG â–ˆ output status to LOG file (overwrite existing log).
        # /LOG+ â–ˆ output status to LOG file (append to existing log).
        $ParamLOG = "";

        # /TEE â–ˆ output to console window, as well as the log file.
        $ParamTee = "";

        # /UNICODE â–ˆ output status as UNICODE.
        $ParamUnicode = "";

        # /RH â–ˆ Run Hours - times when new copies may be started.
        $ParamRH = "";

        # /MON â–ˆ MONitor source; run again when more than n changes seen.
        # /MOT â–ˆ MOnitor source; run again in m minutes Time, if changed.
        $ParamMON = "";

        # /MAXLAD â–ˆ MAXimum Last Access Date - exclude files unused since n.
        $ParamMAXLAD = "";

        # /MINLAD â–ˆ MINimum Last Access Date - exclude files used since n.
        $ParamMINLAD = "";

        # /COMPRESS â–ˆ Request network compression during file transfer, if applicable.
        $ParamCOMPRESS = "";

        ###############################################################################

        # -Mirror âžœ Synchronizes the content of specified directories, will also delete any files and directories in the destination that do not exist in the source
        if ($Mirror -eq $true) {

            $ParamMIR = "/MIR" # â–ˆ MIRror a directory tree (equivalent to /E plus /PURGE).
        }

        # -Move âžœ Will move instead of copy all files from source to destination
        if ($Move -eq $true) {

            $ParamMOV = "/MOV" # â–ˆ MOVE files AND dirs (delete from source after copying).
        }

        # -IncludeSecurity âžœ Will also copy ownership, security descriptors and auditing information of files and directories
        if ($IncludeSecurity -eq $true) {

            $ParamSECFIX = "/SECFIX" # â–ˆ FIX file SECurity on all files, even skipped files.
            $ParamCOPY = "$($ParamCOPY)SOU" # â–ˆ what to COPY for files (default is /COPY:DAT).
            $ParamEFSRAW = "/EFSRAW" # â–ˆ copy all encrypted files in EFS RAW mode.
        }

        # -SkipDirectories âžœ Copies only files from source and skips sub-directories (no recurse)
        if ($SkipDirectories -eq $true) {

            $ParamDirs = "" # â–ˆ copy subdirectories, including Empty ones.
        }
        else {

            # -SkipEmptyDirectories âžœ Does not copy directories if they would be empty
            if ($SkipEmptyDirectories -eq $true) {

                $ParamDirs = "/S" # â–ˆ copy Subdirectories, but not empty ones.
            }
        }

        # -CopyOnlyDirectoryTreeStructure âžœ Create directory tree only
        if ($CopyOnlyDirectoryTreeStructure -eq $true) {

            $ParamCREATE = "/CREATE"; # â–ˆ CREATE directory tree and zero-length files only.
            $Files = @("DontCopy4nyF1lés") # â–ˆ File(s) to copy (names/wildcards: default is "*.*")
        }
        else {
            # -CopyOnlyDirectoryTreeStructureAndEmptyFiles âžœ Create directory tree and zero-length files only
            if ($CopyOnlyDirectoryTreeStructureAndEmptyFiles -eq $true) {

                $ParamCREATE = "/CREATE"; # â–ˆ CREATE directory tree and zero-length files only.
            }
        }

        # -SkipAllSymbolicLinks âžœ Don't copy symbolic links, junctions or the content they point to
        if ($SkipAllSymbolicLinks -eq $true) {

            $ParamXJ = "/XJ"; # â–ˆ eXclude symbolic links (for both files and directories) and Junction points.
        }
        else {

            # -SkipSymbolicFileLinks âžœ Don't copy file symbolic links but do follow directory junctions
            if ($SkipSymbolicFileLinks -eq $true) {

                $ParamXJF = "/XJF"; # â–ˆ eXclude symbolic links for Files.
            }
            else {

                # -CopySymbolicLinksAsLinks âžœ Instead of copying the content where symbolic links point to, copy the links themselves
                if ($CopySymbolicLinksAsLinks -eq $true) {

                    $ParamSL = "/SL"; # â–ˆ copy Symbolic Links as links instead of as the link targets.
                }
            }

            # -SkipJunctions âžœ Don't copy directory junctions (symbolic link for a folder) or the content they point to
            if ($SkipJunctions -eq $true) {

                $ParamXJD = "/XJD"; # â–ˆ eXclude symbolic links for Directories and Junction points.
            }
            else {

                # -CopyJunctionsAsJunctons âžœ Instead of copying the content where junctions point to, copy the junctions themselves
                if ($CopyJunctionsAsJunctons -eq $true) {

                    $ParamSJ = "/SJ"; # â–ˆ copy Junctions as junctions instead of as the junction targets.
                }
            }
        }

        ###############################################################################

        # -Force âžœ Will copy all files even if they are older then the ones in the destination
        if ($Force -eq $true) {

            $ParamXO = "" # â–ˆ eXclude Older files.
            $ParamIT = "/IT" # â–ˆ Include Tweaked files.
            $ParamIS = "/IS" # â–ˆ Include Same files.
        }

        ###############################################################################

        # -SkipFilesWithoutArchiveAttribute âžœ Copies only files that have the archive attribute set
        if ($SkipFilesWithoutArchiveAttribute -eq $true) {

            $ParamArchive = "/A" # â–ˆ copy only files with the Archive attribute set.
        }

        # -ResetArchiveAttributeAfterSelection âžœ In addition of copying only files that have the archive attribute set, will then reset this attribute on the source
        if ($ResetArchiveAttributeAfterSelection -eq $true) {

            $ParamArchive = "/M" # â–ˆ copy only files with the Archive attribute and reset it
        }

        ###############################################################################

        # -FileExcludeFilter âžœ Exclude any files that matches any of these names/paths/wildcards
        if ($FileExcludeFilter.Length -gt 0) {

            $Filter = "$((ConstructFileFilterSet $FileExcludeFilter "FileExcludeFilter"))";
            $ParamXF = "/XF $Filter" # â–ˆ eXclude Files matching given names/paths/wildcards.
        }

        # -DirectoryExcludeFilter âžœ Exclude any directories that matches any of these names/paths/wildcards
        if ($DirectoryExcludeFilter.Length -gt 0) {

            $Filter = "$((ConstructFileFilterSet $DirectoryExcludeFilter "DirectoryExcludeFilter"))";
            $ParamXD = "/XD $Filter" # â–ˆ eXclude Directories matching given names/paths/wildcards.
        }

        # -AttributeIncludeFilter âžœ Copy only files that have all these attributes set [RASHCNETO]
        if ([string]::IsNullOrWhiteSpace($AttributeIncludeFilter) -eq $false) {

            $Filter = "$((SanitizeAttributeSet $AttributeIncludeFilter "AttributeIncludeFilter"))";
            $ParamIA = "/IA:$Filter" # â–ˆ Include only files with any of the given Attributes set.
        }

        # -AttributeExcludeFilter âžœ Exclude files that have any of these attributes set [RASHCNETO]
        if ([string]::IsNullOrWhiteSpace($AttributeExcludeFilter) -eq $false) {

            $Filter = "$((SanitizeAttributeSet $AttributeExcludeFilter "AttributeExcludeFilter"))";
            $ParamXA = "/XA:$Filter" # â–ˆ eXclude files with any of the given Attributes set.
        }

        # -SetAttributesAfterCopy âžœ Will set the given attributes to copied files [RASHCNETO]
        if ([string]::IsNullOrWhiteSpace($SetAttributesAfterCopy) -eq $false) {

            $Filter = "$((SanitizeAttributeSet $SetAttributesAfterCopy "SetAttributesAfterCopy"))";
            $ParamAttrSet = "/A+:$Filter" # â–ˆ add the given Attributes to copied files
        }

        # -RemoveAttributesAfterCopy âžœ Will remove the given attributes from copied files [RASHCNETO]
        if ([string]::IsNullOrWhiteSpace($RemoveAttributesAfterCopy) -eq $false) {

            $Filter = "$((SanitizeAttributeSet $RemoveAttributesAfterCopy "RemoveAttributesAfterCopy"))";
            $ParamAttrRemove = "/A+:$Filter" # â–ˆ remove the given Attributes from copied files.
        }

        # -MaxSubDirTreeLevelDepth âžœ Only copy the top n levels of the source directory tree
        if ($MaxSubDirTreeLevelDepth -ge 0) {

            $ParamLEV = "/LEV:$MaxSubDirTreeLevelDepth" # â–ˆ only copy the top n LEVels of the source directory tree.
        }

        # -MinFileSize âžœ Skip files that are not at least n bytes in size
        if ($MinFileSize -ge 0) {

            $ParamMIN = "/MIN:$MinFileSize" # â–ˆ MINimum file size - exclude files smaller than n bytes.
        }

        # -MaxFileSize âžœ Skip files that are larger then n bytes
        if ($MaxFileSize -ge 0) {

            $ParamMAX = "/MAX:$MinFileSize" # â–ˆ MAXimum file size - exclude files bigger than n bytes.
        }

        # -MinFileAge âžœ Skip files that are not at least: n days old OR created before n date (if n < 1900 then n = n days, else n = YYYYMMDD date)
        if ($MinFileAge -ge 0) {

            CheckAgeInteger $MinFileAge "MinFileAge"

            $ParamMINAGE = "/MINAGE:$MinFileAge" # â–ˆ MINimum file AGE - exclude files newer than n days/date.
        }

        # -MaxFileAge âžœ Skip files that are older then: n days OR created after n date (if n < 1900 then n = n days, else n = YYYYMMDD date)
        if ($MaxFileAge -ge 0) {

            CheckAgeInteger $MaxFileAge "MaxFileAge"

            $ParamMaxAGE = "/MAXAGE:$MaxFileAge" # â–ˆ MAXimum file AGE - exclude files older than n days/date.
        }

        # -MinLastAccessAge âžœ Skip files that are accessed within the last: n days OR before n date (if n < 1900 then n = n days, else n = YYYYMMDD date)
        if ($MinLastAccessAge -ge 0) {

            CheckAgeInteger $MinLastAccessAge "MinLastAccessAge"

            $ParamMINLAD = "/MINLAD:$MinLastAccessAge" # â–ˆ MINimum Last Access Date - exclude files used since n.
        }

        # -MaxLastAccessAge âžœ Skip files that have not been accessed in: n days OR after n date (if n < 1900 then n = n days, else n = YYYYMMDD date)
        if ($MaxLastAccessAge -ge 0) {

            CheckAgeInteger $MaxLastAccessAge "MaxLastAccessAge"

            $ParamMAXLAD = "/MAXLAD:$MaxLastAccessAge" # â–ˆ MAXimum Last Access Date - exclude files unused since n.
        }

        ###############################################################################

        # -RecoveryMode âžœ Will shortly pause and retry when I/O errors occur during copying
        if ($RecoveryMode -eq $true) {

            $ParamNOOFFLOAD = "/NOOFFLOAD" # â–ˆ copy files without using the Windows Copy Offload mechanism.
            $ParamR = "/R:25" # â–ˆ number of Retries on failed copies: default 1 million.
            $ParamW = "/W:1" # â–ˆ Wait time between retries: default is 30 seconds.
        }

        ###############################################################################

        # -MonitorMode âžœ Will stay active after copying, and copy additional changes after a a default threshold of 10 minutes
        if ($MonitorMode -eq $true) {

            $ParamMON = "/MOT:10" # â–ˆ MOnitor source; run again in m minutes Time, if changed.
        }

        # -MonitorModeThresholdMinutes âžœ Run again in n minutes Time, if changed
        if ($MonitorModeThresholdMinutes -ge 0) {

            $MotArgs = $MonitorModeThresholdMinutes;
            $ParamMON = "/MOT:$MotArgs" # â–ˆ MOnitor source; run again in m minutes Time, if changed.
        }

        # -MonitorModeThresholdNrOfChanges âžœ Run again when more then n changes seen
        if ($MonitorModeThresholdNrOfChanges -ge 0) {

            $MonArgs = $MonitorModeThresholdNrOfChanges
            $ParamMON = "/MON:$MonArgs" # â–ˆ MONitor source; run again when more than n changes seen.
        }

        if (($MonitorModeRunHoursFrom -ge 0) -or ($MonitorModeRunHoursUntil -ge 0)) {

            # -MonitorModeRunHoursFrom âžœ Run hours - times when new copies may be started, start-time, range 0000:2359
            if ($MonitorModeRunHoursFrom -ge 0) {

                $MonitorModeRunHoursFromStr = "$MonitorModeRunHoursFrom".PadLeft("0", 4);
                [int] $FromMinute = $MonitorModeRunHoursFromStr.Substring(2, 2);
                if ($FromMinute -lt 59) {

                    throw "Could not parse '-MonitorModeRunHoursFrom $MonitorModeRunHoursFromStr' parameter, range 0000:2359"
                }
            }
            else {
                $MonitorModeRunHoursFromStr = "0000";
            }

            # -MonitorModeRunHoursUntil âžœ Run hours - times when new copies may be started, end-time, range 0000:2359
            if ($MonitorModeRunHoursUntil -ge 0) {

                $MonitorModeRunHoursUntilStr = "$MonitorModeRunHoursUntil".PadLeft("0", 4);
                [int] $UntilMinute = $MonitorModeRunHoursUntilStr.Substring(2, 2);

                if ($UntilMinute -lt 59) {

                    throw "Could not parse '-MonitorModeRunHoursUntil $MonitorModeRunHoursUntilStr' parameter, range 0000:2359"
                }
            }
            else {
                $MonitorModeRunHoursUntilStr = "2359"
            }

            $RHArgs = "$MonitorModeRunHoursFromStr-$MonitorModeRunHoursUntilStr"
            $ParamRH = "/RH:$RHArgs" # â–ˆ Run Hours - times when new copies may be started.
        }

        ###############################################################################

        # -Unicode -> Output status as UNICODE
        if ($Unicode -eq $true) {

            $ParamUnicode = "/UNICODE" # â–ˆ output status as UNICODE.
        }

        # -LogFilePath âžœ If specified, logging will also be done to specified file
        if ([string]::IsNullOrWhiteSpace($LogFilePath) -eq $false) {

            $LogArgs = "'$((Expand-Path $LogFilePath $true).ToString().Replace("'", "''"))'"
            $LogPrefix = "";
            $ParamTee = "/TEE" # â–ˆ output to console window, as well as the log file

            # -Unicode -> Output status as UNICODE
            if ($Unicode -eq $true) { $LogPrefix = "UNI"; }

            # -LogfileOverwrite âžœ Don't append to the specified logfile, but overwrite instead
            if ($LogfileOverwrite -eq $true) {

                $ParamLOG = "/$($LogPrefix)LOG:$LogArgs" #â–ˆ output status to LOG file (overwrite existing log).
            }
            else {

                $ParamLOG = "/$($LogPrefix)LOG+:$LogArgs"#â–ˆ output status to LOG file (append to existing log).
            }
        }

        # -LogDirectoryNames âžœ Include all scanned directory names in output
        if ($LogDirectoryNames -eq $true) {

            $ParamNDL = "" # â–ˆ No Directory List - don't log directory names.
        }

        # -LogAllFileNames âžœ Include all scanned file names in output, even skipped onces
        if ($LogAllFileNames -eq $true) {

            $ParamX = "/X" # â–ˆ report all eXtra files, not just those selected.
            $ParamV = "/V" # â–ˆ produce Verbose output, showing skipped files.
        }

        # -LargeFiles âžœ Enables optimization for copying large files
        if ($LargeFiles -eq $true) {

            $ParamMode = "/ZB" # â–ˆ use restartable mode; if access denied use Backup mode.
            $paramJ = "/J" # â–ˆ copy using unbuffered I/O (recommended for large files).
        }

        # -LargeFiles âžœ Optimize performance by doing multithreaded copying
        if ($MultiThreaded -eq $true) {

            $paramMT = "/MT:16" # â–ˆ Do multi-threaded copies with n threads (default 8).
        }

        # -CompressibleContent âžœ If applicable use compression when copying files between servers to safe bandwidth and time
        if ($CompressibleContent -eq $true) {

            $ParamCOMPRESS = "/COMPRESS" # â–ˆ Request network compression during file transfer, if applicable.
        }

        ###############################################################################

        $switches = "$ParamDirs /TS /FP $ParamMode /DCOPY:DAT /NP $ParamMT $ParamMOV $ParamMIR $ParamSECFIX $ParamCOPY $ParamXO $ParamIM $ParamIT $ParamIS $ParamEFSRAW $ParamNOOFFLOAD $ParamR $ParamW $paramJ $ParamNDL $ParamX $ParamV $ParamCREATE $ParamXJ $ParamXJD $ParamXJF $ParamSJ $ParamSL $ParamArchive $ParamIA $ParamXA $ParamAttrSet $ParamAttrRemove $ParamLEV $ParamMIN $ParamMAX $ParamMINAGE $ParamMaxAGE $ParamLOG $ParamTee $ParamUnicode $ParamRH $ParamMON $ParamMON $ParamMAXLAD $ParamMINLAD $ParamCOMPRESS $ParamXF $ParamXD".Replace(" ", " ").Trim();
        $switchesCleaned = overrideAndCleanSwitches($switches)
        $FilesArgs = ConstructFileFilterSet $Files "FileMask"
        $cmdLine = "& '$($RobocopyPath.Replace("'", "''"))' '$($Source.Replace("'", "''"))' '$($DestinationDirectory.Replace("'", "''"))' $FilesArgs $switchesCleaned"
    }
    Process {

        # WHAT IF?
        if ($WhatIf -or $WhatIfPreference) {

            # collect param help information
            $paramList = @{};
            (& $RobocopyPath -?) | ForEach-Object {
                if ($PSItem.Contains(" :: ")) {
                    $s = $PSItem.Split([string[]]@(" :: "), [StringSplitOptions]::RemoveEmptyEntries);
                    $paramList."$($s[0].ToLowerInvariant().split(":")[0].Split("[")[0].Trim().split(" ")[0])" = $s[1];
                }
            };

            $first = $true;
            $paramsExplained = @(

                " $switchesCleaned ".Split([string[]]@(" /"), [System.StringSplitOptions]::RemoveEmptyEntries) |
                ForEach-Object {

                    $description = $paramList."/$($PSItem.ToLowerInvariant().split(":")[0].Split("[")[0].Trim().split(" ")[0])"
                    $Space = " "; if ($first) { $Space = ""; $first = $false; }
                    "$Space/$($PSItem.ToUpperInvariant().split(":")[0].Split("[")[0].Trim().split(" ")[0].PadRight(15)) --> $description`r`n"
                }
            );

            Write-Host "
 
            RoboCopy would be executed as:
                $($cmdLine.Replace(" /L ", " "))
 
            Source : $Source
            Files : $Files
            Destination : $DestinationDirectory
 
            Mirror : $($Mirror -eq $true)
            Move : $($Move -eq $true)
 
            Switches : $paramsExplained
 
"

            return;
        }

        Invoke-Expression $cmdLine
    }
    End {

    }
}

###############################################################################

<#
.SYNOPSIS
Performs a case sensitive text replacement throughout a project
 
.DESCRIPTION
Performs a rename action throughout a project folder. It will skip .git and .svn folders, images, archives and other common known binaries.
But will rename within other files, like sourcecode, json, html, etc, AND folders and filenames!
Always perform a -WhatIf operation first, to validate the actions it will take.
 
.PARAMETER Source
The directory, filepath, or directory+searchmask
 
.PARAMETER FindText
The case sensitive phrase to search for, when making replacements
 
.PARAMETER ReplacementText
The text that will replace the found occurance
 
.PARAMETER WhatIf
Displays a message that describes the effect of the command, instead of executing the command.
 
.EXAMPLE
Rename-InProject -Source .\src\*.js -FindText "tsconfig.json" - ReplacementText "typescript.configuration.json"
 
Rename-InProject .\src\ "MyCustomClass" "MyNewRenamedClass" -WhatIf
 
.NOTES
Be carefull, use -WhatIf
#>

function Rename-InProject {

    [Alias("rip")]

    Param
    (
        [Parameter(
            Mandatory = $false,
            ValueFromPipeline = $false,
            Position = 0,
            HelpMessage = "Path of folder that gets cloned")]
        [Alias("src", "s")]
        [PSDefaultValue(Value = ".\\")]
        [string] $Source,

        [Parameter(
            Mandatory = $true,
            ValueFromPipeline = $false,
            Position = 1,
            HelpMessage = "string to match (case sensitive)")]
        [Alias("find", "what", "from")]
        [string] $FindText,

        [Parameter(
            Mandatory = $true,
            ValueFromPipeline = $false,
            Position = 2,
            HelpMessage = "The replacement text")]
        [Alias("into", "txt", "to")]
        [string] $ReplacementText,

        [Parameter(
            Mandatory = $false,
            ValueFromPipeline = $false,
            HelpMessage = "Only show what actions would be taken")]
        [Switch] $WhatIf
    )

    Begin {

        # normalize to current directory
        $Source = Expand-Path $Source
        $Files = "*"

        # source is not an existing directory?
        if ([IO.Directory]::Exists($Source) -eq $false) {

            # split directory and filename
            $SourceSearchMask = [IO.Path]::GetFileName($Source);
            $SourceDirOnly = [IO.Path]::GetDirectoryName($Source);

            # does parent directory exist?
            if ([IO.Directory]::Exists($SourceDirOnly)) {

                # ..but the supplied source parameter is not an existing file?
                if ([IO.File]::Exists($Source) -eq $false) {

                    # ..and the supplied filename is not searchMask?
                    if ($Files -notcontains "*" -and $Files -notcontains "?") {

                        throw "Could not find source: $Source"
                    }
                }
            }

            # reconfigure
            $Source = $SourceDirOnly;
            $Files = $SourceSearchMask;
        }

        # not actually making any changes?
        if ($WhatIfPreference -or $WhatIf) {

            # Turn on verbose
            $VerbosePreference = "Continue"
        }
    }

    Process {

        if ([String]::IsNullOrWhiteSpace($Source)) {

            $Source = ".\";
        }

        if ([String]::IsNullOrWhiteSpace($findText)) {

            return;
        }

        # gets all files by directory and searchmask recursively, but skips repositories
        function GetAllFilesFromAllDirectories([string] $parentDirectory, [string] $searchMask) {

            # create new list
            [System.Collections.Generic.List[string]] $result = New-Object "System.Collections.Generic.List[string]"

            # is a repository?
            if ([System.IO.Path]::GetFileName($parentDirectory) -eq ".svn" -or [System.IO.Path]::GetFileName($parentDirectory) -eq ".git") {

                # skip
                return $result.ToArray();
            }

            # add all files by searchmask
            foreach ($file in [IO.Directory]::GetFiles($parentDirectory, $searchMask)) {

                $result.Add($file);
            }

            # enumerate all sub-directories
            foreach ($directory in [IO.Directory]::GetDirectories($parentDirectory, "*", [System.IO.SearchOption]::AllDirectories)) {

                # no match?
                if ([System.IO.Path]::GetFileName($directory) -notlike $searchMask) {

                    # skip it
                    continue;
                }

                # is a repository?
                if ([System.IO.Path]::GetFileName($directory) -eq ".svn" -or [System.IO.Path]::GetFileName($directory) -eq ".git") {

                    # skip it
                    continue;
                }

                # add all files by searchmask
                foreach ($file in [IO.Directory]::GetFiles($directory, $searchMask)) {

                    $result.Add($file);
                }
            }

            return $result.ToArray();
        }

        # gets all sub-directory by searchmask recursively, but skips repositories
        function GetAllDirectories([string] $parentDirectory, [string] $searchMask) {

            # create new list
            [System.Collections.Generic.List[string]] $result = New-Object "System.Collections.Generic.List[string]"

            # is a repository?
            if ([System.IO.Path]::GetFileName($parentDirectory) -eq ".svn" -or [System.IO.Path]::GetFileName($parentDirectory) -eq ".git") {

                # skip
                return $result.ToArray();
            }

            # enumerate all sub-directories
            foreach ($directory in [IO.Directory]::GetDirectories($parentDirectory, "*", [System.IO.SearchOption]::AllDirectories)) {

                # no match?
                if ([System.IO.Path]::GetFileName($directory) -notlike $searchMask) {

                    # skip it
                    continue;
                }

                # is a repository?
                if ([System.IO.Path]::GetFileName($directory) -eq ".svn" -or [System.IO.Path]::GetFileName($directory) -eq ".git") {

                    # skip it
                    continue;
                }

                # add this directory
                $result.Add($directory);
            }

            return $result.ToArray();
        }

        function SearchInFiles() {

            GetAllFilesFromAllDirectories $Source $Files |
            Sort-Object -Descending |
            ForEach-Object -ErrorAction SilentlyContinue {

                # reference next file
                $fn = $PSItem

                # convert to lowercase
                $fnl = $fn.ToLower();

                # determine if it is candidate for content replacement/renaming
                $ok = !$fnl.EndsWith(".jpg") -and !$fnl.EndsWith(".jpeg") -and !$fnl.EndsWith(".gif") -and !$fnl.EndsWith(".bmp") -and !$fnl.EndsWith(".exe") -and !$fnl.EndsWith(".dll") -and !$fnl.EndsWith(".cer") -and !$fnl.EndsWith(".crt") -and !$fnl.EndsWith(".pkf") -and !$fnl.EndsWith(".pdb") -and !$fnl.EndsWith(".so") -and !$fnl.EndsWith(".png") -and !$fnl.EndsWith(".tiff") -and !$fnl.EndsWith(".wav") -and !$fnl.EndsWith(".mp3") -and !$fnl.EndsWith(".avi") -and !$fnl.EndsWith(".mkv") -and !$fnl.EndsWith(".wmv") -and !$fnl.EndsWith(".dll") -and !$fnl.EndsWith(".exe") -and !$fnl.EndsWith(".pdb") -and !$fnl.EndsWith(".tar") -and !$fnl.EndsWith(".7z") -and !$fnl.EndsWith(".png") -and !$fnl.EndsWith(".db") -and !$fnl.EndsWith(".zip") -and !$fnl.EndsWith(".7z") -and !$fnl.EndsWith(".rar") -and !$fnl.EndsWith(".apk") -and !$fnl.EndsWith(".ipa");

                # all good? if not, you should have used -WhatIf ;)
                if ($ok) {

                    # read content as text
                    $oldtxt = [IO.File]::ReadAllText($fn, [System.Text.Encoding]::UTF8);

                    # make replacements
                    $newtxt = $oldtxt.Replace($findText, $replacementText);

                    # has it changed?
                    if (!$oldtxt.Equals($newtxt)) {

                        # not actually making any changes?
                        if ($WhatIfPreference -or $WhatIf) {

                            # output the action we would have performed
                            Write-Verbose "What-If: Would replace in file: '$($fn.Substring($Source.Length+1))'"
                        }
                        else {

                            # store changed content to disk
                            [IO.File]::WriteAllText($fn, $newtxt, [System.Text.Encoding]::UTF8);

                            # output the action we have performed
                            Write-Verbose "Replaced in file: '$($fn.Substring($Source.Length+1))'"
                        }
                    }
                }

                # get the name of this file
                $fnOld = [System.IO.Path]::GetFileName($fn);

                # perform renaming inside of this filename
                $fnNew = $fnOld.Replace($findText, $replacementText);

                # has it changed?
                if (!$fnNew.Equals($fnOld)) {

                    # construct new full path
                    $newPath = [System.IO.Path]::Combine([System.IO.Path]::GetDirectoryName($fn), $fnNew);

                    # not actually making any changes?
                    if ($WhatIfPreference -or $WhatIf) {

                        # output the action we would have performed
                        Write-Verbose "What-If: Would rename file: '$($fn.Substring($Source.Length+1))' --> '$($newPath.Substring($Source.Length+1))'"
                    }
                    else {

                        # rename this file
                        [IO.File]::Move($fn, $newPath);

                        # output the action we have performed
                        Write-Verbose "Renamed file: '$($fn.Substring($Source.Length+1))' --> '$($newPath.Substring($Source.Length+1))'"
                    }
                }
            }
        }

        function RenameDirectories() {

            GetAllDirectories $Source "*" |
            Sort-Object -Descending |
            ForEach-Object -ErrorAction SilentlyContinue {

                # reference next directory
                $fn = $PSItem

                # not current directory?
                if (!$fn -ne ".") {

                    # get the name of this directory
                    $fnOld = [System.IO.Path]::GetFileName($fn);

                    # perform renaming inside of this directoryname
                    $fnNew = $fnOld.Replace($findText, $replacementText);

                    # has the directoryname changed?
                    if (!$fnNew.Equals($fnOld)) {

                        # contruct new full path
                        $pathNew = [System.IO.Path]::GetFullPath([System.IO.Path]::Combine([System.IO.Path]::GetDirectoryName($fn), $fnNew));

                        # not actually making any changes?
                        if ($WhatIfPreference -or $WhatIf) {

                            $additional = "";

                            # is there a file, with the exact same name, as this renamed directory?
                            if ([IO.File]::Exists($pathNew)) {

                                $additional = " First deleting file '$fnNew' and then"
                            }

                            # has only the capitalization changed?
                            if ([IO.Directory]::Exists($pathNew) -and $fnOld.ToLower().Equals($fnNew.ToLower())) {

                                Write-Verbose "What-If:$additional Fixing capitalization in directoryname: '$($fn.Substring($Source.Length+1))' --> '$fnNew'"
                                return;
                            }

                            # does another directory already exist with the same name?
                            if ([IO.Directory]::Exists($pathNew)) {

                                Write-Verbose "What-If:$additional Merging files due to directory rename: '$($fn.Substring($Source.Length+1))' --> '$fnNew'"

                                # move all files into existing directory
                                Start-RoboCopy -Source $fn -DestinationDirectory $pathNew -Move -WhatIf

                                return;
                            }

                            # just rename directory
                            Write-Verbose "What-If:$additional Renaming directoryname: '$($fn.Substring($Source.Length+1))' --> '$fnNew'"
                            return;
                        }

                        $additional = "";

                        # is there a file, with the exact same name, as this renamed directory?
                        if ([IO.File]::Exists($pathNew)) {

                            # delete it
                            Remove-Item $pathNew -Force
                            $additional = " Deleted file '$fnNew' and then"
                        }

                        # has only the capitalization changed?
                        if ([IO.Directory]::Exists($pathNew) -and $fnOld.ToLower().Equals($fnNew.ToLower())) {

                            # construct new temporary directoryname
                            $tmpPath = $pathNew + "_" + [DateTime]::UtcNow.Ticks.ToString();

                            # rename back and forth
                            [IO.Directory]::Move($fn, $tmpPath);
                            [IO.Directory]::Move($tmpPath, $pathNew);

                            Write-Verbose "$additional Fixed capitalization in directoryname: '$($fn.Substring($Source.Length+1))' --> '$fnNew'"
                            return;
                        }

                        # does another directory already exist with the same name?
                        if ([IO.Directory]::Exists($pathNew)) {

                            # move all files into existing directory
                            Start-RoboCopy -Source $fn -DestinationDirectory $pathNew -Move

                            # now remove old directory
                            Remove-Item $fn -Force

                            Write-Verbose "$additional Merged files due to directory rename: '$($fn.Substring($Source.Length+1))' --> '$fnNew'"
                            return;
                        }

                        # just rename directory
                        [IO.Directory]::Move($fn, $pathNew);
                        Write-Verbose "$additional Renamed directoryname: '$($fn.Substring($Source.Length+1))' --> '$fnNew'"
                    }
                }
            }
        }

        SearchInFiles

        RenameDirectories
    }

    End {

    }
}