Login


An FtpClient Class and WinForm Control

By Jonathan Wood on 3/10/2011
Language: C#
Technology: .NETWinForms
Platform: Windows
License: CPOL
Views: 15,752
Desktop Development » Web Clients » General » An FtpClient Class and WinForm Control

ScreenShot of FtpClient User Control

Download Source Code Download Source Code

Introduction

The .NET Frameworks provide a rich assortment of classes for interfacing with the Internet from a client. However, putting together working FTP code still requires a fair amount of work.

In this article, I'll present a ready-made FTP client class that allows you to upload and download files, and create and delete directories. The class also keeps track of the current directory, making it easier to navagate and make changes.

The FtpClient Class

Listing 1 shows my FtpClient class. Instead of using FtpWebRequest and related classes to perform FTP tasks, you can use the FtpClient class to make things easier.

Use the Host, Username, and Password properties to set and retrieve login credentials.

Unlike the .NET FTP classes, this class keeps track of the current directory. Call the ChangeDirectory() to change the current directory. Passing ".." will change to the parent directory. Use the IsRootDirectory property to determine if the current directory is the root directory. And use the DirectoryExists() method to determine if a particular directory exists on the server.

FtpClient uses the FtpDirectory helper class to keep track of the current directory. FtpDirectory simplifies tasks such translating between a URL and the domain/directory it stores internally. It also provides logic for applying a new directory to the current one such as when changing directories.

The FtpClient class provides the ListDirectory() method to return a List<> of FtpDirectoryEntry objects that reflect the contents of the current directory. The ListDirectory() method will attempt to query and then parse the listing, making it much easier to work with.

Additional methods such as DownloadFiles(), UploadFiles(), DeleteFiles(), CreateDirectory() and DeleteDirectory() can all be used to modify files and directories.

Listing 1: The FtpClient Class

public enum FtpDirectoryFormat
{
    Unix,
    Windows,
    Unknown
}

public class FtpDirectoryEntry
{
    public string Name;
    public DateTime CreateTime;
    public bool IsDirectory;
    public Int64 Size;
    public string Group;    // UNIX only
    public string Owner;
    public string Flags;
}

/// <summary>
/// General, easy-to-use FTP class.
/// </summary>
public class FtpClient
{
    protected FtpDirectory _host;

    /// <summary>
    /// Gets or sets the current FTP domain and optional directory
    /// </summary>
    public string Host
    {
        set { _host.SetUrl(value); }
        get { return _host.GetUrl(); }
    }

    /// <summary>
    /// Gets or sets the login username.
    /// </summary>
    public string Username { get; set; }

    /// <summary>
    /// Gets or sets the login password.
    /// </summary>
    public string Password { get; set; }

    /// <summary>
    /// Indicates if the current directory is the
    /// root directory.
    /// </summary>
    public bool IsRootDirectory
    {
        get { return _host.IsRootDirectory; }
    }

    // Construction
    public FtpClient()
    {
        _host = new FtpDirectory();
    }

    /// <summary>
    /// Returns a directory listing of the current working directory.
    /// </summary>
    public List<FtpDirectoryEntry> ListDirectory()
    {
        FtpWebRequest request = GetRequest();
        request.Method = WebRequestMethods.Ftp.ListDirectoryDetails;

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

    /// <summary>
    /// Changes the current working directory. If directory starts with "/" then it
    /// is relative to the root directory. If directory is ".." then it refers to
    /// the parent directory.</param>
    /// </summary>
    /// <param name="directory">Directory to make active.</param>
    public void ChangeDirectory(string directory)
    {
        _host.CurrentDirectory = directory;
    }

    /// <summary>
    /// Indicates if the specified directory exists. This function returns false
    /// if a filename existing with the given name.
    /// </summary>
    /// <param name="directory">Directory to test. May be relative or absolute.</param>
    /// <returns></returns>
    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;
    }

    /// <summary>
    /// Creates the specified directory. This method will create multiple levels of
    /// subdirectories as needed.
    /// </summary>
    /// <param name="directory">Directory to create. May be relative or absolute.</param>
    public void CreateDirectory(string directory)
    {
        // Get absolute directory
        directory = _host.ApplyDirectory(directory);

        // Split into path components
        string[] steps = directory.Split(new char[] {'/'}, StringSplitOptions.RemoveEmptyEntries);

        // Build list of full paths to each path component
        List<string> paths = new List<string>();
        for (int i = 1; i <= steps.Length; i++)
            paths.Add(FtpDirectory.ForwardSlash + String.Join(FtpDirectory.ForwardSlash, steps, 0, i));

        // Find first path component that needs creating
        int createIndex;
        for (createIndex = paths.Count; createIndex > 0; createIndex--)
        {
            if (DirectoryExists(paths[createIndex - 1]))
                    break;
        }

        // Created needed paths
        for ( ; createIndex < paths.Count; createIndex++)
        {
            FtpWebRequest request = GetRequest(paths[createIndex]);
            request.Method = WebRequestMethods.Ftp.MakeDirectory;
            FtpWebResponse response = (FtpWebResponse)request.GetResponse();
            response.Close();
        }
    }

    /// <summary>
    /// Uploads the given list of files to the current working directory.
    /// </summary>
    /// <param name="paths">List of local files to upload</param>
    public void UploadFiles(params string[] paths)
    {
        foreach (string path in paths)
        {
            FtpWebRequest request = GetRequest(Path.GetFileName(path));
            request.Method = WebRequestMethods.Ftp.UploadFile;
            request.UseBinary = true;

            FileInfo info = new FileInfo(path);
            request.ContentLength = info.Length;

            // Create buffer for file contents
            int buffLength = 16384;
            byte[] buff = new byte[buffLength];

            // Upload this file
            using (FileStream instream = info.OpenRead())
            {
                using (Stream outstream = request.GetRequestStream())
                {
                    int bytesRead = instream.Read(buff, 0, buffLength);
                    while (bytesRead > 0)
                    {
                        outstream.Write(buff, 0, bytesRead);
                        bytesRead = instream.Read(buff, 0, buffLength);
                    }
                    outstream.Close();
                }
                instream.Close();
            }

            FtpWebResponse response = (FtpWebResponse)request.GetResponse();
            response.Close();
        }
    }

    /// <summary>
    /// Downloads the given list of files to the specified local target path
    /// </summary>
    /// <param name="path">Location where downloaded files will be saved</param>
    /// <param name="files">Names of files to download from current FTP directory</param>
    public void DownloadFiles(string path, params string[] files)
    {
        foreach (string file in files)
        {
            FtpWebRequest request = GetRequest(file);
            request.Method = WebRequestMethods.Ftp.DownloadFile;
            request.UseBinary = true;

            using (FileStream outstream = new FileStream(Path.Combine(path, file), FileMode.Create))
            {
                FtpWebResponse response = (FtpWebResponse)request.GetResponse();
                using (Stream instream = response.GetResponseStream())
                {
                    int buffLength = 16384;
                    byte[] buffer = new byte[buffLength];

                    int bytesRead = instream.Read(buffer, 0, buffLength);
                    while (bytesRead > 0)
                    {
                        outstream.Write(buffer, 0, bytesRead);
                        bytesRead = instream.Read(buffer, 0, buffLength);
                    }
                    instream.Close();
                }
                outstream.Close();
                response.Close();
            }
        }
    }

    /// <summary>
    /// Deletes the given list of files from the current working directory.
    /// </summary>
    /// <param name="files">List of files to delete.</param>
    public void DeleteFiles(params string[] files)
    {
        foreach (string file in files)
        {
            FtpWebRequest request = GetRequest(file);
            request.Method = WebRequestMethods.Ftp.DeleteFile;
            FtpWebResponse response = (FtpWebResponse)request.GetResponse();
            response.Close();
        }
    }

    /// <summary>
    /// Deletes the specified directory. The directory should be empty.
    /// </summary>
    /// <param name="files">Directory to delete.</param>
    public void DeleteDirectory(string directory)
    {
        FtpWebRequest request = GetRequest(directory);
        request.Method = WebRequestMethods.Ftp.RemoveDirectory;
        FtpWebResponse response = (FtpWebResponse)request.GetResponse();
        response.Close();
    }

    #region Protected helper methods

    // Constructs an FTP web request
    protected FtpWebRequest GetRequest()
    {
        return GetRequest("");
    }

    // Constructs an FTP web request with the given filename
    protected FtpWebRequest GetRequest(string filename)
    {
        string url = _host.GetUrl(filename);
        FtpWebRequest request = WebRequest.Create(url) as FtpWebRequest;
        request.Credentials = new NetworkCredential(Username, Password);
        request.Proxy = null;
        request.KeepAlive = false;
        return request;
    }

    delegate FtpDirectoryEntry ParseLine(string lines);

    // Converts a directory listing to a list of FtpDirectoryEntrys
    protected List<FtpDirectoryEntry> ParseDirectoryListing(string listing)
    {
        ParseLine parseFunction = null;
        List<FtpDirectoryEntry> entries = new List<FtpDirectoryEntry>();
        string[] lines = listing.Split('\n');
        FtpDirectoryFormat format = GuessDirectoryFormat(lines);

        if (format == FtpDirectoryFormat.Windows)
            parseFunction = ParseWindowsDirectoryListing;
        else if (format == FtpDirectoryFormat.Unix)
            parseFunction = ParseUnixDirectoryListing;

        if (parseFunction != null)
        {
            foreach (string line in lines)
            {
                if (line.Length > 0)
                {
                    FtpDirectoryEntry entry = parseFunction(line);
                    if (entry.Name != "." && entry.Name != "..")
                        entries.Add(entry);
                }
            }
        }
        return entries; ;
    }

    // Attempts to determine the directory format.
    protected FtpDirectoryFormat GuessDirectoryFormat(string[] lines)
    {
        foreach (string s in lines)
        {
            if (s.Length > 10 && Regex.IsMatch(s.Substring(0, 10),
                "(-|d)(-|r)(-|w)(-|x)(-|r)(-|w)(-|x)(-|r)(-|w)(-|x)"))
            {
                return FtpDirectoryFormat.Unix;
            }
            else if (s.Length > 8 && Regex.IsMatch(s.Substring(0, 8),
                "[0-9][0-9]-[0-9][0-9]-[0-9][0-9]"))
            {
                return FtpDirectoryFormat.Windows;
            }
        }
        return FtpDirectoryFormat.Unknown;
    }

    // Parses a line from a Windows-format listing
    // 
    // Assumes listing style as:
    // 02-03-04  07:46PM       <DIR>          Append
    protected FtpDirectoryEntry ParseWindowsDirectoryListing(string text)
    {
        FtpDirectoryEntry entry = new FtpDirectoryEntry();

        text = text.Trim();
        string dateStr = text.Substring(0, 8);
        text = text.Substring(8).Trim();
        string timeStr = text.Substring(0, 7);
        text = text.Substring(7).Trim();
        entry.CreateTime = DateTime.Parse(String.Format("{0} {1}", dateStr, timeStr));
        if (text.Substring(0, 5) == "<DIR>")
        {
            entry.IsDirectory = true;
            text = text.Substring(5).Trim();
        }
        else
        {
            entry.IsDirectory = false;
            int pos = text.IndexOf(' ');
            entry.Size = Int64.Parse(text.Substring(0, pos));
            text = text.Substring(pos).Trim();
        }
        entry.Name = text;  // Rest is name

        return entry;
    }

    // Parses a line from a UNIX-format listing
    // 
    // Assumes listing style as:
    // dr-xr-xr-x   1 owner    group               0 Nov 25  2002 bussys
    protected FtpDirectoryEntry ParseUnixDirectoryListing(string text)
    {
        // Assuming record style as
        // dr-xr-xr-x   1 owner    group               0 Nov 25  2002 bussys
        FtpDirectoryEntry entry = new FtpDirectoryEntry();
        string processstr = text.Trim();
        entry.Flags = processstr.Substring(0, 9);
        entry.IsDirectory = (entry.Flags[0] == 'd');
        processstr = (processstr.Substring(11)).Trim();
        CutSubstringWithTrim(ref processstr, ' ', 0);   //skip one part
        entry.Owner = CutSubstringWithTrim(ref processstr, ' ', 0);
        entry.Group = CutSubstringWithTrim(ref processstr, ' ', 0);
        CutSubstringWithTrim(ref processstr, ' ', 0);   //skip one part
        entry.CreateTime = DateTime.Parse(CutSubstringWithTrim(ref processstr, ' ', 8));
        entry.Name = processstr;   //Rest of the part is name
        return entry;
    }

    // Removes the token ending in the specified character
    protected string CutSubstringWithTrim(ref string s, char c, int startIndex)
    {
        int pos = s.IndexOf(c, startIndex);
        if (pos < 0) pos = s.Length;
        string retString = s.Substring(0, pos);
        s = (s.Substring(pos)).Trim();
        return retString;
    }

    #endregion

}

/// <summary>
/// Helper class for managing current FTP directory.
/// </summary>
public class FtpDirectory
{
    // Static members
    protected static char[] _slashes = { '/', '\\' };
    public static string BackSlash = "\\";
    public static string ForwardSlash = "/";

    // Member variables
    protected string _domain;    // No trailing slash
    protected string _cwd;        // Leading, no trailing slash
    public string Domain { get { return _domain; } }

    // Construction
    public FtpDirectory()
    {
        _domain = String.Empty;
        _cwd = ForwardSlash;    // Root directory
    }

    /// <summary>
    /// Determines if the current directory is the root directory.
    /// </summary>
    public bool IsRootDirectory
    {
        get { return _cwd == ForwardSlash; }
    }

    /// <summary>
    /// Gets or sets the current FTP directory.
    /// </summary>
    public string CurrentDirectory
    {
        get { return _cwd; }
        set { _cwd = ApplyDirectory(value); }
    }

    /// <summary>
    /// Sets the domain and current directory from a URL.
    /// </summary>
    /// <param name="url">URL to set to</param>
    public void SetUrl(string url)
    {
        // Separate domain from directory
        int pos = url.IndexOf("://");
        pos = url.IndexOfAny(_slashes, (pos < 0) ? 0 : pos + 3);
        if (pos < 0)
        {
            _domain = url;
            _cwd = ForwardSlash;
        }
        else
        {
            _domain = url.Substring(0, pos);
            // Normalize directory string
            _cwd = ApplyDirectory(url.Substring(pos));
        }
    }

    /// <summary>
    /// Returns the domain and current directory as a URL.
    /// </summary>
    public string GetUrl()
    {
        return GetUrl(String.Empty);
    }

    /// <summary>
    /// Returns the domain and specified directory as a URL.
    /// </summary>
    /// <param name="directory">Partial directory or filename applied to the 
    /// current working directory.</param>
    public string GetUrl(string directory)
    {
        if (directory.Length == 0)
            return _domain + _cwd;
        return _domain + ApplyDirectory(directory);
    }

    /// <summary>
    /// Applies the given directory to the current directory and returns the
    /// result.
    /// 
    /// If directory starts with "/", it replaces all of the current directory.
    /// If directory is "..", the top-most subdirectory is removed from
    /// the current directory.
    /// </summary>
    /// <param name="directory">The directory to apply</param>
    public string ApplyDirectory(string directory)
    {
        // Normalize directory
        directory = directory.Trim();
        directory = directory.Replace(BackSlash, ForwardSlash);
        directory = directory.TrimEnd(_slashes);

        if (directory == "..")
        {
            int pos = _cwd.LastIndexOf(ForwardSlash);
            return (pos <= 0) ? ForwardSlash : _cwd.Substring(0, pos);
        }
        else if (directory.StartsWith(ForwardSlash))
        {
            // Specifies complete directory path
            return directory;
        }
        else
        {
            // Relative to current directory
            if (_cwd == ForwardSlash)
                return _cwd + directory;
            else
                return _cwd + ForwardSlash + directory;
        }
    }

    /// <summary>
    /// Returns the domain and current directory as a URL
    /// </summary>
    /// <returns></returns>
    public override string ToString()
    {
        return GetUrl();
    }
}

The FtpClientCtl User Control

In addition to the FtpClient class presented above, a closely related task is to allow the user to navigate and modify an FTP directory from within your application. So the attached download also includes my FtpClientCtl user control.

This control can simply be dropped onto a form to provide a working FTP control that the user can use to explorer an FTP server. It exposes most of the functionality provided by the FtpClient class including drag and drop functionality. (Unfortunately, due to limitations with .NET, I was not able to implement drag-and-drop downloads. But uploads can be achieved by dragging one or more files onto the control.)

As you might expect, this control makes heavy use of the FtpClient class. It has a built-in tool strip and context menu. Users can can add and delete files and directories. And they can navigate the directory structure of your FTP server. However, the AllowDirectoryNavigation property can be set to false to prevent the user from navigating to another directory.

The FtpClient property of the FtpClientCtl user control returns the private instance of the FtpClient object. This gives you full access to the underlying code. In addition, the control also has properties for setting and retrieving the login credentials, and several methods for performing operations on the current FTP directory.

Conclusion

There may be limitations to the code presented here. I've really only tested it against Microsoft FTP servers, and there are variations in the way FTP servers work. In addition, it's fairly slow. There may be a number of ways to make it faster, such as retaining connections or other resources between requests.

Nonetheless, the code is easy to use and works fine for me. This is a very easy way to automate FTP access, or to add an FTP browser to your application.

End-User License

Use of this article and any related source code or other files is governed by the terms and conditions of The Code Project Open License.

Author Information

Jonathan Wood

I'm a software and website developer working out of the greater Salt Lake City area of Utah. I've developed many websites including Black Belt Coder, Trail Calendar, and others.

I hike each week with my dogs Suki and Sasha. You can see my hiking blog at Hiking Salt Lake.