Browse Source

Add support for directory listings in MS-DOS DIR format.

Andy Balholm 10 years ago
parent
commit
95346071de
2 changed files with 153 additions and 80 deletions
  1. 149 80
      ftp.go
  2. 4 0
      parse_test.go

+ 149 - 80
ftp.go

@@ -286,98 +286,169 @@ func (c *ServerConn) cmdDataConnFrom(offset uint64, format string, args ...inter
 	return conn, nil
 }
 
-// parseListLine parses the various non-standard format returned by the LIST
-// FTP command.
-func parseListLine(line string) (*Entry, error) {
-	var err error
-
-	// RFC3659 style
-	if i := strings.Index(line, ";"); i > 0 && i < strings.Index(line, " ") {
-		e := &Entry{}
-		arr := strings.Split(line, "; ")
-		e.Name = arr[1]
+var errUnsupportedListLine = errors.New("Unsupported LIST line")
 
-		for _, field := range strings.Split(arr[0], ";") {
-			i := strings.Index(field, "=")
-			if i < 1 {
-				return nil, errors.New("Unsupported LIST line")
-			}
+// parseRFC3659ListLine parses the style of directory line defined in RFC 3659.
+func parseRFC3659ListLine(line string) (*Entry, error) {
+	if i := strings.Index(line, ";"); i < 0 || i > strings.Index(line, " ") {
+		return nil, errUnsupportedListLine
+	}
+	e := &Entry{}
+	arr := strings.Split(line, "; ")
+	e.Name = arr[1]
 
-			key := field[:i]
-			value := field[i+1:]
-
-			switch key {
-			case "modify":
-				e.Time, _ = time.Parse("20060102150405", value)
-			case "type":
-				switch value {
-				case "dir", "cdir", "pdir":
-					e.Type = EntryTypeFolder
-				case "file":
-					e.Type = EntryTypeFile
-				}
-			case "size":
-				e.setSize(value)
-			}
+	for _, field := range strings.Split(arr[0], ";") {
+		i := strings.Index(field, "=")
+		if i < 1 {
+			return nil, errUnsupportedListLine
 		}
-		return e, nil
 
-	} else {
-		fields := strings.Fields(line)
-		if len(fields) >= 7 && fields[1] == "folder" && fields[2] == "0" {
-			e := &Entry{
-				Type: EntryTypeFolder,
-				Name: strings.Join(fields[6:], " "),
-			}
-			if err = e.setTime(fields[3:6]); err != nil {
+		key := field[:i]
+		value := field[i+1:]
+
+		switch key {
+		case "modify":
+			var err error
+			e.Time, err = time.Parse("20060102150405", value)
+			if err != nil {
 				return nil, err
 			}
+		case "type":
+			switch value {
+			case "dir", "cdir", "pdir":
+				e.Type = EntryTypeFolder
+			case "file":
+				e.Type = EntryTypeFile
+			}
+		case "size":
+			e.setSize(value)
+		}
+	}
+	return e, nil
+}
 
-			return e, nil
+// parseLsListLine parses a directory line in a format based on the output of
+// the UNIX ls command.
+func parseLsListLine(line string) (*Entry, error) {
+	fields := strings.Fields(line)
+	if len(fields) >= 7 && fields[1] == "folder" && fields[2] == "0" {
+		e := &Entry{
+			Type: EntryTypeFolder,
+			Name: strings.Join(fields[6:], " "),
+		}
+		if err := e.setTime(fields[3:6]); err != nil {
+			return nil, err
 		}
 
-		if fields[1] == "0" {
-			e := &Entry{
-				Type: EntryTypeFile,
-				Name: strings.Join(fields[7:], " "),
-			}
+		return e, nil
+	}
 
-			if err = e.setSize(fields[2]); err != nil {
-				return nil, err
-			}
-			if err = e.setTime(fields[4:7]); err != nil {
-				return nil, err
-			}
+	if fields[1] == "0" {
+		e := &Entry{
+			Type: EntryTypeFile,
+			Name: strings.Join(fields[7:], " "),
+		}
 
-			return e, nil
+		if err := e.setSize(fields[2]); err != nil {
+			return nil, err
+		}
+		if err := e.setTime(fields[4:7]); err != nil {
+			return nil, err
 		}
 
-		if len(fields) < 9 {
-			return nil, errors.New("Unsupported LIST line")
+		return e, nil
+	}
+
+	if len(fields) < 9 {
+		return nil, errUnsupportedListLine
+	}
+
+	e := &Entry{}
+	switch fields[0][0] {
+	case '-':
+		e.Type = EntryTypeFile
+		if err := e.setSize(fields[4]); err != nil {
+			return nil, err
 		}
+	case 'd':
+		e.Type = EntryTypeFolder
+	case 'l':
+		e.Type = EntryTypeLink
+	default:
+		return nil, errors.New("Unknown entry type")
+	}
 
-		e := &Entry{}
-		switch fields[0][0] {
-		case '-':
-			e.Type = EntryTypeFile
-			if err = e.setSize(fields[4]); err != nil {
-				return nil, err
-			}
-		case 'd':
-			e.Type = EntryTypeFolder
-		case 'l':
-			e.Type = EntryTypeLink
-		default:
-			return nil, errors.New("Unknown entry type")
+	if err := e.setTime(fields[5:8]); err != nil {
+		return nil, err
+	}
+
+	e.Name = strings.Join(fields[8:], " ")
+	return e, nil
+}
+
+var dirTimeFormats = []string{
+	"01-02-06  03:04PM",
+	"2006-01-02  15:04",
+}
+
+// parseDirListLine parses a directory line in a format based on the output of
+// the MS-DOS DIR command.
+func parseDirListLine(line string) (*Entry, error) {
+	e := &Entry{}
+	var err error
+
+	// Try various time formats that DIR might use, and stop when one works.
+	for _, format := range dirTimeFormats {
+		e.Time, err = time.Parse(format, line[:len(format)])
+		if err == nil {
+			line = line[len(format):]
+			break
 		}
+	}
+	if err != nil {
+		// None of the time formats worked.
+		return nil, errUnsupportedListLine
+	}
 
-		if err = e.setTime(fields[5:8]); err != nil {
-			return nil, err
+	line = strings.TrimLeft(line, " ")
+	if strings.HasPrefix(line, "<DIR>") {
+		e.Type = EntryTypeFolder
+		line = strings.TrimPrefix(line, "<DIR>")
+	} else {
+		space := strings.Index(line, " ")
+		if space == -1 {
+			return nil, errUnsupportedListLine
 		}
+		e.Size, err = strconv.ParseUint(line[:space], 10, 64)
+		if err != nil {
+			return nil, errUnsupportedListLine
+		}
+		e.Type = EntryTypeFile
+		line = line[space:]
+	}
 
-		e.Name = strings.Join(fields[8:], " ")
-		return e, nil
+	e.Name = strings.TrimLeft(line, " ")
+	return e, nil
+}
+
+var listLineParsers = []func(line string) (*Entry, error){
+	parseRFC3659ListLine,
+	parseLsListLine,
+	parseDirListLine,
+}
+
+// parseListLine parses the various non-standard format returned by the LIST
+// FTP command.
+func parseListLine(line string) (*Entry, error) {
+	for _, f := range listLineParsers {
+		e, err := f(line)
+		if err == errUnsupportedListLine {
+			// Try another format.
+			continue
+		}
+		return e, err
 	}
+	return nil, errUnsupportedListLine
 }
 
 func (e *Entry) setSize(str string) (err error) {
@@ -430,19 +501,17 @@ func (c *ServerConn) List(path string) (entries []*Entry, err error) {
 	r := &response{conn, c}
 	defer r.Close()
 
-	bio := bufio.NewReader(r)
-	for {
-		line, e := bio.ReadString('\n')
-		if e == io.EOF {
-			break
-		} else if e != nil {
-			return nil, e
-		}
+	scanner := bufio.NewScanner(r)
+	for scanner.Scan() {
+		line := scanner.Text()
 		entry, err := parseListLine(line)
 		if err == nil {
 			entries = append(entries, entry)
 		}
 	}
+	if err := scanner.Err(); err != nil {
+		return nil, err
+	}
 	return
 }
 

+ 4 - 0
parse_test.go

@@ -45,6 +45,10 @@ var listTests = []line{
 	{"modify=20150806235817;perm=fle;type=dir;unique=1B20F360U4;UNIX.group=0;UNIX.mode=0755;UNIX.owner=0; movies", "movies", 0, EntryTypeFolder, time.Date(2015, time.August, 6, 23, 58, 17, 0, time.UTC)},
 	{"modify=20150814172949;perm=flcdmpe;type=dir;unique=85A0C168U4;UNIX.group=0;UNIX.mode=0777;UNIX.owner=0; _upload", "_upload", 0, EntryTypeFolder, time.Date(2015, time.August, 14, 17, 29, 49, 0, time.UTC)},
 	{"modify=20150813175250;perm=adfr;size=951;type=file;unique=119FBB87UE;UNIX.group=0;UNIX.mode=0644;UNIX.owner=0; welcome.msg", "welcome.msg", 951, EntryTypeFile, time.Date(2015, time.August, 13, 17, 52, 50, 0, time.UTC)},
+
+	// DOS DIR command output
+	{"08-07-15  07:50PM                  718 Post_PRR_20150901_1166_265118_13049.dat", "Post_PRR_20150901_1166_265118_13049.dat", 718, EntryTypeFile, time.Date(2015, time.August, 7, 19, 50, 0, 0, time.UTC)},
+	{"08-10-15  02:04PM       <DIR>          Billing", "Billing", 0, EntryTypeFolder, time.Date(2015, time.August, 10, 14, 4, 0, 0, time.UTC)},
 }
 
 // Not supported, we expect a specific error message