This project is read-only.

Using MLSD to get directory listing instead of LIST (to get hours and minutes in datetime)

Oct 31, 2013 at 5:47 PM
Edited Oct 31, 2013 at 5:51 PM
Hi,

I noticed that the CreationTime lacks the hour, min, second (always set to zero). Basically, it seems the LIST command doesn't output this info. Filezilla client does show this info against the same ftp server. So I looked into it, and this is because Filezilla uses MLSD to get a directory list instead of LIST if it's supported. MLSD contains a more precise time stamp, which can be parsed safely as it's not region specific (us or uk date for example).

Based on rev77306, I've modified my code as follow (not saved at all in official source code!)

FTPSClient.cs:

Modify this method:
        private void ListCmd(string dirName)
        {
            if (CheckFeature("MLSD")) //MLSD RFC3659
                HandleCmd("MLSD" + (dirName != null ? (" " + dirName) : ""));
            else
                HandleCmd("LIST" + (dirName != null ? (" " + dirName) : ""));
            
        }
DirectoryListParser.cs:

Added to the enum at the top:
enum EDirectoryListingStyle { UnixStyle, WindowsStyle, Unknown, Mlsd }
Inside GetDirectoryList method, added a case under the switch:
                        switch (_directoryListStyle)
                        {
                            [...]
                            case EDirectoryListingStyle.Mlsd:
                                f = ParseDirectoryListItemFromMlsdStyleRecord(s);
                                break;
                        }
Inside GuessDirectoryListingStyle method, added an extra if statement:
                else if (s.Length >= 7 //4 chars min for a fact name, 1 equal, 1 semicolon, 1 char min for value = 7; but strictly, can contain no fact at all!
                 && s.Contains(";") && s.Contains("=") )
                {
                    return EDirectoryListingStyle.Mlsd;
                }
Finally, the main method:
private static DirectoryListItem ParseDirectoryListItemFromMlsdStyleRecord(string record)
{
    //see format in http://tools.ietf.org/html/rfc3659#page-23
    //code inspired from Filezilla CDirectoryListingParser::ParseAsMlsd
            
    DirectoryListItem f = new DirectoryListItem();
    string facts = record.Trim();
            
    string owner = string.Empty, group= string.Empty, uid= string.Empty, gid= string.Empty;
    DateTime creationTime = DateTime.MinValue, modificationTime = DateTime.MinValue;

    while (facts.Contains(";"))
    {
        int delim = facts.IndexOf(';');
        if (delim < 3)
        {
            if (delim != -1)
                throw new ArgumentException("Invalid format 1 in '"+facts+"'.",facts);
            else
                delim = facts.Length;
        }

        int pos = facts.IndexOf('=');
        if (pos < 1 || pos > delim)
            throw new ArgumentException("Invalid format 2 in '"+facts+"'.",facts);

        string factname = facts.Substring(0,pos).ToLower();
        string value = facts.Substring(pos + 1, delim - pos - 1).ToLower();
        if (factname == "type")
        {
            if (value == "dir")
                f.IsDirectory = true;
            else if (value.StartsWith("OS.unix=slink"))
            {
                f.IsDirectory = true;
                f.IsSymLink = true;
                        
                if (value[13] == ':' && value[14] != 0)
                    f.SymLinkTargetPath = value.Substring(0, 14);
            }
            else if (value=="cdir" || value=="pdir")
                return null; //valid entry, current or parent dir, but we want to ignore it, so don't throw
            else if (value == "file")
                f.IsDirectory = false;
        }
        else if (factname == "size")
        {
            f.Size = 0;
            ulong size;
            if (!ulong.TryParse(value, out size))
                    throw new ArgumentException("Invalid format 3 in '"+facts+"'.",facts);
            f.Size = size;
        }
        else if (factname == "create")
        {
            string[] formats = new string[] { "yyyyMMddHHmmss", "yyyyMMddHHmmss.fff" };
            DateTime parsed;
            if (DateTime.TryParseExact(value, formats, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out parsed))
                creationTime = parsed;
        }
        else if (factname == "modify")
        {
            string[] formats = new string[] { "yyyyMMddHHmmss", "yyyyMMddHHmmss.fff" };
            DateTime parsed;
            if (DateTime.TryParseExact(value, formats, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out parsed))
                modificationTime = parsed;
        }
        else if (factname == "perm")
        {
            f.Flags = value;
        }
        else if (factname == "unix.mode")
        {
            f.Flags = value;
        }
        else if (factname == "unix.owner" || factname == "unix.user")
            owner = value;
        else if (factname == "unix.group")
            group = value;
        else if (factname == "unix.uid")
            uid = value;
        else if (factname == "unix.gid")
            gid = value;

        facts = facts.Substring(delim + 1);
    }

    if (modificationTime != DateTime.MinValue) //use modification for CreationTime property, I don't want to add another property
        f.CreationTime = modificationTime;
    else
        f.CreationTime = creationTime;

    if (!string.IsNullOrEmpty(owner))
        f.Owner += owner;
    else if (!string.IsNullOrEmpty(uid))
        f.Owner += " " + uid;
    if (!string.IsNullOrEmpty(group))
        f.Group += group;
    else if (!string.IsNullOrEmpty(gid))
        f.Group += " " + gid;

    f.Name = facts.Trim(); //Rest is name  (Note that RFC indicates a name made uniquely of spaces is valid)

    return f;
}
I've done some testing. I'm not 100% happy with the fact it's returning a null value in some conditions, as previous code never returned that, it could cause a bug somewhere else.

It's far from a full implementation of the RFC. It's inspired from the Filezilla Client code.

This might help someone...
T

ps: seems this site replaces all the plus sign (+) in the code by its encoded value +