Introduction
System.Net.FtpClient
is a client implementation of the FTP protocol written entirely in C#. It supports SSL/TLS encryption for the control and data connections. It is comprised of many classes however most people will only be interested in a select few. The starting place is FtpClient which is a high level implementation RFC959 and various extensions to the protocol. The library supports many more features than described in this article however the information here should be enough to get most people started using the library in their own projects.
Getting Started
The FtpClient class is pretty simple and straight forward. The class is setup to try to use explicit (TLS) encryption by default. This means that upon connection, FtpClient will execute the AUTH command before sending any credentials. This behavior can be controlled through the SslMode
property which is an FtpSslMode
enumeration. Using SSL means that your code needs to be prepared to handle certain scenarios such as an invalid SSL certificate or in the case of TLS your code needs to be prepared to handle cases where the server does not support or possibly allow the AUTH command. The example below shows basic usage of the FtpClient
class with the above considerations taken into account:
using System.Net.FtpClient;
...
static void Main(string[] args) {
using (FtpClient cl = new FtpClient("user", "pass", "ftp.server.tld")) {
cl.FtpLogStream = Console.OpenStandardOutput();
cl.FtpLogFlushOnWrite = true;
cl.DataChannelType = FtpDataChannelType.ExtendedPassive;
cl.SslMode = FtpSslMode.Explicit;
cl.SecurityNotAvailable += new SecurityNotAvailable(OnSecurityNotAvailable);
cl.InvalidCertificate += new FtpInvalidCertificate(OnInvalidCertficate);
}
}
static void OnSecurityNotAvailable(FtpSecurityNotAvailable e) {
e.Cancel = false;
}
static void OnInvalidCertficate(FtpChannel c, InvalidCertificateInfo e) {
e.Ignore = true;
}
Downloading and Uploading Files
There are two methods for downloading and uploading files. The first is to attach to the TransferProgress
event of FtpClient
and use the Download
and Upload
methods or to open stream which you can read or write accordingly. The Download
and Upload
methods handle everything for you. The TransferProgress event is fired everytime data is read from the underlying data stream. The EventArgs
contain progress information (if available) and has a property that indicates if the transfer is an upload or download. You only need one event handler for downloads and uploads. Here's an example expanding on the above FtpClient example:
try {
cl.TransferProgress += new FtpTransferProgress(OnTransferProgress);
cl.Download("/path/to/file.ext");
}
catch (Exception ex) {
Console.WriteLine();
Console.WriteLine(ex.ToString());
}
....
static void OnTransferProgress(FtpTransferInfo e) {
Console.Write("\r{0}/{1} {2}% {3}/s ",
FormatBytes(e.Transferred), FormatBytes(e.Length), e.Percentage, FormatBytes(e.BytesPerSecond));
if (e.Complete) {
Console.WriteLine();
}
}
static string FormatBytes(long size) {
string[] units = new string[] { "B", "KB", "MB", "GB" };
double val = size;
int count = 0;
while (val > 1024 && count < units.Length) {
val /= 1024;
count++;
}
return string.Format("{0}{1}", Math.Round(val, 2), units[count]);
}
The Download
and Upload
methods are overloaded. You can transfer to or from local files or streams. If you want more control over the download process you can open a FtpDataStream
object. The streams are not bi-directional. The FTP protocol does not support reading and writing to the same stream mode data connection. FtpDataStream
objects are designed to be used and thrown away. Here's an example expanding on the FtpClient
code from the first listing:
using (FtpDataStream istream = cl.OpenRead("/path/to/file")) {
byte[] buf = new byte[istream.ReceiveBufferSize];
int read = 0;
while ((read = istream.Read(buf, 0, buf.Length)) > 0) {
}
}
Uploading files is nearly an identical process except you will use FtpClient.Upload()
or FtpClient.OpenWrite()
accordingly.
Listing Files
Listing files can be done in two ways. The bare minimal way is to use FtpClient.GetListing()
which returns an array of FtpListItem
objects. These objects contain the file name (not path), file size and the last write time however there are some exceptions where not all of the information will be available or, in regards to modification times, necessarily accurate. System.Net.FtpClient attempts to parse LIST and MLSD formats. MLSD formats should always be 100% accurate unless there is a bug in System.Net.FtpClient itself. With that said, it should be preferred that your client is connecting to a modern ftp server that supports the MLST/MLSD extensions. LIST formats come in many flavors, most commonly UNIX long listing style and DOS long listing styles (IIS). The LIST formats are very difficult to parse and do not always provide the most accurate date/time representations. In addtion, there is no written specification on how LIST results should be formatted so it's possible some servers may give results that are not even handled by System.Net.FtpClient. With that said, the preferred way to work with files and directories is to use the FtpFile
and FtpDirectory
classes. These classes are very similar to System.IO.FileInfo
and System.IO.DirectoryInfo
. They attempt to do all of the dirty work for loading the correct modification times and file sizes when System.Net.FtpClient is dealing with a server that does not support the MLST/MLSD extensions. Here's an example of listing files and directories:
foreach(FtpFile o in cl.CurrentDirectory.Files) {
}
foreach(FtpDirectory o in cl.CurrentDirectory.Directories) {
}
foreach(FtpListItem o in cl.GetListing("/path")) {
if (!cl.HasCapability(FtpCapability.MLSD) && cl.HasCapability(FtpCapability.MDTM)) {
DateTime modify = cl.GetLastWriteTime(string.Format("/path/{0}", item.Name));
if (modify != DateTime.MinValue) {
item.Modify = modify;
}
}
if(o.Type == FtpObjectType.File) {
}
else if(o.Type == FtpObjectType.Directory) {
}
}
If you are in control of the server software you are advised to use a modern implementation that supports the MLST/MLSD extensions if you want the best possible results and reliability.
Custom Parsers
As mentioned before, LIST formats can and do vary. If you run across a server that is giving file listings in formats not handled by System.Net.FtpClient you can add your own parsers to the static FtpListFormatParser.Parsers
collection. In order to make use of this feature you must be handy with Regular Expressions. Because this functionality extends well beyond basic usage you won't find more than an acknowledgement that it exists in this article. If you would like to make use of this feature please refer to the Documentation on the CodePlex site. If you need further help feel free to contact the developers.
Bulk Transfers
File listings and transfers require the use of a secondary connection called the data connection. This connection is opened and thrown away when it's finished. If a new connection is needed a new socket is created. This is the basis of stream mode data transfers. Closing the stream signifies EOF between the server and client. This matters because a used socket goes into a linger state for a period of time defined by the operating system. If you are transferring a large number of small files on a fast connection it's possible that you can exhaust the available client sockets the OS has available. Please take this into consideration when recursively transferring and listing files. On windows, the number of sockets and the length of the linger state can be modified via the registry however any changes you make are at your own risk. Reducing the linger state time can cause upredictable problems with all network enabled applications.
Thread Safety
Let me just start by saying the FTP protocol is not thread safe. You cannot execute multiple commands on the same command channel simultaneously. You can't even execute a command on the command channel while you are reading from a data channel. There are some exceptions to that rule however they are just that, exceptions. Some severs will indeed allow you to execute commands on the command channel while you're, for instance, downloading a large file but these servers are the exception and not the rule. If you need to access a single instance of a FtpClient object from multiple threads you should block access until a given operation (transaction) is completed, that includes a file download. An example of safe usage of FtpClient from 2 or more threads:
static readonly object m_FtpLock = new object();
static void Thread1() {
lock(m_FtpLock) {
DateTime modify = m_ftpClient.GetLastWriteTime(file1);
}
}
static void Thread2() {
lock(m_FtpLock) {
DateTime modify = m_ftpClient.GetLastWriteTime(file2);
}
}
If you try to open multiple data streams on a single command channel there are 2 likely outcomes. The server will close any existing data stream and let you proceed or it will give you a permanent failure reply. So as a general rule of thumb, if you want to transfer files or get file listings simultaneously you need exactly 1 instance of the FtpClient class per data connection. Two downloads and a file listing running in parallel would require 3 instances of FtpClient.
Final Thoughts
System.Net.FtpClient is an actively changing project. Not all features are described in this article and the behavior may change in the future. The CodePlex site includes a CHM API Reference as well as several example projects that are kept up to date with the current state of affairs. If you run across bugs or have feature requests I would greatly appreciate that you use the bug tracker on CodePlex. With that said I hope you find this code easy to use and incorporate into your own projects!