Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Using FTP with .NET including sending server commands

0.00/5 (No votes)
19 May 2004 5  
How to send and receive files, enumerate directories and subdirectories remotely on demand, send server commands through FTP control port.

Introduction

This article describes the usage of the CFTPConnection class wrapped in a managed C++ assembly. The following functions are used and discussed:

  1. Sending / receiving files from FTP server.
  2. Enumerating directory contents.
  3. Sending server commands, i.e., NLST to the FTP server using CFTPConnection::Command method and parsing the server's response synchronously. The FTP data (20) and control (21) ports.
  4. Using these functions from a simple UI.

Body

Recently, I had a need to develop an FTP client as a sub-function of a large C# Windows Form application. I quickly discovered that FTP support was not part of the FCL. I decided that the simplest way to address my needs was to develop a wrapper around the CFTPConnection class MFC WinInet wrapper (yes a wrapper of a wrapper -- :) ).

I started out by just creating a purely unmanaged C++ .dll and just exporting a couple of functions, and then using PInvoke to call the functions from C#. I found this to be both inelegant and largely infeasible when it came to enumerating directory contents and returning these contents to a C# application. The solution I finally settled on was to create a managed C++ assembly that would interact with CFTPConnection and would package things up into nice managed classes for the C# clients.

Sending / Receiving files

Sending and receiving files is supported right out of the box by CFTPConnection. My wrapper around these functions is thin at best. Here's a simple example of sending a file with the wrapper:

bSent = FTP_Wrapper.SendFTPFile(Mode, Convert.ToInt32(szFTPServerPort), 
                     szFTPServerName, szFTPUserName, 
                     szFTPUserPassword, szFTPFile, 
                     szFTPDestFile, 
                     ref szErrorMsg);
if (!bSent)
    Console.WriteLine(szErrorMsg);

Enumerating Directory Contents

Getting the contents of an FTP directory is pretty straightforward using CFtpFileFind. I wanted to use this class and return a collection of files and subfolders to my managed clients. One thing to watch out for when enumerating directory contents of a server is that you don't try to pull back an entire server's contents before showing them in your GUI. For a larger server, this reading of the entire directory structure could take quite a while. A better approach (and one that is shown in the sample application) is to just enumerate the root directory, show the results, and then when a user wants to go down into a subdirectory, enumerate that subdirectory.

Enter the DirectoryContents class:

namespace FTP_Wrap
{
  public __gc class DirectoryContents;
  public __gc class DirectoryItem 
  {
    public:
      System::String* m_Name;
      System::String* m_PathName;
      int m_Type; // 0 = file, 1 = subdirectory;

      long m_Size; // file size in bytes

      bool Equals(System::Object* pObj); 
  };

  public __gc class DirectoryContents
  {
    public:
      DirectoryContents();
      virtual ~DirectoryContents() {}; 
      bool LoadList(int SourceType, System::String* pServerNm, 
         int ServerPort, System::String* pUserNm, 
         System::String* pPwd, System::String* pRemoteDir, 
         System::String* pMask, System::String*& pErrorMsg);
      System::Collections::ArrayList* m_DirectoryList; 
  };
}

Using the DirectoryContents class from C#:

static void Main(string[] args)
{
  //

  // TODO: Add code to start application here

  // 


  //Do the FTP dir command and recurse all sub folders:

  ShowDirContents("/");
}

static void ShowDirContents(String Dir)
{
  FTP_Wrap.DirectoryContents dc = new DirectoryContents();
  String ErrorMsg = "";
  bool Loaded = dc.LoadList("localhost", 21, "USERID", "PASSWORD", 
                Dir, "*", ref ErrorMsg);

  if (!Loaded)
  {
    Console.WriteLine(ErrorMsg);
    return;
  } 
  foreach (DirectoryItem item in dc.m_DirectoryList)
  {
    if (item.m_Type == 0) //files

    {
      Console.WriteLine("File: " + item.m_PathName + " Size: " + item.m_Size);
    }
    else //sub directories

      ShowDirContents(item.m_PathName);
  }
}

CFTPConnection::Command

One additional challenge I faced in implementing my client functionality was that one of the FTP machine types that would typically be interfaced with did not support truly standard FTP. As a result, using CFTPFileFind did not work with the server to enumerate directories. Trying to use CFTPFileFind would return a bunch of garbage that was barely if at all interpretable as a list of files. I needed a better way to get the file lists from the server.

After digging around a little more, I noticed that there is a set of 'server' commands that can be sent to an FTP server using the Command method of CFTPConnection. The commands are sent on port 21 (the FTP control port) but the responses are sent back on port 20 (the data port). Luckily (or I'd have more code to deal with), CFTPConnection handles these communications.

In order to get the directory contents using a server command, I issued a NLST command to the server and parsed the response.

Here's what happens at the TCP/IP level:

Send NLST through port 21 to the server:

Sample screenshot

Get response to NLST from server to my local port 20:

Sample screenshot

Code from directorycontents.cpp:

// Issue the command to the server:

CString dir(pRemoteDir);
InternetFile* pFile = NULL;
System::String* pRes = NULL;
BOOL bChg = pFtpConn->SetCurrentDirectory(dir);
CString strDir;
pFtpConn->GetCurrentDirectory(strDir);
File = pFtpConn->Command(_T("NLST"), CFtpConnection::CmdRespRead, 
FTP_TRANSFER_TYPE_BINARY);

//Process the response (the file list):

ULONGLONG a = pFile->GetLength();
char readBuf[256];
unsigned int rd = 0;
System::Text::StringBuilder* pSb = new System::Text::StringBuilder();
do 
{
  rd = pFile->Read(readBuf, 256);
  if (rd > 0)
  {
    System::String* pStr = new System::String(readBuf);
    pSb->Append(pStr, 0, rd);
  }
} while (rd > 0);

pRes = pSb->ToString();

// ok, parse the response:

System::String* pDelim = "\r\n";
System::Text::RegularExpressions::Regex* pRegex = 
  new System::Text::RegularExpressions::Regex(pDelim);
System::String* parts[] = pRegex->Split(pRes);

for (int n = 0; n < parts->Length; n++)
{
  String* pItemNm = parts[n];
  if (pItemNm->Length == 0)
    continue;

  DirectoryItem* pItem = new DirectoryItem; 
  if (pItemNm->EndsWith("/"))
  {
    pItem->m_Type = 1; // a directory

    String* pTemp = pItemNm->Substring(0, pItemNm->Length - 1);
    int nNameBegin = pTemp->LastIndexOf("/");
    pItem->m_Name = pTemp->Substring(nNameBegin + 1);
    System::Text::StringBuilder* pSb = new System::Text::StringBuilder();
    pSb->Append(pRemoteDir);
    pSb->Append(pItem->m_Name);
    pSb->Append("/");
    pItem->m_PathName = pSb->ToString();
  }
  else
  {
    pItem->m_Type = 0; // a file

    int nNameBegin = pItemNm->LastIndexOf("/");
    pItem->m_Name = pItemNm->Substring(nNameBegin + 1);
    System::Text::StringBuilder* pSb = new System::Text::StringBuilder();
    pSb->Append(pRemoteDir);
    pSb->Append(pItem->m_Name);
    pItem->m_PathName = pSb->ToString();
  }
  pItem->m_Size = 0;
  m_DirectoryList->Add(pItem);
}

The big thing to note is that the Command method returns a CInternetFile. To get the actual response data, you just read that file till EOF.

The sample application

I've included a very simple stripped down sample application to show the functionality of the classes. In order to play with the DirectoryContents enumerations, select the '...' button beside 'file to get' in the get file form, or the '...' button beside the 'FTP Destination File' in the send file form. Go ahead and step through these to get a better idea if I've left something out.

The sample app will save your FTP settings (port, server name, most recent file names, etc.) to the current user registry under the software key.

Conclusion

These classes solved what I was trying to do. I hope they can help someone here.

References

I used numerous resources for figuring out how to use the CFTPConnection class. Plenty of good information on it is in MSDN. Also, I used the ethereal packet capture utility not only for this but for a lot of network troubleshooting. It can be downloaded from here.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here