'  
'   Rebex Sample Code License
' 
'   Copyright 2025, REBEX CR s.r.o.
'   All rights reserved.
'   https://www.rebex.net/
' 
'   Permission to use, copy, modify, and/or distribute this software for any
'   purpose with or without fee is hereby granted.
' 
'   THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
'   EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
'   OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
'   NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
'   HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
'   WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
'   FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
'   OTHER DEALINGS IN THE SOFTWARE.
' 

Imports System
Imports System.Text
Imports System.IO

Imports Rebex.IO
Imports Rebex.Net


'' <summary>
'' This sample is a simple command line SFTP client.
'' </summary>
Class ConsoleClient

    Public Function ReadLine() As String
        Dim line As StringBuilder = New StringBuilder
        Dim complete As Boolean = False

        While (Not complete)
            Dim key As ConsoleKeyInfo = Console.ReadKey(True)
            Select Case key.Key
                Case ConsoleKey.Enter
                    complete = True
                Case ConsoleKey.Backspace
                    If (line.Length > 0) Then
                        line = line.Remove(line.Length - 1, 1)
                        Console.Write(key.KeyChar)
                        Console.Write(" ")
                        Console.Write(key.KeyChar)
                    End If
                Case Else
                    If ((key.KeyChar >= " "c) OrElse (key.Key = ConsoleKey.Tab)) Then
                        line = line.Append(key.KeyChar)
                        Console.Write("*")
                    End If
            End Select
        End While

        Console.WriteLine()
        Return line.ToString()
    End Function

    Dim _debug As Boolean = False
    Dim privateKeyPath As String = ""

    Private Sub ResponseRead(ByVal sender As Object, ByVal e As SftpResponseReadEventArgs)
        If _debug Then
            Console.WriteLine("<- {0}", e.Response)
        End If
    End Sub 'ResponseRead

    Private Sub CommandSent(ByVal sender As Object, ByVal e As SftpCommandSentEventArgs)
        If _debug Then
            Console.WriteLine("-> {0}", e.Command)
        End If
    End Sub 'ResponseRead

    Private lastReport As DateTime = DateTime.MinValue

    Private Sub TransferProgressChanged(ByVal sender As Object, ByVal e As SftpTransferProgressChangedEventArgs)
        If e.TransferState <> TransferProgressState.DataBlockProcessed Then
            Return
        End If
        If (DateTime.Now.Subtract(lastReport).TotalSeconds) > 1 Then
            Console.WriteLine((e.BytesTransferred.ToString() + " bytes."))
            lastReport = DateTime.Now
        End If
    End Sub 'TransferProgressChanged

    Private Sub AuthenticationRequest(ByVal sender As Object, ByVal e As SshAuthenticationRequestEventArgs)
        ' this is only needed if the server utilizes keyboard-interactive authentication to ask non-trivial questions

        Console.WriteLine("Server: {0}", e.Name)
        Console.WriteLine("Instructions: {0}", e.Instructions)
        For Each item As SshAuthenticationRequestItem In e.Items
            ' display question
            Console.Write(item.Prompt)

            ' get answer
            Dim response As String
            If item.IsSecret Then
                response = ReadLine()
            Else
                response = Console.ReadLine()
            End If
            item.Response = response
        Next
    End Sub

    Private ftp As Sftp

    Public Sub New()
        ' create Sftp object and set response handler
        ftp = New Sftp
        AddHandler ftp.CommandSent, AddressOf CommandSent
        AddHandler ftp.ResponseRead, AddressOf ResponseRead
        AddHandler ftp.TransferProgressChanged, AddressOf TransferProgressChanged

        ' this is only needed if the server utilizes keyboard-interactive authentication to ask non-trivial questions
        AddHandler ftp.AuthenticationRequest, AddressOf AuthenticationRequest
    End Sub 'New


    Private Sub Help()
        Console.WriteLine("!           get       put")
        Console.WriteLine("?           getr      putr")
        Console.WriteLine("bye         open      mkdir")
        Console.WriteLine("cd          pwd       putdir")
        Console.WriteLine("close       quit      getdir")
        Console.WriteLine("dir         rmdir     rmtree")
        Console.WriteLine("ls          chmod     chown")
        Console.WriteLine("disconnect  user      debug")
        Console.WriteLine("exit        help")
    End Sub 'Help



    Private Sub Close()
        Console.WriteLine("Disconnecting...")
        ftp.Disconnect()
    End Sub 'Close


    Private Function Open(ByVal host As String) As Boolean
        Try

            If ftp.State <> SftpState.Disconnected Then
                Console.WriteLine("Already connected. Disconnect first.")
                Return False
            End If

            If host Is Nothing OrElse host.Trim().Length = 0 Then
                Console.Write("Hostname: ")
                host = Console.ReadLine()
            End If

            Dim p As String() = host.Split(" "c)
            If p.Length < 0 OrElse p.Length > 3 Then
                Console.WriteLine("Usage: hostname [port] [private key]")
                Return False
            End If

            host = p(0)
            Dim port As Integer = Sftp.DefaultPort

            Select Case p.Length
                Case 2
                    Try
                        port = Integer.Parse(p(1))
                    Catch
                        port = -1
                    End Try

                    If port <= 0 OrElse port > 65535 Then
                        port = Sftp.DefaultPort
                        privateKeyPath = p(1)
                    End If
                Case 3
                    privateKeyPath = p(2)
            End Select

            ftp.Connect(host, port)

            Console.WriteLine("Fingerprint: " + ftp.Fingerprint.ToString())
            Console.WriteLine("Cipher info: " + ftp.Session.Cipher.ToString())

            Return True
        Catch e As SftpException
            Console.WriteLine(e.Message)
            Return False
        End Try
    End Function 'Open

    Private Sub Login()
        Console.Write("User: ")
        Dim user As String = Console.ReadLine()

        If privateKeyPath = "" Then
            Console.Write("Password: ")
        Else
            Console.Write("Passphrase for the key file: ")
        End If

        Dim pass As String = ReadLine()

        If privateKeyPath = "" Then
            ftp.Login(user, pass)
        Else
            Dim pk As SshPrivateKey = New SshPrivateKey(privateKeyPath, pass)
            ftp.Login(user, pk)

            pk = Nothing
            privateKeyPath = ""
        End If
    End Sub 'Login


    Private Sub Pwd()
        If ftp.State = SftpState.Disconnected Then
            Console.WriteLine("Not connected.")
            Return
        End If

        ftp.GetCurrentDirectory()
    End Sub 'Pwd

    Private Sub Cd(ByVal param As String)
        If ftp.State = SftpState.Disconnected Then
            Console.WriteLine("Not connected.")
            Return
        End If

        ftp.ChangeDirectory(param)
    End Sub 'Cd


    Private Sub Quit()
        If ftp.State <> SftpState.Disconnected Then
            Close()
        End If
        Console.WriteLine()
    End Sub 'Quit


    Private Sub Mkdir(ByVal param As String)
        If ftp.State = SftpState.Disconnected Then
            Console.WriteLine("Not connected.")
            Exit Sub
        End If

        If param Is Nothing OrElse param.Length = 0 Then
            Console.WriteLine("Usage: mkdir dirname")
            Return
        End If

        ftp.CreateDirectory(param)
    End Sub 'Mkdir

    Private Sub Rmdir(ByVal param As String)
        If ftp.State = SftpState.Disconnected Then
            Console.WriteLine("Not connected.")
            Exit Sub
        End If

        If param Is Nothing OrElse param.Length = 0 Then
            Console.WriteLine("Usage: rmdir dirname")
            Return
        End If

        ftp.RemoveDirectory(param)
    End Sub 'Rmdir

    '' <summary>
    '' Removes a whole directory tree. Use with care.
    '' </summary>
    '' <param name="ftp">An instance of Sftp object.</param>
    '' <param name="path">A path to a directory to remove.</param>
    Public Shared Sub RemoveTree(ByVal ftp As Sftp, ByVal path As String)
        '' Save the current directory and change it to 'path'.
        Dim original As String = ftp.GetCurrentDirectory()
        ftp.ChangeDirectory(path)

        '' Get this list of files and directories
        Dim list As SftpItemCollection = ftp.GetList()

        '' Delete all files and directories in the list recursively
        Dim i As Integer
        For i = 0 To list.Count - 1
            If list(i).IsDirectory Then
                RemoveTree(ftp, list(i).Name)
            Else
                ftp.DeleteFile(list(i).Name)
            End If
        Next i

        '' Go back to the original directory
        ftp.ChangeDirectory(original)

        '' Remove the directory that was just cleaned
        ftp.RemoveDirectory(path)
    End Sub ' RemoveTree


    Private Sub Rmtree(ByVal param As String)
        If ftp.State = SftpState.Disconnected Then
            Console.WriteLine("Not connected.")
            Exit Sub
        End If

        If param Is Nothing OrElse param.Length = 0 Then
            Console.WriteLine("Usage: rmtree dirname")
            Return
        End If

        RemoveTree(ftp, param)
    End Sub 'Rmtree

    Private Sub Dir()
        If ftp.State = SftpState.Disconnected Then
            Console.WriteLine("Not connected.")
            Return
        End If

        lastReport = DateTime.Now
        Dim list As SftpItemCollection = ftp.GetList()

        Dim i As Integer
        For i = 0 To list.Count - 1
            Dim item As SftpItem = list(i)
            If item.IsLink Then
                Console.Write("s ")
            ElseIf item.IsDirectory Then
                Console.Write("d ")
            Else
                Console.Write("- ")
            End If

            Console.Write((Convert.ToString(CInt(item.Permissions) And &H1FF, 8).PadLeft(4, "0"c) + " "))
            Console.Write(item.LastWriteTime.Value.ToString("u").Substring(0, 16))
            Console.Write(item.Length.ToString().PadLeft(10, " "c))
            Console.Write(" {0}", item.Name)
            Console.WriteLine()
        Next i
    End Sub 'Dir


    Private Sub Ls()
        If ftp.State = SftpState.Disconnected Then
            Console.WriteLine("Not connected.")
            Return
        End If

        lastReport = DateTime.Now
        Dim list As SftpItemCollection = ftp.GetList()

        Dim i As Integer
        For i = 0 To list.Count - 1
            Console.WriteLine(list(i).Name)
        Next i
    End Sub 'Ls


    Private Sub [Get](ByVal remotePath As String)
        If ftp.State = SftpState.Disconnected Then
            Console.WriteLine("Not connected.")
            Return
        End If

        If remotePath Is Nothing OrElse remotePath.Length = 0 Then
            Console.WriteLine("Usage: get remotePath")
            Return
        End If

        Try
            lastReport = DateTime.Now
            ftp.GetFile(remotePath, Path.GetFileName(remotePath))
        Catch e As Exception
            If TypeOf e Is SftpException Then Throw
            Console.WriteLine(e.Message)
        End Try
    End Sub 'Get



    Private Sub GetResume(ByVal remotePath As String)
        If ftp.State = SftpState.Disconnected Then
            Console.WriteLine("Not connected.")
            Return
        End If

        If remotePath Is Nothing OrElse remotePath.Length = 0 Then
            Console.WriteLine("Usage: getr remotePath")
            Return
        End If

        Dim localStream As FileStream = Nothing
        Try
            ' Get local file name corresponding to remote file name.
            Dim localPath As String = Path.GetFileName(remotePath)

            ' Get the length of remote file. This will throw exception
            ' if the file does not exists.
            Dim remoteLength As Long = ftp.GetFileLength(remotePath)

            ' Create or open the local file for writing and seek to the end.
            localStream = File.Open(localPath, FileMode.Append, FileAccess.Write, FileShare.Read)

            ' Check lengths too see if the resume makes sense.
            If localStream.Length = remoteLength Then
                Console.WriteLine("File has already been downloaded.")
            ElseIf localStream.Length > remoteLength Then
                Console.WriteLine("Local file is longer than remote file.")
            Else
                ' Display resume info if we are really resuming download.
                If localStream.Length > 0 Then
                    Console.WriteLine(("Download resumed at offset " + localStream.Length + "."))
                End If
                lastReport = DateTime.Now

                ' Download the file. Start at the position of the length of the local stream.
                ftp.GetFile(remotePath, localStream, localStream.Length, -1)
            End If
        Catch e As Exception
            If TypeOf e Is SftpException Then Throw
            Console.WriteLine(e.Message)
        Finally
            ' Close the stream if it was created.
            If Not (localStream Is Nothing) Then
                localStream.Close()
            End If
        End Try
    End Sub 'GetResume

    '' <summary>
    '' Uploads a local directory with all files and subdirectories to an SFTP server.
    '' </summary>
    '' <param name="ftp">Instance of an Sftp object.</param>
    '' <param name="localPath">The local path of the root of the directory structure to be uploaded.</param>
    '' <remarks>
    '' <p>
    '' Sftp object must be connected and should have the directory set to the desired path.
    '' </p>
    '' <p>
    '' Method reports information to console. This behaviour should be changed to reflect actual needs.
    '' </p>
    '' </remarks>
    Public Shared Sub UploadDirectory(ByVal ftp As Sftp, ByVal localPath As String)
        ' strip ending slashes (GetFileName returns empty string otherwise...)
        localPath = localPath.TrimEnd("\"c, "/"c)

        Console.WriteLine(("Uploading directory: '" + localPath + "'"))

        ' get the name of the directory and try to create it
        Dim remotePath As String = Path.GetFileName(localPath)
        Try
            ftp.CreateDirectory(remotePath)
        Catch e As SftpException
            ' throw exception if an error other than ProtocolError occurs
            If e.Status <> SftpExceptionStatus.ProtocolError Then Throw

            ' protocol error - directory probably already exists
            Console.WriteLine(e.Message)
        End Try

        ' change the directory
        ftp.ChangeDirectory(remotePath)

        ' upload all files from the localPath directory
        Dim files As String() = Directory.GetFiles(localPath)
        Dim i As Integer
        For i = 0 To files.Length - 1
            Console.WriteLine(("Uploading file: '" + files(i) + "'"))
            Try
                ftp.PutFile(files(i), Path.GetFileName(files(i)))
            Catch e As SftpException
                ' throw exception if an error other than ProtocolError occurs
                If e.Status <> SftpExceptionStatus.ProtocolError Then Throw

                ' protocol error - report it and continue with the remaining files
                Console.WriteLine(e.Message)
            End Try
        Next i

        ' upload all directories from the localPath directory
        Dim dirs As String() = Directory.GetDirectories(localPath)
        For i = 0 To dirs.Length - 1
            UploadDirectory(ftp, dirs(i))
        Next i
        ftp.ChangeDirectory("..")
    End Sub 'UploadDirectory


    Private Sub PutDir(ByVal localPath As String)
        If ftp.State = SftpState.Disconnected Then
            Console.WriteLine("Not connected.")
            Return
        End If

        If localPath Is Nothing OrElse localPath.Length = 0 Then
            Console.WriteLine("Usage: putdir localPath")
            Return
        End If

        If Not Directory.Exists(localPath) Then
            Console.WriteLine("Path does not exist.")
            Return
        End If

        lastReport = DateTime.Now
        UploadDirectory(ftp, localPath)
    End Sub 'PutDir


    Private Shared Sub DownloadDirectory(ByVal ftp As Sftp, ByVal remotePath As String)
        ' strip ending slashes (GetFileName returns empty string otherwise...)
        remotePath = remotePath.TrimEnd("\", "/")

        Console.WriteLine("Downloading directory: '" & remotePath & "'")

        ' get the name of the directory and try to create it
        Dim localPath As String = Path.GetFileName(remotePath)
        Try
            Directory.CreateDirectory(localPath)
        Catch e As Exception
            ' error - directory probably already exists
            Console.WriteLine(e.Message)
        End Try

        ' change the directory
        Directory.SetCurrentDirectory(localPath)
        ftp.ChangeDirectory(remotePath)

        ' download all files from the remotePath directory
        Dim files As SftpItemCollection = ftp.GetList()
        Dim i As Integer
        For i = 0 To files.Count - 1
            If files(i).IsFile Then
                Console.WriteLine("Downloading file: '" & files(i).Name & "'")
                Try
                    If Not File.Exists(files(i).Name) Then
                        ftp.GetFile(files(i).Name, files(i).Name)
                    End If
                Catch e As SftpException
                    ' throw exception if an error other than ProtocolError occurs
                    If e.Status <> SftpExceptionStatus.ProtocolError Then Throw

                    ' protocol error - report it and continue with the remaining files
                    Console.WriteLine(e.Message)
                End Try
            End If
        Next

        ' download all directories from the remotePath directory
        For i = 0 To i < files.Count - 1
            If files(i).IsDirectory Then
                DownloadDirectory(ftp, files(i).Name)
            End If
        Next

        Directory.SetCurrentDirectory("..")
        ftp.ChangeDirectory("..")
    End Sub 'DownloadDirectory

    Private Sub GetDir(ByVal remotePath As String)
        If ftp.State = SftpState.Disconnected Then
            Console.WriteLine("Not connected.")
            Exit Sub
        End If

        If remotePath = Nothing OrElse remotePath.Length = 0 Then
            Console.WriteLine("Usage: getdir remotePath")
            Exit Sub
        End If

        lastReport = DateTime.Now
        DownloadDirectory(ftp, remotePath)
    End Sub 'GetDir

    Private Sub ChMod(ByVal param As String)
        If ftp.State = SftpState.Disconnected Then
            Console.WriteLine("Not connected.")
            Exit Sub
        End If

        If param = Nothing OrElse param.Length = 0 Then
            Console.WriteLine("Usage: chmod mode remotePath")
            Exit Sub
        End If

        Dim pars As String() = param.Split(" "c)

        If pars.Length < 2 Then
            Console.WriteLine("Usage: chmod mode remotePath")
            Exit Sub
        End If

        Try
            Dim attr As SftpAttributes = New SftpAttributes
            attr.Permissions = CType(Convert.ToInt32(pars(0), 8), SftpPermissions)
            ftp.SetAttributes(pars(1), attr)
        Catch e As Exception
            If TypeOf e Is SftpException Then Throw

            Console.WriteLine(e.Message)
        End Try
    End Sub

    Private Sub ChOwn(ByVal param As String)
        Dim usageMessage As String = "Usage: chown uid.gid remotePath"

        If ftp.State = SftpState.Disconnected Then
            Console.WriteLine("Not connected.")
            Exit Sub
        End If

        If param = Nothing OrElse param.Length = 0 Then
            Console.WriteLine(usageMessage)
            Exit Sub
        End If

        Dim pars As String() = param.Split(" "c)

        If pars.Length < 2 Then
            Console.WriteLine(usageMessage)
            Exit Sub
        End If

        Dim parsId As String() = pars(0).Split("."c)

        If parsId.Length < 2 Then
            Console.WriteLine(usageMessage)
            Exit Sub
        End If

        Try
            Dim attr As SftpAttributes = New SftpAttributes
            attr.SetUserAndGroup(Convert.ToInt32(parsId(0)), Convert.ToInt32(parsId(1)))
            ftp.SetAttributes(pars(1), attr)
        Catch e As Exception
            If TypeOf e Is SftpException Then Throw

            Console.WriteLine(e.Message)
        End Try
    End Sub

    Private Sub Put(ByVal localPath As String)
        If ftp.State = SftpState.Disconnected Then
            Console.WriteLine("Not connected.")
            Return
        End If

        If localPath Is Nothing OrElse localPath.Length = 0 Then
            Console.WriteLine("Usage: put localPath")
            Return
        End If

        Try
            lastReport = DateTime.Now
            ftp.PutFile(localPath, Path.GetFileName(localPath))
        Catch e As Exception
            If TypeOf e Is SftpException Then Throw

            Console.WriteLine(e.Message)
        End Try
    End Sub 'Put

    Private Sub PutResume(ByVal localPath As String)
        If ftp.State = SftpState.Disconnected Then
            Console.WriteLine("Not connected.")
            Return
        End If

        If localPath Is Nothing OrElse localPath.Length = 0 Then
            Console.WriteLine("Usage: putr localPath")
            Return
        End If

        Dim localStream As FileStream = Nothing
        Try
            ' Get local file name corresponding to remote file name.
            Dim remotePath As String = Path.GetFileName(localPath)

            ' Try to get the length of the remote file. If the file
            ' does not exist, it will throw exception with status
            ' of ProtocolError and response code 550. Ignore this
            ' exception and report the others.
            Dim remoteLength As Long = 0
            Try
                remoteLength = ftp.GetFileLength(remotePath)
            Catch e As SftpException
                If e.Status <> SftpExceptionStatus.ProtocolError Then Throw
            End Try
            ' Open the local file for reading.
            localStream = File.OpenRead(localPath)

            ' Check lengths too see if the resume makes sense.
            If localStream.Length = remoteLength Then
                Console.WriteLine("File has already been uploaded.")
            ElseIf localStream.Length < remoteLength Then
                Console.WriteLine("Remote file is longer than local file.")
            Else
                ' Display resume info if we are really resuming upload.
                If remoteLength > 0 Then
                    Console.WriteLine(("Upload resumed at offset " + remoteLength + "."))
                End If
                lastReport = DateTime.Now

                ' Upload the file. Start at the position of the length of the remote stream.
                localStream.Position = remoteLength
                ftp.PutFile(localStream, remotePath, remoteLength, -1)
            End If
        Catch e As Exception
            If TypeOf e Is SftpException Then Throw

            Console.WriteLine(e.Message)
        Finally
            ' Close the stream if it was created.
            If Not (localStream Is Nothing) Then
                localStream.Close()
            End If
        End Try
    End Sub 'PutResume

    Public Sub Run()
        While True
            Console.Write("sftp> ")
            Dim command As String = Console.ReadLine().Trim()
            Dim param As String = Nothing
            Dim i As Integer = command.IndexOf(" "c)
            If i > 0 Then
                param = command.Substring((i + 1))
                command = command.Substring(0, i)
            End If

            Try
                Select Case command.ToLower()
                    Case "!"
                        GoTo Casequit
                    Case "?"
                        GoTo Casehelp
                    Case "bye"
                        GoTo Casequit
                    Case "exit"
                        GoTo Casequit
                    Case "disconnect"
                        GoTo Caseclose
                    Case "quit"
Casequit:

                        Quit()
                        Return
                    Case "close"
Caseclose:

                        Close()
                    Case "open"
                        If Not Open(param) Then
                            Exit Select
                        End If
                        Login()
                    Case "help"
Casehelp:

                        Help()
                    Case "cd"
                        Cd(param)
                    Case "pwd"
                        Pwd()
                    Case "mkdir"
                        Mkdir(param)
                    Case "rmdir"
                        Rmdir(param)
                    Case "rmtree"
                        Rmtree(param)
                    Case "get"
                        [Get](param)
                    Case "put"
                        Put(param)
                    Case "getr"
                        GetResume(param)
                    Case "putr"
                        PutResume(param)
                    Case "putdir"
                        PutDir(param)
                    Case "getdir"
                        GetDir(param)
                    Case "chmod"
                        ChMod(param)
                    Case "chown"
                        ChOwn(param)
                    Case "dir"
                        Dir()
                    Case "ls"
                        If Not (param Is Nothing) AndAlso param.StartsWith("-l") Then
                            Dir()
                        Else
                            Ls()
                        End If
                    Case "debug"
                        If _debug Then
                            Console.WriteLine("Debug is disabled.")
                            _debug = False
                        Else
                            Console.WriteLine("Debug is enabled.")
                            _debug = True
                        End If
                    Case Else
                        Console.WriteLine("Invalid command.")
                End Select
            Catch e As SftpException
                Console.WriteLine(e.ToString())
                If e.Status <> SftpExceptionStatus.ProtocolError And e.Status <> SftpExceptionStatus.ConnectionClosed Then
                    ftp.Disconnect()
                End If
            End Try
        End While
    End Sub 'Run

    Public Shared Sub Main()
            ' set the trial license key
            Rebex.Licensing.Key = Rebex.Samples.TrialKey.Key


        Dim args() As String = Environment.GetCommandLineArgs()

        Dim client As New ConsoleClient

        If args.Length > 1 Then
            Dim param As String = args(1)
            Dim i As Integer
            For i = 2 To args.Length - 1
                param += " " + args(i)
            Next i
            If client.Open(param) Then
                client.Login()
            End If
        End If
        client.Run()
    End Sub 'Main
End Class 'ConsoleClient
