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)
    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) { $Hashtable.Remove($Key) } else { Remove-EmptyValue -Hashtable $Hashtable[$Key] -Recursive:$Recursive } } else { if ($null -eq $Hashtable[$Key] -or ($Hashtable[$Key] -is [string] -and $Hashtable[$Key] -eq '') -or ($Hashtable[$Key] -is [System.Collections.IList] -and $Hashtable[$Key].Count -eq 0)) { $Hashtable.Remove($Key) } } } else { if ($null -eq $Hashtable[$Key] -or ($Hashtable[$Key] -is [string] -and $Hashtable[$Key] -eq '') -or ($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')]
        [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)
    if ($FtpProfile) {
        $Client = [FluentFTP.FtpClient]::new()
        $Client.LoadProfile($FtpProfile)
    } else {
        $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 {}
    }
    if ($Port) { $Client.Port = $Port }
    if ($DataConnectionType) { $Client.DataConnectionType = $DataConnectionType }
    if ($DisableDataConnectionEncryption) { $Client.DataConnectionEncryption = $false }
    if ($EncryptionMode) { $Client.EncryptionMode = $EncryptionMode }
    if ($ValidateAnyCertificate) { $Client.ValidateAnyCertificate = $true }
    if ($DisableValidateCertificateRevocation) { $Client.ValidateCertificateRevocation = $false }
    if ($SendHost) { $Client.SendHost = $true }
    if ($SocketKeepAlive) { $Client.SocketKeepAlive = $true }
    if ($FtpsBuffering) { $Client.SslBuffering = $SslBuffering }
    try {
        if ($AutoConnect) {
            $TempFtpProfile = $Client.AutoConnect()
            if ($TempFtpProfile -and $Client.IsConnected) {
                Write-Verbose "Following options where used to autoconnect: "
                foreach ($Name in $TempFtpProfile.PSObject.Properties.Name) { Write-Verbose "[x] $Name -> $($TempFtpProfile.$Name)" }
            }
        } else { $Client.Connect() }
        $Client | Add-Member -Name 'Error' -Value $null -Force -MemberType NoteProperty
    } catch {
        $Client | 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)" }
    }
    $Client
}
function Connect-SFTP {
    [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')]
        [int] $Port)
    if ($Username -and $Password) { $SftpClient = [Renci.SshNet.SftpClient]::new($Server, $Username, $Password) } elseif ($Credential) { $SftpClient = [Renci.SshNet.SftpClient]::new($Server, $Credential.Username, $Credential.GetNetworkCredential().Password) } else { throw 'Not implemented. Add certificate' }
    if ($Port) { $SftpClient.Port = $Port }
    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')]
        [string] $Server,
        [Parameter(Mandatory, ParameterSetName = 'ClearText')]
        [string] $Username,
        [Parameter(Mandatory, ParameterSetName = 'ClearText')]
        [string] $Password,
        [Parameter(Mandatory, ParameterSetName = 'Password')]
        [pscredential] $Credential,
        [Parameter(ParameterSetName = 'ClearText')]
        [Parameter(ParameterSetName = 'Password')]
        [int] $Port)
    if ($Username -and $Password) { $SshClient = [Renci.SshNet.SshClient]::new($Server, $Username, $Password) } elseif ($Credential) { $SshClient = [Renci.SshNet.SshClient]::new($Server, $Credential.Username, $Credential.GetNetworkCredential().Password) } else { throw 'Not implemented. Add certificate' }
    if ($Port) { $SshClient.Port = $Port }
    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 {
    [cmdletBinding()]
    param([Parameter(Mandatory)][Renci.SshNet.SftpClient] $SftpClient,
        [string] $RemotePath,
        [switch] $Suppress)
    if ($SftpClient -and $SftpClient.IsConnected) {
        try {
            $SftpClient.RenameFile($RemotePath)
            $Status = [PSCustomObject] @{Action = 'RenameFile'
                LocalPath                       = ''
                RemotePath                      = $RemotePath
                Status                          = $true
                Message                         = ""
            }
        } catch {
            $Status = [PSCustomObject] @{Action = 'RenameFile'
                Status                          = $false
                LocalPath                       = ''
                RemotePath                      = $RemotePath
                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 {
    [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.FtpVerify] $VerifyOptions,
        [FluentFTP.Rules.FtpRule[]] $Rules)
    if ($Client -and $Client.IsConnected -and -not $Client.Error) { $Client.UploadDirectory($LocalPath, $RemotePath, $FolderSyncMode) }
}
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 {
    [cmdletBinding()]
    param([string] $LogPath,
        [switch] $Enable,
        [switch] $Disable,
        [switch] $ShowPassword,
        [switch] $ShowUsername,
        [switch] $HideIP,
        [switch] $HideFunctions,
        [switch] $DisplayConsole)
    if ($Enable) { [FluentFTP.FtpTrace]::EnableTracing = $true }
    if ($Disable) { [FluentFTP.FtpTrace]::EnableTracing = $false }
    if ($LogPath) { [FluentFTP.FtpTrace]::LogToFile = $LogPath }
    if (-not $HideFunctions) { [FluentFTP.FtpTrace]::LogFunctions = $true }
    if ($DisplayConsole) { [FluentFTP.FtpTrace]::LogToConsole = $true }
    if ($ShowUsername) { [FluentFTP.FtpTrace]::LogUserName = $true }
    if ($ShowPassword) { [FluentFTP.FtpTrace]::LogPassword = $false }
    if (-not $HideIP) { [FluentFTP.FtpTrace]::LogIP = $true }
}
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-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 ($PSEdition -eq 'Core') {
    Add-Type -Path $PSScriptRoot\Lib\Standard\FluentFTP.dll
    Add-Type -Path $PSScriptRoot\Lib\Standard\Renci.SshNet.dll
    Add-Type -Path $PSScriptRoot\Lib\Standard\SshNet.Security.Cryptography.dll
} else {
    Add-Type -Path $PSScriptRoot\Lib\Standard\FluentFTP.dll
    Add-Type -Path $PSScriptRoot\Lib\Standard\Renci.SshNet.dll
    Add-Type -Path $PSScriptRoot\Lib\Standard\SshNet.Security.Cryptography.dll
}
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-FTPFile') -Alias @('Add-FTPDirectory', 'Add-FTPFile', 'Add-SFTPFile', 'Get-FTPDirectory', 'Get-FTPFile', 'Get-SFTPFile', 'Start-FXPDirectory', 'Start-FXPFile')
# SIG # Begin signature block
# MIIdWQYJKoZIhvcNAQcCoIIdSjCCHUYCAQExCzAJBgUrDgMCGgUAMGkGCisGAQQB
# gjcCAQSgWzBZMDQGCisGAQQBgjcCAR4wJgIDAQAABBAfzDtgWUsITrck0sYpfvNR
# AgEAAgEAAgEAAgEAAgEAMCEwCQYFKw4DAhoFAAQU4xY+s7z0AH+szfGP7eJ9sPSt
# g9mgghhnMIIDtzCCAp+gAwIBAgIQDOfg5RfYRv6P5WD8G/AwOTANBgkqhkiG9w0B
# AQUFADBlMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYD
# VQQLExB3d3cuZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVk
# IElEIFJvb3QgQ0EwHhcNMDYxMTEwMDAwMDAwWhcNMzExMTEwMDAwMDAwWjBlMQsw
# CQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cu
# ZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3Qg
# Q0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCtDhXO5EOAXLGH87dg
# +XESpa7cJpSIqvTO9SA5KFhgDPiA2qkVlTJhPLWxKISKityfCgyDF3qPkKyK53lT
# XDGEKvYPmDI2dsze3Tyoou9q+yHyUmHfnyDXH+Kx2f4YZNISW1/5WBg1vEfNoTb5
# a3/UsDg+wRvDjDPZ2C8Y/igPs6eD1sNuRMBhNZYW/lmci3Zt1/GiSw0r/wty2p5g
# 0I6QNcZ4VYcgoc/lbQrISXwxmDNsIumH0DJaoroTghHtORedmTpyoeb6pNnVFzF1
# roV9Iq4/AUaG9ih5yLHa5FcXxH4cDrC0kqZWs72yl+2qp/C3xag/lRbQ/6GW6whf
# GHdPAgMBAAGjYzBhMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0G
# A1UdDgQWBBRF66Kv9JLLgjEtUYunpyGd823IDzAfBgNVHSMEGDAWgBRF66Kv9JLL
# gjEtUYunpyGd823IDzANBgkqhkiG9w0BAQUFAAOCAQEAog683+Lt8ONyc3pklL/3
# cmbYMuRCdWKuh+vy1dneVrOfzM4UKLkNl2BcEkxY5NM9g0lFWJc1aRqoR+pWxnmr
# EthngYTffwk8lOa4JiwgvT2zKIn3X/8i4peEH+ll74fg38FnSbNd67IJKusm7Xi+
# fT8r87cmNW1fiQG2SVufAQWbqz0lwcy2f8Lxb4bG+mRo64EtlOtCt/qMHt1i8b5Q
# Z7dsvfPxH2sMNgcWfzd8qVttevESRmCD1ycEvkvOl77DZypoEd+A5wwzZr8TDRRu
# 838fYxAe+o0bJW1sj6W3YQGx0qMmoRBxna3iw/nDmVG3KwcIzi7mULKn+gpFL6Lw
# 8jCCBP4wggPmoAMCAQICEA1CSuC+Ooj/YEAhzhQA8N0wDQYJKoZIhvcNAQELBQAw
# cjELMAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQ
# d3d3LmRpZ2ljZXJ0LmNvbTExMC8GA1UEAxMoRGlnaUNlcnQgU0hBMiBBc3N1cmVk
# IElEIFRpbWVzdGFtcGluZyBDQTAeFw0yMTAxMDEwMDAwMDBaFw0zMTAxMDYwMDAw
# MDBaMEgxCzAJBgNVBAYTAlVTMRcwFQYDVQQKEw5EaWdpQ2VydCwgSW5jLjEgMB4G
# A1UEAxMXRGlnaUNlcnQgVGltZXN0YW1wIDIwMjEwggEiMA0GCSqGSIb3DQEBAQUA
# A4IBDwAwggEKAoIBAQDC5mGEZ8WK9Q0IpEXKY2tR1zoRQr0KdXVNlLQMULUmEP4d
# yG+RawyW5xpcSO9E5b+bYc0VkWJauP9nC5xj/TZqgfop+N0rcIXeAhjzeG28ffnH
# bQk9vmp2h+mKvfiEXR52yeTGdnY6U9HR01o2j8aj4S8bOrdh1nPsTm0zinxdRS1L
# sVDmQTo3VobckyON91Al6GTm3dOPL1e1hyDrDo4s1SPa9E14RuMDgzEpSlwMMYpK
# jIjF9zBa+RSvFV9sQ0kJ/SYjU/aNY+gaq1uxHTDCm2mCtNv8VlS8H6GHq756Wwog
# L0sJyZWnjbL61mOLTqVyHO6fegFz+BnW/g1JhL0BAgMBAAGjggG4MIIBtDAOBgNV
# HQ8BAf8EBAMCB4AwDAYDVR0TAQH/BAIwADAWBgNVHSUBAf8EDDAKBggrBgEFBQcD
# CDBBBgNVHSAEOjA4MDYGCWCGSAGG/WwHATApMCcGCCsGAQUFBwIBFhtodHRwOi8v
# d3d3LmRpZ2ljZXJ0LmNvbS9DUFMwHwYDVR0jBBgwFoAU9LbhIB3+Ka7S5GGlsqIl
# ssgXNW4wHQYDVR0OBBYEFDZEho6kurBmvrwoLR1ENt3janq8MHEGA1UdHwRqMGgw
# MqAwoC6GLGh0dHA6Ly9jcmwzLmRpZ2ljZXJ0LmNvbS9zaGEyLWFzc3VyZWQtdHMu
# Y3JsMDKgMKAuhixodHRwOi8vY3JsNC5kaWdpY2VydC5jb20vc2hhMi1hc3N1cmVk
# LXRzLmNybDCBhQYIKwYBBQUHAQEEeTB3MCQGCCsGAQUFBzABhhhodHRwOi8vb2Nz
# cC5kaWdpY2VydC5jb20wTwYIKwYBBQUHMAKGQ2h0dHA6Ly9jYWNlcnRzLmRpZ2lj
# ZXJ0LmNvbS9EaWdpQ2VydFNIQTJBc3N1cmVkSURUaW1lc3RhbXBpbmdDQS5jcnQw
# DQYJKoZIhvcNAQELBQADggEBAEgc3LXpmiO85xrnIA6OZ0b9QnJRdAojR6OrktIl
# xHBZvhSg5SeBpU0UFRkHefDRBMOG2Tu9/kQCZk3taaQP9rhwz2Lo9VFKeHk2eie3
# 8+dSn5On7UOee+e03UEiifuHokYDTvz0/rdkd2NfI1Jpg4L6GlPtkMyNoRdzDfTz
# ZTlwS/Oc1np72gy8PTLQG8v1Yfx1CAB2vIEO+MDhXM/EEXLnG2RJ2CKadRVC9S0y
# OIHa9GCiurRS+1zgYSQlT7LfySmoc0NR2r1j1h9bm/cuG08THfdKDXF+l7f0P4Tr
# weOjSaH6zqe/Vs+6WXZhiV9+p7SOZ3j5NpjhyyjaW4emii8wggUwMIIEGKADAgEC
# AhAECRgbX9W7ZnVTQ7VvlVAIMA0GCSqGSIb3DQEBCwUAMGUxCzAJBgNVBAYTAlVT
# MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5j
# b20xJDAiBgNVBAMTG0RpZ2lDZXJ0IEFzc3VyZWQgSUQgUm9vdCBDQTAeFw0xMzEw
# MjIxMjAwMDBaFw0yODEwMjIxMjAwMDBaMHIxCzAJBgNVBAYTAlVTMRUwEwYDVQQK
# EwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xMTAvBgNV
# BAMTKERpZ2lDZXJ0IFNIQTIgQXNzdXJlZCBJRCBDb2RlIFNpZ25pbmcgQ0EwggEi
# MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQD407Mcfw4Rr2d3B9MLMUkZz9D7
# RZmxOttE9X/lqJ3bMtdx6nadBS63j/qSQ8Cl+YnUNxnXtqrwnIal2CWsDnkoOn7p
# 0WfTxvspJ8fTeyOU5JEjlpB3gvmhhCNmElQzUHSxKCa7JGnCwlLyFGeKiUXULaGj
# 6YgsIJWuHEqHCN8M9eJNYBi+qsSyrnAxZjNxPqxwoqvOf+l8y5Kh5TsxHM/q8grk
# V7tKtel05iv+bMt+dDk2DZDv5LVOpKnqagqrhPOsZ061xPeM0SAlI+sIZD5SlsHy
# DxL0xY4PwaLoLFH3c7y9hbFig3NBggfkOItqcyDQD2RzPJ6fpjOp/RnfJZPRAgMB
# AAGjggHNMIIByTASBgNVHRMBAf8ECDAGAQH/AgEAMA4GA1UdDwEB/wQEAwIBhjAT
# BgNVHSUEDDAKBggrBgEFBQcDAzB5BggrBgEFBQcBAQRtMGswJAYIKwYBBQUHMAGG
# GGh0dHA6Ly9vY3NwLmRpZ2ljZXJ0LmNvbTBDBggrBgEFBQcwAoY3aHR0cDovL2Nh
# Y2VydHMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0QXNzdXJlZElEUm9vdENBLmNydDCB
# gQYDVR0fBHoweDA6oDigNoY0aHR0cDovL2NybDQuZGlnaWNlcnQuY29tL0RpZ2lD
# ZXJ0QXNzdXJlZElEUm9vdENBLmNybDA6oDigNoY0aHR0cDovL2NybDMuZGlnaWNl
# cnQuY29tL0RpZ2lDZXJ0QXNzdXJlZElEUm9vdENBLmNybDBPBgNVHSAESDBGMDgG
# CmCGSAGG/WwAAgQwKjAoBggrBgEFBQcCARYcaHR0cHM6Ly93d3cuZGlnaWNlcnQu
# Y29tL0NQUzAKBghghkgBhv1sAzAdBgNVHQ4EFgQUWsS5eyoKo6XqcQPAYPkt9mV1
# DlgwHwYDVR0jBBgwFoAUReuir/SSy4IxLVGLp6chnfNtyA8wDQYJKoZIhvcNAQEL
# BQADggEBAD7sDVoks/Mi0RXILHwlKXaoHV0cLToaxO8wYdd+C2D9wz0PxK+L/e8q
# 3yBVN7Dh9tGSdQ9RtG6ljlriXiSBThCk7j9xjmMOE0ut119EefM2FAaK95xGTlz/
# kLEbBw6RFfu6r7VRwo0kriTGxycqoSkoGjpxKAI8LpGjwCUR4pwUR6F6aGivm6dc
# IFzZcbEMj7uo+MUSaJ/PQMtARKUT8OZkDCUIQjKyNookAv4vcn4c10lFluhZHen6
# dGRrsutmQ9qzsIzV6Q3d9gEgzpkxYz0IGhizgZtPxpMQBvwHgfqL2vmCSfdibqFT
# +hKUGIUukpHqaGxEMrJmoecYpJpkUe8wggUxMIIEGaADAgECAhAKoSXW1jIbfkHk
# Bdo2l8IVMA0GCSqGSIb3DQEBCwUAMGUxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxE
# aWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xJDAiBgNVBAMT
# G0RpZ2lDZXJ0IEFzc3VyZWQgSUQgUm9vdCBDQTAeFw0xNjAxMDcxMjAwMDBaFw0z
# MTAxMDcxMjAwMDBaMHIxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJ
# bmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xMTAvBgNVBAMTKERpZ2lDZXJ0
# IFNIQTIgQXNzdXJlZCBJRCBUaW1lc3RhbXBpbmcgQ0EwggEiMA0GCSqGSIb3DQEB
# AQUAA4IBDwAwggEKAoIBAQC90DLuS82Pf92puoKZxTlUKFe2I0rEDgdFM1EQfdD5
# fU1ofue2oPSNs4jkl79jIZCYvxO8V9PD4X4I1moUADj3Lh477sym9jJZ/l9lP+Cb
# 6+NGRwYaVX4LJ37AovWg4N4iPw7/fpX786O6Ij4YrBHk8JkDbTuFfAnT7l3ImgtU
# 46gJcWvgzyIQD3XPcXJOCq3fQDpct1HhoXkUxk0kIzBdvOw8YGqsLwfM/fDqR9mI
# UF79Zm5WYScpiYRR5oLnRlD9lCosp+R1PrqYD4R/nzEU1q3V8mTLex4F0IQZchfx
# FwbvPc3WTe8GQv2iUypPhR3EHTyvz9qsEPXdrKzpVv+TAgMBAAGjggHOMIIByjAd
# BgNVHQ4EFgQU9LbhIB3+Ka7S5GGlsqIlssgXNW4wHwYDVR0jBBgwFoAUReuir/SS
# y4IxLVGLp6chnfNtyA8wEgYDVR0TAQH/BAgwBgEB/wIBADAOBgNVHQ8BAf8EBAMC
# AYYwEwYDVR0lBAwwCgYIKwYBBQUHAwgweQYIKwYBBQUHAQEEbTBrMCQGCCsGAQUF
# BzABhhhodHRwOi8vb2NzcC5kaWdpY2VydC5jb20wQwYIKwYBBQUHMAKGN2h0dHA6
# Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydEFzc3VyZWRJRFJvb3RDQS5j
# cnQwgYEGA1UdHwR6MHgwOqA4oDaGNGh0dHA6Ly9jcmw0LmRpZ2ljZXJ0LmNvbS9E
# aWdpQ2VydEFzc3VyZWRJRFJvb3RDQS5jcmwwOqA4oDaGNGh0dHA6Ly9jcmwzLmRp
# Z2ljZXJ0LmNvbS9EaWdpQ2VydEFzc3VyZWRJRFJvb3RDQS5jcmwwUAYDVR0gBEkw
# RzA4BgpghkgBhv1sAAIEMCowKAYIKwYBBQUHAgEWHGh0dHBzOi8vd3d3LmRpZ2lj
# ZXJ0LmNvbS9DUFMwCwYJYIZIAYb9bAcBMA0GCSqGSIb3DQEBCwUAA4IBAQBxlRLp
# UYdWac3v3dp8qmN6s3jPBjdAhO9LhL/KzwMC/cWnww4gQiyvd/MrHwwhWiq3BTQd
# aq6Z+CeiZr8JqmDfdqQ6kw/4stHYfBli6F6CJR7Euhx7LCHi1lssFDVDBGiy23UC
# 4HLHmNY8ZOUfSBAYX4k4YU1iRiSHY4yRUiyvKYnleB/WCxSlgNcSR3CzddWThZN+
# tpJn+1Nhiaj1a5bA9FhpDXzIAbG5KHW3mWOFIoxhynmUfln8jA/jb7UBJrZspe6H
# USHkWGCbugwtK22ixH67xCUrRwIIfEmuE7bhfEJCKMYYVs9BNLZmXbZ0e/VWMyIv
# IjayS6JKldj1po5SMIIFPTCCBCWgAwIBAgIQBNXcH0jqydhSALrNmpsqpzANBgkq
# hkiG9w0BAQsFADByMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5j
# MRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMTEwLwYDVQQDEyhEaWdpQ2VydCBT
# SEEyIEFzc3VyZWQgSUQgQ29kZSBTaWduaW5nIENBMB4XDTIwMDYyNjAwMDAwMFoX
# DTIzMDcwNzEyMDAwMFowejELMAkGA1UEBhMCUEwxEjAQBgNVBAgMCcWabMSFc2tp
# ZTERMA8GA1UEBxMIS2F0b3dpY2UxITAfBgNVBAoMGFByemVteXPFgmF3IEvFgnlz
# IEVWT1RFQzEhMB8GA1UEAwwYUHJ6ZW15c8WCYXcgS8WCeXMgRVZPVEVDMIIBIjAN
# BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAv7KB3iyBrhkLUbbFe9qxhKKPBYqD
# Bqlnr3AtpZplkiVjpi9dMZCchSeT5ODsShPuZCIxJp5I86uf8ibo3vi2S9F9AlfF
# jVye3dTz/9TmCuGH8JQt13ozf9niHecwKrstDVhVprgxi5v0XxY51c7zgMA2g1Ub
# +3tii0vi/OpmKXdL2keNqJ2neQ5cYly/GsI8CREUEq9SZijbdA8VrRF3SoDdsWGf
# 3tZZzO6nWn3TLYKQ5/bw5U445u/V80QSoykszHRivTj+H4s8ABiforhi0i76beA6
# Ea41zcH4zJuAp48B4UhjgRDNuq8IzLWK4dlvqrqCBHKqsnrF6BmBrv+BXQIDAQAB
# o4IBxTCCAcEwHwYDVR0jBBgwFoAUWsS5eyoKo6XqcQPAYPkt9mV1DlgwHQYDVR0O
# BBYEFBixNSfoHFAgJk4JkDQLFLRNlJRmMA4GA1UdDwEB/wQEAwIHgDATBgNVHSUE
# DDAKBggrBgEFBQcDAzB3BgNVHR8EcDBuMDWgM6Axhi9odHRwOi8vY3JsMy5kaWdp
# Y2VydC5jb20vc2hhMi1hc3N1cmVkLWNzLWcxLmNybDA1oDOgMYYvaHR0cDovL2Ny
# bDQuZGlnaWNlcnQuY29tL3NoYTItYXNzdXJlZC1jcy1nMS5jcmwwTAYDVR0gBEUw
# QzA3BglghkgBhv1sAwEwKjAoBggrBgEFBQcCARYcaHR0cHM6Ly93d3cuZGlnaWNl
# cnQuY29tL0NQUzAIBgZngQwBBAEwgYQGCCsGAQUFBwEBBHgwdjAkBggrBgEFBQcw
# AYYYaHR0cDovL29jc3AuZGlnaWNlcnQuY29tME4GCCsGAQUFBzAChkJodHRwOi8v
# Y2FjZXJ0cy5kaWdpY2VydC5jb20vRGlnaUNlcnRTSEEyQXNzdXJlZElEQ29kZVNp
# Z25pbmdDQS5jcnQwDAYDVR0TAQH/BAIwADANBgkqhkiG9w0BAQsFAAOCAQEAmr1s
# z4lsLARi4wG1eg0B8fVJFowtect7SnJUrp6XRnUG0/GI1wXiLIeow1UPiI6uDMsR
# XPHUF/+xjJw8SfIbwava2eXu7UoZKNh6dfgshcJmo0QNAJ5PIyy02/3fXjbUREHI
# NrTCvPVbPmV6kx4Kpd7KJrCo7ED18H/XTqWJHXa8va3MYLrbJetXpaEPpb6zk+l8
# Rj9yG4jBVRhenUBUUj3CLaWDSBpOA/+sx8/XB9W9opYfYGb+1TmbCkhUg7TB3gD6
# o6ESJre+fcnZnPVAPESmstwsT17caZ0bn7zETKlNHbc1q+Em9kyBjaQRcEQoQQNp
# ezQug9ufqExx6lHYDjGCBFwwggRYAgEBMIGGMHIxCzAJBgNVBAYTAlVTMRUwEwYD
# VQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xMTAv
# BgNVBAMTKERpZ2lDZXJ0IFNIQTIgQXNzdXJlZCBJRCBDb2RlIFNpZ25pbmcgQ0EC
# EATV3B9I6snYUgC6zZqbKqcwCQYFKw4DAhoFAKB4MBgGCisGAQQBgjcCAQwxCjAI
# oAKAAKECgAAwGQYJKoZIhvcNAQkDMQwGCisGAQQBgjcCAQQwHAYKKwYBBAGCNwIB
# CzEOMAwGCisGAQQBgjcCARUwIwYJKoZIhvcNAQkEMRYEFJvgXbh5Tt5rfBnFbvPz
# apfb56fxMA0GCSqGSIb3DQEBAQUABIIBAJxkyR2bpqfXAZnSJ5vq89+FLCCVulDR
# V9BRWejEum7pempDdzJvwVNB1GSVt4/Q4JIG8iTGz6xyTnMQ7F/+4UAHcWDFXVcN
# 2TKZRHQ4yoyb5pcJ8qbKAjqblIHumKujEm+6ejmBimIoD7lxaHPally1H4sB/Syg
# syl/1zfKPwOQas6bxnU22XHru2y6mBNq2b/kVFBNtfv44pnRePj6+Srmqqc4YI2G
# Cue7yrFjyG6IiWrZatRFnALq3Ry4OxFNSeVlXPPeiCjoeq4rnNtCKpZqOQVq1eEC
# ZkUYDCYix5CpkaV8EQUU/SPz4gCOnqZZGyUcUOSaaHfAhztV5sAt2gShggIwMIIC
# LAYJKoZIhvcNAQkGMYICHTCCAhkCAQEwgYYwcjELMAkGA1UEBhMCVVMxFTATBgNV
# BAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3LmRpZ2ljZXJ0LmNvbTExMC8G
# A1UEAxMoRGlnaUNlcnQgU0hBMiBBc3N1cmVkIElEIFRpbWVzdGFtcGluZyBDQQIQ
# DUJK4L46iP9gQCHOFADw3TANBglghkgBZQMEAgEFAKBpMBgGCSqGSIb3DQEJAzEL
# BgkqhkiG9w0BBwEwHAYJKoZIhvcNAQkFMQ8XDTIxMDQwMzE4MjE0MFowLwYJKoZI
# hvcNAQkEMSIEIJ/Nf24Y3sukYuf2kpGzevFR3SdV4kekfnQYXjcuQbUnMA0GCSqG
# SIb3DQEBAQUABIIBAEGvbw+uE+RAauYEKyVxp3YaEytDdN7YNeDnl4qvjBCuEIyN
# LDWhgsfLJwouwFrJuCvSgoIOfkdjWElHjLWVn7vinv3HeZhqBDrFnGHtkg79lHx5
# QkaUeblNAV1cCukNW+BGlD5N3krPqkclIbwSJVcRB0YDU8Xmx5B1rMat+t2eGsAs
# gEKdbm3vf5oR8dqYMl6Cgbh+yoyqfjRgon6YnNL/jb0c9ye77nKbfGXr1jp4rjVn
# 2xFopmm2dPvqJd4Q3XMKDzu7cuI5l2WEsdpjArmwAapMQ6gQ9/ubUahSp8ZqjWKG
# bq1ixMAxLCk4UC0hc0mm/ecHQNa9onW0aIQl96Q=
# SIG # End signature block