using System;
using System.Collections.Generic;
using System.Text;
using System.Text.RegularExpressions;
using System.Globalization;
using System.Threading;
namespace FluentFTP {
///
/// Represents a file system object on the server
///
///
public class FtpListItem {
FtpFileSystemObjectType m_type = 0;
///
/// Gets the type of file system object. This property can be
/// set however this functionality is intended to be done by
/// custom parsers.
///
public FtpFileSystemObjectType Type {
get {
return m_type;
}
set {
m_type = value;
}
}
string m_path = null;
///
/// Gets the full path name to the object. This property can be
/// set however this functionality is intended to be done by
/// custom parsers.
///
public string FullName {
get {
return m_path;
}
set {
m_path = value;
}
}
string m_name = null;
///
/// Gets the name of the object. This property can be
/// set however this functionality is intended to be done by
/// custom parsers.
///
public string Name {
get {
if (m_name == null && m_path != null)
return m_path.GetFtpFileName();
return m_name;
}
set {
m_name = value;
}
}
string m_linkTarget = null;
///
/// Gets the target a symbolic link points to. This property can be
/// set however this functionality is intended to be done by
/// custom parsers.
///
public string LinkTarget {
get {
return m_linkTarget;
}
set {
m_linkTarget = value;
}
}
FtpListItem m_linkObject = null;
///
/// Gets the object the LinkTarget points to. This property is null unless pass the
/// FtpListOption.DerefLink flag in which case GetListing() will try to resolve
/// the target itself.
///
public FtpListItem LinkObject {
get {
return m_linkObject;
}
set {
m_linkObject = value;
}
}
DateTime m_modified = DateTime.MinValue;
///
/// Gets the last write time of the object. This property can be
/// set however this functionality is intended to be done by
/// custom parsers.
///
public DateTime Modified {
get {
return m_modified;
}
set {
m_modified = value;
}
}
DateTime m_created = DateTime.MinValue;
///
/// Gets the created date of the object. This property can be
/// set however this functionality is intended to be done by
/// custom parsers.
///
public DateTime Created {
get {
return m_created;
}
set {
m_created = value;
}
}
long m_size = -1;
///
/// Gets the size of the object. This property can be
/// set however this functionality is intended to be done by
/// custom parsers.
///
public long Size {
get {
return m_size;
}
set {
m_size = value;
}
}
FtpSpecialPermissions m_specialPermissions = FtpSpecialPermissions.None;
///
/// Gets special UNIX permissions such as Stiky, SUID and SGID. This property can be
/// set however this functionality is intended to be done by
/// custom parsers.
///
public FtpSpecialPermissions SpecialPermissions {
get {
return m_specialPermissions;
}
set {
m_specialPermissions = value;
}
}
FtpPermission m_ownerPermissions = FtpPermission.None;
///
/// Gets the owner permissions. This property can be
/// set however this functionality is intended to be done by
/// custom parsers.
///
public FtpPermission OwnerPermissions {
get {
return m_ownerPermissions;
}
set {
m_ownerPermissions = value;
}
}
FtpPermission m_groupPermissions = FtpPermission.None;
///
/// Gets the group permissions. This property can be
/// set however this functionality is intended to be done by
/// custom parsers.
///
public FtpPermission GroupPermissions {
get {
return m_groupPermissions;
}
set {
m_groupPermissions = value;
}
}
FtpPermission m_otherPermissions = FtpPermission.None;
///
/// Gets the others permissions. This property can be
/// set however this functionality is intended to be done by
/// custom parsers.
///
public FtpPermission OthersPermissions {
get {
return m_otherPermissions;
}
set {
m_otherPermissions = value;
}
}
int m_chmod = 0;
///
/// Gets the file permissions in the CHMOD format.
///
public int Chmod {
get {
return m_chmod;
}
set {
m_chmod = value;
}
}
string m_input = null;
///
/// Gets the input string that was parsed to generate the
/// values in this object. This property can be
/// set however this functionality is intended to be done by
/// custom parsers.
///
public string Input {
get {
return m_input;
}
private set {
m_input = value;
}
}
///
/// Returns a string representation of this object and its properties
///
/// A string value
public override string ToString() {
StringBuilder sb = new StringBuilder();
if (Type == FtpFileSystemObjectType.File) {
sb.Append("FILE");
} else if (Type == FtpFileSystemObjectType.Directory) {
sb.Append("DIR ");
} else if (Type == FtpFileSystemObjectType.Link) {
sb.Append("LINK");
}
sb.Append(" ");
sb.Append(Name);
if (Type == FtpFileSystemObjectType.File) {
sb.Append(" ");
sb.Append("(");
sb.Append(Size.FileSizeToString());
sb.Append(")");
}
if (Created != null && Created != DateTime.MinValue) {
sb.Append(" ");
sb.Append("Created : ");
sb.Append(Created.ToString());
}
if (Modified != null && Modified != DateTime.MinValue) {
sb.Append(" ");
sb.Append("Modified : ");
sb.Append(Modified.ToString());
}
return sb.ToString();
}
///
/// Parses a line from a file listing using the first successful match in the Parsers collection.
///
/// The source path of the file listing
/// A line from the file listing
/// Server capabilities
/// A FtpListItem object representing the parsed line, null if the line was
/// unable to be parsed. If you have encountered an unsupported list type add a parser
/// to the public static Parsers collection of FtpListItem.
public static FtpListItem Parse(string path, string buf, FtpCapability capabilities) {
if (buf != null && buf.Length > 0) {
FtpListItem item;
foreach (Parser parser in Parsers) {
if ((item = parser(buf, capabilities)) != null) {
// if this is a vax/openvms file listing
// there are no slashes in the path name
if (parser == (new Parser(ParseVaxList)))
item.FullName = path + item.Name;
else {
FtpTrace.WriteLine(item.Name);
// remove globbing/wildcard from path
if (path.GetFtpFileName().Contains("*")) {
path = path.GetFtpDirectoryName();
}
if (item.Name != null) {
// absolute path? then ignore the path input to this method.
if (item.Name.StartsWith("/") || item.Name.StartsWith("./") || item.Name.StartsWith("../")) {
item.FullName = item.Name;
item.Name = item.Name.GetFtpFileName();
} else if (path != null) {
item.FullName = path.GetFtpPath(item.Name); //.GetFtpPathWithoutGlob();
} else {
FtpTrace.WriteLine("Couldn't determine the full path of this object:{0}{1}",
Environment.NewLine, item.ToString());
}
}
// if a link target is set and it doesn't include an absolute path
// then try to resolve it.
if (item.LinkTarget != null && !item.LinkTarget.StartsWith("/")) {
if (item.LinkTarget.StartsWith("./"))
item.LinkTarget = path.GetFtpPath(item.LinkTarget.Remove(0, 2));
else
item.LinkTarget = path.GetFtpPath(item.LinkTarget);
}
}
item.Input = buf;
return item;
}
}
}
return null;
}
///
/// Used for synchronizing access to the Parsers collection
///
static Object m_parserLock = new Object();
///
/// Initalizes the default list of parsers
///
static void InitParsers() {
lock (m_parserLock) {
if (m_parsers == null) {
m_parsers = new List();
m_parsers.Add(new Parser(ParseMachineList));
m_parsers.Add(new Parser(ParseUnixList));
m_parsers.Add(new Parser(ParseDosList));
m_parsers.Add(new Parser(ParseVaxList));
}
}
}
static List m_parsers = null;
///
/// Collection of parsers. Each parser object contains
/// a regex string that uses named groups, i.e., (?<group_name>foobar).
/// The support group names are modify for last write time, size for the
/// size and name for the name of the file system object. Each group name is
/// optional, if they are present then those values are retrieved from a
/// successful match. In addition, each parser contains a Type property
/// which gets set in the FtpListItem object to distinguish between different
/// types of objects.
///
static Parser[] Parsers {
get {
Parser[] parsers;
lock (m_parserLock) {
if (m_parsers == null)
InitParsers();
parsers = m_parsers.ToArray();
}
return parsers;
}
}
///
/// Adds a custom parser
///
/// The parser delegate to add
///
public static void AddParser(Parser parser) {
lock (m_parserLock) {
if (m_parsers == null)
InitParsers();
m_parsers.Add(parser);
}
}
///
/// Removes all parser delegates
///
public static void ClearParsers() {
lock (m_parserLock) {
if (m_parsers == null)
InitParsers();
m_parsers.Clear();
}
}
///
/// Removes the specified parser
///
/// The parser delegate to remove
public static void RemoveParser(Parser parser) {
lock (m_parserLock) {
if (m_parsers == null)
InitParsers();
m_parsers.Remove(parser);
}
}
///
/// Parses MLS* format listings
///
/// A line from the listing
/// Server capabilities
/// FtpListItem if the item is able to be parsed
static FtpListItem ParseMachineList(string buf, FtpCapability capabilities) {
FtpListItem item = new FtpListItem();
Match m;
if (!(m = Regex.Match(buf, "type=(?.+?);", RegexOptions.IgnoreCase)).Success)
return null;
switch (m.Groups["type"].Value.ToLower()) {
case "dir":
case "pdir":
case "cdir":
item.Type = FtpFileSystemObjectType.Directory;
break;
case "file":
item.Type = FtpFileSystemObjectType.File;
break;
// These are not supported for now.
case "link":
case "device":
default:
return null;
}
if ((m = Regex.Match(buf, "; (?.*)$", RegexOptions.IgnoreCase)).Success)
item.Name = m.Groups["name"].Value;
else // if we can't parse the file name there is a problem.
return null;
if ((m = Regex.Match(buf, "modify=(?.+?);", RegexOptions.IgnoreCase)).Success)
item.Modified = m.Groups["modify"].Value.GetFtpDate(DateTimeStyles.AssumeUniversal);
if ((m = Regex.Match(buf, "created?=(?.+?);", RegexOptions.IgnoreCase)).Success)
item.Created = m.Groups["create"].Value.GetFtpDate(DateTimeStyles.AssumeUniversal);
if ((m = Regex.Match(buf, @"size=(?\d+);", RegexOptions.IgnoreCase)).Success) {
long size;
if (long.TryParse(m.Groups["size"].Value, out size))
item.Size = size;
}
if ((m = Regex.Match(buf, @"unix.mode=(?\d+);", RegexOptions.IgnoreCase)).Success) {
if (m.Groups["mode"].Value.Length == 4) {
item.SpecialPermissions = (FtpSpecialPermissions)int.Parse(m.Groups["mode"].Value[0].ToString());
item.OwnerPermissions = (FtpPermission)int.Parse(m.Groups["mode"].Value[1].ToString());
item.GroupPermissions = (FtpPermission)int.Parse(m.Groups["mode"].Value[2].ToString());
item.OthersPermissions = (FtpPermission)int.Parse(m.Groups["mode"].Value[3].ToString());
CalcChmod(item);
} else if (m.Groups["mode"].Value.Length == 3) {
item.OwnerPermissions = (FtpPermission)int.Parse(m.Groups["mode"].Value[0].ToString());
item.GroupPermissions = (FtpPermission)int.Parse(m.Groups["mode"].Value[1].ToString());
item.OthersPermissions = (FtpPermission)int.Parse(m.Groups["mode"].Value[2].ToString());
CalcChmod(item);
}
}
return item;
}
///
/// Parses LIST format listings
///
/// A line from the listing
/// Server capabilities
/// FtpListItem if the item is able to be parsed
static FtpListItem ParseUnixList(string buf, FtpCapability capabilities) {
string regex =
@"(?.+)\s+" +
@"(?\d+)\s+" +
@"(?.+)\s+" +
@"(?.+)\s+" +
@"(?\d+)\s+" +
@"(?\w+\s+\d+\s+\d+:\d+|\w+\s+\d+\s+\d+)\s" +
@"(?.*)$";
FtpListItem item = new FtpListItem();
Match m;
if (!(m = Regex.Match(buf, regex, RegexOptions.IgnoreCase)).Success)
return null;
// if this field is missing we can't determine
// what the object is.
if (m.Groups["permissions"].Value.Length == 0)
return null;
switch (m.Groups["permissions"].Value[0]) {
case 'd':
item.Type = FtpFileSystemObjectType.Directory;
break;
case '-':
case 's':
item.Type = FtpFileSystemObjectType.File;
break;
case 'l':
item.Type = FtpFileSystemObjectType.Link;
break;
default:
return null;
}
// if we can't determine a file name then
// we are not considering this a successful parsing operation.
if (m.Groups["name"].Value.Length < 1)
return null;
item.Name = m.Groups["name"].Value;
switch (item.Type) {
case FtpFileSystemObjectType.Directory:
// ignore these...
if (item.Name == "." || item.Name == "..")
return null;
break;
case FtpFileSystemObjectType.Link:
if (!item.Name.Contains(" -> "))
return null;
item.LinkTarget = item.Name.Remove(0, item.Name.IndexOf("-> ") + 3);
item.Name = item.Name.Remove(item.Name.IndexOf(" -> "));
break;
}
// for date parser testing only
//capabilities = ~(capabilities & FtpCapability.MDTM);
////
// Ignore the Modify times sent in LIST format for files
// when the server has support for the MDTM command
// because they will never be as accurate as what can be had
// by using the MDTM command. MDTM does not work on directories
// so if a modify time was parsed from the listing we will try
// to convert it to a DateTime object and use it for directories.
////
if (((capabilities & FtpCapability.MDTM) != FtpCapability.MDTM || item.Type == FtpFileSystemObjectType.Directory) && m.Groups["modify"].Value.Length > 0) {
item.Modified = m.Groups["modify"].Value.GetFtpDate(DateTimeStyles.AssumeLocal);
if (item.Modified == DateTime.MinValue) {
FtpTrace.WriteLine("GetFtpDate() failed on {0}", m.Groups["modify"].Value);
}
} else {
if (m.Groups["modify"].Value.Length == 0)
FtpTrace.WriteLine("RegEx failed to parse modified date from {0}.", buf);
else if (item.Type == FtpFileSystemObjectType.Directory)
FtpTrace.WriteLine("Modified times of directories are ignored in UNIX long listings.");
else if ((capabilities & FtpCapability.MDTM) == FtpCapability.MDTM)
FtpTrace.WriteLine("Ignoring modified date because MDTM feature is present. If you aren't already, pass FtpListOption.Modify or FtpListOption.SizeModify to GetListing() to retrieve the modification time.");
}
if (m.Groups["size"].Value.Length > 0) {
long size;
if (long.TryParse(m.Groups["size"].Value, out size))
item.Size = size;
}
if (m.Groups["permissions"].Value.Length > 0) {
Match perms = Regex.Match(m.Groups["permissions"].Value,
@"[\w-]{1}(?[\w-]{3})(?[\w-]{3})(?[\w-]{3})",
RegexOptions.IgnoreCase);
if (perms.Success) {
if (perms.Groups["owner"].Value.Length == 3) {
if (perms.Groups["owner"].Value[0] == 'r') {
item.OwnerPermissions |= FtpPermission.Read;
}
if (perms.Groups["owner"].Value[1] == 'w') {
item.OwnerPermissions |= FtpPermission.Write;
}
if (perms.Groups["owner"].Value[2] == 'x' || perms.Groups["owner"].Value[2] == 's') {
item.OwnerPermissions |= FtpPermission.Execute;
}
if (perms.Groups["owner"].Value[2] == 's' || perms.Groups["owner"].Value[2] == 'S') {
item.SpecialPermissions |= FtpSpecialPermissions.SetUserID;
}
}
if (perms.Groups["group"].Value.Length == 3) {
if (perms.Groups["group"].Value[0] == 'r') {
item.GroupPermissions |= FtpPermission.Read;
}
if (perms.Groups["group"].Value[1] == 'w'){
item.GroupPermissions |= FtpPermission.Write;
}
if (perms.Groups["group"].Value[2] == 'x' || perms.Groups["group"].Value[2] == 's'){
item.GroupPermissions |= FtpPermission.Execute;
}
if (perms.Groups["group"].Value[2] == 's' || perms.Groups["group"].Value[2] == 'S'){
item.SpecialPermissions |= FtpSpecialPermissions.SetGroupID;
}
}
if (perms.Groups["others"].Value.Length == 3) {
if (perms.Groups["others"].Value[0] == 'r') {
item.OthersPermissions |= FtpPermission.Read;
}
if (perms.Groups["others"].Value[1] == 'w'){
item.OthersPermissions |= FtpPermission.Write;
}
if (perms.Groups["others"].Value[2] == 'x' || perms.Groups["others"].Value[2] == 't'){
item.OthersPermissions |= FtpPermission.Execute;
}
if (perms.Groups["others"].Value[2] == 't' || perms.Groups["others"].Value[2] == 'T'){
item.SpecialPermissions |= FtpSpecialPermissions.Sticky;
}
}
CalcChmod(item);
}
}
return item;
}
public static void CalcChmod(FtpListItem item) {
item.Chmod = FtpClient.CalcChmod(item.OwnerPermissions, item.GroupPermissions, item.OthersPermissions);
}
///
/// Parses IIS DOS format listings
///
/// A line from the listing
/// Server capabilities
/// FtpListItem if the item is able to be parsed
static FtpListItem ParseDosList(string buf, FtpCapability capabilities) {
FtpListItem item = new FtpListItem();
string[] datefmt = new string[] {
"MM-dd-yy hh:mmtt",
"MM-dd-yyyy hh:mmtt"
};
Match m;
// directory
if ((m = Regex.Match(buf, @"(?\d+-\d+-\d+\s+\d+:\d+\w+)\s+\s+(?.*)$", RegexOptions.IgnoreCase)).Success) {
DateTime modify;
item.Type = FtpFileSystemObjectType.Directory;
item.Name = m.Groups["name"].Value;
//if (DateTime.TryParse(m.Groups["modify"].Value, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out modify))
if (DateTime.TryParseExact(m.Groups["modify"].Value, datefmt, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out modify))
item.Modified = modify;
}
// file
else if ((m = Regex.Match(buf, @"(?\d+-\d+-\d+\s+\d+:\d+\w+)\s+(?\d+)\s+(?.*)$", RegexOptions.IgnoreCase)).Success) {
DateTime modify;
long size;
item.Type = FtpFileSystemObjectType.File;
item.Name = m.Groups["name"].Value;
if (long.TryParse(m.Groups["size"].Value, out size))
item.Size = size;
//if (DateTime.TryParse(m.Groups["modify"].Value, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out modify))
if (DateTime.TryParseExact(m.Groups["modify"].Value, datefmt, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out modify))
item.Modified = modify;
} else
return null;
return item;
}
static FtpListItem ParseVaxList(string buf, FtpCapability capabilities) {
string regex =
@"(?.+)\.(?.+);(?\d+)\s+" +
@"(?\d+)\s+" +
@"(?\d+-\w+-\d+\s+\d+:\d+)";
Match m;
if ((m = Regex.Match(buf, regex)).Success) {
FtpListItem item = new FtpListItem();
item.m_name = string.Format("{0}.{1};{2}",
m.Groups["name"].Value,
m.Groups["extension"].Value,
m.Groups["version"].Value);
if (m.Groups["extension"].Value.ToUpper() == "DIR")
item.m_type = FtpFileSystemObjectType.Directory;
else
item.m_type = FtpFileSystemObjectType.File;
if (!long.TryParse(m.Groups["size"].Value, out item.m_size))
item.m_size = -1;
if (!DateTime.TryParse(m.Groups["modify"].Value, CultureInfo.InvariantCulture, DateTimeStyles.AssumeLocal, out item.m_modified))
item.m_modified = DateTime.MinValue;
return item;
}
return null;
}
///
/// Ftp listing line parser
///
/// The line from the listing
/// The server capabilities
/// FtpListItem if the line can be parsed, null otherwise
public delegate FtpListItem Parser(string line, FtpCapability capabilities);
}
}