I have been working on a little project that requires downloading a file from an FTP server. The problem is that it just doesn't want to work. I will upload my whole code so you can maybe give me clues on what the problem is. This is just my test project but it's basically the same as the original project.
Now the problem is; in this test project I created two buttons, the first one downloads the file with the standard future of VB.NET(FtpWebRequest). The problem is that it says that the file is downloaded but the file isn't saved anywhere on the harddisk.
The second button downloads the file using the library I've found online. Everything goes fine until it gives me the error that the file doesn't exist or that I don't have the rights to acces the file (Error code 550).
I had some more problems with downloading this file but found a solution by myself but this just won't work. I really have no clue on what I'm doing wrong.
PS: The file exists and has all the rights configured to acces the file so I don't think that that is the problem.
Hope someone can help me with this one,
Brecht
MY CODE:
Imports FTPClient.Utilities.FTP Public Class Form1 Private Sub btnTest_Click(sender As System.Object, e As System.EventArgs) Handles btnTest.Click 'Downloads the file using the library (code of the library below!) Dim ftpDownloader As New FTPClient.Utilities.FTP.FTPclient("HOST", "USERNAME", "PASSWORD") ftpDownloader.Download("/public_html/Current_NewVersion.txt", "C:\test_file.txt", True) End Sub Private Sub btnTestWebRequest_Click(sender As System.Object, e As System.EventArgs) Handles btnTestWebRequest.Click 'Downloads the file using standard FtpWebRequest Const localFile As String = "C:\LocalFile.txt" Const remoteFile As String = "/Current_NewVersion.txt" Const host As String = "HOST" Const username As String = "USERNAME" Const password As String = "PASSWORD" Dim URI As String = host & remoteFile Dim ftp As System.Net.FtpWebRequest = CType(System.Net.FtpWebRequest.Create(URI), System.Net.FtpWebRequest) ftp.Credentials = New System.Net.NetworkCredential(username, password) ftp.KeepAlive = False ftp.UseBinary = True ftp.Method = System.Net.WebRequestMethods.Ftp.DownloadFile MessageBox.Show("succes") 'Gives me the message 'Succes', no errors occurred here End Sub End Class
THE CODE OF THE LIBRARY:
Imports System.Collections.Generic Imports System.Net Imports System.IO Imports System.Text.RegularExpressions Namespace Utilities.FTP #Region "FTP client class" ''' <summary> ''' A wrapper class for .NET 2.0 FTP ''' </summary> ''' <remarks> ''' This class does not hold open an FTP connection but ''' instead is stateless: for each FTP request it ''' connects, performs the request and disconnects. ''' </remarks> Public Class FTPclient #Region "CONSTRUCTORS" ''' <summary> ''' Blank constructor ''' </summary> ''' <remarks>Hostname, username and password must be set manually</remarks> Sub New() End Sub ''' <summary> ''' Constructor just taking the hostname ''' </summary> ''' <param name="Hostname">in either ftp://ftp.host.com or ftp.host.com form</param> ''' <remarks></remarks> Sub New(ByVal Hostname As String) _hostname = Hostname End Sub ''' <summary> ''' Constructor taking hostname, username and password ''' </summary> ''' <param name="Hostname">in either ftp://ftp.host.com or ftp.host.com form</param> ''' <param name="Username">Leave blank to use 'anonymous' but set password to your email</param> ''' <param name="Password"></param> ''' <remarks></remarks> Sub New(ByVal Hostname As String, ByVal Username As String, ByVal Password As String) _hostname = Hostname _username = Username _password = Password End Sub #End Region #Region "Directory functions" ''' <summary> ''' Return a simple directory listing ''' </summary> ''' <param name="directory">Directory to list, e.g. /pub</param> ''' <returns>A list of filenames and directories as a List(of String)</returns> ''' <remarks>For a detailed directory listing, use ListDirectoryDetail</remarks> Public Function ListDirectory(Optional ByVal directory As String = "") As List(Of String) 'return a simple list of filenames in directory Dim ftp As Net.FtpWebRequest = GetRequest(GetDirectory(directory)) 'Set request to do simple list ftp.Method = Net.WebRequestMethods.Ftp.ListDirectory Dim str As String = GetStringResponse(ftp) 'replace CRLF to CR, remove last instance str = str.Replace(vbCrLf, vbCr).TrimEnd(Chr(13)) 'split the string into a list Dim result As New List(Of String) result.AddRange(str.Split(Chr(13))) Return result End Function ''' <summary> ''' Return a detailed directory listing ''' </summary> ''' <param name="directory">Directory to list, e.g. /pub/etc</param> ''' <returns>An FTPDirectory object</returns> Public Function ListDirectoryDetail(Optional ByVal directory As String = "") As FTPdirectory Dim ftp As Net.FtpWebRequest = GetRequest(GetDirectory(directory)) 'Set request to do simple list ftp.Method = Net.WebRequestMethods.Ftp.ListDirectoryDetails Dim str As String = GetStringResponse(ftp) 'replace CRLF to CR, remove last instance str = str.Replace(vbCrLf, vbCr).TrimEnd(Chr(13)) 'split the string into a list Return New FTPdirectory(str, _lastDirectory) End Function #End Region #Region "Upload: File transfer TO ftp server" ''' <summary> ''' Copy a local file to the FTP server ''' </summary> ''' <param name="localFilename">Full path of the local file</param> ''' <param name="targetFilename">Target filename, if required</param> ''' <returns></returns> ''' <remarks>If the target filename is blank, the source filename is used ''' (assumes current directory). Otherwise use a filename to specify a name ''' or a full path and filename if required.</remarks> Public Function Upload(ByVal localFilename As String, Optional ByVal targetFilename As String = "") As Boolean '1. check source If Not File.Exists(localFilename) Then Throw New ApplicationException("File " & localFilename & " not found") End If 'copy to FI Dim fi As New FileInfo(localFilename) Return Upload(fi, targetFilename) End Function ''' <summary> ''' Upload a local file to the FTP server ''' </summary> ''' <param name="fi">Source file</param> ''' <param name="targetFilename">Target filename (optional)</param> ''' <returns></returns> Public Function Upload(ByVal fi As FileInfo, Optional ByVal targetFilename As String = "") As Boolean 'copy the file specified to target file: target file can be full path or just filename (uses current dir) '1. check target Dim target As String If targetFilename.Trim = "" Then 'Blank target: use source filename & current dir target = Me.CurrentDirectory & fi.Name ElseIf targetFilename.Contains("/") Then 'If contains / treat as a full path target = AdjustDir(targetFilename) Else 'otherwise treat as filename only, use current directory target = CurrentDirectory & targetFilename End If Dim URI As String = Hostname & target 'perform copy Dim ftp As Net.FtpWebRequest = GetRequest(URI) 'Set request to upload a file in binary ftp.Method = Net.WebRequestMethods.Ftp.UploadFile ftp.UseBinary = True 'Notify FTP of the expected size ftp.ContentLength = fi.Length 'create byte array to store: ensure at least 1 byte! Const BufferSize As Integer = 2048 Dim content(BufferSize - 1) As Byte, dataRead As Integer 'open file for reading Using fs As FileStream = fi.OpenRead() Try 'open request to send Using rs As Stream = ftp.GetRequestStream Do dataRead = fs.Read(content, 0, BufferSize) rs.Write(content, 0, dataRead) Loop Until dataRead < BufferSize rs.Close() End Using Catch ex As Exception Finally 'ensure file closed fs.Close() End Try End Using ftp = Nothing Return True End Function #End Region #Region "Download: File transfer FROM ftp server" ''' <summary> ''' Copy a file from FTP server to local ''' </summary> ''' <param name="sourceFilename">Target filename, if required</param> ''' <param name="localFilename">Full path of the local file</param> ''' <returns></returns> ''' <remarks>Target can be blank (use same filename), or just a filename ''' (assumes current directory) or a full path and filename</remarks> Public Function Download(ByVal sourceFilename As String, ByVal localFilename As String, Optional ByVal PermitOverwrite As Boolean = False) As Boolean '2. determine target file Dim fi As New FileInfo(localFilename) Return Me.Download(sourceFilename, fi, PermitOverwrite) End Function 'Version taking an FtpFileInfo Public Function Download(ByVal file As FTPfileInfo, ByVal localFilename As String, Optional ByVal PermitOverwrite As Boolean = False) As Boolean Return Me.Download(file.FullName, localFilename, PermitOverwrite) End Function 'Another version taking FtpFileInfo and FileInfo Public Function Download(ByVal file As FTPfileInfo, ByVal localFI As FileInfo, Optional ByVal PermitOverwrite As Boolean = False) As Boolean Return Me.Download(file.FullName, localFI, PermitOverwrite) End Function 'Version taking string/FileInfo Public Function Download(ByVal sourceFilename As String, ByVal targetFI As FileInfo, Optional ByVal PermitOverwrite As Boolean = False) As Boolean '1. check target If targetFI.Exists And Not (PermitOverwrite) Then Throw New ApplicationException("Target file already exists") '2. check source Dim target As String If sourceFilename.Trim = "" Then Throw New ApplicationException("File not specified") ElseIf sourceFilename.Contains("/") Then 'treat as a full path target = AdjustDir(sourceFilename) Else 'treat as filename only, use current directory target = CurrentDirectory & sourceFilename End If Dim URI As String = Hostname & target '3. perform copy Dim ftp As Net.FtpWebRequest = GetRequest(URI) 'Set request to download a file in binary mode ftp.Method = Net.WebRequestMethods.Ftp.DownloadFile ftp.UseBinary = True 'open request and get response stream 'The error occurres on the this line below (The remote server returned an error: (550) File unavailable (e.g., file not found, no access). But as I said the file does exist. Using response As FtpWebResponse = CType(ftp.GetResponse, FtpWebResponse) Using responseStream As Stream = response.GetResponseStream 'loop to read & write to file Using fs As FileStream = targetFI.OpenWrite Try Dim buffer(2047) As Byte Dim read As Integer = 0 Do read = responseStream.Read(buffer, 0, buffer.Length) fs.Write(buffer, 0, read) Loop Until read = 0 responseStream.Close() fs.Flush() fs.Close() Catch ex As Exception 'catch error and delete file only partially downloaded fs.Close() 'delete target file as it's incomplete targetFI.Delete() Throw End Try End Using responseStream.Close() End Using response.Close() End Using Return True End Function #End Region #Region "Other functions: Delete rename etc." ''' <summary> ''' Delete remote file ''' </summary> ''' <param name="filename">filename or full path</param> ''' <returns></returns> ''' <remarks></remarks> Public Function FtpDelete(ByVal filename As String) As Boolean 'Determine if file or full path Dim URI As String = Me.Hostname & GetFullPath(filename) Dim ftp As Net.FtpWebRequest = GetRequest(URI) 'Set request to delete ftp.Method = Net.WebRequestMethods.Ftp.DeleteFile Try 'get response but ignore it Dim str As String = GetStringResponse(ftp) Catch ex As Exception Return False End Try Return True End Function ''' <summary> ''' Determine if file exists on remote FTP site ''' </summary> ''' <param name="filename">Filename (for current dir) or full path</param> ''' <returns></returns> ''' <remarks>Note this only works for files</remarks> Public Function FtpFileExists(ByVal filename As String) As Boolean 'Try to obtain filesize: if we get error msg containing "550" 'the file does not exist Try Dim size As Long = GetFileSize(filename) Return True Catch ex As Exception 'only handle expected not-found exception If TypeOf ex Is System.Net.WebException Then 'file does not exist/no rights error = 550 If ex.Message.Contains("550") Then 'clear Return False Else Throw End If Else Throw End If End Try End Function ''' <summary> ''' Determine size of remote file ''' </summary> ''' <param name="filename"></param> ''' <returns></returns> ''' <remarks>Throws an exception if file does not exist</remarks> Public Function GetFileSize(ByVal filename As String) As Long Dim path As String If filename.Contains("/") Then path = AdjustDir(filename) Else path = Me.CurrentDirectory & filename End If Dim URI As String = Me.Hostname & path Dim ftp As Net.FtpWebRequest = GetRequest(URI) 'Try to get info on file/dir? ftp.Method = Net.WebRequestMethods.Ftp.GetFileSize Dim tmp As String = Me.GetStringResponse(ftp) Return GetSize(ftp) End Function Public Function FtpRename(ByVal sourceFilename As String, ByVal newName As String) As Boolean 'Does file exist? Dim source As String = GetFullPath(sourceFilename) If Not FtpFileExists(source) Then Throw New FileNotFoundException("File " & source & " not found") End If 'build target name, ensure it does not exist Dim target As String = GetFullPath(newName) If target = source Then Throw New ApplicationException("Source and target are the same") ElseIf FtpFileExists(target) Then Throw New ApplicationException("Target file " & target & " already exists") End If 'perform rename Dim URI As String = Me.Hostname & source Dim ftp As Net.FtpWebRequest = GetRequest(URI) 'Set request to delete ftp.Method = Net.WebRequestMethods.Ftp.Rename ftp.RenameTo = target Try 'get response but ignore it Dim str As String = GetStringResponse(ftp) Catch ex As Exception Return False End Try Return True End Function Public Function FtpCreateDirectory(ByVal dirpath As String) As Boolean 'perform create Dim URI As String = Me.Hostname & AdjustDir(dirpath) Dim ftp As Net.FtpWebRequest = GetRequest(URI) 'Set request to MkDir ftp.Method = Net.WebRequestMethods.Ftp.MakeDirectory Try 'get response but ignore it Dim str As String = GetStringResponse(ftp) Catch ex As Exception Return False End Try Return True End Function Public Function FtpDeleteDirectory(ByVal dirpath As String) As Boolean 'perform remove Dim URI As String = Me.Hostname & AdjustDir(dirpath) Dim ftp As Net.FtpWebRequest = GetRequest(URI) 'Set request to RmDir ftp.Method = Net.WebRequestMethods.Ftp.RemoveDirectory Try 'get response but ignore it Dim str As String = GetStringResponse(ftp) Catch ex As Exception Return False End Try Return True End Function #End Region #Region "private supporting fns" 'Get the basic FtpWebRequest object with the 'common settings and security Private Function GetRequest(ByVal URI As String) As FtpWebRequest 'create request Dim result As FtpWebRequest = CType(FtpWebRequest.Create(URI), FtpWebRequest) 'Set the login details result.Credentials = GetCredentials() 'Do not keep alive (stateless mode) result.KeepAlive = False Return result End Function ''' <summary> ''' Get the credentials from username/password ''' </summary> Private Function GetCredentials() As Net.ICredentials Return New Net.NetworkCredential(Username, Password) End Function ''' <summary> ''' returns a full path using CurrentDirectory for a relative file reference ''' </summary> Private Function GetFullPath(ByVal file As String) As String If file.Contains("/") Then Return AdjustDir(file) Else Return Me.CurrentDirectory & file End If End Function ''' <summary> ''' Amend an FTP path so that it always starts with / ''' </summary> ''' <param name="path">Path to adjust</param> ''' <returns></returns> ''' <remarks></remarks> Private Function AdjustDir(ByVal path As String) As String Return CStr(IIf(path.StartsWith("/"), "", "/")) & path End Function Private Function GetDirectory(Optional ByVal directory As String = "") As String Dim URI As String If directory = "" Then 'build from current URI = Hostname & Me.CurrentDirectory _lastDirectory = Me.CurrentDirectory Else If Not directory.StartsWith("/") Then Throw New ApplicationException("Directory should start with /") URI = Me.Hostname & directory _lastDirectory = directory End If Return URI End Function 'stores last retrieved/set directory Private _lastDirectory As String = "" ''' <summary> ''' Obtains a response stream as a string ''' </summary> ''' <param name="ftp">current FTP request</param> ''' <returns>String containing response</returns> ''' <remarks>FTP servers typically return strings with CR and ''' not CRLF. Use respons.Replace(vbCR, vbCRLF) to convert ''' to an MSDOS string</remarks> Private Function GetStringResponse(ByVal ftp As FtpWebRequest) As String 'Get the result, streaming to a string Dim result As String = "" Using response As FtpWebResponse = CType(ftp.GetResponse, FtpWebResponse) Dim size As Long = response.ContentLength Using datastream As Stream = response.GetResponseStream Using sr As New StreamReader(datastream) result = sr.ReadToEnd() sr.Close() End Using datastream.Close() End Using response.Close() End Using Return result End Function ''' <summary> ''' Gets the size of an FTP request ''' </summary> ''' <param name="ftp"></param> ''' <returns></returns> ''' <remarks></remarks> Private Function GetSize(ByVal ftp As FtpWebRequest) As Long Dim size As Long Using response As FtpWebResponse = CType(ftp.GetResponse, FtpWebResponse) size = response.ContentLength response.Close() End Using Return size End Function #End Region #Region "Properties" Private _hostname As String ''' <summary> ''' Hostname ''' </summary> ''' <value></value> ''' <remarks>Hostname can be in either the full URL format ''' ftp://ftp.myhost.com or just ftp.myhost.com ''' </remarks> Public Property Hostname() As String Get If _hostname.StartsWith("ftp://") Then Return _hostname Else Return "ftp://" & _hostname End If End Get Set(ByVal value As String) _hostname = value End Set End Property Private _username As String ''' <summary> ''' Username property ''' </summary> ''' <value></value> ''' <remarks>Can be left blank, in which case 'anonymous' is returned</remarks> Public Property Username() As String Get Return IIf(_username = "", "anonymous", _username) End Get Set(ByVal value As String) _username = value End Set End Property Private _password As String Public Property Password() As String Get Return _password End Get Set(ByVal value As String) _password = value End Set End Property ''' <summary> ''' The CurrentDirectory value ''' </summary> ''' <remarks>Defaults to the root '/'</remarks> Private _currentDirectory As String = "/" Public Property CurrentDirectory() As String Get 'return directory, ensure it ends with / Return _currentDirectory & CStr(IIf(_currentDirectory.EndsWith("/"), "", "/")) End Get Set(ByVal value As String) If Not value.StartsWith("/") Then Throw New ApplicationException("Directory should start with /") _currentDirectory = value End Set End Property #End Region End Class #End Region #Region "FTP file info class" ''' <summary> ''' Represents a file or directory entry from an FTP listing ''' </summary> ''' <remarks> ''' This class is used to parse the results from a detailed ''' directory list from FTP. It supports most formats of ''' </remarks> Public Class FTPfileInfo 'Stores extended info about FTP file #Region "Properties" Public ReadOnly Property FullName() As String Get Return Path & Filename End Get End Property Public ReadOnly Property Filename() As String Get Return _filename End Get End Property Public ReadOnly Property Path() As String Get Return _path End Get End Property Public ReadOnly Property FileType() As DirectoryEntryTypes Get Return _fileType End Get End Property Public ReadOnly Property Size() As Long Get Return _size End Get End Property Public ReadOnly Property FileDateTime() As Date Get Return _fileDateTime End Get End Property Public ReadOnly Property Permission() As String Get Return _permission End Get End Property Public ReadOnly Property Extension() As String Get Dim i As Integer = Me.Filename.LastIndexOf(".") If i >= 0 And i < (Me.Filename.Length - 1) Then Return Me.Filename.Substring(i + 1) Else Return "" End If End Get End Property Public ReadOnly Property NameOnly() As String Get Dim i As Integer = Me.Filename.LastIndexOf(".") If i > 0 Then Return Me.Filename.Substring(0, i) Else Return Me.Filename End If End Get End Property Private _filename As String Private _path As String Private _fileType As DirectoryEntryTypes Private _size As Long Private _fileDateTime As Date Private _permission As String #End Region ''' <summary> ''' Identifies entry as either File or Directory ''' </summary> Public Enum DirectoryEntryTypes File Directory End Enum ''' <summary> ''' Constructor taking a directory listing line and path ''' </summary> ''' <param name="line">The line returned from the detailed directory list</param> ''' <param name="path">Path of the directory</param> ''' <remarks></remarks> Sub New(ByVal line As String, ByVal path As String) 'parse line Dim m As Match = GetMatchingRegex(line) If m Is Nothing Then 'failed Throw New ApplicationException("Unable to parse line: " & line) Else _filename = m.Groups("name").Value _path = path _size = CLng(m.Groups("size").Value) _permission = m.Groups("permission").Value Dim _dir As String = m.Groups("dir").Value If (_dir <> "" And _dir <> "-") Then _fileType = DirectoryEntryTypes.Directory Else _fileType = DirectoryEntryTypes.File End If Try _fileDateTime = Date.Parse(m.Groups("timestamp").Value) Catch ex As Exception _fileDateTime = Nothing End Try End If End Sub Private Function GetMatchingRegex(ByVal line As String) As Match Dim rx As Regex, m As Match For i As Integer = 0 To _ParseFormats.Length - 1 rx = New Regex(_ParseFormats(i)) m = rx.Match(line) If m.Success Then Return m Next Return Nothing End Function #Region "Regular expressions for parsing LIST results" ''' <summary> ''' List of REGEX formats for different FTP server listing formats ''' </summary> ''' <remarks> ''' The first three are various UNIX/LINUX formats, fourth is for MS FTP ''' in detailed mode and the last for MS FTP in 'DOS' mode. ''' I wish VB.NET had support for Const arrays like C# but there you go ''' </remarks> Private Shared _ParseFormats As String() = { _ "(?<dir>[\-d])(?<permission>([\-r][\-w][\-xs]){3})\s+\d+\s+\w+\s+\w+\s+(?<size>\d+)\s+(?<timestamp>\w+\s+\d+\s+\d{4})\s+(?<name>.+)", _ "(?<dir>[\-d])(?<permission>([\-r][\-w][\-xs]){3})\s+\d+\s+\d+\s+(?<size>\d+)\s+(?<timestamp>\w+\s+\d+\s+\d{4})\s+(?<name>.+)", _ "(?<dir>[\-d])(?<permission>([\-r][\-w][\-xs]){3})\s+\d+\s+\d+\s+(?<size>\d+)\s+(?<timestamp>\w+\s+\d+\s+\d{1,2}:\d{2})\s+(?<name>.+)", _ "(?<dir>[\-d])(?<permission>([\-r][\-w][\-xs]){3})\s+\d+\s+\w+\s+\w+\s+(?<size>\d+)\s+(?<timestamp>\w+\s+\d+\s+\d{1,2}:\d{2})\s+(?<name>.+)", _ "(?<dir>[\-d])(?<permission>([\-r][\-w][\-xs]){3})(\s+)(?<size>(\d+))(\s+)(?<ctbit>(\w+\s\w+))(\s+)(?<size2>(\d+))\s+(?<timestamp>\w+\s+\d+\s+\d{2}:\d{2})\s+(?<name>.+)", _ "(?<timestamp>\d{2}\-\d{2}\-\d{2}\s+\d{2}:\d{2}[Aa|Pp][mM])\s+(?<dir>\<\w+\>){0,1}(?<size>\d+){0,1}\s+(?<name>.+)"} #End Region End Class #End Region #Region "FTP Directory class" ''' <summary> ''' Stores a list of files and directories from an FTP result ''' </summary> ''' <remarks></remarks> Public Class FTPdirectory Inherits List(Of FTPfileInfo) Sub New() 'creates a blank directory listing End Sub ''' <summary> ''' Constructor: create list from a (detailed) directory string ''' </summary> ''' <param name="dir">directory listing string</param> ''' <param name="path"></param> ''' <remarks></remarks> Sub New(ByVal dir As String, ByVal path As String) For Each line As String In dir.Replace(vbLf, "").Split(CChar(vbCr)) 'parse If line <> "" Then Me.Add(New FTPfileInfo(line, path)) Next End Sub ''' <summary> ''' Filter out only files from directory listing ''' </summary> ''' <param name="ext">optional file extension filter</param> ''' <returns>FTPdirectory listing</returns> Public Function GetFiles(Optional ByVal ext As String = "") As FTPdirectory Return Me.GetFileOrDir(FTPfileInfo.DirectoryEntryTypes.File, ext) End Function ''' <summary> ''' Returns a list of only subdirectories ''' </summary> ''' <returns>FTPDirectory list</returns> ''' <remarks></remarks> Public Function GetDirectories() As FTPdirectory Return Me.GetFileOrDir(FTPfileInfo.DirectoryEntryTypes.Directory) End Function 'internal: share use function for GetDirectories/Files Private Function GetFileOrDir(ByVal type As FTPfileInfo.DirectoryEntryTypes, Optional ByVal ext As String = "") As FTPdirectory Dim result As New FTPdirectory() For Each fi As FTPfileInfo In Me If fi.FileType = type Then If ext = "" Then result.Add(fi) ElseIf ext = fi.Extension Then result.Add(fi) End If End If Next Return result End Function Public Function FileExists(ByVal filename As String) As Boolean For Each ftpfile As FTPfileInfo In Me If ftpfile.Filename = filename Then Return True End If Next Return False End Function Private Const slash As Char = "/" Public Shared Function GetParentDirectory(ByVal dir As String) As String Dim tmp As String = dir.TrimEnd(slash) Dim i As Integer = tmp.LastIndexOf(slash) If i > 0 Then Return tmp.Substring(0, i - 1) Else Throw New ApplicationException("No parent for root") End If End Function End Class #End Region End Namespace