This article aims at explaining recursive ftp deletes without using the concept of recursion. As far as I know there is no way of deleting the ftp folder recursively using inbuilt .NET API. However, I made use of the available FTP API that ship with .NET 2.0 in accomplishing my mission.
Introduction
This article aims at explaining the recursive ftp deletes without using the concept of recursion. As far as I know there is no way of deleting the ftp folder recursively using inbuilt .NET API. However, I made use of the available FTP API that ship with .NET 2.0 in accomplishing my mission.
The following helper classes will help us delete a folder hosted on an ftp site. Remember a folder can be deleted from an FTP Site only when
- The user accessing the ftp site has proper credentials.
- The target directory/folder should be empty. Meaning no sub directories/folders or files should be available in the target folder/directory.
So my little helper classes here delete all the files and directories in the hierarchy starting from the root directory/folder. Since, this is a helper class; all exception handling has been left to the user. This way the user is at will on how one handles exceptions and which most of the times depend on the business.
Also, coding is self explanatory, because the method names it self specify the task they perform.
A closer look at the method, public bool DeleteDirectoryHierarcy(string dirPath)
This method uses a self-modifying collection. A self modifying collection has a set of values. Basing on the available values one or more values will be added to the same collection. So, the for loop employs a classic index style(for()) rather than the latest enumeration style (foreach()).
Details:
If the directory hierarchy of some arbitrary server path, ftp://testftpserver/testfolders/testfolder was the following:
testfolder
|- TestFolder1
|- TestFolder2
|-TestFolder3
|- Testfile.txt
|- TestFolder4
The self modifying collection in the method being described is, DirectoriesList.
Before the first for loop starts this has been initialized to the root folder, ftp://testftpserver/testfolders/testfolder.
Now, for each of the values, the for loop is executed. Now that we have only one entry it would loop once. But, the logic inside the for loop adds all the sub directories under the current entry to the collection. So, the collection will have the following values after the first iteration.
ftp://testftpserver/testfolders/testfolder
ftp://testftpserver/testfolders/testfolder/TestFolder1
ftp://testftpserver/testfolders/testfolder/TestFolder2
ftp://testftpserver/testfolders/testfolder/TestFolder4
So, it started with one entry and within one iteration it had appended three more entries.
Now in the second iteration it will add all the sub-directories under ftp://testftpserver/testfolders/testfolder/TestFolder1. Since there are no sub-directories under TestFolder1, nothing will be added.
Now in the third iteration, it will add all the sub-directories under ftp://testftpserver/testfolders/testfolder/TestFolder2.
So, the collection will now have,
ftp://testftpserver/testfolders/testfolder
ftp://testftpserver/testfolders/testfolder/TestFolder1
ftp://testftpserver/testfolders/testfolder/TestFolder2
ftp://testftpserver/testfolders/testfolder/TestFolder4
ftp://testftpserver/testfolders/testfolder/TestFolder2/TestFolder3
This will continue until it scans each and every folder starting from the root folder, which is ftp://testftpserver/testfolders/testfolder. In each scan it would ensure that any files under the directory being scanned were deleted.
Coding:
public class FtpListing
{
#region Constructor
public FtpListing()
{
}
#endregion
# region Private Members
private string _fileName = string.Empty; // Represents the filename without extension
private string _fileExtension = string.Empty; // Represents the file extension
private string _path = string.Empty; // Represents the complete path
private DirectoryEntryType _fileType; // Represents if the current listing represents a file/directory.
private long _size; // Represents the size.
private DateTime _fileDateTime; // DateTime of file/Directory
private string _permissions; // Permissions on the directory
private string _fullName; // Represents FileName with extension
IFormatProvider culture = System.Globalization.CultureInfo.InvariantCulture; //Eliminate DateTime parsing issues.
# endregion
# region Public Properties
public string FileName
{
get { return _fileName; }
set
{
_fileName = value;
// Set the FileExtension.
if (_fileName.LastIndexOf(".") > -1)
{
FileExtension = _fileName.Substring(_fileName.LastIndexOf(".") + 1);
}
}
}
public string FileExtension
{
get { return _fileExtension; }
set { _fileExtension = value; }
}
public string FullName
{
get { return _fullName; }
set { _fullName = value; }
}
public string Path
{
get { return _path; }
set { _path = value; }
}
internal DirectoryEntryType FileType;
public long Size
{
get { return _size; }
set { _size = value; }
}
public DateTime FileDateTime
{
get { return _fileDateTime; }
set { _fileDateTime = value; }
}
public string Permissions
{
get { return _permissions; }
set { _permissions = value; }
}
public string NameOnly
{
get
{
int i = this.FileName.LastIndexOf(".");
if (i > 0)
return this.FileName.Substring(0, i);
else
return this.FileName;
}
}
public enum DirectoryEntryType
{
File,
Directory
}
# endregion
# region Regular expressions for parsing List results
/// <summary>
/// List of REGEX formats for different FTP server listing formats
/// The first three are various UNIX/LINUX formats,
/// fourth is for MS FTP in detailed mode and the last for MS FTP in 'DOS' mode.
/// </summary>
// These regular expressions will be used to match a directory/file
// listing as explained at the top of this class.
internal string[] _ParseFormats =
{
@"(?<dir>[\-d])(?<permission>([\-r][\-w][\-xs]){3})\s+\d+\s+\w+\s+\w+\s+(?<size>\d+)\s+(?<timestamp>\w+\s+\d+\s+\d{4})\s+(?<name>.+)",
@"(?<dir>[\-d])(?<permission>([\-r][\-w][\-xs]){3})\s+\d+\s+\d+\s+(?<size>\d+)\s+(?<timestamp>\w+\s+\d+\s+\d{4})\s+(?<name>.+)",
@"(?<dir>[\-d])(?<permission>([\-r][\-w][\-xs]){3})\s+\d+\s+\d+\s+(?<size>\d+)\s+(?<timestamp>\w+\s+\d+\s+\d{1,2}:\d{2})\s+(?<name>.+)",
@"(?<dir>[\-d])(?<permission>([\-r][\-w][\-xs]){3})\s+\d+\s+\w+\s+\w+\s+(?<size>\d+)\s+(?<timestamp>\w+\s+\d+\s+\d{1,2}:\d{2})\s+(?<name>.+)",
@"(?<dir>[\-d])(?<permission>([\-r][\-w][\-xs]){3})(\s+)(?<size>(\d+))(\s+)(?<ctbit>(\w+\s\w+))(\s+)(?<size2>(\d+))\s+(?<timestamp>\w+\s+\d+\s+\d{2}:\d{2})\s+(?<name>.+)",
@"(?<timestamp>\d{2}\-\d{2}\-\d{2}\s+\d{2}:\d{2}[Aa|Pp][mM])\s+(?<dir>\<\w+\>){0,1}(?<size>\d+){0,1}\s+(?<name>.+)",
@"([<timestamp>]*\d{2}\-\d{2}\-\d{2}\s+\d{2}:\d{2}[Aa|Pp][mM])\s+([<dir>]*\<\w+\>){0,1}([<size>]*\d+){0,1}\s+([<name>]*.+)"
};
# endregion
# region Private Functions
/// <summary>
/// Depending on the various directory listing formats,
/// the current listing will be matched against the set of available matches.
/// </summary>
/// <param name="line"></param>
/// <returns></returns>
// This method evaluates the directory/file listing by applying
// each of the regular expression defined by the string array, _ParseFormats and returns success on a successful match.
private Match GetMatchingRegex(string line)
{
Regex regExpression;
Match match;
int counter;
for (counter = 0; counter < _ParseFormats.Length - 1; counter++)
{
regExpression = new Regex(_ParseFormats[counter], RegexOptions.IgnoreCase);
match = regExpression.Match(line);
if (match.Success)
return match;
}
return null;
}
# endregion
# region Public Functions
/// <summary>
/// The method accepts a directory listing and initialises all the attributes of a file.
/// </summary>
/// <param name="line">Directory Listing line returned by the DetailedDirectoryList method</param>
/// <param name="path">The path of the Directory</param>
// This method populates the needful properties such as filename,path etc.
public void GetFtpFileInfo(string line, string path)
{
string directory;
Match match = GetMatchingRegex(line); //Get the match of the current listing.
if (match == null)
throw new Exception("Unable to parse the line " + line);
else
{
_fileName = match.Groups["name"].Value; //Set the name of the file/directory.
_path = path; // Set the path from which the listing needs to be obtained.
_permissions = match.Groups["permission"].Value; // Set the permissions available for the listing
directory = match.Groups["dir"].Value;
//Set the filetype to either Directory or File basing on the listing.
if (!string.IsNullOrEmpty(directory) && directory != "-")
_fileType = DirectoryEntryType.Directory;
else
{
_fileType = DirectoryEntryType.File;
_size = long.Parse(match.Groups["size"].Value, culture);
}
try
{
_fileDateTime = DateTime.Parse(match.Groups["timestamp"].Value, culture); // Set the datetime of the listing.
}
catch
{
_fileDateTime = DateTime.Now;
}
}
// Initialize the readonly properties.
FileName = _fileName;
Path = _path;
FileType = _fileType;
FullName = Path + FileName;
Size = _size;
FileDateTime = _fileDateTime;
Permissions = _permissions;
}
# endregion
}
public class DeleteFTPDirectory
{
public DeleteFTPDirectory()
{
}
// Set the credentials here to access the target FTP Site.
// For ananymous access, leave it blank.
public ICredentials GetCredentials()
{
return new NetworkCredential("", "");
}
//Send the command/request to the target FTP location identified by the parameter, URI
public FtpWebRequest GetRequest(string URI)
{
FtpWebRequest result = ((FtpWebRequest)(FtpWebRequest.Create(URI)));
result.Credentials = GetCredentials();
result.KeepAlive = false;
result.EnableSsl = false;
result.UsePassive = false;
result.ReadWriteTimeout = 100;
return result;
}
//Deletes the target FTP file identified by the parameter, filePath
public bool DeleteFile(string filePath)
{
string URI = (filePath.TrimEnd());
FtpWebRequest ftp = GetRequest(URI);
ftp.Credentials = GetCredentials();
ftp.Method = WebRequestMethods.Ftp.DeleteFile;
string str = GetStringResponse(ftp);
return true;
}
//The master method that deletes the entire directory hierarchy. Root directory is identified by the parameter, dirPath
// Remember: if the path was ftp://testserver/testfolder/testorder then all the folders inside the testorder folder will be deleted including testorder folder.
public bool DeleteDirectoryHierarcy(string dirPath)
{
FtpListing fileObj = new FtpListing(); //Create the FTPListing object
ArrayList DirectoriesList = new ArrayList(); //Create a collection to hold list of directories within the root and including the root directory.
DirectoriesList.Add(dirPath); //Add the root folder to the collection
string currentDirectory = string.Empty;
//For each of the directories in the DirectoriesListCollection, obtain the
// sub directories. Add them to the collection.Repeat the process until every path
// was traversed. For example consider the following hierarchy.
// Ex: ftp://testftpserver/testfolders/testfolder
// Sample Hierarchy of testfolder
// testfolder
// - testfolder1
// - testfolder2
// - testfolder3
// - testfile.txt
// - testfolder4
// DirectorieList is a self modifying collection. Meaning it loops through itself and adds directories to itself.
// DirectoriesList will have the following entries basing on the above hierarchy by the time the first for loop gets complete executed.
//ftp://testftpserver/testfolders/testfolder
//ftp://testftpserver/testfolders/testfolder/testfolder1
//ftp://testftpserver/testfolders/testfolder/testfolder2
//ftp://testftpserver/testfolders/testfolder/testfolder4
//ftp://testftpserver/testfolders/testfolder/testfolder1/testfolder3
//At the end of the for loop the DirectorLists is traversed bottom up
//Meaning deletion of folders will be from the last entry towards to first,
//which would ensure that all the subdirectories are deleted before the root folder is going to be deleted.
for (int directoryCount = 0; directoryCount < DirectoriesList.Count; directoryCount++)
{
currentDirectory = DirectoriesList[directoryCount].ToString() + "/";
if (currentDirectory.StartsWith("ftp://", StringComparison.OrdinalIgnoreCase) == false)
{
currentDirectory = string.Concat("ftp://", currentDirectory);
}
string[] directoryInfo = GetDirectoryList(currentDirectory);
for (int counter = 0; counter < directoryInfo.Length; counter++)
{
string currentFileOrDir = directoryInfo[counter];
if (currentFileOrDir.Length < 1) // If all entries were scanned then break
{
break;
}
currentFileOrDir = currentFileOrDir.Replace("\r", "");
fileObj.GetFtpFileInfo(currentFileOrDir, currentDirectory);
if (fileObj.FileType == FtpListing.DirectoryEntryType.Directory)
{
DirectoriesList.Add(fileObj.FullName); //If Directory add to the collection.
}
else if (fileObj.FileType == FtpListing.DirectoryEntryType.File)
{
DeleteFile(fileObj.FullName); //If file,then delete.
}
}
}
//Remove the directories in the collection from bottom toward top.
//This would ensure that all the sub directories were deleted first before deleting the root folder.
for (int count = DirectoriesList.Count; count > 0; count--)
{
RemoveDirectory(DirectoriesList[count - 1].ToString());
}
return true;
}
//Deletes one target directory at a time, identified by the parameter, dirPath
public bool RemoveDirectory(string dirPath)
{
dirPath = dirPath.Trim(new char[] { '/' }); //Trim the path.
FtpWebRequest ftp = GetRequest(dirPath); //Prepare the request.
ftp.Credentials = GetCredentials(); //Set the Credentials to access the ftp target.
ftp.Method = WebRequestMethods.Ftp.RemoveDirectory; //set request method, RemoveDirectory
string str = GetStringResponse(ftp); // Fire the command to remove directory.
return true;
}
//Gets the directory listing given the path
public string[] GetDirectoryList(string path)
{
string[] result = null;
FtpWebRequest ftpReq = GetRequest(path.TrimEnd()); //Create the request
ftpReq.Method = WebRequestMethods.Ftp.ListDirectoryDetails; //Set the request method
ftpReq.Credentials = GetCredentials(); //Set the credentials
FtpWebResponse ftpResp = (FtpWebResponse)ftpReq.GetResponse();//Fire the command
Stream ftpResponseStream = ftpResp.GetResponseStream(); //Get the output
StreamReader reader = new StreamReader(ftpResponseStream, System.Text.Encoding.UTF8);//Encode the output to UTF8 format
result = (reader.ReadToEnd().Split('\n')); //Split the output for newline characters.
ftpResp.Close(); //Close the response object.
return result; // return the output
}
//Gets the response for a given request. Meaning executes the command identified
// by FtpWebRequest and outputs the response/output.
public string GetStringResponse(FtpWebRequest ftpRequest)
{
//Get the result, streaming to a string
string result = "";
using (FtpWebResponse response = ((FtpWebResponse)(ftpRequest.GetResponse()))) //Get the response for the request identified by the parameter ftpRequest
{
using (Stream datastream = response.GetResponseStream())
{
using (StreamReader sr = new StreamReader(datastream))
{
result = sr.ReadToEnd();
sr.Close();
}
datastream.Close();
}
response.Close();
}
return result;
}
}
Using the code
using RecursiveFTPFolderDeletes;
private void button1_Click(object sender, EventArgs e)
{
try
{
RecursiveFTPFolderDeletes.DeleteFTPDirectory myObj = new DeleteFTPDirectory();
myObj.DeleteDirectoryHierarcy("ftp://<YourServer>/<folder path>");
}
catch (Exception exp)
{
//your exception handling goes here.
}
}
Conclusion
This is one nice little way of deleting folders recursively from an FTP location. Any suggestions or more sophisticated approach is always welcome. :) Happy programming :)