Testing

webTiger Logo Wide

A Better Class of .NET FTP Client

The .NET Framework includes some FTP capabilities within the System.Net namespace. My experience using those classes has been mixed to say the least, and after struggling to get solutions to work reliably I decided to write my own client class instead.

Before getting into the code, let’s discuss why we might need our own FTP client implementation. Well, Microsoft’s answer to FTP connectivity is in the form of the System.Net.FtpWebRequest class. Complaints about this class from those that have used it include failing to be able to traverse sub-folders, failing to upload to anywhere except the root folder, inconsistent behaviour and frequent ‘the underlying connection was closed’ errors. This last issue seems to occur because the class randomly disconnects from the server, leaving the connection marked as open but not responding and over time the pool manager can fill up with waiting connections and will eventually start raising these ‘the underlying connection was closed’ errors because there aren’t any more spare connections to use. To compound matters there are issues with credentials/authentication too.

So, how do we go about solving these problems and getting ourselves a more reliable FTP client class we can depend on? Well, FTP is built on top of the TCP protocol so why not just develop our own capability directly using that?

namespace My.Net
{
    using System.Collections.Generic;
    using System.IO;
    using System.Net;
    using System.Net.Sockets;
    using System.Text;

    /// <summary>
    /// Represents the available FTP status codes.
    /// </summary>
    internal enum FtpStatusCode
    {
        /// <summary>(110) Restart reply.</summary>
        PreliminaryRestartMarkerReply = 110,

        /// <summary>(120) Service ready in x minutes.</summary>
        PreliminaryServiceReadyMessage = 120,

        /// <summary>(125) Connection currently open, transfer starting.</summary>
        PreliminaryTransferStarting = 125,

        /// <summary>(150) File status okay, about to open data connection.</summary>
        PreliminaryOpeningDataConnection = 150,

        /// <summary>(200) Command OK.</summary>
        CommandExecSuccess = 200,

        /// <summary>(202) Command not implemented, superfluous at this site.</summary>
        CommandExecNotImplemented = 202,

        /// <summary>(211) System status/help reply.</summary>
        CommandExecSystemStatusReply = 211,

        /// <summary>(212) Directory status.</summary>
        CommandExecDirectoryStatus = 212,

        /// <summary>(213) File status.</summary>
        CommandExecFileStatus = 213,

        /// <summary>(214) System Help message.</summary>
        CommandExecHelpMessage = 214,

        /// <summary>
        /// (215) NAME system type. 
        /// (Where NAME is an official system name from the list in the 
        /// Assigned Numbers document.)
        /// </summary>
        CommandExecNameSystemType = 215,

        /// <summary>(220) Service ready for next user.</summary>
        CommandExecServiceReady = 220,

        /// <summary>
        /// (221) Service closing control connection. 
        /// Logged off where appropriate.
        /// </summary>
        CommandExecUserLoggedOut = 221,

        /// <summary>(225) Data connection open; no transfer in progress.</summary>
        CommandExecConnectionOpen = 225,

        /// <summary>
        /// (226) Closing data connection. Requested action successful.
        /// </summary>
        CommandExecTransferComplete = 226,

        /// <summary>(227) Entering passive mode.</summary>
        CommandExecPassiveMode = 227,

        /// <summary>(230) User logged in, continue.</summary>
        CommandExecUserLoggedIn = 230,

        /// <summary>(250) Requested file action okay, completed.</summary>
        CommandExecFileActionOk = 250,

        /// <summary>(257) "PATHNAME" created.</summary>
        CommandExecPathCreated = 257,

        /// <summary>(331) User name OK, password required.</summary>
        FurtherInfoPassword = 331,

        /// <summary>(332) Need account for login.</summary>
        FurtherInfoCredentials = 332,

        /// <summary>(350) Requested file action pending further information.</summary>
        FurtherInfoData = 350,

        /// <summary>
        /// (421) Service not available, closing control connection. 
        /// This may be a reply to any command if the service knows it must 
        /// shut down.
        /// </summary>
        TempErrorNotAvailable = 421,

        /// <summary>(425) Can't open data connection.</summary>
        TempErrorDataConnectionFailed = 425,

        /// <summary>(426) Connection closed; transfer aborted.</summary>
        TempErrorTransferAborted = 426,

        /// <summary>
        /// (450) Requested file action not taken. 
        /// File not available, server busy, etc.
        /// </summary>
        TempErrorFileActionNotExecuted = 450,

        /// <summary>(451) Request aborted: error on server in processing.</summary>
        TempErrorRequestAborted = 451,

        /// <summary>
        /// (452) Requested action not taken. 
        /// Insufficient resources on system.
        /// </summary>
        TempErrorInsufficientResources = 452,

        /// <summary>
        /// (500) Syntax error, command unrecognized. 
        /// This may include errors such as command line too long.
        /// </summary>
        PermanentErrorSyntax = 500,

        /// <summary>(501) Syntax error in parameters or arguments.</summary>
        PermanentErrorParameters = 501,

        /// <summary>(502) Command not implemented.</summary>
        PermanentErrorCommandNotImplemented = 502,

        /// <summary>(503) Bad sequence of commands.</summary>
        PermanentErrorBadCommands = 503,

        /// <summary>(504) Command not implemented for that parameter.</summary>
        PermanentErrorCommandNotImplementedForParameter = 504,

        /// <summary>(530) Not logged in.</summary>
        PermanentErrorNotLoggedIn = 530,

        /// <summary>(532) Need account for storing files.</summary>
        PermanentErrorAccountNeeded = 532,

        /// <summary>
        /// (550) Requested action not taken. 
        /// File unavailable (e.g., file not found, no access).
        /// </summary>
        PermanentErrorFileUnavailable = 550,

        /// <summary>
        /// (552) Requested file action aborted. 
        /// Exceeded storage allocation (for current directory or dataset).
        /// </summary>
        PermanentErrorFileActionAborted = 552,

        /// <summary>(553) Requested action not taken. File name not allowed.</summary>
        PermanentErrorFilenameNotAllowed = 553
    }

    /// <summary>
    /// Provides a means to interact with a remote FTP server.
    /// </summary>
    public class FtpClient : IDisposable
    {
        private TcpClient connection = null;
        private string hostAddress = "";
        private bool isDisposed = false;

        /// <summary>
        /// Creates a new instance of the <see cref="FtpClient"/> class.
        /// </summary>
        public FtpClient()
        {
        }

        /// <summary>
        /// Releases any resources consumed by the class.
        /// </summary>
        ~FtpClient()
        {
            this.Dispose(false);
        }

        /// <summary>
        /// Gets or sets a value that controls if transfers are being 
        /// performed in passive mode (false) or active mode (true). 
        /// (Defaults to Passive-Mode.)
        /// </summary>
        public bool ActiveMode { get; set; }

        /// <summary>
        /// Changes the current location on the remote server to the 
        /// specified sub-folder. 
        /// (Or .. can be used to navigate up to the parent folder.)
        /// </summary>
        /// <param name="name">The name of the folder to navigate into.</param>
        public void ChangeDir(string name)
        {
            if (this.connection == null) 
            {
                throw new Exception(
                    "The connection to the remote server is closed.");
            }

            string response = this.Send("CWD " + name);
            if (this.GetStatusCode(response) !=
                (int)FtpStatusCode.CommandExecFileActionOk)
            {
                throw new Exception(
                    "Failed to navigate to the revised directory.");
            }
        }

        /// <summary>
        /// Closes the connection to the FTP server.
        /// </summary>
        public void Close()
        {
            if (this.connection == null) return;

            try
            {
                this.Send("QUIT");
                this.Read();
            }
            catch (Exception)
            {
                // Suppress any errors - just trying to clean up the 
                // session before closing the connection.
            }

            this.connection.Close();
            this.connection = null;
        }

        /// <summary>
        /// Gets the current directory on the remote FTP server.
        /// </summary>
        public string CurrentDir
        {
            get
            {
                if (this.connection == null) 
                {
                    throw new Exception(
                        "The connection to the remote server is closed.");
                }

                string response = this.Send("PWD");
                if (this.GetStatusCode(response) != 
                    (int)FtpStatusCode.CommandExecPathCreated)
                {
                    throw new Exception(
                        "Failed to retrieve the current directory on the " + 
                        "remote server.");
                }

                // Extract the directory name.
                int start = response.IndexOf('\"') + 1;
                int end = response.IndexOf('\"', start);
                return response.Substring(start, end - start);
            }
        }

        /// <summary>
        /// Downloads a file from the remote FTP server.
        /// </summary>
        /// <param name="filename">
        /// The filename of the file to download.
        /// </param>
        /// <returns>The raw file content.</returns>
        public byte[] Get(string filename)
        {
            throw new NotImplementedException();
        }

        /// <summary>
        /// Gets a value indicating if the FTP connection is currently established.
        /// </summary>
        public bool IsConnected
        {
            get 
            {
                return this.connection != null; 
            }
        }

        /// <summary>
        /// Retrieves a listing for the current folder on the remote FTP server.
        /// </summary>
        /// <param name="filter">
        /// Filtering to apply to the filenames, or an empty string 
        /// to return all filenames.
        /// </param>
        /// <returns>The directory listing.</returns>
        public List<string> List(string filter)
        {
            if (this.connection == null) 
            {
                throw new Exception(
                    "The connection to the remote server is closed.");
            }

            if (filter == null) filter = "";
            this.SetTextMode();

            TcpListener listener = null;
            TcpClient client = null;

            try
            {
                if (this.ActiveMode)
                {
                    listener = this.CreateActiveTransfersListener();
                    listener.Start();
                    client = listener.AcceptTcpClient();
                }
                else
                {
                    client = this.CreatePassiveTransfersClient();
                }

                string response = this.Send("NLST " + filter);
                int status = this.GetStatusCode(response);

                if (status != (int)FtpStatusCode.PreliminaryOpeningDataConnection &&
                    status != (int)FtpStatusCode.PreliminaryTransferStarting &&
                    status != (int)FtpStatusCode.PermanentErrorFileUnavailable)
                {
                    throw new Exception("Failed to get directory listing.");
                }

                if (status == (int)FtpStatusCode.PermanentErrorFileUnavailable)
                {
                    return new List<string>();
                }

                DateTime timeout = DateTime.Now.AddSeconds(10D);

                // Wait for the server to respond.
                while (!client.GetStream().DataAvailable && timeout > DateTime.Now)
                {
                    System.Threading.Thread.Sleep(100);
                }

                if (timeout <= DateTime.Now)
                {
                    throw new Exception(
                        "Timeout while waiting for the remote server to respond.");
                }

                StringBuilder info = new StringBuilder(256);
                byte[] buffer = new byte[1];

                while (client.GetStream().DataAvailable)
                {
                    client.GetStream().Read(buffer, 0, buffer.Length);
                    info.Append(Encoding.ASCII.GetString(buffer));
                }

                string[] lines = info.ToString().Split(
                    new[] { '\r', '\n' }, 
                    StringSplitOptions.RemoveEmptyEntries);
                List<int> statusCodes = this.GetStatusCodes(lines);

                response = this.Read();
                if (this.GetStatusCode(response) != 
                    (int)FtpStatusCode.CommandExecTransferComplete)
                {
                    throw new Exception("Retrieval of directory listing failed.");
                }

                return new List<string>(lines);
            }
            finally
            {
                if (client != null)
                {
                    client.GetStream().Close();
                    client.Close();
                }

                if (listener != null)
                {
                    listener.Stop();
                }
            }
        }

        /// <summary>
        /// Extracts recognised 3-digit FTP status codes from the 
        /// supplied collection of response lines.
        /// </summary>
        /// <param name="lines">The lines of text to parse.</param>
        /// <returns>A collection of status codes.</returns>
        private List<int> GetStatusCodes(string[] lines)
        {
            List<int> statusCodes = new List<int>();
            int code = 0;

            foreach (string line in lines)
            {
                if (int.TryParse(line.Substring(0, 3), out code) && (
                    code == (int)FtpStatusCode.PreliminaryOpeningDataConnection ||
                    code == (int)FtpStatusCode.PreliminaryRestartMarkerReply ||
                    code == (int)FtpStatusCode.PreliminaryServiceReadyMessage ||
                    code == (int)FtpStatusCode.PreliminaryTransferStarting ||
                    code == (int)FtpStatusCode.CommandExecConnectionOpen ||
                    code == (int)FtpStatusCode.CommandExecDirectoryStatus ||
                    code == (int)FtpStatusCode.CommandExecFileActionOk ||
                    code == (int)FtpStatusCode.CommandExecFileStatus ||
                    code == (int)FtpStatusCode.CommandExecHelpMessage ||
                    code == (int)FtpStatusCode.CommandExecNameSystemType ||
                    code == (int)FtpStatusCode.CommandExecNotImplemented ||
                    code == (int)FtpStatusCode.CommandExecPassiveMode ||
                    code == (int)FtpStatusCode.CommandExecPathCreated ||
                    code == (int)FtpStatusCode.CommandExecServiceReady ||
                    code == (int)FtpStatusCode.CommandExecSuccess ||
                    code == (int)FtpStatusCode.CommandExecSystemStatusReply ||
                    code == (int)FtpStatusCode.CommandExecTransferComplete ||
                    code == (int)FtpStatusCode.CommandExecUserLoggedIn ||
                    code == (int)FtpStatusCode.CommandExecUserLoggedOut ||
                    code == (int)FtpStatusCode.FurtherInfoCredentials ||
                    code == (int)FtpStatusCode.FurtherInfoData ||
                    code == (int)FtpStatusCode.FurtherInfoPassword ||
                    code == (int)FtpStatusCode.TempErrorDataConnectionFailed ||
                    code == (int)FtpStatusCode.TempErrorFileActionNotExecuted ||
                    code == (int)FtpStatusCode.TempErrorInsufficientResources ||
                    code == (int)FtpStatusCode.TempErrorNotAvailable ||
                    code == (int)FtpStatusCode.TempErrorRequestAborted ||
                    code == (int)FtpStatusCode.TempErrorTransferAborted ||
                    code == (int)FtpStatusCode.PermanentErrorAccountNeeded ||
                    code == (int)FtpStatusCode.PermanentErrorBadCommands ||
                    code == (int)FtpStatusCode.PermanentErrorCommandNotImplemented ||
                    code == (int)FtpStatusCode.PermanentErrorCommandNotImplementedForParameter ||
                    code == (int)FtpStatusCode.PermanentErrorFileActionAborted ||
                    code == (int)FtpStatusCode.PermanentErrorFilenameNotAllowed ||
                    code == (int)FtpStatusCode.PermanentErrorFileUnavailable ||
                    code == (int)FtpStatusCode.PermanentErrorNotLoggedIn ||
                    code == (int)FtpStatusCode.PermanentErrorParameters ||
                    code == (int)FtpStatusCode.PermanentErrorSyntax))
                {
                    statusCodes.Add(code);
                }
            }

            return statusCodes;
        }

        /// <summary>
        /// Creates a new folder on the remote FTP server.
        /// </summary>
        /// <param name="name">The name of the folder to create.</param>
        public void MakeDir(string name)
        {
            if (this.connection == null) 
            {
                throw new Exception(
                    "The connection to the remote server is closed.");
            }

            if (string.IsNullOrEmpty(name) || name.Trim() == "") 
            {
                throw new ArgumentException(
                    "The directory name cannot be blank.");
            }

            string response = this.Send("MKD " + name);
            if (this.GetStatusCode(response) != 
                (int)FtpStatusCode.CommandExecPathCreated)
            {
                throw new Exception(string.Format(
                    "Failed to create the directory '{0}'."));
            }
        }

        /// <summary>
        /// Opens a new connection to a specified remote FTP server. 
        /// (If an existing connection is already opened it will be closed first.)
        /// </summary>
        /// <param name="host">
        /// The host name or IP-address of the remote server.
        /// </param>
        /// <param name="port">
        /// The remote port number to connect on. 
        /// Use a port number of 21 if in doubt (this is the default FTP port.)
        /// </param>
        /// <param name="username">The username to log into the server with.</param>
        /// <param name="password">The password to log into the server with.</param>
        public void Open(string host, int port, string username, string password)
        {
            if (this.connection != null) this.Close();

            // localhost is a problematic host name and needs to be intercepted 
            // and resolved.
            if (host.ToLower().Trim() == "localhost")
            {
                host = Dns.GetHostAddresses(Dns.GetHostName())[0].ToString();
            }

            this.connection = new TcpClient();
            this.connection.Connect(host, port);
            string response = this.Read();
            int status = this.GetStatusCode(response);

            if (status != (int)FtpStatusCode.CommandExecServiceReady)
            {
                this.Close();
                throw new Exception(
                    "Failed to establish a connection with the remote FTP server.");
            }

            // Flush any welcome messages that the server may send when 
            // initially connecting.
            this.Read();
 
            response = this.Send("USER " + username);
            status = this.GetStatusCode(response);

            if (status != (int)FtpStatusCode.FurtherInfoPassword)
            {
                this.Close();
                throw new Exception(
                    "Failed to log into the remote FTP server.");
            }

            response = this.Send("PASS " + password);
            status = this.GetStatusCode(response);

            if (status != (int)FtpStatusCode.CommandExecUserLoggedIn)
            {
                this.Close();
                throw new Exception("Failed to log into the remote FTP server.");
            }

            this.hostAddress = host;
        }

        /// <summary>
        /// Uploads a file to the remote FTP server.
        /// </summary>
        /// <param name="source">
        /// The full local filepath of the file being uploaded.
        /// </param>
        public void Put(string source)
        {
            if (string.IsNullOrEmpty(source) || source.Trim() == "") 
            {
                throw new ArgumentException(
                    "The source (filepath) cannot be blank.");
            }

            if (!File.Exists(source)) 
            {
                throw new ArgumentException("The source file does not exist.");
            }

            string filename = Path.GetFileName(source);
            this.Put(filename, new List<byte>(File.ReadAllBytes(source)));
        }

        /// <summary>
        /// Uploads a file to the remote FTP server.
        /// </summary>
        /// <param name="source">
        /// The full local filepath of the file being uploaded.
        /// </param>
        /// <param name="remoteFilename">
        /// The filename on the remote server to upload the file contents to.
        /// </param>
        public void Put(string source, string remoteFilename)
        {
            if (string.IsNullOrEmpty(source) || source.Trim() == "") 
            {
                throw new ArgumentException(
                    "The source (filepath) cannot be blank.");
            }

            if (!File.Exists(source)) 
            {
                throw new ArgumentException("The source file does not exist.");
            }

            if (string.IsNullOrEmpty(remoteFilename) || 
                remoteFilename.Trim() == "") 
            {
                remoteFilename = Path.GetFileName(source);
            }

            if (remoteFilename.Contains(@"\") || 
                remoteFilename.Contains("/")) 
            {
                throw new ArgumentException(
                    "The remote filename should not contain any path information.");
            }

            this.Put(remoteFilename, new List<byte>(File.ReadAllBytes(source)));
        }

        /// <summary>
        /// Uploads a file to the remote FTP server.
        /// </summary>
        /// <param name="filename">
        /// The filename on the remote server to upload the file contents to.
        /// </param>
        /// <param name="content">The raw file contents to upload.</param>
        public void Put(string filename, List<byte> content)
        {
            if (this.connection == null) 
            {
                throw new Exception(
                    "The connection to the remote server is closed.");
            }

            if (string.IsNullOrEmpty(filename) || 
                filename.Trim() == "") 
            {
                throw new ArgumentException("The target filename cannot be blank.");
            }

            if (filename.Contains(@"\") || 
                filename.Contains("/")) 
            {
                throw new ArgumentException(
                    "The remote filename should not contain any path information.");
            }

            TcpListener listener = null;
            TcpClient client = null;

            try
            {
                if (this.ActiveMode)
                {
                    listener = this.CreateActiveTransfersListener();
                    listener.Start();
                    client = listener.AcceptTcpClient();
                }
                else
                {
                    client = this.CreatePassiveTransfersClient();
                }

                string response = this.Send("STOR " + filename);
                int status = this.GetStatusCode(response);
                if (status != (int)FtpStatusCode.PreliminaryOpeningDataConnection &&
                    status != (int)FtpStatusCode.PreliminaryTransferStarting)
                {
                    throw new Exception("Failed to inialise the file upload.");
                }

                const int Chunk = 1024;
                byte[] buffer;
                int written = 0;

                while (written < content.Count)
                {
                    buffer = content.GetRange(
                        written, 
                        content.Count - written > Chunk 
                            ? Chunk 
                            : content.Count - written).ToArray();
                    client.GetStream().Write(buffer, 0, buffer.Length);
                    written += buffer.Length;
                }

            }
            finally
            {
                if (client != null)
                {
                    client.GetStream().Close();
                    client.Close();
                }

                if (listener != null)
                {
                    listener.Stop();
                }
            }
        }

        /// <summary>
        /// Deletes a folder on the remote FTP server.
        /// </summary>
        /// <param name="name">The name of the folder to delete.</param>
        public void RemoveDir(string name)
        {
            if (this.connection == null) 
            {
                throw new Exception(
                    "The connection to the remote server is closed.");
            }

            if (string.IsNullOrEmpty(name) || name.Trim() == "") return;

            string response = this.Send("RMD " + name);
            if (this.GetStatusCode(response) != 
                (int)FtpStatusCode.CommandExecFileActionOk)
            {
                throw new Exception(string.Format(
                    "Failed to delete the directory '{0}'.", name));
            }
        }

        /// <summary>
        /// Sets the FTP transfer mode to BINARY.
        /// </summary>
        public void SetBinMode()
        {
            if (this.connection == null) 
            {
                throw new Exception(
                    "The connection to the remote server is closed.");
            }

            string response = this.Send("TYPE I");
            if (this.GetStatusCode(response) != 
                (int)FtpStatusCode.CommandExecSuccess)
            {
                throw new Exception("Failed to set the transfer mode to BINARY.");
            }
        }

        /// <summary>
        /// Sets the FTP transfer mode to ASCII.
        /// </summary>
        public void SetTextMode()
        {
            if (this.connection == null) 
            {
                throw new Exception(
                    "The connection to the remote server is closed.");
            }

            string response = this.Send("TYPE A");
            if (this.GetStatusCode(response) != 
                (int)FtpStatusCode.CommandExecSuccess)
            {
                throw new Exception("Failed to set the transfer mode to ASCII.");
            }
        }

        /// <summary>
        /// Performs application-defined tasks associated with freeing, 
        /// releasing, or resetting unmanaged resources.
        /// </summary>
        public void Dispose()
        {
            this.Dispose(!this.isDisposed);
            GC.SuppressFinalize(this);
            this.isDisposed = true;
        }

        /// <summary>
        /// Creates a TCP listener to use for active-mode file transfers.
        /// </summary>
        /// <returns>A configured TCP listener.</returns>
        private TcpListener CreateActiveTransfersListener()
        {
            // The specified range avoids published reserved port numbers.
            int port = new Random().Next(41952, 65000); 
            IPAddress local = Dns.GetHostAddresses(Dns.GetHostName())[0];

            // Send the port number to the remote server - the value is 
            // encoded as two values (number / 256, and lowest 8-bits of number)
            int firstByte = port >> 8;
            int secondByte = port & 0xFF;

            string response = this.Send(
                string.Format("PORT {0},{1},{2}",
                local.ToString().Replace(".", ","), 
                firstByte, 
                secondByte));

            if (this.GetStatusCode(response) != 
                (int)FtpStatusCode.CommandExecSuccess)
            {
                throw new Exception(
                    "Failed to establish the active-mode connection.");
            }

            return new TcpListener(local, port);
        }

        /// <summary>
        /// Creates a TCP client to use for passive-mode file transfers.
        /// </summary>
        /// <returns>
        /// A configured and connected passive-mode transfer client.
        /// </returns>
        private TcpClient CreatePassiveTransfersClient()
        {
            string response = this.Send("PASV");
            if (this.GetStatusCode(response) != 
                (int)FtpStatusCode.CommandExecPassiveMode)
            {
                throw new Exception(
                    "Failed to change transfers into passive mode.");
            }

            // Extract the IP-address of the remote FTP server and the 
            // port number to connect using from the response.
            int port = 0;
            response = response.Replace("\r", "");
            string[] parts = response.Split(
                new[] { ',' }, 
                StringSplitOptions.RemoveEmptyEntries);

            // Strip trailing info from the last element in the response 
            // as we need to extract the port number from it.
            while (!int.TryParse(parts[5], out port) && parts[5].Length > 0)
            {
                parts[5] = parts[5].Substring(0, parts[5].Length - 1);
            }

            // Calculate the revised data port that will be used for transfers.
            port = int.Parse(parts[4]) * 256 + port;

            return new TcpClient(this.hostAddress, port);
        }

        /// <summary>
        /// Disposes of all resources consumed by the class.
        /// </summary>
        /// <param name="disposing">
        /// True if disposing of all resources, 
        /// or False to release only unmanaged resources.</param>
        private void Dispose(bool disposing)
        {
            if (disposing)
            {
                // Free managed resources.
                this.Close();
            }

            // Always free any unmanaged resources 
            // (but there aren't any in this case.)
        }

        /// <summary>
        /// Returns the first FTP status code in the supplied response 
        /// from the remote FTP server, or returns 0 if a status code 
        /// wasn't found.
        /// </summary>
        /// <param name="response">The server response to decode.</param>
        /// <returns>The first status code found, or 0.</returns>
        private int GetStatusCode(string response)
        {
            int code = 0;
            if (response.Length > 2) 
            {
                int.TryParse(response.Substring(0, 3), out code);
            }

            return code;
        }

        /// <summary>
        /// Reads a response from the remote FTP server.
        /// </summary>
        /// <returns>The response details.</returns>
        private string Read()
        {
            if (this.connection == null) return "";

            DateTime timeout = DateTime.Now.AddSeconds(5D);

            // Wait a little while for the response to be returned 
            // (in case the network is slow.)
            while (!this.connection.GetStream().DataAvailable && 
                timeout > DateTime.Now)
            {
                System.Threading.Thread.Sleep(100);
            }

            byte[] buffer = new byte[1];
            StringBuilder data = new StringBuilder(1024);

            // NOTE: We'd like to be able to read in chunks but 
            // NetworkStream.Length isn't supported (and will throw 
            // a NotSupportedException) so we've got to read 1 byte 
            // at a time!
            while (this.connection.GetStream().DataAvailable)
            {
                buffer[0] = (byte)this.connection.GetStream().ReadByte();
                data.Append(Encoding.ASCII.GetString(buffer, 0, 1));
            }

            return data.ToString();
        }

        /// <summary>
        /// Send a command to the remote FTP server, and returns the response.
        /// </summary>
        /// <param name="command">The command to transmit.</param>
        /// <returns>The remote server's response.</returns>
        private string Send(string command)
        {
            if (this.connection == null) 
            {
                throw new Exception(
                    "The connection to the remote server is closed.");
            }

            if (!command.Trim().EndsWith("\r\n")) 
            {
                command = command.Trim() + "\r\n";
            }

            byte[] data = Encoding.ASCII.GetBytes(command);
            this.connection.GetStream().Write(data, 0, data.Length);
            return this.Read();
        }
    }
}Code language: C# (cs)

There you go, a more resilient solution for FTP transfers. You may notice that there is a ‘Get’ method but that it won’t work. I haven’t ever needed to implement pull functionality on the projects I’ve worked on so I haven’t got around to finishing that part of the class off. I will at some point and then I’ll update this article!