'  
'   Rebex Sample Code License
' 
'   Copyright 2026, 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.Web.UI
Imports Rebex.Net
Imports Rebex
Imports Rebex.IO

Partial Public Class _Default
    Inherits Page
#Region "Private fields"

    ' connection information
    Private _connectionInfo As ConnectionInfo

    ' path to the current directory 
    Private _currentDirectory As String

    ' an Ftp object used to communicate with remote FTP server
    Private _ftp As Ftp

#End Region

#Region "Properties"

    ''' <summary>
    ''' Gets the path to the current directory.
    ''' </summary>
    Public ReadOnly Property CurrentDirectory() As String
        Get
            If _currentDirectory Is Nothing Then Return String.Empty
            Return _currentDirectory
        End Get
    End Property

#End Region

#Region "Event handlers for buttons"

    ''' <summary>
    ''' Handles the 'Upload' button click.
    ''' </summary>
    Protected Sub UploadFile(ByVal sender As Object, ByVal e As EventArgs)
        UploadFile()
    End Sub

#End Region

    Protected Sub Page_Load(ByVal sender As Object, ByVal e As EventArgs) Handles Me.Load
        ' load saved ConnectionInfo from the Session
        _connectionInfo = ConnectionInfo.Load()

        ' Ensure, that we have all information needed for establishing connection to the FTP server.
        ' Redirect to login page if connection info is not set 
        ' (e.g. this page was visited for the first time or session has expired or logout button was clicked)
        If _connectionInfo Is Nothing Then
            If Request.QueryString.Count = 0 Then
                Response.Redirect("Login.aspx")
            Else
                Response.Redirect(String.Format("Login.aspx?{0}={1}", UrlParameter.ReturnUrl, Uri.EscapeDataString(Request.Url.PathAndQuery)))
            End If
        End If

        ' PostBack events logic is done in their handlers
        If Not Page.IsPostBack Then
            ProcessCommand()
        End If
    End Sub

    ''' <summary>
    ''' Reads HttpRequest parameters, connects to the FTP server, executes requested command and disconnects from the server.
    ''' </summary>
    Private Sub ProcessCommand()
        Try
            ' connect to the FTP server
            ConnectFtp()

            ' execute requested FTP command
            ExecuteFtpCommand()
        Catch ex As ApplicationException
            ' show the error message
            lblError.Text = ex.Message
        Catch ex As FtpException
            ' show the error message
            lblError.Text = ex.Message
        Finally
            If _ftp IsNot Nothing Then
                ' close connect if connected
                If _ftp.State = FtpState.Ready Then
                    _ftp.Disconnect()
                End If

                ' dispose the oject
                _ftp.Dispose()
                _ftp = Nothing
            End If
        End Try
    End Sub

    ''' <summary>
    ''' Executes requested FTP command. 
    ''' </summary>
    ''' <remarks>
    ''' FTP Command and required parameters are taken from HttpRequest. The
    ''' method requires fully connected and logged in _ftp object.
    ''' </remarks>
    Private Sub ExecuteFtpCommand()
        ' requested FTP command
        Dim command As String = Request(UrlParameter.Command)

        ' choose appropriate action according to the requested command
        Select Case command
            Case UrlParameterCommand.List
                ShowFileList()
                Exit Select
            Case UrlParameterCommand.Download
                DownloadFile()
                Exit Select
            Case UrlParameterCommand.DeleteFile
                DeleteFile()
                Exit Select
            Case UrlParameterCommand.CreateDirectory
                CreateDirectory()
                Exit Select
            Case UrlParameterCommand.DeleteDirectory
                DeleteDirectory()
                Exit Select
            Case UrlParameterCommand.Rename
                Rename()
                Exit Select
            Case UrlParameterCommand.Symlink
                ResolveSymlink()
                Exit Select
            Case UrlParameterCommand.LogOut
                LogOut()
                Exit Select
            Case Else
                Throw New InvalidOperationException(String.Format("Unknown COMMAND '{0}'.", command))
        End Select
    End Sub

    ''' <summary>
    ''' Receives the directory list and binds it to the grid view.
    ''' </summary>
    Private Sub ShowFileList()
        ' set current directory
        _ftp.ChangeDirectory(_currentDirectory)

        ' get list of the current directory
        Dim list As FtpItemCollection = _ftp.GetList()

        Dim comparer1 = New FileSystemItemComparer(FileSystemItemComparerType.FileType, SortingOrder.Ascending)
        Dim comparer2 = New FileSystemItemComparer(FileSystemItemComparerType.Name, SortingOrder.Ascending)

        Dim multiComparer = New MultiComparer(comparer1, comparer2)

        ' sort list by the default comparer
        list.Sort(multiComparer)

        ' bind GridView
        gwList.DataSource = list
        gwList.DataBind()
    End Sub

    ''' <summary>
    ''' Parses the HTTP Range header.
    ''' </summary>
    ''' <param name="range">The HTTP Range header.</param>
    ''' <param name="length">Resource length.</param>
    ''' <param name="from">First byte position.</param>
    ''' <param name="to">Last byte position.</param>
    ''' <returns>True if parsing was succesful; otherwise false.</returns>
    Private Function ParseRange(range As String, length As Long, ByRef from As Long, ByRef [to] As Long) As Boolean
        from = -1
        [to] = -1

        If ((range Is Nothing) Or (Not range.StartsWith("bytes="))) Then
            Return False
        End If

        range = range.Substring(6)

        Dim index As Integer = range.IndexOf(",")
        If index >= 0 Then
            range = range.Substring(0, index)
        End If

        Dim ranges As String() = range.Split("-")

        If ranges.Length <> 2 Then
            Return False
        End If

        If Not Long.TryParse(ranges(0), from) Then
            If ranges(0) <> String.Empty Then
                Return False
            End If

            from = -1
        End If

        If Not Long.TryParse(ranges(1), [to]) Then
            If ranges(1) <> String.Empty Then
                Return False
            End If

            [to] = -1
        End If

        If from = -1 Then
            If [to] = -1 Then
                Return False
            End If

            from = length - [to]
            [to] = length - 1
        ElseIf [to] = -1 Then
            [to] = length - 1
        End If

        Return True
    End Function

    ''' <summary>
    ''' Downloads a file from the FTP server to asp.net server. 
    ''' Then sends the file to client's web browser via HTTP.
    ''' </summary>
    Private Sub DownloadFile()
        ' Communication scheme:
        '
        ' 1. Web browser connects to ASP.NET web server and requests
        '    file download.
        '
        ' 2. ASP.NET server connects to FTP server and requests the download.
        '
        ' 3. FTP server starts sending the file back to ASP.NET server.
        '
        ' 4. ASP.NET page starts receiving the file. All received data are 
        '    immediately sent to web browser on client machine.
        '
        ' 5. ASP.NET closes the connection to the FTP server.
        ' 

        ' absolute path to the file
        Dim remotePath As String = Request(UrlParameter.UrlCommandArgument1)
        Dim range As String = Request.Headers("Range")

        ' prepare name of the file
        Dim fileName As String = GetFileName(remotePath)
        Dim length As Long = _ftp.GetFileLength(remotePath)

        ' clear response stream and set response header and content type
        Response.Clear()
        Response.AddHeader("Content-Disposition", String.Format("attachment; filename={0}", fileName))
        Response.ContentType = "application/octet-stream"
        Response.AddHeader("Accept-Ranges", "bytes")
        Response.BufferOutput = False

        Dim from As Long
        Dim [to] As Long
        If (range Is Nothing) Then
            ' transfer the whole file (no range specified)

            Response.AddHeader("Content-Length", length.ToString())

            ' write the file content to the response
            _ftp.GetFile(remotePath, Response.OutputStream)
        ElseIf (ParseRange(range, length, from, [to]) And from >= 0 And [to] = length - 1 And from < length) Then
            ' transfer the requested file part

            Dim contentLength As Long = length - from
            Response.AddHeader("Content-Length", contentLength.ToString())
            Response.AddHeader("Content-Range", String.Format("bytes {0}-{1}/{2}", from, [to], length))
            Response.Status = "206 Partial Content"

            ' write the file content to the response
            _ftp.GetFile(remotePath, Response.OutputStream, from)
        Else
            ' report an error
            Response.AddHeader("Content-Range", String.Format("bytes */{0}", length))
            Response.Status = "416 Range Not Satisfiable"
        End If

        ' close connection to the FTP server
        _ftp.Disconnect()

        Response.End()
    End Sub

    ''' <summary>
    ''' Uploads a file.
    ''' </summary>
    Private Sub UploadFile()
        ' Communication scheme:
        ' 
        ' 1. Web browser connects to ASP.NET web server and sends file
        '    via FileUpload control.
        ' 
        ' 2. ASP.NET server connects to FTP server
        ' 
        ' 3. ASP.NET server starts receiving the file from the web browsers.
        '    All received data are immediately sent to FTP server.
        ' 
        ' 4. ASP.NET closes the connection to the FTP server.
        '


        Try
            ' connect to the FTP server
            ConnectFtp()

            ' FileUpload control has the file ready
            If fileToUpload.HasFile Then
                ' upload the file
                Dim remotePath As String = PathCombine(_currentDirectory, fileToUpload.FileName)
                _ftp.PutFile(fileToUpload.FileContent, remotePath)

                ' show succes message
                lblSuccess.Text = "File was successfully uploaded."

                ' reload the directory list
                ShowFileList()
            Else
                ' no file has been uploaded using FileUpload control, show the error message
                lblError.Text = "No file to upload."
            End If

            ' disconnect from the FTP server
            _ftp.Disconnect()
        Catch ex As FtpException
            ' show error message
            lblError.Text = ex.Message
        Finally
            ' dispose the Ftp object
            If _ftp IsNot Nothing Then
                _ftp.Dispose()
            End If
        End Try
    End Sub

    ''' <summary>
    ''' Deletes the file.
    ''' </summary>
    Private Sub DeleteFile()
        ' absolute path to the file
        Dim remotePath As String = Request(UrlParameter.UrlCommandArgument1)

        ' delete the file
        _ftp.DeleteFile(remotePath)

        ShowFileList()
    End Sub

    ''' <summary>
    ''' Creates a new directory.
    ''' </summary>
    Private Sub CreateDirectory()
        ' new direcoty name
        Dim name As String = Request(UrlParameter.UrlCommandArgument1)

        ' create the directory
        _ftp.CreateDirectory(PathCombine(_currentDirectory, name))

        ShowFileList()
    End Sub

    ''' <summary>
    ''' Deletes the directory.
    ''' </summary>
    Private Sub DeleteDirectory()
        ' absolute path to the directory
        Dim remotePath As String = Request(UrlParameter.UrlCommandArgument1)

        ' delete the directory
        _ftp.RemoveDirectory(remotePath)

        ShowFileList()
    End Sub

    ''' <summary>
    ''' Renames the file or directory.
    ''' </summary>
    Private Sub Rename()
        ' name of the original file or directory
        Dim originalName As String = Request(UrlParameter.UrlCommandArgument1)

        ' new name of the file or directory
        Dim newName As String = Request(UrlParameter.UrlCommandArgument2)

        If originalName Is Nothing Then
            originalName = String.Empty
        Else
            originalName.Trim()
        End If

        If newName Is Nothing Then
            newName = String.Empty
        Else
            newName.Trim()
        End If

        If originalName = String.Empty Then
            Throw New InvalidOperationException("UrlCommandArgument1 parameter is not filled.")
        End If

        If newName = String.Empty Then
            Throw New ApplicationException("New name was not specified.")
        End If

        ' rename the file or directory
        _ftp.Rename(PathCombine(_currentDirectory, originalName), PathCombine(_currentDirectory, newName))

        ShowFileList()
    End Sub

    ''' <summary>
    ''' Resolves symlink and chooses appropriate action to do 
    ''' (download it if it's a file; otherwise list it as a directory).
    ''' </summary>
    ''' <remarks>
    ''' This is a very simple and not accurate heuristic.
    ''' </remarks>
    Private Sub ResolveSymlink()
        ' absolute path to the symlink
        Dim remotePath As String = Request(UrlParameter.UrlCommandArgument1)

        ' if the symlink is a regular file, than FileExists method should return true
        If _ftp.FileExists(remotePath) Then
            DownloadFile()
        Else
            ShowFileList()
        End If
    End Sub

    ''' <summary>
    ''' Redirects to Login page where logout logic is performed.
    ''' </summary>
    Private Sub LogOut()
        ' redirect to Login page
        Response.Redirect(String.Format("Login.aspx?{0}={1}", UrlParameter.Command, UrlParameterCommand.LogOut))
    End Sub

#Region "Helper methods"

    ''' <summary>
    ''' Connects and logs in to the FTP server. Prepares the toolbar navigation also.
    ''' </summary>
    Private Sub ConnectFtp()
        ' create new Ftp object
        _ftp = New Ftp()

        _ftp.Settings.SslAcceptAllCertificates = _connectionInfo.AcceptAllCertificates

        ' connect to the FTP server
        _ftp.Connect(_connectionInfo.ServerName, _connectionInfo.Port, _connectionInfo.Security)

        ' log in
        _ftp.Login(_connectionInfo.UserName, _connectionInfo.Password)

        ' set the current directory
        If Not String.IsNullOrEmpty(Request(UrlParameter.CurrentFolder)) Then
            _currentDirectory = Request(UrlParameter.CurrentFolder)
        Else
            _currentDirectory = _ftp.GetCurrentDirectory()
        End If

        ' set current directory label
        ltPath.Text = String.Format("Index of {0}: {1}", _connectionInfo.ServerName, _currentDirectory)

        ' set refresh link URL
        hlRefresh.NavigateUrl = AssembleParamterUrl(UrlParameterCommand.List, String.Empty, _currentDirectory)

        ' set higher level directory link URL or hide it if it's a root directory
        If _currentDirectory = Constants.PathDelimiter.ToString() Then
            liCmdUp.Visible = False
        Else
            hlCmdUp.NavigateUrl = AssembleParamterUrl(UrlParameterCommand.List, String.Empty, GetParentDirectory(_currentDirectory))
        End If
    End Sub

    ''' <summary>
    ''' Combines two FTP path strings.
    ''' </summary>
    ''' <param name="path1">First path.</param>
    ''' <param name="path2">Second path.</param>
    ''' <returns>A string containing the combined paths.</returns>
    Protected Shared Function PathCombine(ByVal path1 As String, ByVal path2 As String) As String
        Return String.Format("{0}{1}{2}", path1.TrimEnd(Constants.PathDelimiter), Constants.PathDelimiter, path2.TrimStart(Constants.PathDelimiter))
    End Function

    ''' <summary>
    ''' Returns the file name and extension of the specified path string.
    ''' </summary>
    ''' <param name="path">The path string from which to obtain the file name and extension.</param>
    ''' <returns>
    ''' A string containing characters after the last directory delimiter character
    ''' in path. If the last character of path is a directory delimiter
    ''' character, this method returns System.String.Empty. If path is null, this
    ''' method returns null.
    ''' </returns>
    Protected Shared Function GetFileName(ByVal path As String) As String
        If path Is Nothing Then
            Return Nothing
        End If

        Dim idx As Integer = path.LastIndexOf(Constants.PathDelimiter)
        If idx < 0 Then
            Return path
        ElseIf idx = path.Length - 1 Then
            Return String.Empty
        Else
            Return path.Substring(idx + 1)
        End If
    End Function

    ''' <summary>
    ''' Returns the parent directory of the specified path string.
    ''' </summary>
    ''' <param name="path">The path string from which to obtain the parent directory.</param>
    ''' <returns>
    ''' A string containing characters before the last directory delimiter character
    ''' in path. If a directory delimiter character is the only one character, 
    ''' this method returns directory delimiter character. If path is null, this
    ''' method returns null.
    ''' </returns>
    Protected Shared Function GetParentDirectory(ByVal path As String) As String
        If path Is Nothing Then
            Return Nothing
        End If

        Dim idx As Integer = path.LastIndexOf(Constants.PathDelimiter)
        If idx <= 0 Then
            Return Constants.PathDelimiter.ToString()
        Else
            Return path.Remove(idx)
        End If
    End Function

#End Region

#Region "URL methods"

    ''' <summary>
    ''' Determines action URL for the current FtpItem.
    ''' </summary>
    ''' <param name="item">Current FtpItem.</param>
    ''' <returns>Action URL for the current FtpItem.</returns>
    Protected Function GetUrl(ByVal item As FtpItem) As String
        If item Is Nothing Then
            Return String.Empty
        End If

        Dim path As String = PathCombine(_currentDirectory, item.Name)

        Select Case item.Type
            Case FtpItemType.Directory
                Return AssembleParamterUrl(UrlParameterCommand.List, String.Empty, path)

            Case FtpItemType.File
                Return AssembleParamterUrl(UrlParameterCommand.Download, path, _currentDirectory)

            Case FtpItemType.Symlink
                Return AssembleParamterUrl(UrlParameterCommand.Symlink, path, path)
            Case Else

                Throw New InvalidOperationException(String.Format("Unknown FtpItemType '{0}'.", item.Type))
        End Select
    End Function

    ''' <summary>
    ''' Determines a URL for rename operation of the current FtpItem.
    ''' </summary>
    ''' <param name="item">Current FtpItem.</param>
    ''' <returns>URL for rename operation.</returns>
    Protected Function GetRenameUrl(ByVal item As FtpItem) As String
        If item Is Nothing Then
            Return String.Empty
        End If

        Return AssembleParamterUrl(UrlParameterCommand.Rename, item.Name, _currentDirectory)
    End Function

    ''' <summary>
    ''' Determines a URL for delete operation of the current FtpItem.
    ''' </summary>
    ''' <param name="item">Current FtpItem.</param>
    ''' <returns>Delete operation URL for the current FtpItem.</returns>
    Protected Function GetDeleteUrl(ByVal item As FtpItem) As String
        If item Is Nothing Then
            Return String.Empty
        End If

        Dim path As String = PathCombine(_currentDirectory, item.Name)

        Select Case item.Type
            Case FtpItemType.Directory
                Return AssembleParamterUrl(UrlParameterCommand.DeleteDirectory, path, _currentDirectory)

                ' regular file delete can be used for deleting symlinks
            Case FtpItemType.Symlink, FtpItemType.File
                Return AssembleParamterUrl(UrlParameterCommand.DeleteFile, path, _currentDirectory)
            Case Else

                Throw New InvalidOperationException(String.Format("Unknown FtpItemType '{0}'.", item.Type))
        End Select
    End Function

    ''' <summary>
    ''' Returns a URL query string.
    ''' </summary>
    ''' <param name="commandType">Command to perform.</param>
    ''' <param name="urlCommandArgument1">First command argument.</param>
    ''' <param name="currentDirectory">Current directory path.</param>
    ''' <returns>A URL query string.</returns>
    Protected Shared Function AssembleParamterUrl(ByVal commandType As String, ByVal urlCommandArgument1 As String, ByVal currentDirectory As String) As String
        Return String.Format("?{0}={1}&{2}={3}&{4}={5}", UrlParameter.Command, commandType, UrlParameter.UrlCommandArgument1, Uri.EscapeDataString(urlCommandArgument1), UrlParameter.CurrentFolder, _
         Uri.EscapeDataString(currentDirectory))
    End Function

#End Region

#Region "Design methods"

    ''' <summary>
    ''' Returns a file size of the current FtpItem as a formated string.
    ''' </summary>
    ''' <param name="item">Current FtpItem.</param>
    ''' <returns>String containing file size.</returns>
    Public Function GetSize(ByVal item As FtpItem) As System.Object
        If item Is Nothing OrElse item.IsDirectory Then
            Return String.Empty
        End If

        If item.Length < 1024 Then
            Return String.Format("{0} B", item.Length)
        End If

        Return String.Format("{0:N} KB", item.Length / 1024)
    End Function

    ''' <summary>
    ''' Determines appropriate image file name from the current item's type.
    ''' </summary>
    ''' <param name="item">Current FtpItem.</param>
    ''' <returns>File name of an image associated with the current item's type.</returns>
    Public Function GetIcon(ByVal item As FtpItem) As System.Object
        If item Is Nothing Then
            Return String.Empty
        End If

        Select Case item.Type
            Case FtpItemType.Directory
                Return GetIcon(IconType.Folder)

            Case FtpItemType.File
                Return GetIcon(IconType.File)

            Case FtpItemType.Symlink
                Return GetIcon(IconType.Symlink)
            Case Else

                Throw New InvalidOperationException(String.Format("Unknown FtpItemType '{0}'.", item.Type))
        End Select
    End Function

    ''' <summary>
    ''' Determines file name of an image from the IconType.
    ''' </summary>
    ''' <param name="iType">An IconType.</param>
    ''' <returns>File name of an image.</returns>
    Public Function GetIcon(ByVal iType As IconType) As String
        Select Case iType
            Case IconType.File
                Return "file.png"
            Case IconType.Folder
                Return "folder.png"
            Case IconType.Symlink
                Return "link.png"
            Case IconType.Rename
                Return "rename.png"
            Case IconType.Delete
                Return "delete.png"
            Case IconType.DirectoryUp
                Return "directory_up.png"
            Case IconType.AddFolder
                Return "create_folder.png"
            Case IconType.UploadFile
                Return "upload_file.png"
            Case IconType.Refresh
                Return "refresh.png"
            Case Else
                Throw New InvalidOperationException(String.Format("Unknown IconType '{0}'.", iType))
        End Select
    End Function

#End Region
End Class
