• (2 Pages)
  • +
  • 1
  • 2

Create an FTP Class Library in C#

#1 PsychoCoder  Icon User is offline

  • Google.Sucks.Init(true);
  • member icon

Reputation: 1642
  • View blog
  • Posts: 19,853
  • Joined: 26-July 07

Post icon  Posted 22 October 2007 - 07:30 PM

Welcome to my tutorial on FTP Class Library in C#. In this tutorial I will walk you through creating an FTP Class, which can be implemented into your own FTP interface, using Raw FTP Commands and the following Namespaces in the .Net Framework:

The latter of the 4, System.Runtime.Remoting.Messaging, gives us the remoting infrastructure needed to communicate with the remote FTP Server. Support for FTP in .Net 2.0 is not only a new feature, but it doesn't get nearly enough attention as it should. Before 2.0 we were delegated to relying on 3rd party libraries if we wanted to access FTP functionality. I have been asked many times how to go about creating an FTP Class Library in C#, both professionally and from friends I know in the industry, so I thought it was time to dive into the world of Sockets and remoting.

At the time of this tutorial my FTP Class Library doesn't yet have support for FTPS or recursive downloading, but I plan on implementing that in the future. The features you will have available when you complete this tutorial are:
  • Uploading
  • Recursive Uploading
  • Download
  • Resume (Download & Upload)
  • Delete (Files & Directories)
  • Rename Files
  • Create Directory

Keep in mind that this is a large class library, responsible for a lot of functionality, so this tutorial is rather lengthy. Now into the making of your class library. As with any .Net code you write you need to add your using Directives. The using Directive allows us access to the types and methods in Namespace's without having to fully qualify the Namespace as well:

using System;
using System.IO;
using System.Net;
using System.Text;
using System.Diagnostics;
using System.Net.Sockets;
using System.Runtime.Remoting;
using System.Runtime.Remoting.Messaging;



Next we need our Global variables. Some of these are for populating our Public Properties. Variables are required for properties as properties don't allocate any storage space on their own, and to prevent the variables from being directly modified, always declare them as private:

#region Variables
//Property Variables
private string _ftpServer = string.Empty;
private string _ftpPath = ".";
private string _ftpUsername = string.Empty;
private string _ftpPassword = string.Empty;
private int _ftpPort = 21;
private bool _isLoggedIn = false;
private bool _isBinary = false;
private int _timeOut = 10;
//Static variables
private static int BUFFER_SIZE = 512;
private static Encoding ASCII = Encoding.ASCII;
//Misc Global variables
private bool _doVerbose = false;
private string statusMessage = string.Empty;
private string result = string.Empty;
private int bytes = 0;
private int _statusCode = 0;
private Byte[] buffer = new Byte[BUFFER_SIZE];
private Socket ftpSocket = null;
#endregion



Now we need to look at the Properties for our library. All our properties are Public, making them accessible from outside the class, but we have Read/Write variables, where the value is set outside our library, and we have ReadOnly properties, where the value is set from within the library itself, and retrieve from elsewhere. In this version of out library we have properties for the following:
  • Server name
  • Working directory path
  • Username
  • Password
  • Port number
  • Is the user logged in
  • Use binary mode or Ascii
  • Timeout value

In future versions I will be adding these as storable values, to keep a list of FTP Servers I connect to, and hashed values for the username/password combination of each server. Here are our properties:

#region Class Properties
#region Read/Write Properties
/// <summary>
/// Display all communications to the debug log
/// </summary>
public bool DoVerbose
{
	get { return _doVerbose; }
	set { _doVerbose = value; }
}

/// <summary>
/// FTP Server port to use, default is usually 21
/// </summary>
public int FtpPort
{
	get { return _ftpPort; }
	set { _ftpPort = value; }
}

/// <summary>
/// Timeout waiting for a response from server, in seconds.
/// </summary>
public int TimeoutValue
{
	get { return _timeOut; }
	set { _timeOut = value; }
}

/// <summary>
/// Name of the FTP server we wish to connect to
/// </summary>
/// <returns></returns>
public string FtpServer
{
	get { return _ftpServer; }
	set { _ftpServer = value; }
}

/// <summary>
/// The remote port we wish to connect through
/// </summary>
/// <returns></returns>
public int RemotePort
{
	get { return _ftpPort; }
	set { _ftpPort = value; }
}

/// <summary>
/// The working directory
/// </summary>
public string FtpPath
{
	get { return _ftpPath; }
	set { _ftpPath = value; }

}

/// <summary>
/// Server username
/// </summary>
public string FtpUsername
{
	get { return _ftpUsername; }
	set { _ftpUsername = value; }
}

/// <summary>
/// Server password
/// </summary>
public string FtpPassword
{
	get { return _ftpPassword; }
	set { _ftpPassword = value; }
}

/// <summary>
/// If the value of mode is true, set 
/// binary mode for downloads, else, Ascii mode.
/// </summary>
public bool IsBinary
{
	get { return _isBinary; }
	set
	{
		//if _isBinary already exit
		if (_isBinary == value) return;
		//check the value being passed
		//if it's true send the command
		//for binary download
		if (value)
			Execute("TYPE I");
		else
			//otherwise stay in Ascii mode
			Execute("TYPE A");
		//now check the status code, if
		//its not 200 throw an exception
		if (_statusCode != 200)
		{
			throw new FtpException(result.Substring(4));
		}
	}
}
#endregion

#region ReadOnly Properties
/// <summary>
/// determine if the user is logged in
/// </summary>
public bool IsLoggedIn
{
	get { return _isLoggedIn; }
}

/// <summary>
/// returns the status code of the command
/// </summary>
public int StatusCode
{
	get { return _statusCode; }
}
#endregion
#endregion



Notice the property StatusCode, this is a readonly property that will hold the FTP code returned from the remote server. This value is crucial when creating an FTP client. Now we move on to the methods of our class library. Before a user can do anything on an FTP Server, they first need to login to that server with the correct username and password. For our login method we create a Socket to communicate with the server through, once we have successfully connection to the host, we can execute 2 FTP commands:
  • USER: Used to send the username to the server
  • PASS: Used to send the password to the server

Then based on the returned status code we act accordingly, we either let the user know they logged on successfully, or we throw our custom exception FtpException (We'll get to that later):

#region FTPFtpLogin
/// <summary>
/// method to log in to the remote ftp server
/// </summary>
public void FtpLogin()
{
//check if the connection is currently open
if (_isLoggedIn)
{
	//its open so we need to close it
	CloseConnection();
}
//message that we're connection to the server
Debug.WriteLine("Opening connection to " + _ftpServer, "FtpClient");
//create our ip address object
IPAddress remoteAddress = null;
//create our end point object
IPEndPoint addrEndPoint = null;
try
{
	//create our ftp socket
	ftpSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
	//retrieve the server ip
	remoteAddress = Dns.GetHostEntry(_ftpServer).AddressList[0];
	//set the endpoint value
	addrEndPoint = new IPEndPoint(remoteAddress, _ftpPort);
	//connect to the ftp server
	ftpSocket.Connect(addrEndPoint);
}
catch (Exception ex)
{
	// since an error happened, we need to
	//close the connection and throw an exception
	if (ftpSocket != null && ftpSocket.Connected)
	{
		ftpSocket.Close();
	}
	throw new FtpException("Couldn't connect to remote server", ex);
}
//read the host response
readResponse();
//check for a status code of 220
if (_statusCode != 220)
{
	//failed so close the connection
	CloseConnection();
	//throw an exception
	throw new FtpException(result.Substring(4));
}
//execute the USER ftp command (sends the username)
Execute("USER " + _ftpUsername);
//check the returned status code
if (!(_statusCode == 331 || _statusCode == 230))
{
	//not what we were looking for so
	//logout and throw an exception
	LogOut();
	throw new FtpException(result.Substring(4));
}
//if the status code isnt 230
if (_statusCode != 230)
{
	//execute the PASS ftp command (sends the password)
	Execute("PASS " + _ftpPassword);
	//check the returned status code
	if (!(_statusCode == 230 || _statusCode == 202))
	{
		//not what we were looking for so
		//logout and throw an exception
		LogOut();
		throw new FtpException(result.Substring(4));
	}
}
//we made it this far so we're logged in
_isLoggedIn = true;
//verbose the login message
Debug.WriteLine("Connected to " + _ftpServer, "FtpClient");
//set the initial working directory
ChangeWorkingDirectory(_ftpPath);
}
#endregion



Next we will look at the CloseConnection method. This method is called mainly when we close our application. Here we check to see if the socket is still active, if so we execute the FTP command of QUIT which immediately terminates the connection tot he FTP server:

#region CloseConnection
/// <summary>
/// method to close the connection
/// </summary>
public void CloseConnection()
{
//display the closing message
Debug.WriteLine("Closing connection to " + _ftpServer, "FtpClient");
//check to see if the connection is still active
//if it is then execute the ftp quit command
//which terminates the connection
if (ftpSocket != null)
{
	Execute("QUIT");
}
//log the user out
LogOut();
}
#endregion



You will notice that in the first 2 methods we looked at, both called a method named Execute. The Execute method is what we will be using to send our FTP commands to the remote server. Here we convert the command string into a byte array, then use the Send Method of the Socket to send the converted command to the remote server. Once we send the command we wait for and read the host's response:

private void Execute(String msg)
{
	//check to see if verbose debugging is enabled
	//if so write the command to the window
	if (_doVerbose) Debug.WriteLine(msg,"FtpClient");
	//convert the command to a byte array
	Byte[] cmdBytes = Encoding.ASCII.GetBytes((msg + "\r\n").ToCharArray());
	//send the command to the host
	ftpSocket.Send(cmdBytes, cmdBytes.Length, 0);
	//read the returned response
	readResponse();
}
#endregion



You'll notice the method call to readResponse as the last line in the Execute method. It is in here we set the values of our _statusCode variable, along with the result and statusMessage variables. These values come from a method called ParseHostResponse. First the readResponse method:

#region readResponse
/// <summary>
/// 
/// </summary>
private void readResponse()
{
	statusMessage = "";
	result = ParseHostResponse();
	_statusCode = int.Parse(result.Substring(0,3));
}
#endregion



Now for ParseHostResponse:

#region ParseHostResponse
/// <summary>
/// Method to parse the response from the remote host
/// </summary>
/// <returns></returns>
private string ParseHostResponse()
{
while(true)
{
	//retrieve the host response and convert it to
	//a byte array
	bytes = ftpSocket.Receive(buffer,buffer.Length, 0);
	//decode the byte array and set the
	//statusMessage to its value
	statusMessage += ASCII.GetString(buffer,0,bytes);
	//check the size of the byte array
	if ( bytes < buffer.Length )
	{
		break;
	}
}
//split the host response
string[] msg = statusMessage.Split('\n');
//check the length of the response
if (statusMessage.Length > 2)
	statusMessage = msg[msg.Length - 2];
else
	statusMessage = msg[0];

//check for a space in the host response, if it exists return
//the message to the client
if (!statusMessage.Substring(3,1).Equals(" ")) return ParseHostResponse();
//check if the user selected verbose Debugging
if (_doVerbose)
{
	//loop through the message from the host
	for(int i = 0; i < msg.Length - 1; i++)
	{
		//write each line out to the window
		Debug.Write( msg[i], "FtpClient" );
	}
}
//return the message
return statusMessage;
}
#endregion



Now, when doing FTP you need to open a new socket for any transfers. To do this we use the passive approach, by letting the remote server send us the IP and port we need to connect through. We do this by sending the PASV command to the server, it then returns the IP and post in this format 227 Entering Passive Mode (192,168,8,36,8,75), so we need to parse this so we have the information for transferring the data. We do this by looping through the return message, building the IP and port number, then we attempt a connect with that information:

/// <summary>
/// when doing data transfers, we need to open another socket for it.
/// </summary>
/// <returns>Connected socket</returns>
private Socket OpenSocketForTransfer()
{
//send the PASV command (Passive command)
Execute("PASV");
//check the status code, if it
//isnt 227 (successful) then throw an exception
if (_statusCode != 227)
{
	throw new FtpException(result.Substring(4));
} 
//find the index of the opening "("
//and the closing ")". The return
//message from the server, if successful, has
//the IP and port number for the client in
//enclosed in "(" & ")"
int idx1 = result.IndexOf('(');
int idx2 = result.IndexOf(')');
//now we need to get everything in the parenthesis
string ipData = result.Substring((idx1+1),(idx2-idx1)-1);
//create new integer array with size of 6
//the returning message is in 6 segments
int[] msgSegments = new int[6];
//get the length of the message
int msgLength = ipData.Length;
int partCount = 0;
string buffer = "";
//now we need to loop through the host response
for (int i = 0; i < msgLength && partCount <= 6; i++)
{
	//convert each character to a char
	char chr = char.Parse( ipData.Substring(i,1) );
	//check to see if the current character is numeric
	if (char.IsDigit(chr))
	{
		//since its a number we add it to our buffer variable
		buffer+=chr;
	}
	//now we need to check for the
	//comma seperating the digits
	else if (chr != ',')
	{
		//no comma so throw an exception
		throw new FtpException("Malformed PASV result: " + result);
	}
	else
	{
		//check to see if the current character is a comma
		//or if the counter + 1 equals the host response length
		if (chr == ',' || i + 1 == msgLength)
		{
			try
			{
				//since its one of the 2 we add it to the
				//current index of the message segments
				msgSegments[partCount++] = int.Parse(buffer);
				buffer = "";
			}
				//handle any exceptions thrown
			catch (Exception ex)
			{
				throw new FtpException("Malformed PASV result (not supported?): " + result, ex);
			}
		}
	}	
}
//now we assemble the IP address returned from the host
string ipAddress = msgSegments[0] + "."+ msgSegments[1]+ "." + msgSegments[2] + "." + msgSegments[3];
//the last 2 segments are the port we need to use
int port = (msgSegments[4] << 8) + msgSegments[5];

Socket tranferSocket = null;
IPEndPoint ipEndPoint = null;

try
{
	//create our new socket for transfering data
	tranferSocket = new Socket(AddressFamily.InterNetwork,SocketType.Stream,ProtocolType.Tcp);
	ipEndPoint = new IPEndPoint(Dns.GetHostEntry(ipAddress).AddressList[0], port);
	tranferSocket.Connect(ipEndPoint);
}
catch(Exception ex)
{
	// doubtfull....
	if ( tranferSocket != null && tranferSocket.Connected ) tranferSocket.Close();
	//throw an FtpException
	throw new FtpException("Can't connect to remote server", ex);
}
//return the socket
return tranferSocket;
}



Now we have all the information for connecting to the remote server, for logging into the server, and for opening a data socket for the data transfer, now lets look at how we would go about uploading a file to an FTP Server. With uploading a file, we will attempt to enable resuming the download in case the connection is interrupted, but remember, all servers don't support resuming.

To do this we initially get the size of the file, then when we start the upload we look for a file with the same name, if it exists then we check its size against the file on the clients computer. If they don't match we send the REST command, which will initialize a resume, it is here when we will be informed if the FTP server supports the resume feature. If it doesn't support resume, we overwrite the file there with the one being uploaded:

#region UploadFile
/// <summary>
/// Upload a file and set the resume flag.
/// </summary>
/// <param name="fileName"></param>
/// <param name="resume"></param>
public void UploadFile(string fileName, bool resume)
{
	//make sure the user is logged in
	if (!_isLoggedIn)
	{
		//FtpLogin();
		throw new FtpException("You need to log in before you can perform this operation");
	}

	Socket dataSocket = null;
	long resumeOffset = 0;
	//if resume is true
	if (resume)
	{
		try
		{
			//set _isBinary to true
			IsBinary = true;
			//get the size of the file
			resumeOffset = GetFileSize(Path.GetFileName(fileName));
		}
		catch (Exception)
		{
			// file not exist
			resumeOffset = 0;
		}
	}

	// open stream to read file
	FileStream input = new FileStream(fileName, FileMode.Open);
	//if resume is true
	//and the size of the file read is
	//less than the initial value
	if (resume && input.Length < resumeOffset)
	{
		// different file size
		Debug.WriteLine("Overwriting " + fileName, "FtpClient");
		resumeOffset = 0;
	}
	else if (resume && input.Length == resumeOffset)
	{
		// file done
		input.Close();
		Debug.WriteLine("Skipping completed " + fileName + " - turn resume off to not detect.", "FtpClient");
		return;
	}

	//now create our socket needed for
	//the file transfer
	dataSocket = OpenSocketForTransfer();
	//if the file size is greater than 0
	if (resumeOffset > 0)
	{
		//execute the rest command, which
		//sets the point the resume will occurr
		//if the upload is interrupted
		Execute("REST " + resumeOffset);
		//check the status code, if it's not
		//350 the resume isnt supported by the server
		if (_statusCode != 350)
		{
			Debug.WriteLine("Resuming not supported", "FtpClient");
			resumeOffset = 0;
		}
	}
	//execute the store ftp command (starts the transfer of the file)
	Execute("STOR " + Path.GetFileName(fileName));
	//check the status code, we need a
	//value of 150 or 125, otherwise throw an exception
	if (_statusCode != 125 && _statusCode != 150)
	{
		throw new FtpException(result.Substring(4));
	}
	//now check the resumeOffset value,
	//if its not zero then we need to resume
	//the upload process where it ended
	if (resumeOffset != 0)
	{
		//let the user know the upload is resuming
		Debug.WriteLine("Resuming at offset " + resumeOffset, "FtpClient");
		//use the Seek method to get to where the upload ended
		input.Seek(resumeOffset, SeekOrigin.Begin);
	}
	//let the user know the uploading has begun
	Debug.WriteLine("Uploading file " + fileName + " to " + _ftpPath, "FtpClient");
	//upload the file
	while ((bytes = input.Read(buffer, 0, buffer.Length)) > 0)
	{
		dataSocket.Send(buffer, bytes, 0);
	}
	//close our reader
	input.Close();
	//check to see if the socket is still connected
	//if it is then disconnect it
	if (dataSocket.Connected)
	{
		dataSocket.Close();
	}
	//read the host's response
	readResponse();
	//checking for a successful upload code (226 or 250)
	//if not either then throw an exception
	if (_statusCode != 226 && _statusCode != 250)
	{
		throw new FtpException(result.Substring(4));
	}
}
#endregion



You will notice we reference a method named GetFileSize when we upload the file. This is a method we use to retrieve the size of the file they're uploading (we also use it for the downloading as well). Here we sent the SIZE command to the server, and if successful it returns the file size, in decimal format, otherwise we throw an exception:

#region GetFileSize
/// <summary>
/// Method to retrieve the size of the file based
/// on the name provided
/// </summary>
/// <param name="file">Name of the file to get the size of</param>
/// <returns>The files size</returns>
public long GetFileSize(string file)
{
//make sure the user is logged in
if (!_isLoggedIn)
{
	//FtpLogin();
	throw new FtpException("You need to log in before you can perform this operation");
}
//execute the size command, which
//returns the files size as a decimal number
Execute("SIZE " + file);
long fileSize = 0;
//check our returning status code
//if it's not 213 the command failed
if (_statusCode == 213)
{
	//set the file size
	fileSize = long.Parse(result.Substring(4));
}
else
{
	//command failed so throw an exception
	throw new FtpException(result.Substring(4));
}
//return the file size
return fileSize;
}
#endregion



For downloading a file, we also attempt to implement a resume, checking in the same fashion to see if the server supports this command. Here we require the name of the file on the server to download, the name you want the file on the clients computer, and a boolean resume value. Once we send the REST command, we then send the RETR command to retrieve the remote file. We then set a timeout value, and check that value in a loop, while calling the Receive Method of the Socket Class until we either reach the timeout value, of the number of bytes remaining is zero:

#region DownloadFile
/// <summary>
/// Download a remote file to a local file name which can include
/// a path, and set the resume flag. The local file name will be
/// created or overwritten, but the path must exist.
/// </summary>
/// <param name="ftpFile">File on the server to download</param>
/// <param name="localFile">Name of the file on the local machine</param>
/// <param name="resume"></param>
public void DownloadFile(string ftpFile, string localFile, Boolean resume)
{
//make sure the user is logged in
if (!_isLoggedIn)
{
	//FtpLogin();
	throw new FtpException("You need to log in before you can perform this operation");
}

IsBinary = true;
//display a downloading file message
Debug.WriteLine("Downloading file " + ftpFile + " from " + _ftpServer + "/" + _ftpPath, "FtpClient");
//check if a local file name was provided
//if not then set its value to the ftp file name
if (localFile.Equals(""))
{
	localFile = ftpFile;
}
//create our filestream object
FileStream output = null;
//check to see if the local file exists
//if it doesnt then create the file
//otherwise overwrite it
if (!File.Exists(localFile))
{
	//create the new file
	output = File.Create(localFile);
}
else
{
	//overwrite the existsing file
	output = new FileStream(localFile, FileMode.Open);
}

//create our new socket for the transfer
Socket dataSocket = OpenSocketForTransfer();
//create our resume point
long resumeOffset = 0;
//if resume was set to true
if (resume)
{
	//set the value of our resume variable
	resumeOffset = output.Length;
	//check if its value is greater than 0 (zero)
	if (resumeOffset > 0)
	{
		//execute our rest command, which sets the
		//resume point for the download in case
		//the download is interrupted
		Execute("REST " + resumeOffset);
		//check the status code, if not a 350
		//code then the server doesnt support resuming
		if (_statusCode != 350)
		{
			//Server dosnt support resuming
			resumeOffset = 0;
			Debug.WriteLine("Resuming not supported:" + result.Substring(4), "FtpClient");
		}
		else
		{
			Debug.WriteLine("Resuming at offset " + resumeOffset, "FtpClient");
			//seek to the interrupted point
			output.Seek(resumeOffset, SeekOrigin.Begin);
		}
	}
}
//execute out retr command
//which starts the file transfer
Execute("RETR " + ftpFile);
//check the status code, we need 150 or 125
//otherwise the download failed
if (_statusCode != 150 && _statusCode != 125)
{
	//throw an FtpException
	throw new FtpException(result.Substring(4));
}
//set a timeout value
DateTime timeout = DateTime.Now.AddSeconds(_timeOut);
//check the timeout value against the current time
//if its less then download the file
while (timeout > DateTime.Now)
{
	//receive the binary data from the socket
	bytes = dataSocket.Receive(buffer, buffer.Length, 0);
	//write the file
	output.Write(buffer, 0, bytes);
	//make sure the file is greater than
	//zero in size, if not exit the method
	if (bytes <= 0)
	{
		break;
	}
}
//close our stream
output.Close();
//check to see if the socket is still open,
//if it is then close it
if (dataSocket.Connected)
{
	dataSocket.Close();
}
//read the host's response
readResponse();
//we're looking for a status code of 226 or 250,
//if that isnt returned the download failed
if (_statusCode != 226 && _statusCode != 250)
{
	throw new FtpException(result.Substring(4));
}
}
#endregion



How will someone know what file(s) they want to download? Thats good question, which I have a good answer for. When you plug your library into a UI for the user, you have a method to list all the files that are currently on the FTP server. We do this by executing the NLST command. This will retrieve the name and size of all files on the server, then we simply populate a string array with that information, and use it to populate the UI for the client to view:

#region ListFiles
/// <summary>
/// Return a string array containing the remote directory's file list.
/// </summary>
/// <param name="mask"></param>
/// <returns></returns>
public string[] ListFiles(string mask)
{
//make sure the user is logged in
if (!_isLoggedIn)
{
	//FtpLogin();
	throw new FtpException("You need to log in before you can perform this operation");
}
//create new socket
Socket dataSocket = OpenSocketForTransfer();
//execute the ftp nlst command, which
//returns a list of files on the remote server
Execute("NLST " + mask);
//check the return code, we're looking for
//either 150 or 125, otherwise the command failed
if (!(_statusCode == 150 || _statusCode == 125))
{
	//failed, throw an exception
	throw new FtpException(result.Substring(4));
}
//set the message to empty
statusMessage = "";
//create a timeout value based on our timeout property
DateTime timeout = DateTime.Now.AddSeconds(_timeOut);
//loop while out timeout value is
//greater than the current time
while (timeout > DateTime.Now)
{
	//retrieve the data from the host
	int bytes = dataSocket.Receive(buffer, buffer.Length, 0);
	//convert it to Ascii format
	statusMessage += ASCII.GetString(buffer, 0, bytes);
	//exit the method is nothing is returned
	if (bytes < buffer.Length) break;
}
//throw the returned message into a string array
string[] msg = statusMessage.Replace("\r", "").Split('\n');
//close the socket connection
dataSocket.Close();
//check the return message
if (statusMessage.IndexOf("No such file or directory") != -1)
	//return an empty message
	msg = new string[] { };
//read the host's response
readResponse();
//if we didnt receive a status code of 226
//then the process failed
if (_statusCode != 226)
	//return an empty message
	msg = new string[] { };
//	throw new FtpException(result.Substring(4));

return msg;
}
#endregion



I had mentioned earlier in this tutorial that our library will offer recursive uploading to the FTP server, so lets take a look at how we offer that functionality. FOr this we require the user to provide the directory path, a boolean value for recursive, and a mask value , such as '*.*', '*.jpg*', etc. We take that directory name and check to see if the directory already exists on the server, if it doesnt we create that directory.

From there we change the working directory to the new directory, we want to be in the new directory when we start the upload, we'll look at that method next.Then we start our loop, we first upload all the files that are in that directory, then, if the user opted for recursive, we loop through all the directories in the specified directory and upload them, and their files as well, then we change the working directory back to the root directory:

#region UploadDirectory
/// <summary>
/// Upload a directory and its file contents
/// </summary>
/// <param name="dirPath">Path of the directory to upload</param>
/// <param name="recursive">Whether to recurse sub directories</param>
/// <param name="fileMask">Only upload files of the given mask(i.e;'*.*','*.jpg', ect..)</param>
public void UploadDirectory(string dirPath, bool recursive, string fileMask)
{
//make sure the user is logged in
if (!_isLoggedIn)
{
	//FtpLogin();
	throw new FtpException("You need to log in before you can perform this operation");
}
string[] directories = dirPath.Replace("/", @"\").Split('\\');
string rootDirectory = directories[directories.Length - 1];

// make the root dir if it does not exist
if (ListFiles(rootDirectory).Length < 1)
{
	CreateDirectory(rootDirectory);
}
//make the new directory the working directory
ChangeWorkingDirectory(rootDirectory);
//loop through the files in the directory
foreach (string file in Directory.GetFiles(dirPath, fileMask))
{
	//upload each file
	UploadFile(file, true);
}
//check if recusrsive was specified
if (recursive)
{
	//since recursive is true, we loop through all the
	//directories in the directory provided
	foreach (string directory in Directory.GetDirectories(dirPath))
	{
		//upload each directory
		UploadDirectory(directory, recursive, fileMask);
	}
}
//change working directory back to root level
ChangeWorkingDirectory("..");
}
#endregion



For changing the working directory for the user all we need is the name of the directory they wish to make the working directory. We then execute the [i[CWD[/i] command, which stands for, Change Working Directory. We then check for a stus code of 250, if we dont receive that code we throw an exception, if we do we then execute the PWD command (Print Working Directory) and check for a status of 257. If we dont receive that status from the server we throw an exception, otherwise we write out the message of the new directory:

#region ChangeWorkingDirectory
/// <summary>
/// Change the current working directory on the remote FTP server.
/// </summary>
/// <param name="dirName"></param>
public void ChangeWorkingDirectory(string dirName)
{
	//check to make sure a directory name was supplied
	if (dirName == null || dirName.Equals(".") || dirName.Length == 0)
	{
		//no directory was provided so throw an exception 
		//and break out of the method
		throw new FtpException("A directory name wasn't provided. Please provide one and try your request again.");
	}
	//before we can change the directory we need
	//to make sure the user is logged in
	if (!_isLoggedIn)
	{
		//FtpLogin();
		throw new FtpException("You need to log in before you can perform this operation");
	}
	//execute the CWD command = Change Working Directory
	Execute("CWD " + dirName);
	//check for a return status code of 250
	if (_statusCode != 250)
	{
		//operation failed, throw an exception
		throw new FtpException(result.Substring(4));
	}
	//execute the PWD command
	//Print Working Directory
	Execute("PWD");
	//check for a status code of 250
	if (_statusCode != 257)
	{
		//operation failed, throw an exception
		throw new FtpException(result.Substring(4));
	}
	// we made it this far so retrieve the
	//directory from the host response
	_ftpPath = statusMessage.Split('"')[1];

	Debug.WriteLine("Current directory is " + _ftpPath, "FtpClient");
}
#endregion



There are 2 more small methods I'd like to go over with you, one is for renaming a file, the other is for deleting a file. There is, of course, more in this library, but this tutorial is getting quite long as it is, so I'll be including the file with this tutorial, so you can look through the rest of the code.

For deleting a file, we send the DELE command to the remote server, then wait for a response. If we receive a status code of 250 all is good and the file was deleted, if not we throw an exception with the error message returned from the server:

#region DeleteFile
/// <summary>
/// method to delete a file from the FTP server.
/// </summary>
/// <param name="file">File to delete</param>
public void DeleteFile(string file)
{
//make sure the user is logged in
if (!_isLoggedIn)
{
	//FtpLogin();
	throw new FtpException("You need to log in before you can perform this operation");
}
//execute the delete command
Execute("DELE " + file);
//check for a status code of 250, if
//not then throw an exception
if (_statusCode != 250)
{
	throw new FtpException(result.Substring(4));
}
Debug.WriteLine("Deleted file " + file, "FtpClient");
}
#endregion



Now, to rename a file we send two commands to the server. First we send the RNFR command, which is rename from, then we send the RNTO, which is rename to. When wwe send the first command we check for a status code of 350, if successful we send the 2nd command, then look for a status of 250. If all goes well then the file is renamed, otherwise we throw an exception with the message from the FTP server:

#region RenameFile
/// <summary>
/// Rename a file on the remote FTP server.
/// </summary>
/// <param name="oldName">File to rename</param>
/// <param name="newName">New name of the file</param>
/// <param name="replace">setting to false will throw exception if it exists</param>
public void RenameFile(string oldName, string newName, bool replace)
{
	//make sure the user is logged in
	if (!_isLoggedIn)
	{
		//FtpLogin();
		throw new FtpException("You need to log in before you can perform this operation");
	}
	//execute the rename from command
	Execute("RNFR " + oldName);
	//check for a status code of 350
	if (_statusCode != 350)
	{
		throw new FtpException(result.Substring(4));
	}
	//if they didnt choose to replace the file, and a 
	//file with that name already exists then throw an exception
	if (!replace && ListFiles(newName).Length > 0)
	{
		throw new FtpException("File already exists");
	}
	//execute the rename to command
	Execute("RNTO " + newName);
	//check for a status code of 250, if
	//not then throw an exception
	if (_statusCode != 250)
	{
		throw new FtpException(result.Substring(4));
	}
	//write the successful message out
	Debug.WriteLine("Renamed file " + oldName + " to " + newName, "FtpClient");
}
#endregion



One finaly method, this method is for creating a new directory on the server. To do this we send the MKD command to the server (Make Directory), then check the status code thats returned. If its not either 250 or 257 we throw an exception containing the message from the server, otherwise the operation was successful:

#region CreateDirectory
/// <summary>
/// Create a directory on the remote FTP server.
/// </summary>
/// <param name="dirName">Name of the directory to create</param>
public void CreateDirectory(string dirName)
{
	//make sure the user is logged in
	if (!_isLoggedIn)
	{
		//FtpLogin();
		throw new FtpException("You need to log in before you can perform this operation");
	}
	//check to make sure a directory name was supplied
	if (dirName == null || dirName.Equals(".") || dirName.Length == 0)
	{
		//no directory was provided so throw an exception 
		//and break out of the method
		throw new FtpException("A directory name wasn't provided. Please provide one and try your request again.");
	}
	//execute the make directory command
	Execute("MKD " + dirName);
	//check for a status code of 250 or 257
	if (_statusCode != 250 && _statusCode != 257)
	{
		//operation failed, throw an exception
		throw new FtpException(result.Substring(4));
	}

	Debug.WriteLine("Created directory " + dirName, "FtpClient");
}
#endregion



There are a couple more methods in the library, but Ill let you look through them on your own, because I now want to show the short class file that makes our cutom FtpException we throw whever a command fails. In this class we inherit from the Exception Class, and it contains nothing but the 2 base contructors Microsoft suggests all custom exception classes have:

public class FtpException : Exception
{
	public FtpException(string message) : base(message) 
	{ 
	}
	public FtpException(string message, Exception innerException) : base(message, innerException) 
	{ 
	}
}



That's it, thats our Custom Exception class, and our FTP Class Library. Like I metioned earlier, I will be including this library with the tutorial, but it is under the GNU General Public License so the licensing information must remain in tact with the library and code at all times. So now you have a good understanding of creating your own custom FTP Class Library utilizing just the objects available in the .Net Framework 2.0.

I know this tutorial was long and indepth, but I hope you found it useful and informative, as I did when creating this library. Thank you so much for reading.

Attached File  PCFtp.zip (46.54K)
Number of downloads: 10843

Happy Coding! :)

Is This A Good Question/Topic? 2
  • +

Replies To: Create an FTP Class Library in C#

#2 born2c0de  Icon User is offline

  • printf("I'm a %XR",195936478);
  • member icon

Reputation: 180
  • View blog
  • Posts: 4,667
  • Joined: 26-November 04

Posted 23 October 2007 - 12:39 PM

I needed something like that.
Awesome Tutorial!!!
Was This Post Helpful? 0
  • +
  • -

#3 orcasquall  Icon User is offline

  • D.I.C Head
  • member icon

Reputation: 12
  • View blog
  • Posts: 158
  • Joined: 14-September 07

Posted 26 October 2007 - 06:26 AM

Wow, that is some tutorial... I remember I had to write something like that to do file sending via FTP in ASP.NET. Since there wasn't any need to write a full class, I just did the STOR command (and USER, PASS, QUIT).

Given that .NET 2.0 has built-in classes for FTP too, what are the pros and cons using the class library here and the built-in classes? For example, I used this (in another program) for sending files
FtpWebRequest ftpreq = (FtpWebRequest)WebRequest.Create("ftp://1.2.3.4/remotefile.txt");
ftpreq.Method = WebRequestMethods.Ftp.UploadFile;
ftpreq.UseBinary = true;
ftpreq.Credentials = new NetworkCredential("scott", "tiger");
// the default when logged into Windows is HTTP proxy.
// This will unset the default.
ftpreq.Proxy = null;
ftpreq.UsePassive = true;

byte[] by = File.ReadAllBytes("localfile.txt");
ftpreq.ContentLength = by.Length;

Stream rs = ftpreq.GetRequestStream();
rs.Write(by, 0, by.Length);
rs.Close();

FtpWebResponse resp = (FtpWebResponse)ftpreq.GetResponse();
resp.Close();



I've never been strong in networking stuff...
Was This Post Helpful? 0
  • +
  • -

#4 jain.rulez  Icon User is offline

  • New D.I.C Head

Reputation: 0
  • View blog
  • Posts: 1
  • Joined: 06-November 07

Posted 06 November 2007 - 03:35 AM

that was a great tutorial but i m not able to use it.
please tell me how to use it in a different program say on a button click.
Was This Post Helpful? 0
  • +
  • -

#5 MrBronz  Icon User is offline

  • New D.I.C Head

Reputation: 0
  • View blog
  • Posts: 1
  • Joined: 02-April 09

Posted 02 April 2009 - 12:51 AM

Simply amazing… Simple to follow simple to use and simple to add to it…. Thanks a million
Was This Post Helpful? 0
  • +
  • -

#6 Todilo  Icon User is offline

  • D.I.C Head

Reputation: 3
  • View blog
  • Posts: 85
  • Joined: 13-November 07

Posted 27 April 2009 - 10:52 PM

Any changes since 2.0, I mean are there new features in 3.0/3.5 which could be helpful? Just wondering out of curiosity, back to reading the tutorial.

And what is the advantage of not using the standard functions? The socket handling seems easier on msdn library. Really not an export, just a beginner but I am really interested in find out.

This post has been edited by Todilo: 28 April 2009 - 04:15 AM

Was This Post Helpful? 0
  • +
  • -

#7 jhewgley  Icon User is offline

  • New D.I.C Head

Reputation: 0
  • View blog
  • Posts: 1
  • Joined: 06-July 09

Posted 06 July 2009 - 12:12 PM

Thanks for what seems to be a great class with full FTP functionality. Unfortunately, the zip file seems to be corrupted so I can't open it after downloading.

This post has been edited by jhewgley: 06 July 2009 - 12:13 PM

Was This Post Helpful? 0
  • +
  • -

#8 el3ashe2  Icon User is offline

  • New D.I.C Head

Reputation: 0
  • View blog
  • Posts: 6
  • Joined: 24-February 08

Posted 13 August 2009 - 01:16 AM

WoW
nice Tut
but i want to ask you if there is any way to do add new Web Drive Connected to FTp??

i tried a lot but failed in encrypt password
but in vb.net
any help??
Was This Post Helpful? 0
  • +
  • -

#9 bsjia8241  Icon User is offline

  • New D.I.C Head

Reputation: 0
  • View blog
  • Posts: 2
  • Joined: 10-September 09

Posted 10 September 2009 - 08:23 AM

It's very good, but I found a method "LogOut" is not defined in this code, Could you code it?
Was This Post Helpful? 0
  • +
  • -

#10 PsychoCoder  Icon User is offline

  • Google.Sucks.Init(true);
  • member icon

Reputation: 1642
  • View blog
  • Posts: 19,853
  • Joined: 26-July 07

Posted 10 September 2009 - 08:35 AM

@bsjia8241: That method is in the download. Here's the code for it

/// <summary>
/// method to release and remove any sockets left open
/// </summary>
private void LogOut()
{
	//check to see if the sock is non existant
	if (ftpSocket != null)
	{
		//since its not we need to
		//close it and dispose of it
		ftpSocket.Close();
		ftpSocket = null;
	}
	//log the user out
	_isLoggedIn = false;
}


Was This Post Helpful? 0
  • +
  • -

#11 bsjia8241  Icon User is offline

  • New D.I.C Head

Reputation: 0
  • View blog
  • Posts: 2
  • Joined: 10-September 09

Posted 10 September 2009 - 08:28 PM

Thanks!!!!!
Was This Post Helpful? 0
  • +
  • -

#12 whiteyk1976  Icon User is offline

  • New D.I.C Head

Reputation: 0
  • View blog
  • Posts: 1
  • Joined: 30-November 09

Posted 30 November 2009 - 03:10 PM

When I try to use this class, I get the following Exception message
"No connection could be made because the target machine actively refused it ..." in OpenSocketForTransfer(). How do I resolve this?
Was This Post Helpful? 0
  • +
  • -

#13 abate  Icon User is offline

  • New D.I.C Head

Reputation: 0
  • View blog
  • Posts: 1
  • Joined: 06-January 10

Posted 06 January 2010 - 01:33 AM

Nice code.

And how can i use FTP in ACTIVE MODE ???

Was This Post Helpful? 0
  • +
  • -

#14 pareidolia  Icon User is offline

  • New D.I.C Head

Reputation: 3
  • View blog
  • Posts: 38
  • Joined: 17-September 09

Posted 11 January 2010 - 03:02 AM

I think some other people were having an issue with the OpenSocketForTransfer method. I had someone monitor the FTP server's open ports and I added in some output to see what port my client was trying to connect to, it was always slightly less. So I added some more output to see how the client was calculating the port. It's supposed to be (p1 * 256) + p2 but for some reason p2 was always 0. After looking at the program for a bit I realized why this was happening.

The loop that loads the buffer into the different sections of the array has three paths, if for numbers being loaded into the buffer, else if for non-commas and else which was for commas and the end of the response message so that the buffer loads into the array. The problem is that to reach the else statement the last character has to be a comma since anything else would be caught by the if or the else if which means the final segment will not get loaded into the array.

So, all you need to do is take the else portion out of the existing code leaving the inner if statement intact.
else
{
		if (chr == ',' || i + 1 == msgLength)
		{
				try
				{
						msgSegments[partCount++] = int.Parse(buffer);
						buffer = "";
				}
				catch (Exception ex)
				{
						throw new FtpException("Malformed PASV result (not supported?): " + result, ex);
				}
		}
}



Great tutorial, by the way. I've learned a lot from it.
Was This Post Helpful? 0
  • +
  • -

#15 Guest_Brian*


Reputation:

Posted 08 March 2010 - 11:53 PM

Does anyone know why my program would be getting stuck at




in the parseHostResponse method after uploading a big file? I watched the FTP server and its saying "226 File received OK" but the code doesn't seem to be receiving it. Also oddly enough it seems to only happen on windows 2000. I've been racking my brain for about a week now and can't figure it out. Any ideas?
Thanks
Was This Post Helpful? 0

  • (2 Pages)
  • +
  • 1
  • 2