Transferetto.psm1

function Remove-EmptyValue {
    [alias('Remove-EmptyValues')]
    [CmdletBinding()]
    param([alias('Splat', 'IDictionary')][Parameter(Mandatory)][System.Collections.IDictionary] $Hashtable,
        [string[]] $ExcludeParameter,
        [switch] $Recursive,
        [int] $Rerun,
        [switch] $DoNotRemoveNull,
        [switch] $DoNotRemoveEmpty,
        [switch] $DoNotRemoveEmptyArray,
        [switch] $DoNotRemoveEmptyDictionary)
    foreach ($Key in [string[]] $Hashtable.Keys) { if ($Key -notin $ExcludeParameter) { if ($Recursive) { if ($Hashtable[$Key] -is [System.Collections.IDictionary]) { if ($Hashtable[$Key].Count -eq 0) { if (-not $DoNotRemoveEmptyDictionary) { $Hashtable.Remove($Key) } } else { Remove-EmptyValue -Hashtable $Hashtable[$Key] -Recursive:$Recursive } } else { if (-not $DoNotRemoveNull -and $null -eq $Hashtable[$Key]) { $Hashtable.Remove($Key) } elseif (-not $DoNotRemoveEmpty -and $Hashtable[$Key] -is [string] -and $Hashtable[$Key] -eq '') { $Hashtable.Remove($Key) } elseif (-not $DoNotRemoveEmptyArray -and $Hashtable[$Key] -is [System.Collections.IList] -and $Hashtable[$Key].Count -eq 0) { $Hashtable.Remove($Key) } } } else { if (-not $DoNotRemoveNull -and $null -eq $Hashtable[$Key]) { $Hashtable.Remove($Key) } elseif (-not $DoNotRemoveEmpty -and $Hashtable[$Key] -is [string] -and $Hashtable[$Key] -eq '') { $Hashtable.Remove($Key) } elseif (-not $DoNotRemoveEmptyArray -and $Hashtable[$Key] -is [System.Collections.IList] -and $Hashtable[$Key].Count -eq 0) { $Hashtable.Remove($Key) } } } }
    if ($Rerun) { for ($i = 0; $i -lt $Rerun; $i++) { Remove-EmptyValue -Hashtable $Hashtable -Recursive:$Recursive } }
}
function Add-PrivateFTPFile {
    [cmdletBinding()]
    param([Parameter(Mandatory)][FluentFTP.FtpClient] $Client,
        [string] $RemotePath,
        [string] $LocalPath,
        [FluentFTP.FtpRemoteExists] $RemoteExists = [FluentFTP.FtpRemoteExists]::Skip,
        [FluentFTP.FtpVerify] $VerifyOptions = [FluentFTP.FtpVerify]::None,
        [switch] $CreateRemoteDirectory)
    try {
        $Message = $Client.UploadFile($LocalPath, $RemotePath, $RemoteExists, $CreateRemoteDirectory.IsPresent, $VerifyOptions)
        if ($Message -eq 'success') { $State = $true } else { $State = $false }
        $Status = [PSCustomObject] @{Action = 'UploadFile'
            LocalPath                       = $LocalPath
            RemotePath                      = $RemotePath
            Status                          = $State
            Message                         = $Message
        }
    } catch {
        $Status = [PSCustomObject] @{Action = 'UploadFile'
            LocalPath                       = $LocalPath
            RemotePath                      = $RemotePath
            Status                          = $false
            Message                         = "Error: $($_.Exception.Message)"
        }
        if ($PSBoundParameters.ErrorAction -eq 'Stop') {
            Write-Error $_
            return
        } else { Write-Warning "Add-PrivateFTPFile - Error: $($_.Exception.Message)" }
    }
    $Status
}
function Add-PrivateFTPFiles {
    [cmdletBinding()]
    param([Parameter(Mandatory)][FluentFTP.FtpClient] $Client,
        [string] $RemotePath,
        [string[]] $LocalPath,
        [System.IO.FileInfo[]] $LocalFile,
        [FluentFTP.FtpRemoteExists] $RemoteExists = [FluentFTP.FtpRemoteExists]::Skip,
        [FluentFTP.FtpVerify] $VerifyOptions = [FluentFTP.FtpVerify]::None,
        [FluentFTP.FtpError] $ErrorHandling = [FluentFTP.FtpError]::None,
        [switch] $CreateRemoteDirectory)
    try {
        if ($LocalFile) { $Message = $Client.UploadFiles([System.IO.FileInfo[]] $LocalFile, $RemotePath, $RemoteExists, $CreateRemoteDirectory.IsPresent, $VerifyOptions, $ErrorHandling) } else { $Message = $Client.UploadFiles([string[]] $LocalPath, $RemotePath, $RemoteExists, $CreateRemoteDirectory.IsPresent, $VerifyOptions, $ErrorHandling) }
        if ($Message -gt 1) { $State = $true } else { $State = $false }
        $Status = [PSCustomObject] @{Action = 'UploadFile'
            LocalPath                       = $LocalPath
            RemotePath                      = $RemotePath
            Status                          = $State
            Message                         = $Message
        }
    } catch {
        $Status = [PSCustomObject] @{Action = 'UploadFile'
            LocalPath                       = $LocalPath
            RemotePath                      = $RemotePath
            Status                          = $false
            Message                         = "Error: $($_.Exception.Message)"
        }
        if ($PSBoundParameters.ErrorAction -eq 'Stop') {
            Write-Error $_
            return
        } else { Write-Warning "Add-PrivateFTPFiles - Error: $($_.Exception.Message)" }
    }
    $Status
}
function Get-PrivateFTPFile {
    [cmdletBinding()]
    param([Parameter(Mandatory)][FluentFTP.FtpClient] $Client,
        [string] $LocalPath,
        [FluentFTP.FtpListItem] $RemoteFile,
        [string] $RemotePath,
        [FluentFTP.FtpLocalExists] $LocalExists,
        [FluentFTP.FtpVerify[]] $VerifyOptions,
        [FluentFTP.FtpError] $FtpError)
    if ($RemoteFile) {
        if ($RemoteFile.Type -eq 'File') { $FileToDownload = $RemoteFile.FullName } else {
            if (-not $Suppress) {
                return [PSCustomObject] @{Action = 'DownloadFile'
                    Status                       = $false
                    LocalPath                    = $LocalPath
                    RemotePath                   = $RemoteFile.FullName
                    Message                      = "Receive-FTPFile - Given path $($RemoteFile.FullName) is $($RemoteFile.Type). Skipping."
                }
            } else {
                Write-Warning "Receive-FTPFile - Given path $($RemoteFile.FullName) is a directory. Skipping."
                return
            }
        }
    } else { $FileToDownload = $RemotePath }
    try {
        $Message = $Client.DownloadFile($LocalPath, $FileToDownload, $LocalExists, $VerifyOptions)
        if ($Message -eq 'success') { $State = $true } else { $State = $false }
        $Status = [PSCustomObject] @{Action = 'DownloadFile'
            Status                          = $State
            LocalPath                       = $LocalPath
            RemotePath                      = $FileToDownload
            Message                         = $Message
        }
    } catch {
        $Status = [PSCustomObject] @{Action = 'DownloadFile'
            Status                          = $false
            LocalPath                       = $LocalPath
            RemotePath                      = $FileToDownload
            Message                         = "Error: $($_.Exception.Message)"
        }
        if ($PSBoundParameters.ErrorAction -eq 'Stop') {
            Write-Error $_
            return
        } else { Write-Warning "Receive-FTPFile - Error: $($_.Exception.Message)" }
    }
    $Status
}
function Get-PrivateFTPFiles {
    [cmdletBinding()]
    param([Parameter(Mandatory)][FluentFTP.FtpClient] $Client,
        [string] $LocalPath,
        [FluentFTP.FtpListItem[]] $RemoteFile,
        [string[]] $RemotePath,
        [FluentFTP.FtpLocalExists] $LocalExists,
        [FluentFTP.FtpVerify[]] $VerifyOptions,
        [FluentFTP.FtpError] $FtpError)
    if ($RemoteFile) {
        $FileToDownload = foreach ($File in $RemoteFile) {
            if ($File.Type -eq 'File') { $File.FullName } else {
                if (-not $Suppress) {
                    [PSCustomObject] @{Action = 'DownloadFile'
                        Status                = $false
                        LocalPath             = $LocalPath
                        RemotePath            = $File.FullName
                        Message               = "Receive-FTPFile - Given path $($RemoteFile.FullName) is $($RemoteFile.Type). Skipping."
                    }
                } else { Write-Warning "Receive-FTPFile - Given path $($RemoteFile.FullName) is a directory. Skipping." }
            }
        }
    } else { $FileToDownload = $RemotePath }
    try {
        $Message = $Client.DownloadFiles($LocalPath, ([string[]] $FileToDownload), $LocalExists, $VerifyOptions, $FtpError)
        if ($Message -gt 0) { $State = $true } else { $State = $false }
        $Status = [PSCustomObject] @{Action = 'DownloadFile'
            Status                          = $State
            LocalPath                       = $LocalPath
            RemotePath                      = $FileToDownload
            Message                         = $Message
        }
    } catch {
        $Status = [PSCustomObject] @{Action = 'DownloadFile'
            Status                          = $false
            LocalPath                       = $LocalPath
            RemotePath                      = $FileToDownload
            Message                         = "Error: $($_.Exception.Message)"
        }
        if ($PSBoundParameters.ErrorAction -eq 'Stop') {
            Write-Error $_
            return
        } else { Write-Warning "Receive-FTPFile - Error: $($_.Exception.Message)" }
    }
    $Status
}
function Compare-FTPFile {
    [cmdletBinding()]
    param([Parameter(Mandatory)][FluentFTP.FtpClient] $Client,
        [Parameter(Mandatory)][string] $LocalPath,
        [Parameter(Mandatory)][string] $RemotePath,
        [FluentFTP.FtpCompareOption] $CompareOption = [FluentFTP.FtpCompareOption]::Auto)
    if ($Client -and $Client.IsConnected -and -not $Client.Error) { $Client.CompareFile($LocalPath, $RemotePath, $CompareOption) }
}
function Connect-FTP {
    [cmdletBinding(DefaultParameterSetName = 'Password')]
    param([Parameter(ParameterSetName = 'FtpProfile')]
        [Parameter(ParameterSetName = 'ClearText')]
        [Parameter(ParameterSetName = 'Password')]
        [string] $ProxyHost,
        [Parameter(ParameterSetName = 'FtpProfile')]
        [Parameter(ParameterSetName = 'ClearText')]
        [Parameter(ParameterSetName = 'Password')]
        [int] $ProxyPort,
        [Parameter(ParameterSetName = 'FtpProfile')]
        [Parameter(ParameterSetName = 'ClearText')]
        [Parameter(ParameterSetName = 'Password')]
        [pscredential] $ProxyCredential,
        [Parameter(ParameterSetName = 'FtpProfile')]
        [Parameter(ParameterSetName = 'ClearText')]
        [Parameter(ParameterSetName = 'Password')]
        [string] $ProxyUserName,
        [Parameter(ParameterSetName = 'FtpProfile')]
        [Parameter(ParameterSetName = 'ClearText')]
        [Parameter(ParameterSetName = 'Password')]
        [string] $ProxyPassword,
        [Parameter(ParameterSetName = 'FtpProfile')]
        [Parameter(ParameterSetName = 'ClearText')]
        [Parameter(ParameterSetName = 'Password')]
        [ValidateSet('FtpClientSocks5Proxy',
            'FtpClientHttp11Proxy',
            'FtpClientSocks4aProxy',
            'FtpClientSocks4Proxy',
            'FtpClientUserAtHostProxy',
            'FtpClientBlueCoatProxy')][string] $ProxyType,
        [Parameter(ParameterSetName = 'FtpProfile')]
        [FluentFTP.FtpProfile] $FtpProfile,
        [Parameter(ParameterSetName = 'ClearText')]
        [Parameter(ParameterSetName = 'Password')]
        [string] $Server,
        [Parameter(ParameterSetName = 'ClearText')]
        [string] $Username,
        [Parameter(ParameterSetName = 'ClearText')]
        [string] $Password,
        [Parameter(ParameterSetName = 'Password')]
        [pscredential] $Credential,
        [Parameter(ParameterSetName = 'ClearText')]
        [Parameter(ParameterSetName = 'Password')]
        [FluentFTP.FtpEncryptionMode[]] $EncryptionMode,
        [Parameter(ParameterSetName = 'ClearText')]
        [Parameter(ParameterSetName = 'Password')]
        [FluentFTP.FtpDataConnectionType] $DataConnectionType,
        [Parameter(ParameterSetName = 'ClearText')]
        [Parameter(ParameterSetName = 'Password')]
        [FluentFTP.FtpsBuffering] $SslBuffering,
        [Parameter(ParameterSetName = 'ClearText')]
        [Parameter(ParameterSetName = 'Password')]
        [switch] $DisableDataConnectionEncryption,
        [Parameter(ParameterSetName = 'ClearText')]
        [Parameter(ParameterSetName = 'Password')]
        [switch] $DisableValidateCertificateRevocation,
        [Parameter(ParameterSetName = 'ClearText')]
        [Parameter(ParameterSetName = 'Password')]
        [switch] $ValidateAnyCertificate,
        [Parameter(ParameterSetName = 'ClearText')]
        [Parameter(ParameterSetName = 'Password')]
        [int] $Port,
        [Parameter(ParameterSetName = 'ClearText')]
        [Parameter(ParameterSetName = 'Password')]
        [switch] $SendHost,
        [Parameter(ParameterSetName = 'ClearText')]
        [Parameter(ParameterSetName = 'Password')]
        [switch] $SocketKeepAlive,
        [Parameter(ParameterSetName = 'ClearText')]
        [Parameter(ParameterSetName = 'Password')]
        [switch] $AutoConnect)
    $ClientInternal = $null
    if ($PSBoundParameters.ContainsKey('ProxyHost') -or $PSBoundParameters.ContainsKey('ProxyPort') -or $PSBoundParameters.ContainsKey('ProxyType')) {
        if ($PSBoundParameters.ContainsKey('ProxyHost') -and $PSBoundParameters.ContainsKey('ProxyType')) {
            $ProxyProfile = [FluentFTP.FtpProxyProfile]::new()
            $ProxyProfile.ProxyHost = $ProxyHost
            $ProxyProfile.ProxyPort = $ProxyPort
            if ($ProxyUsername -and $ProxyPassword) { $ProxyProfile.ProxyCredentials = [System.Net.NetworkCredential]::new($Username, $Password) } elseif ($ProxyCredential) { $ProxyProfile.ProxyCredentials = [System.Net.NetworkCredential]::new($ProxyCredential.Username, $ProxyCredential.Password) } else {}
            $ProxyProfile.FtpHost = $Server
            if ($Port) { $ProxyProfile.FtpPort = $Port }
            if ($Username -and $Password) { $ProxyProfile.FTPCredentials = [System.Net.NetworkCredential]::new($Username, $Password) } elseif ($Credential) { $ProxyProfile.FTPCredentials = [System.Net.NetworkCredential]::new($Credential.Username, $Credential.Password) } else {}
            if ($ProxyType -eq 'FtpClientSocks5Proxy') { $ClientInternal = [FluentFTP.Proxy.SyncProxy.FtpClientSocks5Proxy]::new($ProxyProfile) } elseif ($ProxyType -eq 'FtpClientHttp11Proxy') { $ClientInternal = [FluentFTP.Proxy.SyncProxy.FtpClientHttp11Proxy]::new($ProxyProfile) } elseif ($ProxyType -eq 'FtpClientSocks4aProxy') { $ClientInternal = [FluentFTP.Proxy.SyncProxy.FtpClientSocks4aProxy]::new($ProxyProfile) } elseif ($ProxyType -eq 'FtpClientSocks4Proxy') { $ClientInternal = [FluentFTP.Proxy.SyncProxy.FtpClientSocks4Proxy]::new($ProxyProfile) } elseif ($ProxyType -eq 'FtpClientUserAtHostProxy') { $ClientInternal = [FluentFTP.Proxy.SyncProxy.FtpClientUserAtHostProxy]::new($ProxyProfile) } elseif ($ProxyType -eq 'FtpClientBlueCoatProxy') { $ClientInternal = [FluentFTP.Proxy.SyncProxy.FtpClientBlueCoatProxy]::new($ProxyProfile) }
        } else {
            if ($PSBoundParameters.ErrorAction -eq 'Stop') {
                Write-Error "ProxyHost, ProxyPort, and ProxyType must be specified together when using Proxy. Only ProxyUserName, ProxyPassword or ProxyCredential are optional."
                return
            } else { Write-Warning "Connect-FTP - ProxyHost, ProxyPort, and ProxyType must be specified together when using Proxy. Only ProxyUserName, ProxyPassword or ProxyCredential are optional." }
        }
    }
    if ($FtpProfile) {
        if (-not $ClientInternal) { $ClientInternal = [FluentFTP.FtpClient]::new() }
        $ClientInternal.LoadProfile($FtpProfile)
    } else {
        if (-not $ClientInternal) { $ClientInternal = [FluentFTP.FtpClient]::new($Server) }
        if ($Username -and $Password) { $ClientInternal.Credentials = [System.Net.NetworkCredential]::new($Username, $Password) } elseif ($Credential) { $ClientInternal.Credentials = [System.Net.NetworkCredential]::new($Credential.Username, $Credential.Password) } else {}
    }
    if ($Script:GlobalFTPLogging) {
        $ClientInternal.Config.LogHost = $Script:GlobalFTPLogging.LogHost
        $ClientInternal.Config.LogUsername = $Script:GlobalFTPLogging.LogUsername
        $ClientInternal.Config.LogPassword = $Script:GlobalFTPLogging.LogPassword
        $ClientInternal.Config.LogToConsole = $Script:GlobalFTPLogging.LogToConsole
    }
    if ($Port) { $ClientInternal.Port = $Port }
    if ($DataConnectionType) { $ClientInternal.Config.DataConnectionType = $DataConnectionType }
    if ($DisableDataConnectionEncryption) { $ClientInternal.Config.DataConnectionEncryption = $false }
    if ($EncryptionMode) { $ClientInternal.Config.EncryptionMode = $EncryptionMode }
    if ($ValidateAnyCertificate) { $ClientInternal.Config.ValidateAnyCertificate = $true }
    if ($DisableValidateCertificateRevocation) { $ClientInternal.Config.ValidateCertificateRevocation = $false }
    if ($SendHost) { $ClientInternal.Config.SendHost = $true }
    if ($SocketKeepAlive) { $ClientInternal.Config.SocketKeepAlive = $true }
    if ($FtpsBuffering) { $ClientInternal.Config.SslBuffering = $SslBuffering }
    try {
        if ($AutoConnect) {
            $TempFtpProfile = $ClientInternal.AutoConnect()
            if ($TempFtpProfile -and $ClientInternal.IsConnected) {
                Write-Verbose "Following options where used to autoconnect: "
                foreach ($Name in $TempFtpProfile.PSObject.Properties.Name) { Write-Verbose "[x] $Name -> $($TempFtpProfile.$Name)" }
            }
        } else { $ClientInternal.Connect() }
        $ClientInternal | Add-Member -Name 'Error' -Value $null -Force -MemberType NoteProperty
    } catch {
        $ClientInternal | Add-Member -Name 'Error' -Value $($_.Exception.Message) -Force -MemberType NoteProperty
        if ($PSBoundParameters.ErrorAction -eq 'Stop') {
            Write-Error $_
            return
        } else { Write-Warning "Connect-FTP - Error: $($_.Exception.Message)" }
    }
    $ClientInternal
}
function Connect-SFTP {
    [cmdletBinding(DefaultParameterSetName = 'Password')]
    param([Parameter(ParameterSetName = 'ClearText', Mandatory)]
        [Parameter(ParameterSetName = 'Password', Mandatory)]
        [Parameter(ParameterSetName = 'PrivateKey', Mandatory)]
        [string] $Server,
        [Parameter(ParameterSetName = 'ClearText', Mandatory)]
        [Parameter(ParameterSetName = 'PrivateKey', Mandatory)]
        [string] $Username,
        [Parameter(ParameterSetName = 'ClearText', Mandatory)]
        [string] $Password,
        [Parameter(ParameterSetName = 'Password', Mandatory)]
        [pscredential] $Credential,
        [Parameter(Mandatory, ParameterSetName = 'PrivateKey')]
        [string] $PrivateKey,
        [Parameter(ParameterSetName = 'ClearText')]
        [Parameter(ParameterSetName = 'Password')]
        [Parameter(ParameterSetName = 'PrivateKey')]
        [int] $Port)
    try {
        if ($Username -and $Password) { if ($Port) { $SftpClient = [Renci.SshNet.SftpClient]::new($Server, $Port, $Username, $Password) } else { $SftpClient = [Renci.SshNet.SftpClient]::new($Server, $Username, $Password) } } elseif ($Credential) { if ($Port) { $SftpClient = [Renci.SshNet.SftpClient]::new($Server, $Port, $Credential.Username, $Credential.GetNetworkCredential().Password) } else { $SftpClient = [Renci.SshNet.SftpClient]::new($Server, $Credential.Username, $Credential.GetNetworkCredential().Password) } } elseif ($PrivateKey) {
            if (Test-Path -LiteralPath $PrivateKey) {
                [string]$PrivateKey = Resolve-Path -LiteralPath $PrivateKey -ErrorAction Stop | Select-Object -ExpandProperty ProviderPath
                if ($Port) { $SftpClient = [Renci.SshNet.SftpClient]::new($Server, $Port, $Username, [Renci.SshNet.PrivateKeyFile]$PrivateKey) } else { $SftpClient = [Renci.SshNet.SftpClient]::new($Server, $Username, [Renci.SshNet.PrivateKeyFile]$PrivateKey) }
            } else {
                if ($PSBoundParameters.ErrorAction -eq 'Stop') {
                    throw "PrivateKey $PrivateKey doesn't exists."
                    return
                } else {
                    Write-Warning "Connect-SFTP - PrivateKey $PrivateKey doesn't exists."
                    return
                }
            }
        } else {
            throw 'Not implemented and unexepected.'
            return
        }
    } catch {
        if ($PSBoundParameters.ErrorAction -eq 'Stop') {
            Write-Error $_
            return
        } else { Write-Warning "Connect-SFTP - Error: $($_.Exception.Message)" }
    }
    try {
        $SftpClient.Connect()
        $SftpClient | Add-Member -Name 'Error' -Value $null -Force -MemberType NoteProperty
    } catch {
        $SftpClient | Add-Member -Name 'Error' -Value $($_.Exception.Message) -Force -MemberType NoteProperty
        if ($PSBoundParameters.ErrorAction -eq 'Stop') {
            Write-Error $_
            return
        } else { Write-Warning "Connect-SFTP - Error: $($_.Exception.Message)" }
    }
    $SftpClient
}
function Connect-SSH {
    [cmdletBinding(DefaultParameterSetName = 'Password')]
    param([Parameter(Mandatory, ParameterSetName = 'ClearText')]
        [Parameter(Mandatory, ParameterSetName = 'Password')]
        [Parameter(Mandatory, ParameterSetName = 'PrivateKey')]
        [string] $Server,
        [Parameter(Mandatory, ParameterSetName = 'ClearText')]
        [Parameter(Mandatory, ParameterSetName = 'PrivateKey')]
        [string] $Username,
        [Parameter(Mandatory, ParameterSetName = 'ClearText')]
        [string] $Password,
        [Parameter(Mandatory, ParameterSetName = 'Password')]
        [pscredential] $Credential,
        [Parameter(Mandatory, ParameterSetName = 'PrivateKey')]
        [string] $PrivateKey,
        [Parameter(ParameterSetName = 'ClearText')]
        [Parameter(ParameterSetName = 'Password')]
        [Parameter(ParameterSetName = 'PrivateKey')]
        [int] $Port)
    if ($Username -and $Password) { if ($Port) { $SshClient = [Renci.SshNet.SshClient]::new($Server, $Port, $Username, $Password) } else { $SshClient = [Renci.SshNet.SshClient]::new($Server, $Username, $Password) } } elseif ($Credential) { if ($Port) { $SshClient = [Renci.SshNet.SshClient]::new($Server, $Port, $Credential.Username, $Credential.GetNetworkCredential().Password) } else { $SshClient = [Renci.SshNet.SshClient]::new($Server, $Credential.Username, $Credential.GetNetworkCredential().Password) } } elseif ($PrivateKey) {
        [string]$PrivateKey = Resolve-Path $PrivateKey | Select-Object -ExpandProperty ProviderPath
        if ($Port) { $SshClient = [Renci.SshNet.SshClient]::new($Server, $Port, $Username, [Renci.SshNet.PrivateKeyFile]$PrivateKey) } else { $SshClient = [Renci.SshNet.SshClient]::new($Server, $Username, [Renci.SshNet.PrivateKeyFile]$PrivateKey) }
    } else { throw 'Not implemented and unexpected.' }
    try {
        $SshClient.Connect()
        $SshClient | Add-Member -Name 'Error' -Value $null -Force -MemberType NoteProperty
    } catch {
        $SshClient | Add-Member -Name 'Error' -Value $($_.Exception.Message) -Force -MemberType NoteProperty
        if ($PSBoundParameters.ErrorAction -eq 'Stop') {
            Write-Error $_
            return
        } else { Write-Warning "Connect-SSH - Error: $($_.Exception.Message)" }
    }
    $SshClient
}
function Disconnect-FTP {
    [cmdletBinding()]
    param([FluentFTP.FtpClient] $Client)
    if ($Client -and $Client.IsConnected) {
        try { $Client.Disconnect() } catch {
            if ($PSBoundParameters.ErrorAction -eq 'Stop') {
                Write-Error $_
                return
            } else { Write-Warning "Disconnect-FTP - Error: $($_.Exception.Message)" }
        }
    }
}
function Disconnect-SFTP {
    [cmdletBinding()]
    param([Parameter(Mandatory)][Renci.SshNet.SftpClient] $SftpClient)
    if ($SftpClient -and $SftpClient.IsConnected) {
        try { $SftpClient.Disconnect() } catch {
            if ($PSBoundParameters.ErrorAction -eq 'Stop') {
                Write-Error $_
                return
            } else { Write-Warning "Disconnect-SFTP - Error: $($_.Exception.Message)" }
        }
    }
}
function Get-FTPChecksum {
    [cmdletBinding()]
    param([Parameter(Mandatory)][FluentFTP.FtpClient] $Client,
        [Parameter(Mandatory)][string] $RemotePath,
        [FluentFTP.FtpHashAlgorithm] $HashAlgorithm = [FluentFTP.FtpHashAlgorithm]::MD5)
    if ($Client -and $Client.IsConnected -and -not $Client.Error) { $Client.GetChecksum($RemotePath, $HashAlgorithm) }
}
function Get-FTPChmod {
    [cmdletBinding()]
    param([Parameter(Mandatory)][FluentFTP.FtpClient] $Client,
        [string] $RemotePath)
    if ($Client -and $Client.IsConnected -and -not $Client.Error) { $Client.GetChmod($RemotePath) }
}
function Get-FTPList {
    [cmdletBinding()]
    param([alias('FtpPath')][string] $Path,
        [FluentFTP.FtpListOption] $Options,
        [Parameter(Mandatory)][FluentFTP.FtpClient] $Client)
    if ($Client -and $Client.IsConnected -and -not $Client.Error) {
        try { if ($Path -and $Options) { $Client.GetListing($Path, $Options) } elseif ($Path) { $Client.GetListing($Path) } else { $Client.GetListing() } } catch {
            if ($PSBoundParameters.ErrorAction -eq 'Stop') {
                Write-Error $_
                return
            } else { Write-Warning "Get-FTPList - Error: $($_.Exception.Message)" }
        }
    } else { Write-Warning "Get-FTPList - Skipped (IsConnected $($Client.IsConnected) / Error: $($Client.Error))" }
}
function Get-SFTPList {
    [cmdletBinding()]
    param([alias('FtpPath')][string] $Path,
        [Parameter(Mandatory)][Renci.SshNet.SftpClient] $SftpClient)
    if ($SftpClient -and $SftpClient.IsConnected -and -not $SftpClient.Error) {
        try { if ($Path) { $SftpClient.ListDirectory($Path) } else { $SftpClient.ListDirectory('') } } catch {
            if ($PSBoundParameters.ErrorAction -eq 'Stop') {
                Write-Error $_
                return
            } else { Write-Warning "Get-SFTPList - Error: $($_.Exception.Message)" }
        }
    } else { Write-Warning "Get-SFTPList - Skipped (IsConnected $($SftpClient.IsConnected) / Error: $($SftpClient.Error))" }
}
function Move-FTPDirectory {
    [cmdletBinding()]
    param([Parameter(Mandatory)][FluentFTP.FtpClient] $Client,
        [Parameter(Mandatory)][string] $RemoteSource,
        [Parameter(Mandatory)][string] $RemoteDestination,
        [FluentFTP.FtpRemoteExists] $RemoteExists = [FluentFTP.FtpRemoteExists]::Skip)
    if ($Client -and $Client.IsConnected -and -not $Client.Error) { $Client.MoveDirectory($RemoteSource, $RemoteDestination, $RemoteExists) }
}
function Move-FTPFile {
    [cmdletBinding()]
    param([Parameter(Mandatory)][FluentFTP.FtpClient] $Client,
        [Parameter(Mandatory)][string] $RemoteSource,
        [Parameter(Mandatory)][string] $RemoteDestination,
        [FluentFTP.FtpRemoteExists] $RemoteExists = [FluentFTP.FtpRemoteExists]::Skip)
    if ($Client -and $Client.IsConnected -and -not $Client.Error) { $Client.MoveFile($RemoteSource, $RemoteDestination, $RemoteExists) }
}
function Receive-FTPDirectory {
    [alias('Get-FTPDirectory')]
    [cmdletBinding()]
    param([Parameter(Mandatory)][FluentFTP.FtpClient] $Client,
        [string] $LocalPath,
        [Parameter(Mandatory)][string] $RemotePath,
        [FluentFTP.FtpFolderSyncMode] $FolderSyncMode = [FluentFTP.FtpFolderSyncMode]::Update,
        [FluentFTP.FtpLocalExists] $LocalExists,
        [FluentFTP.FtpVerify] $VerifyOptions,
        [FluentFTP.Rules.FtpRule[]] $Rules)
    if ($Client -and $Client.IsConnected -and -not $Client.Error) { $Client.DownloadDirectory($LocalPath, $RemotePath, $FolderSyncMode) }
}
function Receive-FTPFile {
    [alias('Get-FTPFile')]
    [cmdletBinding(DefaultParameterSetName = 'Text')]
    param([Parameter(ParameterSetName = 'Text')]
        [Parameter(ParameterSetName = 'Native')]
        [Parameter(Mandatory)][FluentFTP.FtpClient] $Client,
        [Parameter(ParameterSetName = 'Native')]
        [FluentFTP.FtpListItem[]] $RemoteFile,
        [Parameter(ParameterSetName = 'Text')]
        [Parameter(ParameterSetName = 'Native')]
        [string[]] $RemotePath,
        [Parameter(ParameterSetName = 'Text')]
        [Parameter(ParameterSetName = 'Native')]
        [string] $LocalPath,
        [Parameter(ParameterSetName = 'Text')]
        [Parameter(ParameterSetName = 'Native')]
        [FluentFTP.FtpLocalExists] $LocalExists = [FluentFTP.FtpLocalExists]::Skip,
        [Parameter(ParameterSetName = 'Text')]
        [Parameter(ParameterSetName = 'Native')]
        [FluentFTP.FtpVerify[]] $VerifyOptions = [FluentFTP.FtpVerify]::None,
        [Parameter(ParameterSetName = 'Text')]
        [Parameter(ParameterSetName = 'Native')]
        [FluentFTP.FtpError] $FtpError = [FluentFTP.FtpError]::Stop,
        [Parameter(ParameterSetName = 'Text')]
        [Parameter(ParameterSetName = 'Native')]
        [switch] $Suppress)
    if ($Client -and $Client.IsConnected -and -not $Client.Error) {
        $Path = Get-Item -LiteralPath $LocalPath -ErrorAction SilentlyContinue
        if ($Path -is [System.IO.DirectoryInfo]) { Get-PrivateFTPFiles -Client $Client -LocalPath $LocalPath -RemoteFile $RemoteFile -RemotePath $RemotePath -LocalExists $LocalExists -VerifyOptions $VerifyOptions -FtpError $FtpError } else {
            if ($RemoteFile.Count -gt 1 -or $RemotePath.Count -gt 1) {
                Write-Warning "Receive-FTPFile - Multiple files detected, but $LocalPath is not a directory or directory doesn't exists. "
                if ($RemoteFile) { $FileToDownload = $RemoteFile.FullName } else { $FileToDownload = $RemotePath }
                $Status = [PSCustomObject] @{Action = 'DownloadFile'
                    Status                          = $false
                    LocalPath                       = $LocalPath
                    RemotePath                      = $FileToDownload
                    Message                         = "Multiple files detected, but $LocalPath is not a directory or directory doesn't exists."
                }
            } else {
                $Splat = @{Client = $Client
                    LocalExists   = $LocalExists
                    VerifyOptions = $VerifyOptions
                    FtpError      = $FtpError
                    LocalPath     = $LocalPath
                }
                if ($RemoteFile) { $Splat.RemoteFile = $RemoteFile[0] } else { $Splat.RemotePath = $RemotePath[0] }
                Get-PrivateFTPFile @Splat
            }
        }
    } else {
        $Status = [PSCustomObject] @{Action = 'DownloadFile'
            Status                          = $false
            LocalPath                       = $LocalPath
            RemotePath                      = $FileToDownload
            Message                         = "Not connected."
        }
    }
    if (-not $Suppress) { $Status }
}
function Receive-SFTPFile {
    [alias('Get-SFTPFile')]
    [cmdletBinding()]
    param([Parameter(Mandatory)][Renci.SshNet.SftpClient] $SftpClient,
        [string] $RemotePath,
        [string] $LocalPath)
    if ($SftpClient -and $SftpClient.IsConnected) {
        try {
            $FileStream = [System.IO.FileStream]::new($LocalPath, [System.IO.FileMode]::OpenOrCreate)
            $SftpClient.DownloadFile($RemotePath, $FileStream)
            $Status = [PSCustomObject] @{Action = 'DownloadFile'
                Status                          = $true
                LocalPath                       = $LocalPath
                RemotePath                      = $RemotePath
                Message                         = ""
            }
        } catch {
            $Status = [PSCustomObject] @{Action = 'DownloadFile'
                Status                          = $false
                LocalPath                       = $LocalPath
                RemotePath                      = $RemotePath
                Message                         = "Error: $($_.Exception.Message)"
            }
            if ($PSBoundParameters.ErrorAction -eq 'Stop') {
                Write-Error $_
                return
            } else { Write-Warning "Receive-SFTPFile - Error: $($_.Exception.Message)" }
        } finally {
            $FileStream.Close()
            if ($Status.Status -eq $false) { Remove-Item -LiteralPath $LocalPath }
        }
        $Status
    }
}
function Remove-FTPDirectory {
    [cmdletBinding()]
    param([Parameter(Mandatory)][FluentFTP.FtpClient] $Client,
        [Parameter(Mandatory)][string] $RemotePath,
        [FluentFTP.FtpListOption] $FtpListOption)
    if ($Client -and $Client.IsConnected -and -not $Client.Error) { if (-not $FtpListOption) { $Client.DeleteDirectory($RemotePath) } else { $Client.DeleteDirectory($RemotePath, $FtpListOption) } }
}
function Remove-FTPFile {
    [cmdletBinding()]
    param([Parameter(Mandatory)][FluentFTP.FtpClient] $Client,
        [Parameter(Mandatory)][string] $RemotePath)
    if ($Client -and $Client.IsConnected -and -not $Client.Error) { $Client.DeleteFile($RemotePath) }
}
function Remove-SFTPFile {
    [cmdletBinding()]
    param([Parameter(Mandatory)][Renci.SshNet.SftpClient] $SftpClient,
        [string] $RemotePath,
        [switch] $Suppress)
    if ($SftpClient -and $SftpClient.IsConnected) {
        try {
            $SftpClient.DeleteFile($RemotePath)
            $Status = [PSCustomObject] @{Action = 'RemoveFile'
                Status                          = $true
                Message                         = ""
            }
        } catch {
            $Status = [PSCustomObject] @{Action = 'RemoveFile'
                Status                          = $false
                Message                         = "Error $($_.Exception.Message)"
            }
            if ($PSBoundParameters.ErrorAction -eq 'Stop') {
                Write-Error $_
                return
            } else { Write-Warning "Remove-SFTPFile - Error: $($_.Exception.Message)" }
        }
        if (-not $Suppress) { $Status }
    }
}
function Rename-FTPFile {
    [cmdletBinding()]
    param([Parameter(Mandatory)][FluentFTP.FtpClient] $Client,
        [Parameter(Mandatory)][string] $Path,
        [Parameter(Mandatory)][string] $DestinationPath)
    if ($Client -and $Client.IsConnected -and -not $Client.Error) { $Client.Rename($Path, $DestinationPath) }
}
function Rename-SFTPFile {
    <#
    .SYNOPSIS
    Allows renaming remote file over SFTP protocol
 
    .DESCRIPTION
    Allows renaming remote file over SFTP protocol
 
    .PARAMETER SftpClient
    Parameter that contains the SFTP client object
 
    .PARAMETER SourcePath
    Path to file that is going to be renamed
 
    .PARAMETER DestinationPath
    New path to file, with new name
 
    .PARAMETER Suppress
    Suppress returning an object with information about the operation
 
    .EXAMPLE
    $SftpClient = Connect-SFTP -Server '192.168.240.29' -Username 'przemek' -Password 'Password'
    Rename-SFTPFile -SftpClient $SftpClient -SourcePath '/home/przemek/mmm.txt' -DestinationPath '/home/przemek/mmm1.txt'
    Disconnect-SFTP -SftpClient $SftpClient
 
    .NOTES
    General notes
    #>

    [cmdletBinding()]
    param([Parameter(Mandatory)][Renci.SshNet.SftpClient] $SftpClient,
        [alias('OldPath')][string] $SourcePath,
        [alias('NewPath')][string] $DestinationPath,
        [switch] $Suppress)
    if ($SftpClient -and $SftpClient.IsConnected) {
        try {
            $SftpClient.RenameFile($SourcePath, $DestinationPath)
            $Status = [PSCustomObject] @{Action = 'RenameFile'
                Status                          = $true
                OldPath                         = $SourcePath
                NewPath                         = $DestinationPath
                Message                         = ""
            }
        } catch {
            $Status = [PSCustomObject] @{Action = 'RenameFile'
                Status                          = $false
                OldPath                         = $SourcePath
                NewPath                         = $DestinationPath
                Message                         = "Error $($_.Exception.Message)"
            }
            if ($PSBoundParameters.ErrorAction -eq 'Stop') {
                Write-Error $_
                return
            } else { Write-Warning "Rename-SFTPFile - Error: $($_.Exception.Message)" }
        }
        if (-not $Suppress) { $Status }
    }
}
function Request-FTPConfiguration {
    <#
    .SYNOPSIS
    Short description
 
    .DESCRIPTION
    Automatically discover working FTP connection settings and return those connection profiles. This method will try every possible connection type combination in a loop until it finds a working combination, and it will return the first found combination or all found combinations. The connection types are tried in this order of preference.
 
    Auto connection attempts to find working connection settings in this order of preference:
 
    Protocol Preference:
    1. None - Let the OS decide which TLS/SSL version to use
    2. Tls12 - TLS 1.2 (TLS 1.3 is not yet stable in .NET Framework)
    3. Tls11 - TLS 1.1
    4. Tls - TLS 1.0
    5. Ssl3 - SSL 3.0 (obsolete, need to use TLS instead)
    6. Ssl2 - SSL 2.0 (obsolete, need to use TLS instead)
    7. Default - Undefined/weird behaviour
 
    Data Connection Type Preference:
 
    1. PASV - We prefer passive as its the most reliable
    2. EPSV - Enhanced passive is not as well supported on servers
    3. PORT - PORT is an older connection type
    4. EPRT - Enhanced PORT is not as well supported on servers
    5. PASVEX
 
    Encoding Type Preference:
 
    1. UTF8 - We prefer Unicode encoding as there will be no issues with file and folder names
    2. ASCII - ASCII/ANSI is a fallback used for older servers
 
    .PARAMETER Server
    Server Name or IP Address to Connect
 
    .PARAMETER Username
    UserName for FTP Connection
 
    .PARAMETER Password
    Password for FTP Connection (cleartext)
 
    .PARAMETER Credential
    UserName and Password in form of Credentials
 
    .PARAMETER FirstOnly
    Returns first working profile
 
    .EXAMPLE
    # Login via UserName/Password
    $ProfileFtp1 = Request-FTPConfiguration -Server 'test.rebex.net' -Verbose -Username 'demo' -Password 'password'
    $ProfileFtp1 | Format-Table
 
    .EXAMPLE
    # Anonymous login
    $ProfileFtp2 = Request-FTPConfiguration -Server 'speedtest.tele2.net' -Verbose
    $ProfileFtp2 | Format-Table
 
    .NOTES
    General notes
    #>

    [cmdletBinding(DefaultParameterSetName = 'Password')]
    param([Parameter(ParameterSetName = 'ClearText')]
        [Parameter(ParameterSetName = 'Password')]
        [string] $Server,
        [Parameter(ParameterSetName = 'ClearText')]
        [string] $Username,
        [Parameter(ParameterSetName = 'ClearText')]
        [string] $Password,
        [Parameter(ParameterSetName = 'Password')]
        [pscredential] $Credential,
        [Parameter(ParameterSetName = 'ClearText')]
        [Parameter(ParameterSetName = 'Password')]
        [switch] $FirstOnly)
    $Client = [FluentFTP.FtpClient]::new($Server)
    if ($Username -and $Password) { $Client.Credentials = [System.Net.NetworkCredential]::new($Username, $Password) } elseif ($Credential) { $Client.Credentials = [System.Net.NetworkCredential]::new($Credential.Username, $Credential.Password) } else {}
    try { $Client.AutoDetect($FirstOnly.IsPresent) } catch {
        $Client | Add-Member -Name 'Error' -Value $($_.Exception.Message) -Force -MemberType NoteProperty
        if ($PSBoundParameters.ErrorAction -eq 'Stop') {
            Write-Error $_
            return
        } else { Write-Warning "Request-FTPConfiguration - Error: $($_.Exception.Message)" }
    }
}
function Send-FTPDirectory {
    <#
    .SYNOPSIS
    Uploads a directory to an FTP server.
 
    .DESCRIPTION
    Uploads a directory to an FTP server.
 
    .PARAMETER Client
    The Client to use for connection.
 
    .PARAMETER LocalPath
    Path on the local machine to upload to FTP Server
 
    .PARAMETER RemotePath
    Path on the FTP Server where to upload the content
 
    .PARAMETER FolderSyncMode
    Update - upload a folder and all its files
    Mirror - upload a folder and all its files, and delete extra files on the server
 
    .PARAMETER RemoteExists
    Provide decision what to do when file on the server exits.
    Options available: Append, AppendNoChek, NoCheck, Skip, , Overwrite
    Default: Skip.
 
    .PARAMETER VerifyOptions
    Provide options for verification of files on the remote server.
    Options available: Delete, OnlyChecksum, None, Retry
    Default: None.
 
    .PARAMETER Rules
    Provide rules and conditions for uploading files.
 
    .EXAMPLE
    Set-FTPTracing -Enable -DisplayConsole
 
    $Client = Connect-FTP -Server '192.168.241.187' -Verbose -Username 'test' -Password 'BiPassword90A' -EncryptionMode Explicit -ValidateAnyCertificate
    $Upload = Send-FTPDirectory -Client $Client -LocalPath $PSScriptRoot\Upload -RemotePath '/Temporary' -Verbose -FolderSyncMode Update
    $Upload | Format-Table *
 
    Disconnect-FTP -Client $Client
 
    Set-FTPTracing -Disable
 
    .NOTES
    General notes
    #>

    [alias('Add-FTPDirectory')]
    [cmdletBinding()]
    param([Parameter(Mandatory)][FluentFTP.FtpClient] $Client,
        [string] $LocalPath,
        [Parameter(Mandatory)][string] $RemotePath,
        [FluentFTP.FtpFolderSyncMode] $FolderSyncMode = [FluentFTP.FtpFolderSyncMode]::Update,
        [FluentFTP.FtpRemoteExists] $RemoteExists = [FluentFTP.FtpRemoteExists]::Skip,
        [FluentFTP.FtpVerify] $VerifyOptions = [FluentFTP.FtpVerify]::None,
        [FluentFTP.Rules.FtpRule[]] $Rules)
    if ($Client -and $Client.IsConnected -and -not $Client.Error) { $Client.UploadDirectory($LocalPath, $RemotePath, $FolderSyncMode, $RemoteExists, $VerifyOptions, @($Rules)) }
}
function Send-FTPFile {
    [alias('Add-FTPFile')]
    [cmdletBinding()]
    param([Parameter(Mandatory)][FluentFTP.FtpClient] $Client,
        [string] $RemotePath,
        [System.IO.FileInfo[]] $LocalFile,
        [string[]] $LocalPath,
        [FluentFTP.FtpRemoteExists] $RemoteExists = [FluentFTP.FtpRemoteExists]::Skip,
        [FluentFTP.FtpVerify] $VerifyOptions = [FluentFTP.FtpVerify]::None,
        [FluentFTP.FtpError] $ErrorHandling = [FluentFTP.FtpError]::None,
        [switch] $CreateRemoteDirectory)
    if ($Client -and $Client.IsConnected -and -not $Client.Error) {
        if ($LocalPath.Count -gt 1 -or $LocalFile.Count -gt 1) {
            $Splat = @{Client         = $Client
                RemoteExists          = $RemoteExists
                VerifyOptions         = $VerifyOptions
                LocalPath             = $LocalPath
                LocalFile             = $LocalFile
                RemotePath            = $RemotePath
                CreateRemoteDirectory = $CreateRemoteDirectory.IsPresent
                ErrorHandling         = $ErrorHandling
            }
            Remove-EmptyValue -Hashtable $Splat
            $Status = Add-PrivateFTPFiles @Splat
            $Status
        } else {
            foreach ($Path in $LocalPath) {
                $Splat = @{Client         = $Client
                    RemoteExists          = $RemoteExists
                    VerifyOptions         = $VerifyOptions
                    LocalPath             = $Path
                    RemotePath            = $RemotePath
                    CreateRemoteDirectory = $CreateRemoteDirectory.IsPresent
                }
                $Status = Add-PrivateFTPFile @Splat
                $Status
            }
        }
    }
}
function Send-SFTPFile {
    [alias('Add-SFTPFile')]
    [cmdletBinding()]
    param([Parameter(Mandatory)][Renci.SshNet.SftpClient] $SftpClient,
        [string] $RemotePath,
        [string] $LocalPath,
        [switch] $AllowOverride)
    if ($SftpClient -and $SftpClient.IsConnected) {
        if (Test-Path -LiteralPath $LocalPath) {
            try {
                $FileStream = [System.IO.FileStream]::new($LocalPath, [System.IO.FileMode]::OpenOrCreate)
                $SftpClient.UploadFile($FileStream, $RemotePath, $AllowOverride)
                $Status = [PSCustomObject] @{Action = 'UploadFile'
                    Status                          = $true
                    LocalPath                       = $LocalPath
                    RemotePath                      = $RemotePath
                    Message                         = ""
                }
            } catch {
                $Status = [PSCustomObject] @{Action = 'UploadFile'
                    Status                          = $false
                    LocalPath                       = $LocalPath
                    RemotePath                      = $RemotePath
                    Message                         = "Error: $($_.Exception.Message)"
                }
                if ($PSBoundParameters.ErrorAction -eq 'Stop') {
                    Write-Error $_
                    return
                } else { Write-Warning "Send-SFTPFile - Error: $($_.Exception.Message)" }
            } finally { $FileStream.Close() }
        } else {
            Write-Warning "Send-SFTPFile - File $LocalPath doesn't exists."
            $Status = [PSCustomObject] @{Action = 'UploadFile'
                Status                          = $false
                LocalPath                       = $LocalPath
                RemotePath                      = $RemotePath
                Message                         = "LocalPath doesn't exists $LocalPath"
            }
        }
        $Status
    }
}
function Send-SSHCommand {
    [cmdletBinding()]
    param([Parameter(Mandatory)][Renci.SshNet.SshClient] $SshClient,
        [scriptblock] $Command,
        [switch] $Status)
    if ($SshClient -and $SshClient.IsConnected -and -not $SshClient.Error) {
        if ($Command) {
            $CommandsToExecute = & $Command
            [string] $SendCommand = foreach ($C in $CommandsToExecute) { if ($C.Trim().EndsWith(';')) { $C } else { "$C;" } }
            try {
                Write-Verbose -Message "Send-SSHCommand - Executing command: $SendCommand"
                if ($Status) {
                    [PSCustomObject] @{Status = $true
                        Output                = $SshClient.CreateCommand($SendCommand).Execute()
                        Error                 = $null
                    }
                } else { $SshClient.CreateCommand($SendCommand).Execute() }
            } catch {
                if ($PSBoundParameters.ErrorAction -eq 'Stop') {
                    Write-Error $_
                    return
                } else { Write-Warning "Send-SSHCommand - Error: $($_.Exception.Message)" }
                if ($Status) {
                    [PSCustomObject] @{Status = $false
                        Output                = ''
                        Error                 = "Error: $($_.Exception.Message)"
                    }
                }
            }
        }
    }
}
function Set-FTPChmod {
    [cmdletBinding(DefaultParameterSetName = 'ByInt')]
    param([Parameter(Mandatory, ParameterSetName = 'ByInt')]
        [Parameter(Mandatory, ParameterSetName = 'Explicit')]
        [FluentFTP.FtpClient] $Client,
        [Parameter(Mandatory, ParameterSetName = 'ByInt')]
        [Parameter(Mandatory, ParameterSetName = 'Explicit')]
        [string] $RemotePath,
        [Parameter(Mandatory, ParameterSetName = 'ByInt')]
        [nullable[int]] $Permissions,
        [Parameter(Mandatory, ParameterSetName = 'Explicit')]
        [FluentFTP.FtpPermission] $Owner,
        [Parameter(Mandatory, ParameterSetName = 'Explicit')]
        [FluentFTP.FtpPermission] $Group,
        [Parameter(Mandatory, ParameterSetName = 'Explicit')]
        [FluentFTP.FtpPermission] $Other)
    if ($Client -and $Client.IsConnected -and -not $Client.Error) { if ($Permissions) { $Client.Chmod($RemotePath, $Permissions) } else { $Client.Chmod($RemotePath, $Owner, $Group, $Other) } }
}
function Set-FTPOption {
    [cmdletBinding()]
    param([Parameter(Mandatory)][FluentFTP.FtpClient] $Client,
        [nullable[int]] $RetryAttempts,
        [nullable[bool]] $DownloadZeroByteFiles)
    if ($RetryAttempts) { $Client.RetryAttempts = $RetryAttempts }
    if ($DownloadZeroByteFiles) { $Client.DownloadZeroByteFiles = $DownloadZeroByteFiles }
}
function Set-FTPTracing {
    <#
    .SYNOPSIS
    Allows enabling/disabling tracing ftp commands being sent and received during communucation with the server.
 
    .DESCRIPTION
    Allows enabling/disabling tracing ftp commands being sent and received during communucation with the server.
 
    .PARAMETER Enable
    Enable tracing
 
    .PARAMETER Disable
    Disable tracing
 
    .PARAMETER ShowPassword
    Include FTP passwords in logs? Default: false.
 
    .PARAMETER HideUserName
    Hide FTP usernames in logs? Default: false.
 
    .PARAMETER HideIP
    Hide server IP addresses in logs? Default: true.
 
    .EXAMPLE
    Set-FTPTracing -Enable
 
    .NOTES
    General notes
    #>

    [cmdletBinding()]
    param([switch] $Enable,
        [switch] $Disable,
        [switch] $ShowPassword,
        [switch] $HideUserName,
        [switch] $HideIP)
    $Script:GlobalFTPLogging = [ordered] @{}
    if ($Enable) { $Script:GlobalFTPLogging.LogToConsole = $true } elseif ($Disable) {
        $Script:GlobalFTPLogging = $null
        return
    } else {
        Write-Warning -Message 'Please specify either -Enable or -Disable'
        return
    }
    if ($HideUserName) { $Script:GlobalFTPLogging.LogUserName = $false } else { $Script:GlobalFTPLogging.LogUserName = $true }
    if ($ShowPassword) { $Script:GlobalFTPLogging.LogPassword = $false } else { $Script:GlobalFTPLogging.LogPassword = $true }
    if (-not $HideIP) { $Script:GlobalFTPLogging.LogHost = $true } else { $Script:GlobalFTPLogging.LogHost = $false }
}
function Start-FXPDirectoryTransfer {
    [alias('Start-FXPDirectory')]
    [cmdletBinding()]
    param([alias('SourceClient')][Parameter(Mandatory)][FluentFTP.FtpClient] $Client,
        [Parameter(Mandatory)][string] $SourcePath,
        [Parameter(Mandatory)][FluentFTP.FtpClient] $DestinationClient,
        [Parameter(Mandatory)][string] $DestinationPath,
        [FluentFTP.FtpFolderSyncMode] $FolderSyncMode = [FluentFTP.FtpFolderSyncMode]::Update,
        [FluentFTP.FtpRemoteExists] $RemoteExists = [FluentFTP.FtpRemoteExists]::Skip,
        [FluentFTP.FtpVerify[]] $VerifyOptions = [FluentFTP.FtpVerify]::None)
    if ($Client -and $Client.IsConnected -and -not $Client.Error) { $Client.TransferDirectory($SourcePath, $DestinationClient, $DestinationPath, $FolderSyncMode, $RemoteExists, $VerifyOptions) }
}
function Start-FXPFileTransfer {
    [alias('Start-FXPFile')]
    [cmdletBinding()]
    param([alias('SourceClient')][Parameter(Mandatory)][FluentFTP.FtpClient] $Client,
        [Parameter(Mandatory)][string] $SourcePath,
        [Parameter(Mandatory)][FluentFTP.FtpClient] $DestinationClient,
        [Parameter(Mandatory)][string] $DestinationPath,
        [switch] $CreateRemoteDirectory,
        [FluentFTP.FtpRemoteExists] $RemoteExists = [FluentFTP.FtpRemoteExists]::Skip,
        [FluentFTP.FtpVerify[]] $VerifyOptions = [FluentFTP.FtpVerify]::None)
    if ($Client -and $Client.IsConnected -and -not $Client.Error) { $Client.TransferFile($SourcePath, $DestinationClient, $DestinationPath, $CreateRemoteDirectory.IsPresent, $RemoteExists, $VerifyOptions) }
}
function Test-FTPDirectory {
    [cmdletBinding()]
    param([Parameter(Mandatory)][FluentFTP.FtpClient] $Client,
        [Parameter(Mandatory)][string] $RemotePath)
    if ($Client -and $Client.IsConnected -and -not $Client.Error) { $Client.DirectoryExists($RemotePath) }
}
function Test-FTPFile {
    [cmdletBinding()]
    param([Parameter(Mandatory)][FluentFTP.FtpClient] $Client,
        [Parameter(Mandatory)][string] $RemotePath)
    if ($Client -and $Client.IsConnected -and -not $Client.Error) { $Client.FileExists($RemotePath) }
}
if ($PSVersionTable.PSEdition -eq 'Desktop' -and (Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\NET Framework Setup\NDP\v4\Full").Release -lt 461808) { Write-Warning "This module requires .NET Framework 4.7.2 or later."; return }
Export-ModuleMember -Function @('Compare-FTPFile', 'Connect-FTP', 'Connect-SFTP', 'Connect-SSH', 'Disconnect-FTP', 'Disconnect-SFTP', 'Get-FTPChecksum', 'Get-FTPChmod', 'Get-FTPList', 'Get-SFTPList', 'Move-FTPDirectory', 'Move-FTPFile', 'Receive-FTPDirectory', 'Receive-FTPFile', 'Receive-SFTPFile', 'Remove-FTPDirectory', 'Remove-FTPFile', 'Remove-SFTPFile', 'Rename-FTPFile', 'Rename-SFTPFile', 'Request-FTPConfiguration', 'Send-FTPDirectory', 'Send-FTPFile', 'Send-SFTPFile', 'Send-SSHCommand', 'Set-FTPChmod', 'Set-FTPOption', 'Set-FTPTracing', 'Start-FXPDirectoryTransfer', 'Start-FXPFileTransfer', 'Test-FTPDirectory', 'Test-FTPFile') -Alias @('Add-FTPDirectory', 'Add-FTPFile', 'Add-SFTPFile', 'Get-FTPDirectory', 'Get-FTPFile', 'Get-SFTPFile', 'Start-FXPDirectory', 'Start-FXPFile')
# SIG # Begin signature block
# MIInPgYJKoZIhvcNAQcCoIInLzCCJysCAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCBAclTNEAdgF6DX
# xPfw6hqVpPTuDqkMiskanfKCEOyzuKCCITcwggO3MIICn6ADAgECAhAM5+DlF9hG
# /o/lYPwb8DA5MA0GCSqGSIb3DQEBBQUAMGUxCzAJBgNVBAYTAlVTMRUwEwYDVQQK
# EwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xJDAiBgNV
# BAMTG0RpZ2lDZXJ0IEFzc3VyZWQgSUQgUm9vdCBDQTAeFw0wNjExMTAwMDAwMDBa
# Fw0zMTExMTAwMDAwMDBaMGUxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2Vy
# dCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xJDAiBgNVBAMTG0RpZ2lD
# ZXJ0IEFzc3VyZWQgSUQgUm9vdCBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC
# AQoCggEBAK0OFc7kQ4BcsYfzt2D5cRKlrtwmlIiq9M71IDkoWGAM+IDaqRWVMmE8
# tbEohIqK3J8KDIMXeo+QrIrneVNcMYQq9g+YMjZ2zN7dPKii72r7IfJSYd+fINcf
# 4rHZ/hhk0hJbX/lYGDW8R82hNvlrf9SwOD7BG8OMM9nYLxj+KA+zp4PWw25EwGE1
# lhb+WZyLdm3X8aJLDSv/C3LanmDQjpA1xnhVhyChz+VtCshJfDGYM2wi6YfQMlqi
# uhOCEe05F52ZOnKh5vqk2dUXMXWuhX0irj8BRob2KHnIsdrkVxfEfhwOsLSSplaz
# vbKX7aqn8LfFqD+VFtD/oZbrCF8Yd08CAwEAAaNjMGEwDgYDVR0PAQH/BAQDAgGG
# MA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFEXroq/0ksuCMS1Ri6enIZ3zbcgP
# MB8GA1UdIwQYMBaAFEXroq/0ksuCMS1Ri6enIZ3zbcgPMA0GCSqGSIb3DQEBBQUA
# A4IBAQCiDrzf4u3w43JzemSUv/dyZtgy5EJ1Yq6H6/LV2d5Ws5/MzhQouQ2XYFwS
# TFjk0z2DSUVYlzVpGqhH6lbGeasS2GeBhN9/CTyU5rgmLCC9PbMoifdf/yLil4Qf
# 6WXvh+DfwWdJs13rsgkq6ybteL59PyvztyY1bV+JAbZJW58BBZurPSXBzLZ/wvFv
# hsb6ZGjrgS2U60K3+owe3WLxvlBnt2y98/Efaww2BxZ/N3ypW2168RJGYIPXJwS+
# S86XvsNnKmgR34DnDDNmvxMNFG7zfx9jEB76jRslbWyPpbdhAbHSoyahEHGdreLD
# +cOZUbcrBwjOLuZQsqf6CkUvovDyMIIFMDCCBBigAwIBAgIQBAkYG1/Vu2Z1U0O1
# b5VQCDANBgkqhkiG9w0BAQsFADBlMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGln
# aUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMSQwIgYDVQQDExtE
# aWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgQ0EwHhcNMTMxMDIyMTIwMDAwWhcNMjgx
# MDIyMTIwMDAwWjByMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5j
# MRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMTEwLwYDVQQDEyhEaWdpQ2VydCBT
# SEEyIEFzc3VyZWQgSUQgQ29kZSBTaWduaW5nIENBMIIBIjANBgkqhkiG9w0BAQEF
# AAOCAQ8AMIIBCgKCAQEA+NOzHH8OEa9ndwfTCzFJGc/Q+0WZsTrbRPV/5aid2zLX
# cep2nQUut4/6kkPApfmJ1DcZ17aq8JyGpdglrA55KDp+6dFn08b7KSfH03sjlOSR
# I5aQd4L5oYQjZhJUM1B0sSgmuyRpwsJS8hRniolF1C2ho+mILCCVrhxKhwjfDPXi
# TWAYvqrEsq5wMWYzcT6scKKrzn/pfMuSoeU7MRzP6vIK5Fe7SrXpdOYr/mzLfnQ5
# Ng2Q7+S1TqSp6moKq4TzrGdOtcT3jNEgJSPrCGQ+UpbB8g8S9MWOD8Gi6CxR93O8
# vYWxYoNzQYIH5DiLanMg0A9kczyen6Yzqf0Z3yWT0QIDAQABo4IBzTCCAckwEgYD
# VR0TAQH/BAgwBgEB/wIBADAOBgNVHQ8BAf8EBAMCAYYwEwYDVR0lBAwwCgYIKwYB
# BQUHAwMweQYIKwYBBQUHAQEEbTBrMCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5k
# aWdpY2VydC5jb20wQwYIKwYBBQUHMAKGN2h0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0
# LmNvbS9EaWdpQ2VydEFzc3VyZWRJRFJvb3RDQS5jcnQwgYEGA1UdHwR6MHgwOqA4
# oDaGNGh0dHA6Ly9jcmw0LmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydEFzc3VyZWRJRFJv
# b3RDQS5jcmwwOqA4oDaGNGh0dHA6Ly9jcmwzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2Vy
# dEFzc3VyZWRJRFJvb3RDQS5jcmwwTwYDVR0gBEgwRjA4BgpghkgBhv1sAAIEMCow
# KAYIKwYBBQUHAgEWHGh0dHBzOi8vd3d3LmRpZ2ljZXJ0LmNvbS9DUFMwCgYIYIZI
# AYb9bAMwHQYDVR0OBBYEFFrEuXsqCqOl6nEDwGD5LfZldQ5YMB8GA1UdIwQYMBaA
# FEXroq/0ksuCMS1Ri6enIZ3zbcgPMA0GCSqGSIb3DQEBCwUAA4IBAQA+7A1aJLPz
# ItEVyCx8JSl2qB1dHC06GsTvMGHXfgtg/cM9D8Svi/3vKt8gVTew4fbRknUPUbRu
# pY5a4l4kgU4QpO4/cY5jDhNLrddfRHnzNhQGivecRk5c/5CxGwcOkRX7uq+1UcKN
# JK4kxscnKqEpKBo6cSgCPC6Ro8AlEeKcFEehemhor5unXCBc2XGxDI+7qPjFEmif
# z0DLQESlE/DmZAwlCEIysjaKJAL+L3J+HNdJRZboWR3p+nRka7LrZkPas7CM1ekN
# 3fYBIM6ZMWM9CBoYs4GbT8aTEAb8B4H6i9r5gkn3Ym6hU/oSlBiFLpKR6mhsRDKy
# ZqHnGKSaZFHvMIIFPTCCBCWgAwIBAgIQBNXcH0jqydhSALrNmpsqpzANBgkqhkiG
# 9w0BAQsFADByMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkw
# FwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMTEwLwYDVQQDEyhEaWdpQ2VydCBTSEEy
# IEFzc3VyZWQgSUQgQ29kZSBTaWduaW5nIENBMB4XDTIwMDYyNjAwMDAwMFoXDTIz
# MDcwNzEyMDAwMFowejELMAkGA1UEBhMCUEwxEjAQBgNVBAgMCcWabMSFc2tpZTER
# MA8GA1UEBxMIS2F0b3dpY2UxITAfBgNVBAoMGFByemVteXPFgmF3IEvFgnlzIEVW
# T1RFQzEhMB8GA1UEAwwYUHJ6ZW15c8WCYXcgS8WCeXMgRVZPVEVDMIIBIjANBgkq
# hkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAv7KB3iyBrhkLUbbFe9qxhKKPBYqDBqln
# r3AtpZplkiVjpi9dMZCchSeT5ODsShPuZCIxJp5I86uf8ibo3vi2S9F9AlfFjVye
# 3dTz/9TmCuGH8JQt13ozf9niHecwKrstDVhVprgxi5v0XxY51c7zgMA2g1Ub+3ti
# i0vi/OpmKXdL2keNqJ2neQ5cYly/GsI8CREUEq9SZijbdA8VrRF3SoDdsWGf3tZZ
# zO6nWn3TLYKQ5/bw5U445u/V80QSoykszHRivTj+H4s8ABiforhi0i76beA6Ea41
# zcH4zJuAp48B4UhjgRDNuq8IzLWK4dlvqrqCBHKqsnrF6BmBrv+BXQIDAQABo4IB
# xTCCAcEwHwYDVR0jBBgwFoAUWsS5eyoKo6XqcQPAYPkt9mV1DlgwHQYDVR0OBBYE
# FBixNSfoHFAgJk4JkDQLFLRNlJRmMA4GA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAK
# BggrBgEFBQcDAzB3BgNVHR8EcDBuMDWgM6Axhi9odHRwOi8vY3JsMy5kaWdpY2Vy
# dC5jb20vc2hhMi1hc3N1cmVkLWNzLWcxLmNybDA1oDOgMYYvaHR0cDovL2NybDQu
# ZGlnaWNlcnQuY29tL3NoYTItYXNzdXJlZC1jcy1nMS5jcmwwTAYDVR0gBEUwQzA3
# BglghkgBhv1sAwEwKjAoBggrBgEFBQcCARYcaHR0cHM6Ly93d3cuZGlnaWNlcnQu
# Y29tL0NQUzAIBgZngQwBBAEwgYQGCCsGAQUFBwEBBHgwdjAkBggrBgEFBQcwAYYY
# aHR0cDovL29jc3AuZGlnaWNlcnQuY29tME4GCCsGAQUFBzAChkJodHRwOi8vY2Fj
# ZXJ0cy5kaWdpY2VydC5jb20vRGlnaUNlcnRTSEEyQXNzdXJlZElEQ29kZVNpZ25p
# bmdDQS5jcnQwDAYDVR0TAQH/BAIwADANBgkqhkiG9w0BAQsFAAOCAQEAmr1sz4ls
# LARi4wG1eg0B8fVJFowtect7SnJUrp6XRnUG0/GI1wXiLIeow1UPiI6uDMsRXPHU
# F/+xjJw8SfIbwava2eXu7UoZKNh6dfgshcJmo0QNAJ5PIyy02/3fXjbUREHINrTC
# vPVbPmV6kx4Kpd7KJrCo7ED18H/XTqWJHXa8va3MYLrbJetXpaEPpb6zk+l8Rj9y
# G4jBVRhenUBUUj3CLaWDSBpOA/+sx8/XB9W9opYfYGb+1TmbCkhUg7TB3gD6o6ES
# Jre+fcnZnPVAPESmstwsT17caZ0bn7zETKlNHbc1q+Em9kyBjaQRcEQoQQNpezQu
# g9ufqExx6lHYDjCCBY0wggR1oAMCAQICEA6bGI750C3n79tQ4ghAGFowDQYJKoZI
# hvcNAQEMBQAwZTELMAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZ
# MBcGA1UECxMQd3d3LmRpZ2ljZXJ0LmNvbTEkMCIGA1UEAxMbRGlnaUNlcnQgQXNz
# dXJlZCBJRCBSb290IENBMB4XDTIyMDgwMTAwMDAwMFoXDTMxMTEwOTIzNTk1OVow
# YjELMAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQ
# d3d3LmRpZ2ljZXJ0LmNvbTEhMB8GA1UEAxMYRGlnaUNlcnQgVHJ1c3RlZCBSb290
# IEc0MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAv+aQc2jeu+RdSjww
# IjBpM+zCpyUuySE98orYWcLhKac9WKt2ms2uexuEDcQwH/MbpDgW61bGl20dq7J5
# 8soR0uRf1gU8Ug9SH8aeFaV+vp+pVxZZVXKvaJNwwrK6dZlqczKU0RBEEC7fgvMH
# hOZ0O21x4i0MG+4g1ckgHWMpLc7sXk7Ik/ghYZs06wXGXuxbGrzryc/NrDRAX7F6
# Zu53yEioZldXn1RYjgwrt0+nMNlW7sp7XeOtyU9e5TXnMcvak17cjo+A2raRmECQ
# ecN4x7axxLVqGDgDEI3Y1DekLgV9iPWCPhCRcKtVgkEy19sEcypukQF8IUzUvK4b
# A3VdeGbZOjFEmjNAvwjXWkmkwuapoGfdpCe8oU85tRFYF/ckXEaPZPfBaYh2mHY9
# WV1CdoeJl2l6SPDgohIbZpp0yt5LHucOY67m1O+SkjqePdwA5EUlibaaRBkrfsCU
# tNJhbesz2cXfSwQAzH0clcOP9yGyshG3u3/y1YxwLEFgqrFjGESVGnZifvaAsPvo
# ZKYz0YkH4b235kOkGLimdwHhD5QMIR2yVCkliWzlDlJRR3S+Jqy2QXXeeqxfjT/J
# vNNBERJb5RBQ6zHFynIWIgnffEx1P2PsIV/EIFFrb7GrhotPwtZFX50g/KEexcCP
# orF+CiaZ9eRpL5gdLfXZqbId5RsCAwEAAaOCATowggE2MA8GA1UdEwEB/wQFMAMB
# Af8wHQYDVR0OBBYEFOzX44LScV1kTN8uZz/nupiuHA9PMB8GA1UdIwQYMBaAFEXr
# oq/0ksuCMS1Ri6enIZ3zbcgPMA4GA1UdDwEB/wQEAwIBhjB5BggrBgEFBQcBAQRt
# MGswJAYIKwYBBQUHMAGGGGh0dHA6Ly9vY3NwLmRpZ2ljZXJ0LmNvbTBDBggrBgEF
# BQcwAoY3aHR0cDovL2NhY2VydHMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0QXNzdXJl
# ZElEUm9vdENBLmNydDBFBgNVHR8EPjA8MDqgOKA2hjRodHRwOi8vY3JsMy5kaWdp
# Y2VydC5jb20vRGlnaUNlcnRBc3N1cmVkSURSb290Q0EuY3JsMBEGA1UdIAQKMAgw
# BgYEVR0gADANBgkqhkiG9w0BAQwFAAOCAQEAcKC/Q1xV5zhfoKN0Gz22Ftf3v1cH
# vZqsoYcs7IVeqRq7IviHGmlUIu2kiHdtvRoU9BNKei8ttzjv9P+Aufih9/Jy3iS8
# UgPITtAq3votVs/59PesMHqai7Je1M/RQ0SbQyHrlnKhSLSZy51PpwYDE3cnRNTn
# f+hZqPC/Lwum6fI0POz3A8eHqNJMQBk1RmppVLC4oVaO7KTVPeix3P0c2PR3WlxU
# jG/voVA9/HYJaISfb8rbII01YBwCA8sgsKxYoA5AY8WYIsGyWfVVa88nq2x2zm8j
# LfR+cWojayL/ErhULSd+2DrZ8LaHlv1b0VysGMNNn3O3AamfV6peKOK5lDCCBq4w
# ggSWoAMCAQICEAc2N7ckVHzYR6z9KGYqXlswDQYJKoZIhvcNAQELBQAwYjELMAkG
# A1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3LmRp
# Z2ljZXJ0LmNvbTEhMB8GA1UEAxMYRGlnaUNlcnQgVHJ1c3RlZCBSb290IEc0MB4X
# DTIyMDMyMzAwMDAwMFoXDTM3MDMyMjIzNTk1OVowYzELMAkGA1UEBhMCVVMxFzAV
# BgNVBAoTDkRpZ2lDZXJ0LCBJbmMuMTswOQYDVQQDEzJEaWdpQ2VydCBUcnVzdGVk
# IEc0IFJTQTQwOTYgU0hBMjU2IFRpbWVTdGFtcGluZyBDQTCCAiIwDQYJKoZIhvcN
# AQEBBQADggIPADCCAgoCggIBAMaGNQZJs8E9cklRVcclA8TykTepl1Gh1tKD0Z5M
# om2gsMyD+Vr2EaFEFUJfpIjzaPp985yJC3+dH54PMx9QEwsmc5Zt+FeoAn39Q7SE
# 2hHxc7Gz7iuAhIoiGN/r2j3EF3+rGSs+QtxnjupRPfDWVtTnKC3r07G1decfBmWN
# lCnT2exp39mQh0YAe9tEQYncfGpXevA3eZ9drMvohGS0UvJ2R/dhgxndX7RUCyFo
# bjchu0CsX7LeSn3O9TkSZ+8OpWNs5KbFHc02DVzV5huowWR0QKfAcsW6Th+xtVhN
# ef7Xj3OTrCw54qVI1vCwMROpVymWJy71h6aPTnYVVSZwmCZ/oBpHIEPjQ2OAe3Vu
# JyWQmDo4EbP29p7mO1vsgd4iFNmCKseSv6De4z6ic/rnH1pslPJSlRErWHRAKKtz
# Q87fSqEcazjFKfPKqpZzQmiftkaznTqj1QPgv/CiPMpC3BhIfxQ0z9JMq++bPf4O
# uGQq+nUoJEHtQr8FnGZJUlD0UfM2SU2LINIsVzV5K6jzRWC8I41Y99xh3pP+OcD5
# sjClTNfpmEpYPtMDiP6zj9NeS3YSUZPJjAw7W4oiqMEmCPkUEBIDfV8ju2TjY+Cm
# 4T72wnSyPx4JduyrXUZ14mCjWAkBKAAOhFTuzuldyF4wEr1GnrXTdrnSDmuZDNIz
# tM2xAgMBAAGjggFdMIIBWTASBgNVHRMBAf8ECDAGAQH/AgEAMB0GA1UdDgQWBBS6
# FtltTYUvcyl2mi91jGogj57IbzAfBgNVHSMEGDAWgBTs1+OC0nFdZEzfLmc/57qY
# rhwPTzAOBgNVHQ8BAf8EBAMCAYYwEwYDVR0lBAwwCgYIKwYBBQUHAwgwdwYIKwYB
# BQUHAQEEazBpMCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdpY2VydC5jb20w
# QQYIKwYBBQUHMAKGNWh0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2Vy
# dFRydXN0ZWRSb290RzQuY3J0MEMGA1UdHwQ8MDowOKA2oDSGMmh0dHA6Ly9jcmwz
# LmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydFRydXN0ZWRSb290RzQuY3JsMCAGA1UdIAQZ
# MBcwCAYGZ4EMAQQCMAsGCWCGSAGG/WwHATANBgkqhkiG9w0BAQsFAAOCAgEAfVmO
# wJO2b5ipRCIBfmbW2CFC4bAYLhBNE88wU86/GPvHUF3iSyn7cIoNqilp/GnBzx0H
# 6T5gyNgL5Vxb122H+oQgJTQxZ822EpZvxFBMYh0MCIKoFr2pVs8Vc40BIiXOlWk/
# R3f7cnQU1/+rT4osequFzUNf7WC2qk+RZp4snuCKrOX9jLxkJodskr2dfNBwCnzv
# qLx1T7pa96kQsl3p/yhUifDVinF2ZdrM8HKjI/rAJ4JErpknG6skHibBt94q6/ae
# sXmZgaNWhqsKRcnfxI2g55j7+6adcq/Ex8HBanHZxhOACcS2n82HhyS7T6NJuXdm
# kfFynOlLAlKnN36TU6w7HQhJD5TNOXrd/yVjmScsPT9rp/Fmw0HNT7ZAmyEhQNC3
# EyTN3B14OuSereU0cZLXJmvkOHOrpgFPvT87eK1MrfvElXvtCl8zOYdBeHo46Zzh
# 3SP9HSjTx/no8Zhf+yvYfvJGnXUsHicsJttvFXseGYs2uJPU5vIXmVnKcPA3v5gA
# 3yAWTyf7YGcWoWa63VXAOimGsJigK+2VQbc61RWYMbRiCQ8KvYHZE/6/pNHzV9m8
# BPqC3jLfBInwAM1dwvnQI38AC+R2AibZ8GV2QqYphwlHK+Z/GqSFD/yYlvZVVCsf
# gPrA8g4r5db7qS9EFUrnEw4d2zc4GqEr9u3WfPwwggbAMIIEqKADAgECAhAMTWly
# S5T6PCpKPSkHgD1aMA0GCSqGSIb3DQEBCwUAMGMxCzAJBgNVBAYTAlVTMRcwFQYD
# VQQKEw5EaWdpQ2VydCwgSW5jLjE7MDkGA1UEAxMyRGlnaUNlcnQgVHJ1c3RlZCBH
# NCBSU0E0MDk2IFNIQTI1NiBUaW1lU3RhbXBpbmcgQ0EwHhcNMjIwOTIxMDAwMDAw
# WhcNMzMxMTIxMjM1OTU5WjBGMQswCQYDVQQGEwJVUzERMA8GA1UEChMIRGlnaUNl
# cnQxJDAiBgNVBAMTG0RpZ2lDZXJ0IFRpbWVzdGFtcCAyMDIyIC0gMjCCAiIwDQYJ
# KoZIhvcNAQEBBQADggIPADCCAgoCggIBAM/spSY6xqnya7uNwQ2a26HoFIV0Mxom
# rNAcVR4eNm28klUMYfSdCXc9FZYIL2tkpP0GgxbXkZI4HDEClvtysZc6Va8z7GGK
# 6aYo25BjXL2JU+A6LYyHQq4mpOS7eHi5ehbhVsbAumRTuyoW51BIu4hpDIjG8b7g
# L307scpTjUCDHufLckkoHkyAHoVW54Xt8mG8qjoHffarbuVm3eJc9S/tjdRNlYRo
# 44DLannR0hCRRinrPibytIzNTLlmyLuqUDgN5YyUXRlav/V7QG5vFqianJVHhoV5
# PgxeZowaCiS+nKrSnLb3T254xCg/oxwPUAY3ugjZNaa1Htp4WB056PhMkRCWfk3h
# 3cKtpX74LRsf7CtGGKMZ9jn39cFPcS6JAxGiS7uYv/pP5Hs27wZE5FX/NurlfDHn
# 88JSxOYWe1p+pSVz28BqmSEtY+VZ9U0vkB8nt9KrFOU4ZodRCGv7U0M50GT6Vs/g
# 9ArmFG1keLuY/ZTDcyHzL8IuINeBrNPxB9ThvdldS24xlCmL5kGkZZTAWOXlLimQ
# prdhZPrZIGwYUWC6poEPCSVT8b876asHDmoHOWIZydaFfxPZjXnPYsXs4Xu5zGcT
# B5rBeO3GiMiwbjJ5xwtZg43G7vUsfHuOy2SJ8bHEuOdTXl9V0n0ZKVkDTvpd6kVz
# HIR+187i1Dp3AgMBAAGjggGLMIIBhzAOBgNVHQ8BAf8EBAMCB4AwDAYDVR0TAQH/
# BAIwADAWBgNVHSUBAf8EDDAKBggrBgEFBQcDCDAgBgNVHSAEGTAXMAgGBmeBDAEE
# AjALBglghkgBhv1sBwEwHwYDVR0jBBgwFoAUuhbZbU2FL3MpdpovdYxqII+eyG8w
# HQYDVR0OBBYEFGKK3tBh/I8xFO2XC809KpQU31KcMFoGA1UdHwRTMFEwT6BNoEuG
# SWh0dHA6Ly9jcmwzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydFRydXN0ZWRHNFJTQTQw
# OTZTSEEyNTZUaW1lU3RhbXBpbmdDQS5jcmwwgZAGCCsGAQUFBwEBBIGDMIGAMCQG
# CCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdpY2VydC5jb20wWAYIKwYBBQUHMAKG
# TGh0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydFRydXN0ZWRHNFJT
# QTQwOTZTSEEyNTZUaW1lU3RhbXBpbmdDQS5jcnQwDQYJKoZIhvcNAQELBQADggIB
# AFWqKhrzRvN4Vzcw/HXjT9aFI/H8+ZU5myXm93KKmMN31GT8Ffs2wklRLHiIY1UJ
# RjkA/GnUypsp+6M/wMkAmxMdsJiJ3HjyzXyFzVOdr2LiYWajFCpFh0qYQitQ/Bu1
# nggwCfrkLdcJiXn5CeaIzn0buGqim8FTYAnoo7id160fHLjsmEHw9g6A++T/350Q
# p+sAul9Kjxo6UrTqvwlJFTU2WZoPVNKyG39+XgmtdlSKdG3K0gVnK3br/5iyJpU4
# GYhEFOUKWaJr5yI+RCHSPxzAm+18SLLYkgyRTzxmlK9dAlPrnuKe5NMfhgFknADC
# 6Vp0dQ094XmIvxwBl8kZI4DXNlpflhaxYwzGRkA7zl011Fk+Q5oYrsPJy8P7mxNf
# arXH4PMFw1nfJ2Ir3kHJU7n/NBBn9iYymHv+XEKUgZSCnawKi8ZLFUrTmJBFYDOA
# 4CPe+AOk9kVH5c64A0JH6EE2cXet/aLol3ROLtoeHYxayB6a1cLwxiKoT5u92Bya
# UcQvmvZfpyeXupYuhVfAYOd4Vn9q78KVmksRAsiCnMkaBXy6cbVOepls9Oie1FqY
# yJ+/jbsYXEP10Cro4mLueATbvdH7WwqocH7wl4R44wgDXUcsY6glOJcB0j862uXl
# 9uab3H4szP8XTE0AotjWAQ64i+7m4HJViSwnGWH2dwGMMYIFXTCCBVkCAQEwgYYw
# cjELMAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQ
# d3d3LmRpZ2ljZXJ0LmNvbTExMC8GA1UEAxMoRGlnaUNlcnQgU0hBMiBBc3N1cmVk
# IElEIENvZGUgU2lnbmluZyBDQQIQBNXcH0jqydhSALrNmpsqpzANBglghkgBZQME
# AgEFAKCBhDAYBgorBgEEAYI3AgEMMQowCKACgAChAoAAMBkGCSqGSIb3DQEJAzEM
# BgorBgEEAYI3AgEEMBwGCisGAQQBgjcCAQsxDjAMBgorBgEEAYI3AgEVMC8GCSqG
# SIb3DQEJBDEiBCCGrLwvQyHBdax6b+z/LNpo7vLGmpeXxDQ9SSYIKfKwATANBgkq
# hkiG9w0BAQEFAASCAQBxWi34BJTl6BsEu2oZsXfBUpm2Yrf0uxJsOMM+PZKZq9ar
# WY5VBFSS+mRMqiHu9yuqVS2oqLO0y3DqPjKEIq/qHnVn6hJvGO5adAwzl4WXTOGA
# jXq5Gh2LOHWkNz1UEV0mwSl65PdksmCFwCYPHdwHsGiJdkg/BHil1TUauxZSPOdR
# gRWCVgAyo+7WjCC9djGpcybGGBuBaZRiYypBC5+Nvagp/tBT410Qk4imkQGislSg
# VpsOjotJXCeqvC3vVOMuKP8ImM0RxBDXFdr2lI2jSCcZYyTntLmR+ccjn7cCc5/J
# YOH9KBVqlcieLGT5uH0y6yutMmTUFCTh3w+LNIzUoYIDIDCCAxwGCSqGSIb3DQEJ
# BjGCAw0wggMJAgEBMHcwYzELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDkRpZ2lDZXJ0
# LCBJbmMuMTswOQYDVQQDEzJEaWdpQ2VydCBUcnVzdGVkIEc0IFJTQTQwOTYgU0hB
# MjU2IFRpbWVTdGFtcGluZyBDQQIQDE1pckuU+jwqSj0pB4A9WjANBglghkgBZQME
# AgEFAKBpMBgGCSqGSIb3DQEJAzELBgkqhkiG9w0BBwEwHAYJKoZIhvcNAQkFMQ8X
# DTIyMTIwNDE0MTcwNVowLwYJKoZIhvcNAQkEMSIEIFbq84x2cvHKrXtGU+pHqjec
# 1M7cJaJG4TaDoEv6BnspMA0GCSqGSIb3DQEBAQUABIICAGLRqjTEyaodcJ7P784P
# iqNgmDVgqACy7jaG96qdfc5g6MB5LSHhB+ANwGu4i550LucIE7/mY2iFSV4UD+Hk
# /g1DT/6py1KAZS3b3u9DcDur+llR5M+4aavmLvpMNRho2rUL0Y2n6Aj+yqgTIvrk
# 6bIJmKshkes52jkHgnSAE0XrvVkpFD1uYxxKogIXbGDnN0f4GI8AgEeOvXS5SU4p
# JGwJCSHiAs5Ar9uXgLZSr4WHF4MGoCY2bVKuc7VlXgeZkhP8vszqJKUFUWcmle21
# 0iul23Xi3pS8A9kTCGK+QWXK9oVfjRrNb62t+0CB1Wtcr/gCaHx4HGv1f1t/GB8R
# 6BNuSFVv4e9oQkQo8ef0A9sy30eP7trANfNUlQHdFENF7jKDMUH+YK1chM7Ttd/R
# DuGrK0PnL0pieMA6ddc3kDVibs/rfflmfCWajnmcOFlKBEmWZ1T1frmjrYrv5nnf
# 5GlxUjyThorwyd+xPXJFQtqpk/o10Ju2DVtqhcfm3JSU+jDm1xyEwEohkZJA3Wkc
# 4Ej4JEFplWBZdoHU8MO64gobbmh5SkbQB4tqCKIInfRxeqctG7icc15gfZ7FVl7l
# eZtEc9XfFR23Kpq/CMkxrDNtI4lTSMVVq41hcmBzhswT/Qi8v3o4rJiZCUqCGdDG
# pWaVnPAN6CMURUwkkoH3qzFd
# SIG # End signature block