5

I wrote the following code a long time ago to determine if an FTP directory exists:

public bool DirectoryExists(string directory)
{
    try
    {
        FtpWebRequest request = GetRequest(directory);
        request.Method = WebRequestMethods.Ftp.ListDirectory;

        using (FtpWebResponse response = request.GetResponse() as FtpWebResponse)
        {
            StreamReader sr = new StreamReader(response.GetResponseStream(), System.Text.Encoding.ASCII);
            sr.ReadToEnd();
            sr.Close();
            response.Close();
        }
        return true;
    }
    catch { }
    return false;
}

protected FtpWebRequest GetRequest(string filename = "")
{
    FtpWebRequest request = WebRequest.Create(_host.GetUrl(filename)) as FtpWebRequest;
    request.Credentials = new NetworkCredential(Username, Password);
    request.Proxy = null;
    request.KeepAlive = false;
    return request;
}

This code has worked for several years, but today it doesn't. When testing a directory that does not exist, the code in DirectoryExists() no longer throws an exception, and the method incorrectly returns true.

If I assign the results of sr.ReadToEnd() to a string, it is an empty string.

In this case, the code _host.GetUrl(filename) returned "ftp://www.mydomain.com/Articles/winforms/accessing-the-windows-registry". This is the expected value. And still my DirectoryExists() method does not throw an exception when this path does not exist on the server. I even passed this non-existing directory to a method that uses WebRequestMethods.Ftp.ListDirectoryDetails to build a directory listing. This method simply returns an empty listing and also throws no exception.

I believe I first encountered this issue when I moved my code to a new computer with Visual Studio 2013. I'm using .NET 4.5 and got the same behavior when using .NET 4.5.1.

Questions:

  1. Why doesn't this code, which has worked for years and uses the same technique used on most of the online examples I found, work? And what could possibly cause this code to suddenly stop working?

  2. Is there a way to detect for the presence of a directory that works? I suppose the other approach is to scan the parent directory, although the logic would need to be different when the routine is supposed to verify the root directory.

Jonathan Wood
  • 65,341
  • 71
  • 269
  • 466
  • 1
    I have a very strong feeling that there is absolutely nothing wrong with the code, but rather some sort of configuration problem with either the FTP server, or your new computer. – Jason Higgins Apr 21 '14 at 20:04
  • @JasonHiggins: Well, the FTP server is owned by my hosting provider (CrystalTech). I'm not sure what it would be with the configuration. It's correctly reading data from the FTP server. The issues seem to be isolated to this one area. Also, how could a server configuration cause a request for a resource that does not exist to not throw an exception? Having trouble wrapping my head around this. – Jonathan Wood Apr 21 '14 at 20:08
  • It's really hard to say for sure, but just based on the description you've given it seems like the code is not at fault. Code doesn't change over time, but the machines it runs on does. The only thing I can think of with the code would be the .NET framework it's running on. Did you switch frameworks when you also switched computers? – Jason Higgins Apr 21 '14 at 20:10
  • @JasonHiggins: Yes, I upgraded to Visual Studio 2013 on the new computer. Hmmm... Maybe I'll try Googling to see if any related behavior is documented as having changed. – Jonathan Wood Apr 21 '14 at 20:12
  • would adding a null check on sr help catch this type of scenario in the future? (ie if null return false) – bill Apr 21 '14 at 20:19
  • @bill: `sr` is not null. It doesn't read any data, but I'm able to read from it without any exception being thrown. If it was `null`, then `sr.ReadToEnd()` would throw a null reference exception. – Jonathan Wood Apr 21 '14 at 20:21
  • I just ran your code in a new project using an invalid FTP server, and once your code hits this block: (FtpWebResponse response = request.GetResponse() as FtpWebResponse) it is caught with exception "Unable to connect to the remote server" Running it with a valid FTP server but wrong port: Returns false; – bill Apr 21 '14 at 20:44
  • If you add if (String.IsNullorWhiteSpace(sr.ToString())) return false; that will either A) Cause an exception which will be caught by your code and return false (object.ToString() when null) or can be returned to false if its an empty string. Your issue does look machine specific but it wouldn't hurt to add an extra check in there. – bill Apr 21 '14 at 20:52
  • @bill: I'm not sure how testing the code with a wrong port helps us. As I understand it, the method returns a list of files in the directory. So if the directory is empty, it should return an empty string. Your suggestion would confuse an empty directory with one that does not exist. – Jonathan Wood Apr 21 '14 at 21:00
  • `FtpWebRequest` has changed between .NET 3.5 and .NET 4.0, perhaps you could retarted .NET 3.5 and see what happens? – C.Evenhuis Apr 23 '14 at 21:05
  • @C.Evenhuis: You might be on to something. [This KB article](http://support.microsoft.com/kb/2134299) describes a different but possibly related issue. Unfortunately, I can't try .NET 3.5 or the suggested fix right now. Feel free to write what info you have as an answer if you are so inclined. – Jonathan Wood Apr 23 '14 at 21:18
  • @JonathanWood I tried your code with my ftp server, and it appears to function correctly on both .NET 3.5 and .NET 4.0 for existing and non-existing directories. I don't have access to .NET 4.5+ atm. – C.Evenhuis Apr 24 '14 at 07:21
  • Have you tried to use ListDirectoryDetails instead of ListDirectory. The result from sr.ReadToEnd() could be different if the directory does not exist. I have not tested this... – dlemstra Apr 24 '14 at 18:41
  • @dlemstra: Yes I also tried `ListDirectoryDetails`. Both return an empty string. Both fail to throw an exception. – Jonathan Wood Apr 24 '14 at 18:43
  • If you use a web browser to browse the FTP on a non-existent folder, does it return a blank page or an error? (eg. Error 404 or Error 550) If it returns a blank page, then the FTP server might have configured that site to returns a custom (empty) page on a non-existent file/folder. – SF Lee Apr 30 '14 at 07:55
  • @SFLee: After logging in, my browser (Chrome) fails with error ERR_FTP_FAILED. – Jonathan Wood Apr 30 '14 at 17:53
  • Check `if(response.StatusCode == FtpStatusCode.ActionNotTakenFileUnavailable)` then the directory is not found. cf: http://stackoverflow.com/questions/2769137/how-to-check-if-an-ftp-directory-exists – geedubb Apr 30 '14 at 20:58
  • I've managed to reproduce your error on another site. I've done some investigations on it and posted an answer below. Please check it out. :) – SF Lee Apr 30 '14 at 23:45
  • did you perform a wireshark packet trace and check if the commands are executing correctly or the response of the server was malformed? – Qiong Wu May 01 '14 at 00:22

6 Answers6

5

I managed to reproduce your error on another site I have access to. After some playing around, here's my conclusion:-

When you make a FtpWebRequest with a FTP URL that does NOT end with a /, such as:

ftp://ftp.someftp.com/somefolder/invalidfolder

AND you specified WebRequestMethods.Ftp.ListDirectory as the method, then what it does behind the scene is to run the following command:

NLST "somefolder/invalidfolder"

Normally, NLST will list the contents of the specified folder, and throws an exception if the folder does not exist. But because you did not specify a / at the end of invalidfolder, NLST will think that invalidfolder may actually be a file (or a filename pattern). If it manages to find a folder named invalidfolder, then and only then will it treat it as a folder. Otherwise it will try to search a file named invalidfolder underneath the parent folder somefolder. If the file does not exist, then one of the following will occur, depending on which FTP server software (and its configurations) is running:

  • It throws an ERROR 550: File or Folder not found. (Example: spftp v1.0)
  • It returns an empty result. (Example: vsFTPd v2.0.5)

In your case, the FTP server returns the latter response, and your code falls over.

The solution? Just add some validation code to make sure the ftp folder you are trying to access always has a / at the end. Something like the following:-

if (!directory.EndsWith('/'))
    directory += '/';
SF Lee
  • 1,767
  • 4
  • 17
  • 32
  • By the way, I've tested this on .NET 2.0, 3.5, and 4.0. They all have the same behaviours. I don't have .NET 4.5 though. – SF Lee May 01 '14 at 00:12
  • I think this is the answer. As my FTP class is written, it does not change the current working directory. Instead, it simply provides the full path each time. This just seemed simpler to me. The result is that I would be providing a valid path followed by an invalid path, as you suggested. Unfortunately, I am not able to find anything that describes this behavior for the NLIST command. At any rate, perhaps this changed on the FTP server I'm using? – Jonathan Wood May 01 '14 at 15:58
  • I don't think there's a standard documentation of how NLST should behave exactly when it encounters non-existent files/folders. So it's up to each FTP server to provide its own implementation. I discovered this while playing with the two FTP servers mentioned above using raw FTP connections. Their behaviours are different. – SF Lee May 01 '14 at 22:59
1

In effect ftp class has strange beahvior however you should achieve your goal in this way using a simple console application project or simply in your original project too.

VBNET VERSION

    Sub Main()
      Dim ftp As New FtpSystem

      If ftp.DirectoryExist("p") Then
        Console.WriteLine("false")
      Else
        Console.WriteLine("true")
      End If
      Console.ReadLine()
    End Sub
    Private Class FtpSystem
    Public Function DirectoryExist(ByVal dir As String)
        Dim uri As New Uri("ftp://domain.com" & "/" & dir)
        Dim netCred As New NetworkCredential("user", "password")
        Dim ftprq As FtpWebRequest = FtpWebRequest.Create(uri)
        With ftprq
            .Credentials = netCred
            .KeepAlive = True
            .Method = WebRequestMethods.Ftp.ListDirectory
            .UsePassive = True
            .UseBinary = False
        End With
        Dim ftprs As FtpWebResponse = Nothing
        Dim Sr As StreamReader = Nothing
        'if ftp try to matching folder ad if it will succeed then return true if not it will thrown an exception 
        'and it will return false. Is not a good way because it should be implement a very granular exception matching but 
        'for test is best simple approach.
        Try
            ftprs = DirectCast(ftprq.GetResponse, FtpWebResponse)
            Sr = New StreamReader(ftprs.GetResponseStream)
            Dim T As String = Sr.ReadToEnd
            Console.Write(T)
            Return True
        Catch ex As Exception
            Return False
        End Try
    End Function
End Class

C# VERSION(TRANSLATE ON THE FLY WITH ONLINE TOOL PLEASE CHECK CODE)

   public void Main()
{
FtpSystem ftp = new FtpSystem();

if (ftp.DirectoryExist("p")) {
    Console.WriteLine("false");
} else {
    Console.WriteLine("true");
}
Console.ReadLine();
}
private class FtpSystem
{
public object DirectoryExist(string dir)
{
    Uri uri = new Uri("ftp://domain.com" + "/" + dir);
    NetworkCredential netCred = new NetworkCredential("user", "password");
    FtpWebRequest ftprq = FtpWebRequest.Create(uri);
    var _with1 = ftprq;
    _with1.Credentials = netCred;
    _with1.KeepAlive = true;
    _with1.Method = WebRequestMethods.Ftp.ListDirectory;
    _with1.UsePassive = true;
    _with1.UseBinary = false;
    FtpWebResponse ftprs = null;
    StreamReader Sr = null;
    //if ftp try to matching folder ad if it will succeed then return true if not it will thrown an exception 
    //and it will return false. Is not a good way because it should be implement a very granular exception matching but 
    //for test is best simple approach.
    try {
        ftprs = (FtpWebResponse)ftprq.GetResponse;
        Sr = new StreamReader(ftprs.GetResponseStream);
        string T = Sr.ReadToEnd;
        Console.Write(T);
        return true;
    } catch (Exception ex) {
        return false;
    }
    }
}

I hope that this help you.Bye

makemoney2010
  • 1,222
  • 10
  • 11
  • This appears to be exactly the technique I am using. One small difference is that this code sets `KeepAlive = true`. But I tried making that change and it has no effect. – Jonathan Wood Apr 25 '14 at 17:53
  • Nope there's an important difference: Uri uri = new Uri('ftp://domain.com'+ "/" + dir); where in my case return exactly in example: ftp://domain.com/dirname in your case what is returned from _host.GetUrl(directory)??????Check it and let me know – makemoney2010 Apr 25 '14 at 18:33
  • I'm not sure I see a difference here. In my original question, I showed what directory was being returned. Of course, it doesn't matter what methods I used to obtain the path--only what path I pass to `FtpWebRequest`. – Jonathan Wood Apr 25 '14 at 23:00
  • Hi Jonathan, i don't know what you retrieve from _host.GetUrl(directory) the question is that generally when you have to ftp something you need to access via ftp protocoll which include hostname and then the path too in some case ftp:\/\/:domain.ext/httpdocs or public_html and so on so i assume that you know which are those fragment.Personally i invite you to take a try and if it will match your needs mark it as answer and then play with ftp method to better understand how they works.I will glad to help you if you need more help. PS execuse me for my poor english but i'm italian :) – makemoney2010 Apr 25 '14 at 23:18
  • *Again*, my original question showed what string is being returned from `_host.GetUrl(directory)`. I have provided this information. There are lots of things I read about, have learned, and continue to learn. Right now, it is the interface with the .NET FTP classes that I'm concerned with. Again, your code uses the same technique as my original code. So I don't understand how this is the answer. – Jonathan Wood Apr 26 '14 at 16:33
  • Jonathan your are checking the directory under the directory and it will no thrown exception if there's no directory.Return an empty string.You you use ftp://www.mydomain.com/Articles/winforms/ instead it will return true coz it will return "accessing-the-windows-registry" as directory listed within the parent directory too.I hope it is more clear.when you get the result from parent directory you may use a foreach loop or a lambra exmpression or any cycle you like to match your directory.Only in case of exception you will exit with false.It work exactly as aspected. – makemoney2010 Apr 26 '14 at 17:30
  • another stuff when you have the access to the directory it implicit means that your directory exist..... that's all otherway exception will thrown.if is clear and it resolve your issue please mark it as answered – makemoney2010 Apr 26 '14 at 17:31
  • I'm sorry but you are wrong. The code you posted above appends the directory being tested to the domain. That's exactly what I am doing. I'm sorry if there is a language barrier but this not an answer to what I asked. – Jonathan Wood Apr 26 '14 at 19:00
  • Hi Jonathan, i make a short clip i hope you can understand what i mean there where i show you something.Excuse me in advance for my english :) take a look here http://italiancallcenter.com/jonathan/video.wmv if you cannot see the clip clik on link whihc will redirect you to the file so you can see it :) Hope it help – makemoney2010 Apr 26 '14 at 21:54
  • I appreciate your effort but your code still seems to be relying on exactly the same technique used by my code. (Remember, my code has been working fine for several years.) If you have a link to your code, I will download it and try it in the latest version of .NET. But, again, your code appears to rely on `FtpWebRequest.GetResponse()` throwing an exception when the directory does not exist. And again, I am not seeing this with the latest version of .NET. – Jonathan Wood Apr 27 '14 at 17:07
  • I will update you with a link to download the solution simple console application just one thing...do you prefer c# or is the same in vbnet .... – makemoney2010 Apr 27 '14 at 19:30
  • I'm making the zip file and i will upload this evening.Sorry for delay but i was a littl bit busy :) and i will update you. – makemoney2010 Apr 29 '14 at 17:07
  • here the link Jonathan http://italiancallcenter.com/jonathan/smples.rar it is a console application in vbnet with same code so you can test it – makemoney2010 Apr 29 '14 at 20:43
  • ps i have changed the credential for privacy reason use your own with your ftp login details to try if it work at your end – makemoney2010 Apr 29 '14 at 20:46
  • Again, I appreciate your effort on this. But if you examine the code, it is using the same technique as my original code. When I run your project on my computer, it simply lists any non-existent directories as empty directories. I was willing to try anything. But, as I've described all along, this code is not working for me in VS2013 for the reasons I've explained. – Jonathan Wood Apr 29 '14 at 21:07
  • That said, I may have figured something out. If I append the path name with "/", then it *does* throw an exception when the directory does not exist. – Jonathan Wood Apr 29 '14 at 21:57
  • This is modified behavior. It was not necessary before. Need to do some more research. – Jonathan Wood Apr 29 '14 at 22:04
  • Jonathan please let me know if you find something about. – makemoney2010 Apr 29 '14 at 22:06
  • Howeveer it is affect only c# apparently coz "/" is not needed in vb.net??!?!?!? – makemoney2010 Apr 29 '14 at 22:07
  • No, that's not it. I had the same issue with your code. And I did not have the issue before with my C# code. – Jonathan Wood Apr 29 '14 at 22:12
0

As mentionned above, perhaps the FTP server configuration changed. Have you tried to explicitly set the value of the UsePassive or UseBinary properties either ways ?

Francis Ducharme
  • 4,848
  • 6
  • 43
  • 81
  • I'm struggling to see how either of these settings could possibly be related to the behavior I'm seeing. At any rate, both settings are `true`. If I set `UseBinary` to false, I don't see any difference. If I set `UsePassive` to false, it just times out. – Jonathan Wood Apr 23 '14 at 16:16
  • @JonathanWood Can you log in with an FTP client and manually send it the command `LIST` or `NLST` and see what the output is ? http://en.wikipedia.org/wiki/List_of_FTP_commands – Francis Ducharme Apr 23 '14 at 17:19
0

You can try sending ftp commands to the ftp server:

        ProcessStartInfo psi = new ProcessStartInfo("cmd.exe");
        psi.UseShellExecute = false;
        psi.RedirectStandardOutput = true;
        psi.RedirectStandardInput = true;
        psi.RedirectStandardError = true;
        Process proc = Process.Start(psi);
        // Open the batch file for reading

        // Attach the output for reading
        StreamReader sOut = proc.StandardOutput;

        StreamReader sErr = proc.StandardError;    

        // Attach the in for writing
        StreamWriter sIn = proc.StandardInput;
        // Write each line of the batch file to standard input
        while (sr.Peek() != -1)
        {
            sIn.WriteLine("ftp");
            sIn.WriteLine("open remote-server-name");
            sIn.WriteLine("username");
            sIn.WriteLine("password");
            sIn.WriteLine("ls");
        }

        string outPutStuff= sOut.ReadToEnd();
        proc.WaitForExit();  //this does not appear to be needed. 
        string outErrStuff = sErr.ReadToEnd();

        Console.WriteLine("Start FileTransfer FTP Output");
        Console.WriteLine(outPutStuff);
        Console.WriteLine("Any errors follow this line---");
        Console.WriteLine(outErrStuff);

        Console.WriteLine(outPutStuff);

        sOut.Close();
        sIn.Close();
Bura Chuhadar
  • 3,653
  • 1
  • 14
  • 17
0

Please,check if there is a file named accessing-the-windows-registry (no file extension) in winforms folder.

_host.GetUrl(filename) should return a string ended with a "/". When there is file with the same name as the intended folder name, no exception will be thrown.

david
  • 171
  • 7
  • There is no such file. Moreover, I can type in any random directory name and I still don't get an exception. – Jonathan Wood Apr 29 '14 at 19:58
  • Random directory name in which level? under articles folder or winforms folder? – david Apr 29 '14 at 20:43
  • Either way, same result. However, as I posted in my response to makemoney2010, appending a forward slash does cause it to raise an exception as it did before when it worked. However, this is not because there is a file with the same name. – Jonathan Wood Apr 29 '14 at 21:58
  • Could you replace the url to a public server like ftp://ftp.mozilla.org/pub/mozilla.org/b2g? It will help to detemine if it is a server/network issue. – david Apr 30 '14 at 14:42
0

If you are not getting exceptions, as a possible work-around, you could try requesting a predictable attribute of the folder, like the date it was created.

Another suggestion is using WireShark to see what FTP requests are being made behind the scenes, which will let you see if it's .NET or the server returning unhelpful responses.

WhoIsRich
  • 4,053
  • 1
  • 33
  • 19