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); } }