How to Determine if FTP Directory Exists
Asked Answered
O

5

5

I wrote the following code to detect if a directory exists. The DirectoryExists method accepts either a fully qualified or relative path.

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

        using (FtpWebResponse response = request.GetResponse() as FtpWebResponse)
        using (StreamReader sr = new StreamReader(response.GetResponseStream(), System.Text.Encoding.ASCII))
        {
            sr.ReadToEnd();
        }
        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;
}

Note: _host.GetUrl(filename) returns the fully qualified path of the specified directory or filename. In my case, this is ftp://www.mydomain.com/Articles/controls/precisely-defining-kilobytes-megabytes-and-gigabytes.

This code has worked for many months. But all of a sudden it stopped working. Now, there is no exception raised in DirectoryExists when the directory does not exist. sr.ReadToEnd() simply returns an empty string.

I posted a similar question and it was suggested that I should always append / to the end of my path. I tried that, and thought I got an exception once, but it's not raising an exception now.

I don't know if the behavior changed on the FTP server I'm communicating with or what. Again, this worked fine when I wrote it and now it doesn't.

How can I determine whether or not an FTP directory exists?

EDIT:

Inspecting the response after calling ReadToEnd(), I see:

BannerMessage="220 Microsoft FTP Service\r\n"

ContentLength=-1

ExitMessage="221 Goodbye.\r\n"

StatusCode=ClosingData

StatusDescription="226 Transfer complete.\r\n"

WelcomeMessage="230 User logged in.\r\n"

UPDATE:

Ultimately, I see that most people have been recommending variations of what I was doing originally. This has lent weight to Hans Passant's suggestion that the issue lies with the server. I am attempting to get them to look at this but they seem a little baffled by the entire discussion. I know they are using a Microsoft server, and I'm skeptical I will be able to get a resolution from them.

If all else fails, I think the answer is to do a listing of the parent directory, which does require some extra work to handle cases such as when the directory in question is the root, and also cases when the parent doesn't exist.

Osteogenesis answered 15/7, 2014 at 15:0 Comment(9)
Might be duplicate of #2769637Moonfaced
Yes, but for me none of those suggestions appear to be working.Osteogenesis
Don't talk to us, talk to the guy that administers the server.Alive
@HansPassant: Why? I don't control all the FTP servers in the World. I'm looking for a reliable way to work with any FTP server.Osteogenesis
You can only expect reliability when the FTP server is reliable. Only he can fix this server to behave properly again of course. If you depend on another machine to get your software to operate reliably then it is very important that you get to know whomever manages that machine.Alive
ListDirectory method might be unreliable on some servers. Maybe combinations of MLSD, CWD, MDTM and SIZE FTP commands would work better as described in: https://mcmap.net/q/162372/-how-to-check-if-an-ftp-directory-existsMoonfaced
What FTP server software are you connecting to? Maybe there is a known glitch or workaround for it?Moonfaced
@MartinVobr: The intent here is to write code that works with any FTP server. I have no interest in writing code that only works with one particular server.Osteogenesis
@Jonathan Wood: I've encountered a FTP server which required a one second delay between sending username and password. Would you slow down your code for every FTP server or would you add code for slowing down only when misbehaving server is detected? There is also a nice, quick and standardized way how get a FTP directory listing (MLSD extension). Unfortunately it's not supported by some FTP servers. Alternative is to guess what human-readable listing format is used and try to parse it (often ambiguous). Would you use the superior method when available, or use the legacy one instead?Moonfaced
E
9

FTP servers reply with a 550 message when you ask for a non existing directory/file. This 550 is translated to an exception in .NET.

Regarding the code you presented I don't see the reason to use a StreamReader in production. A simple modification is the following:

public bool DirectoryExists(string directory)
{
  try
  {
    FtpWebRequest request = GetRequest(directory);
    request.Method = WebRequestMethods.Ftp.ListDirectory;
    return request.GetResponse() != null;
  }
  catch
  {
    return false;
  }
}

I kept the exception handling as is (missing) but I suggest you to work on it as there are many and different cases to catch that are not related to ftp responses.

request.GetResponse() will generate an exception if the directory does not exist.

I tried it with the following paths for true returns:

The last one is an existing but empty directory

ftp://ftp.mozilla.org/pub/data/bloat-reports2/ returns false

There is a big but regarding trailing / in paths.

mozilla.org is a zero byte length file under pub directory

ftp://ftp.mozilla.org/pub/mozilla.org returns true

ftp://ftp.mozilla.org/pub/mozilla.org/ returned false and then true ?????

This behavior looks like with what you described. You can easily reproduce it on a browser. I suggest you to do the same with your own paths and check the contents of the directories. In case you do have empty files, remove them or replace their contents with a space, and finally check your code to ensure that you will not create new files or recreate them with zero byte length.

I hope that this helps.

UPDATE

No news, bad news! I assume that nothing of the above helped you. Maybe by going one level up in your path will solve the problem. The idea is to get a list of the parent directory and check it for the child name. If the parent path is valid it should always return non empty string. The code is the following:

static bool DirectoryExists(string directory)
{
    try
    {
        directory = GetRequest(directory);
        string
            parent = directory.Substring(0, directory.TrimEnd('/').LastIndexOf('/') + 1),
            child = directory.Substring(directory.TrimEnd('/').LastIndexOf('/') + 1).TrimEnd('/');
        FtpWebRequest request = GetRequest(parent);
        request.Method = WebRequestMethods.Ftp.ListDirectory;
        using (FtpWebResponse response = request.GetResponse() as FtpWebResponse)
        {
            if (response == null)
                return false;
            string data = new StreamReader(response.GetResponseStream(), true).ReadToEnd();
            return data.IndexOf(child, StringComparison.InvariantCultureIgnoreCase) >= 0;
        }
    }
    catch
    {
        return false;
    }
}

As you can see there is no need to worry about trailing slashes.

Warning: The code will not cover the case of a subdirectory and a filename with same names under the same path.

Ennui answered 31/7, 2014 at 12:14 Comment(4)
Although your first code block appears to contain some improvements, it basically does the same thing as my code. Namely, it relies on an exception being thrown when WebRequestMethods.Ftp.ListDirectory is called for a directory that does not exist. Unfortunately, the result is also the same. In my case, no exception is thrown. Also, the result of GetResponse() is an empty string and not null.Osteogenesis
As for your second suggestion, it's one I've given some thought to. Of course, that approach kind of breaks down when the given directory is the root directory. I may try something like that if I can't get another solution. I have contacted the people who host the server to see if anything strange is going on there.Osteogenesis
The first approach refers to the case of zero byte length files as a potential reason. No news was a signal for the second. You're right about the root directory, but it's not related to the way you organize your paths (/articles/controls/...). Consider it as a direct workaround for your specific case and not as a globally suggested solution.Ennui
I think this is the most helpful response and I will award it the bounty. I've updated my question to indicate my general impression of the best approach to take.Osteogenesis
N
0

This might happen because FTP server software installed on your server.

Otherwise, you may want to take a look at response.StatusCode to see if there is any hints.

http://msdn.microsoft.com/en-us/library/system.net.ftpstatuscode(v=vs.110).aspx

Finally, if all of that doesn't work, try to write a dumb text file on that un-existed directory.

Nanosecond answered 31/7, 2014 at 3:8 Comment(1)
I've updated my question to show what some of the properties values of response are, including StatusCode.Osteogenesis
B
0

I had similar code that would deploy items via FTP to an IIS 6/Windows 2003 Server. This worked fine until I pointed it at an IIS7/Windows 2008 server. I started seeing the exact same behavior you did. I debugged it and ultimately changed my code to the following. This is a direct snippet from my code. I can include the source from the methods it calls if you like, but I think you understand those operations.

try
{
    //Get a recursive FTP Listing
    items = GetFtpListing(target, true);
}
catch(WebException e)
{
    if (string.Equals(e.Message, "The remote server returned an error: (550) File unavailable (e.g., file not found, no access)."))
    {
        CreateRemoteDirectory(target);
        items = GetFtpListing(target, true);
    }
    else
    {
        throw e;
    }
}

relevant section of GetFtpListing

List<string> ftpRawList = new List<string>();

FtpWebRequest request = (FtpWebRequest)WebRequest.Create(path);
request.Timeout = 60000;
request.ReadWriteTimeout = 60000;
request.Credentials = this.credentials;
request.Method = WebRequestMethods.Ftp.ListDirectoryDetails;

using (FtpWebResponse response = (FtpWebResponse)request.GetResponse())
{
    Stream data = response.GetResponseStream();

    using (StreamReader reader = new StreamReader(data))
    {
        string responseString = reader.ReadToEnd();
        ftpRawList.AddRange(responseString.Split(new char[2] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries));
    }
Banff answered 1/8, 2014 at 13:26 Comment(7)
I'm not sure I see the fix here. Basically, it appears you are using WebRequestMethods.Ftp.ListDirectoryDetails instead of WebRequestMethods.Ftp.ListDirectory. I did try that and was getting the same results. I'm not clear on what part of this fixed the issue you were seeing.Osteogenesis
if (string.Equals(e.Message, "The remote server returned an error: (550) File unavailable (e.g., file not found, no access).")) { CreateRemoteDirectory(target); items = GetFtpListing(target, true); } is what fixed it, specifically the check for the 550 and creating the directory if it does not exist (since in my model I'm creating the structure to which we upload files.Banff
Ok, thanks. But I see two immediate issues with that. First, you're comparing the string in your exception handler and, as I've pointed out, I'm not getting any exception. And second, I think it's suspect to test for this entire string, which could easily vary in the future. At the very least, you should parse it to find the error number, as I believe .NET already has properties that do just that.Osteogenesis
yeah possibly, re: specific string. Given that we're deploying to a known server target that isn't likely to change I'm not concerned about it for my use, but agree that were this to be used by other clients I would want a better solution. Sorry I missed where you weren't getting an exception...is there anything being returned in your response? or is it simply null?Banff
As mentioned in my question, I'm getting back an empty string. Apparently, that's the same thing returned if the director exists, but is empty.Osteogenesis
I just looked at your code a little more thoroughly, and it looks like you might want to set KeepAlive to true http://msdn.microsoft.com/en-us/library/system.net.ftpwebrequest.keepalive(v=vs.110).aspx. I'm just looking at what's different... it shouldn't matter.. but it's a difference I noticedBanff
I don't know if it should matter, but it doesn't appear to. :)Osteogenesis
C
0

I use the below to check if a ftp connection credentials are valid. You may use the same to check if directory exists. Returns true if url, username, and password are correct.

URL: you specify your directory path in here. user: ftp username password: ftp password


public static bool isValidConnection(string url, string user, string password, ref string errorMessage = "")
{
    try {
        FtpWebRequest request = (FtpWebRequest)WebRequest.Create(url);
        request.Method = WebRequestMethods.Ftp.ListDirectory;
        request.Credentials = new NetworkCredential(user, password);
        request.KeepAlive = false;
        request.UsePassive = true;
        FtpWebResponse response = (FtpWebResponse)request.GetResponse();
        response.Close();
    } catch (WebException ex) {
        errorMessage = ex.Message;
        return false;
    }
    return true;
}
Cultural answered 4/8, 2014 at 14:58 Comment(1)
You are using WebRequestMethods.Ftp.ListDirectory. That's exactly what I was using.Osteogenesis
T
0

One way is to list the directory content as present in many solutions, but in my case i also have to read data content so the server checks the directory. If the directory is not present, an webException is throw that should be interpreted to filter from unexpected errors.

Here is my solution:

  bool directoryExists(string path)
    {

        bool? exists = null;


        FtpWebRequest request = (FtpWebRequest)WebRequest.Create(path);
        request.Method = WebRequestMethods.Ftp.ListDirectory;

        request.Credentials = new NetworkCredential("username", "*****");

        FtpWebResponse response = null;
        try
        {
            response = (FtpWebResponse)request.GetResponse();

            using (StreamReader sr = new StreamReader(response.GetResponseStream()))
            {
                string str = sr.ReadLine(); //Just needs to read one line to fire check on server
                sr.Close();
                return true;
            }                                    
        }
        catch (WebException ex)  
        {
            var r = (FtpWebResponse)ex.Response;
            if (r.StatusCode ==
                FtpStatusCode.ActionNotTakenFileUnavailable)
            {
                //Does not exist
                return false;
            }
            throw; //if error is not related to directory existence then the exception needs to be treated above.
        }
        finally
        {
            if (response != null)
                response.Close();
        }
        return false;
    }

The FTP server i am targeting i know to be a windows server but i don't have access to the version information.

Tedmund answered 18/4, 2017 at 18:23 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.