From af043eda15ff25fa1aed1d6c3fb56892c23ef6d6 Mon Sep 17 00:00:00 2001
From: Nick Craig-Wood <nick@craig-wood.com>
Date: Tue, 16 May 2017 18:01:02 +0100
Subject: [PATCH] Vendor github.com/jlaffaye/ftp for ftp backend

---
 Gopkg.lock                                    |   8 +-
 vendor/github.com/jlaffaye/ftp/.travis.yml    |  15 +
 .../jlaffaye/ftp/.travis/prepare.sh           |  18 +
 .../jlaffaye/ftp/.travis/proftpd.conf         |   9 +
 .../jlaffaye/ftp/.travis/vsftpd.conf          |  15 +
 vendor/github.com/jlaffaye/ftp/LICENSE        |  13 +
 vendor/github.com/jlaffaye/ftp/README.md      |  17 +
 .../jlaffaye/ftp/client_multiline_test.go     | 107 ++++
 vendor/github.com/jlaffaye/ftp/client_test.go | 270 +++++++++
 vendor/github.com/jlaffaye/ftp/ftp.go         | 557 ++++++++++++++++++
 vendor/github.com/jlaffaye/ftp/parse.go       | 213 +++++++
 vendor/github.com/jlaffaye/ftp/parse_test.go  | 104 ++++
 vendor/github.com/jlaffaye/ftp/scanner.go     |  58 ++
 .../github.com/jlaffaye/ftp/scanner_test.go   |  28 +
 vendor/github.com/jlaffaye/ftp/status.go      | 106 ++++
 15 files changed, 1537 insertions(+), 1 deletion(-)
 create mode 100644 vendor/github.com/jlaffaye/ftp/.travis.yml
 create mode 100755 vendor/github.com/jlaffaye/ftp/.travis/prepare.sh
 create mode 100644 vendor/github.com/jlaffaye/ftp/.travis/proftpd.conf
 create mode 100644 vendor/github.com/jlaffaye/ftp/.travis/vsftpd.conf
 create mode 100644 vendor/github.com/jlaffaye/ftp/LICENSE
 create mode 100644 vendor/github.com/jlaffaye/ftp/README.md
 create mode 100644 vendor/github.com/jlaffaye/ftp/client_multiline_test.go
 create mode 100644 vendor/github.com/jlaffaye/ftp/client_test.go
 create mode 100644 vendor/github.com/jlaffaye/ftp/ftp.go
 create mode 100644 vendor/github.com/jlaffaye/ftp/parse.go
 create mode 100644 vendor/github.com/jlaffaye/ftp/parse_test.go
 create mode 100644 vendor/github.com/jlaffaye/ftp/scanner.go
 create mode 100644 vendor/github.com/jlaffaye/ftp/scanner_test.go
 create mode 100644 vendor/github.com/jlaffaye/ftp/status.go

diff --git a/Gopkg.lock b/Gopkg.lock
index 289e8a0ab..bb80c047a 100644
--- a/Gopkg.lock
+++ b/Gopkg.lock
@@ -1,4 +1,4 @@
-memo = "28c7ca08636da6264c587a36b72ce4f3a45fdf5c32969ffd2092b98456c73685"
+memo = "8903edaabf2c61a2bb9386fb033b3ff7710880ee02387c7a3f5eaaee82cd6e67"
 
 [[projects]]
   branch = "master"
@@ -78,6 +78,12 @@ memo = "28c7ca08636da6264c587a36b72ce4f3a45fdf5c32969ffd2092b98456c73685"
   packages = ["."]
   revision = "76626ae9c91c4f2a10f34cad8ce83ea42c93bb75"
 
+[[projects]]
+  branch = "master"
+  name = "github.com/jlaffaye/ftp"
+  packages = ["."]
+  revision = "5c7b901224c7880b293e0b5486cb6ebf97bfca37"
+
 [[projects]]
   name = "github.com/jmespath/go-jmespath"
   packages = ["."]
diff --git a/vendor/github.com/jlaffaye/ftp/.travis.yml b/vendor/github.com/jlaffaye/ftp/.travis.yml
new file mode 100644
index 000000000..1b58030ef
--- /dev/null
+++ b/vendor/github.com/jlaffaye/ftp/.travis.yml
@@ -0,0 +1,15 @@
+language: go
+dist: trusty
+sudo: required
+go:
+  - 1.7.5
+  - 1.8.1
+env:
+  - FTP_SERVER=vsftpd
+  - FTP_SERVER=proftpd
+before_install:
+- sudo $TRAVIS_BUILD_DIR/.travis/prepare.sh "$FTP_SERVER"
+- sudo sysctl net.ipv6.conf.lo.disable_ipv6=0
+- go get github.com/mattn/goveralls
+script:
+- goveralls -v
diff --git a/vendor/github.com/jlaffaye/ftp/.travis/prepare.sh b/vendor/github.com/jlaffaye/ftp/.travis/prepare.sh
new file mode 100755
index 000000000..40970d9ac
--- /dev/null
+++ b/vendor/github.com/jlaffaye/ftp/.travis/prepare.sh
@@ -0,0 +1,18 @@
+#!/bin/sh -e
+
+case "$1" in
+  proftpd)
+    mkdir -p /etc/proftpd/conf.d/
+    cp $TRAVIS_BUILD_DIR/.travis/proftpd.conf /etc/proftpd/conf.d/
+    ;;
+  vsftpd)
+    cp $TRAVIS_BUILD_DIR/.travis/vsftpd.conf /etc/vsftpd.conf
+    ;;
+  *)
+    echo "unknown software: $1"
+    exit 1
+esac
+
+mkdir --mode 0777 -p /var/ftp/incoming
+
+apt-get install -qq "$1"
diff --git a/vendor/github.com/jlaffaye/ftp/.travis/proftpd.conf b/vendor/github.com/jlaffaye/ftp/.travis/proftpd.conf
new file mode 100644
index 000000000..342d08fe9
--- /dev/null
+++ b/vendor/github.com/jlaffaye/ftp/.travis/proftpd.conf
@@ -0,0 +1,9 @@
+<Anonymous /var/ftp>
+  User        ftp
+  Group       nogroup
+  MaxClients  2
+  # We want clients to be able to login with "anonymous" as well as "ftp"
+  UserAlias   anonymous ftp
+
+  RequireValidShell  off
+</Anonymous>
diff --git a/vendor/github.com/jlaffaye/ftp/.travis/vsftpd.conf b/vendor/github.com/jlaffaye/ftp/.travis/vsftpd.conf
new file mode 100644
index 000000000..f3d6dbfd3
--- /dev/null
+++ b/vendor/github.com/jlaffaye/ftp/.travis/vsftpd.conf
@@ -0,0 +1,15 @@
+# Used by Travis CI
+
+listen=NO
+listen_ipv6=YES
+
+write_enable=YES
+dirmessage_enable=YES
+secure_chroot_dir=/var/run/vsftpd/empty
+
+anonymous_enable=YES
+anon_root=/var/ftp
+anon_upload_enable=YES
+anon_mkdir_write_enable=YES
+anon_other_write_enable=YES
+anon_umask=022
diff --git a/vendor/github.com/jlaffaye/ftp/LICENSE b/vendor/github.com/jlaffaye/ftp/LICENSE
new file mode 100644
index 000000000..9ab085c51
--- /dev/null
+++ b/vendor/github.com/jlaffaye/ftp/LICENSE
@@ -0,0 +1,13 @@
+Copyright (c) 2011-2013, Julien Laffaye <jlaffaye@FreeBSD.org>
+
+Permission to use, copy, modify, and/or distribute this software for any
+purpose with or without fee is hereby granted, provided that the above
+copyright notice and this permission notice appear in all copies.
+
+THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
diff --git a/vendor/github.com/jlaffaye/ftp/README.md b/vendor/github.com/jlaffaye/ftp/README.md
new file mode 100644
index 000000000..b711be7ad
--- /dev/null
+++ b/vendor/github.com/jlaffaye/ftp/README.md
@@ -0,0 +1,17 @@
+# goftp #
+
+[![Build Status](https://travis-ci.org/jlaffaye/ftp.svg?branch=master)](https://travis-ci.org/jlaffaye/ftp)
+[![Coverage Status](https://coveralls.io/repos/jlaffaye/ftp/badge.svg?branch=master&service=github)](https://coveralls.io/github/jlaffaye/ftp?branch=master)
+[![Go ReportCard](http://goreportcard.com/badge/jlaffaye/ftp)](http://goreportcard.com/report/jlaffaye/ftp)
+
+A FTP client package for Go
+
+## Install ##
+
+```
+go get -u github.com/jlaffaye/ftp
+```
+
+## Documentation ##
+
+http://godoc.org/github.com/jlaffaye/ftp
diff --git a/vendor/github.com/jlaffaye/ftp/client_multiline_test.go b/vendor/github.com/jlaffaye/ftp/client_multiline_test.go
new file mode 100644
index 000000000..9d3b0ce1d
--- /dev/null
+++ b/vendor/github.com/jlaffaye/ftp/client_multiline_test.go
@@ -0,0 +1,107 @@
+package ftp
+
+import (
+	"net"
+	"net/textproto"
+	"reflect"
+	"strings"
+	"sync"
+	"testing"
+)
+
+type ftpMock struct {
+	listener net.Listener
+	commands []string // list of received commands
+	sync.WaitGroup
+}
+
+func newFtpMock(t *testing.T, addresss string) *ftpMock {
+	var err error
+	mock := &ftpMock{}
+	mock.listener, err = net.Listen("tcp", addresss)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	go func() {
+		// Listen for an incoming connection.
+		conn, err := mock.listener.Accept()
+		if err != nil {
+			t.Fatal(err)
+		}
+
+		mock.Add(1)
+		defer mock.Done()
+		defer conn.Close()
+
+		proto := textproto.NewConn(conn)
+		proto.Writer.PrintfLine("220 FTP Server ready.")
+
+		for {
+			command, _ := proto.ReadLine()
+
+			// Strip the arguments
+			if i := strings.Index(command, " "); i > 0 {
+				command = command[:i]
+			}
+
+			// Append to list of received commands
+			mock.commands = append(mock.commands, command)
+
+			// At least one command must have a multiline response
+			switch command {
+			case "FEAT":
+				proto.Writer.PrintfLine("211-Features:\r\nFEAT\r\nPASV\r\nSIZE\r\n211 End")
+			case "USER":
+				proto.Writer.PrintfLine("331 Please send your password")
+			case "PASS":
+				proto.Writer.PrintfLine("230-Hey,\r\nWelcome to my FTP\r\n230 Access granted")
+			case "TYPE":
+				proto.Writer.PrintfLine("200 Type set ok")
+			case "QUIT":
+				proto.Writer.PrintfLine("221 Goodbye.")
+				return
+			default:
+				t.Fatal("unknown command:", command)
+			}
+		}
+	}()
+
+	return mock
+}
+
+// Closes the listening socket
+func (mock *ftpMock) Close() {
+	mock.listener.Close()
+}
+
+// ftp.mozilla.org uses multiline 220 response
+func TestMultiline(t *testing.T) {
+	if testing.Short() {
+		t.Skip("skipping test in short mode.")
+	}
+
+	address := "localhost:2121"
+	mock := newFtpMock(t, address)
+	defer mock.Close()
+
+	c, err := Dial(address)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	err = c.Login("anonymous", "anonymous")
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	c.Quit()
+
+	// Wait for the connection to close
+	mock.Wait()
+
+	expected := []string{"FEAT", "USER", "PASS", "TYPE", "QUIT"}
+	if !reflect.DeepEqual(mock.commands, expected) {
+		t.Fatal("unexpected sequence of commands:", mock.commands, "expected:", expected)
+	}
+}
diff --git a/vendor/github.com/jlaffaye/ftp/client_test.go b/vendor/github.com/jlaffaye/ftp/client_test.go
new file mode 100644
index 000000000..8297e49cf
--- /dev/null
+++ b/vendor/github.com/jlaffaye/ftp/client_test.go
@@ -0,0 +1,270 @@
+package ftp
+
+import (
+	"bytes"
+	"io/ioutil"
+	"net/textproto"
+	"strings"
+	"testing"
+	"time"
+)
+
+const (
+	testData = "Just some text"
+	testDir  = "mydir"
+)
+
+func TestConnPASV(t *testing.T) {
+	testConn(t, true)
+}
+
+func TestConnEPSV(t *testing.T) {
+	testConn(t, false)
+}
+
+func testConn(t *testing.T, disableEPSV bool) {
+	if testing.Short() {
+		t.Skip("skipping test in short mode.")
+	}
+
+	c, err := DialTimeout("localhost:21", 5*time.Second)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	if disableEPSV {
+		delete(c.features, "EPSV")
+		c.DisableEPSV = true
+	}
+
+	err = c.Login("anonymous", "anonymous")
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	err = c.NoOp()
+	if err != nil {
+		t.Error(err)
+	}
+
+	err = c.ChangeDir("incoming")
+	if err != nil {
+		t.Error(err)
+	}
+
+	data := bytes.NewBufferString(testData)
+	err = c.Stor("test", data)
+	if err != nil {
+		t.Error(err)
+	}
+
+	_, err = c.List(".")
+	if err != nil {
+		t.Error(err)
+	}
+
+	err = c.Rename("test", "tset")
+	if err != nil {
+		t.Error(err)
+	}
+
+	// Read without deadline
+	r, err := c.Retr("tset")
+	if err != nil {
+		t.Error(err)
+	} else {
+		buf, err := ioutil.ReadAll(r)
+		if err != nil {
+			t.Error(err)
+		}
+		if string(buf) != testData {
+			t.Errorf("'%s'", buf)
+		}
+		r.Close()
+		r.Close() // test we can close two times
+	}
+
+	// Read with deadline
+	r, err = c.Retr("tset")
+	if err != nil {
+		t.Error(err)
+	} else {
+		r.SetDeadline(time.Now())
+		_, err := ioutil.ReadAll(r)
+		if err == nil {
+			t.Error("deadline should have caused error")
+		} else if !strings.HasSuffix(err.Error(), "i/o timeout") {
+			t.Error(err)
+		}
+		r.Close()
+	}
+
+	// Read with offset
+	r, err = c.RetrFrom("tset", 5)
+	if err != nil {
+		t.Error(err)
+	} else {
+		buf, err := ioutil.ReadAll(r)
+		if err != nil {
+			t.Error(err)
+		}
+		expected := testData[5:]
+		if string(buf) != expected {
+			t.Errorf("read %q, expected %q", buf, expected)
+		}
+		r.Close()
+	}
+
+	fileSize, err := c.FileSize("tset")
+	if err != nil {
+		t.Error(err)
+	}
+	if fileSize != 14 {
+		t.Errorf("file size %q, expected %q", fileSize, 14)
+	}
+
+	data = bytes.NewBufferString("")
+	err = c.Stor("tset", data)
+	if err != nil {
+		t.Error(err)
+	}
+
+	fileSize, err = c.FileSize("tset")
+	if err != nil {
+		t.Error(err)
+	}
+	if fileSize != 0 {
+		t.Errorf("file size %q, expected %q", fileSize, 0)
+	}
+
+	_, err = c.FileSize("not-found")
+	if err == nil {
+		t.Fatal("expected error, got nil")
+	}
+
+	err = c.Delete("tset")
+	if err != nil {
+		t.Error(err)
+	}
+
+	err = c.MakeDir(testDir)
+	if err != nil {
+		t.Error(err)
+	}
+
+	err = c.ChangeDir(testDir)
+	if err != nil {
+		t.Error(err)
+	}
+
+	dir, err := c.CurrentDir()
+	if err != nil {
+		t.Error(err)
+	} else {
+		if dir != "/incoming/"+testDir {
+			t.Error("Wrong dir: " + dir)
+		}
+	}
+
+	err = c.ChangeDirToParent()
+	if err != nil {
+		t.Error(err)
+	}
+
+	entries, err := c.NameList("/")
+	if err != nil {
+		t.Error(err)
+	}
+	if len(entries) != 1 || entries[0] != "/incoming" {
+		t.Errorf("Unexpected entries: %v", entries)
+	}
+
+	err = c.RemoveDir(testDir)
+	if err != nil {
+		t.Error(err)
+	}
+
+	err = c.Logout()
+	if err != nil {
+		if protoErr := err.(*textproto.Error); protoErr != nil {
+			if protoErr.Code != StatusNotImplemented {
+				t.Error(err)
+			}
+		} else {
+			t.Error(err)
+		}
+	}
+
+	c.Quit()
+
+	err = c.NoOp()
+	if err == nil {
+		t.Error("Expected error")
+	}
+}
+
+func TestConnIPv6(t *testing.T) {
+	if testing.Short() {
+		t.Skip("skipping test in short mode.")
+	}
+
+	c, err := DialTimeout("[::1]:21", 5*time.Second)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	err = c.Login("anonymous", "anonymous")
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	_, err = c.List(".")
+	if err != nil {
+		t.Error(err)
+	}
+
+	c.Quit()
+}
+
+// TestConnect tests the legacy Connect function
+func TestConnect(t *testing.T) {
+	if testing.Short() {
+		t.Skip("skipping test in short mode.")
+	}
+
+	c, err := Connect("localhost:21")
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	c.Quit()
+}
+
+func TestTimeout(t *testing.T) {
+	if testing.Short() {
+		t.Skip("skipping test in short mode.")
+	}
+
+	c, err := DialTimeout("localhost:2121", 1*time.Second)
+	if err == nil {
+		t.Fatal("expected timeout, got nil error")
+		c.Quit()
+	}
+}
+
+func TestWrongLogin(t *testing.T) {
+	if testing.Short() {
+		t.Skip("skipping test in short mode.")
+	}
+
+	c, err := DialTimeout("localhost:21", 5*time.Second)
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer c.Quit()
+
+	err = c.Login("zoo2Shia", "fei5Yix9")
+	if err == nil {
+		t.Fatal("expected error, got nil")
+	}
+}
diff --git a/vendor/github.com/jlaffaye/ftp/ftp.go b/vendor/github.com/jlaffaye/ftp/ftp.go
new file mode 100644
index 000000000..c29cb3847
--- /dev/null
+++ b/vendor/github.com/jlaffaye/ftp/ftp.go
@@ -0,0 +1,557 @@
+// Package ftp implements a FTP client as described in RFC 959.
+//
+// A textproto.Error is returned for errors at the protocol level.
+package ftp
+
+import (
+	"bufio"
+	"errors"
+	"io"
+	"net"
+	"net/textproto"
+	"strconv"
+	"strings"
+	"time"
+)
+
+// EntryType describes the different types of an Entry.
+type EntryType int
+
+// The differents types of an Entry
+const (
+	EntryTypeFile EntryType = iota
+	EntryTypeFolder
+	EntryTypeLink
+)
+
+// ServerConn represents the connection to a remote FTP server.
+// It should be protected from concurrent accesses.
+type ServerConn struct {
+	// Do not use EPSV mode
+	DisableEPSV bool
+
+	conn          *textproto.Conn
+	host          string
+	timeout       time.Duration
+	features      map[string]string
+	mlstSupported bool
+}
+
+// Entry describes a file and is returned by List().
+type Entry struct {
+	Name string
+	Type EntryType
+	Size uint64
+	Time time.Time
+}
+
+// Response represents a data-connection
+type Response struct {
+	conn   net.Conn
+	c      *ServerConn
+	closed bool
+}
+
+// Connect is an alias to Dial, for backward compatibility
+func Connect(addr string) (*ServerConn, error) {
+	return Dial(addr)
+}
+
+// Dial is like DialTimeout with no timeout
+func Dial(addr string) (*ServerConn, error) {
+	return DialTimeout(addr, 0)
+}
+
+// DialTimeout initializes the connection to the specified ftp server address.
+//
+// It is generally followed by a call to Login() as most FTP commands require
+// an authenticated user.
+func DialTimeout(addr string, timeout time.Duration) (*ServerConn, error) {
+	tconn, err := net.DialTimeout("tcp", addr, timeout)
+	if err != nil {
+		return nil, err
+	}
+
+	// Use the resolved IP address in case addr contains a domain name
+	// If we use the domain name, we might not resolve to the same IP.
+	remoteAddr := tconn.RemoteAddr().String()
+	host, _, err := net.SplitHostPort(remoteAddr)
+	if err != nil {
+		return nil, err
+	}
+
+	conn := textproto.NewConn(tconn)
+
+	c := &ServerConn{
+		conn:     conn,
+		host:     host,
+		timeout:  timeout,
+		features: make(map[string]string),
+	}
+
+	_, _, err = c.conn.ReadResponse(StatusReady)
+	if err != nil {
+		c.Quit()
+		return nil, err
+	}
+
+	err = c.feat()
+	if err != nil {
+		c.Quit()
+		return nil, err
+	}
+
+	if _, mlstSupported := c.features["MLST"]; mlstSupported {
+		c.mlstSupported = true
+	}
+
+	return c, nil
+}
+
+// Login authenticates the client with specified user and password.
+//
+// "anonymous"/"anonymous" is a common user/password scheme for FTP servers
+// that allows anonymous read-only accounts.
+func (c *ServerConn) Login(user, password string) error {
+	code, message, err := c.cmd(-1, "USER %s", user)
+	if err != nil {
+		return err
+	}
+
+	switch code {
+	case StatusLoggedIn:
+	case StatusUserOK:
+		_, _, err = c.cmd(StatusLoggedIn, "PASS %s", password)
+		if err != nil {
+			return err
+		}
+	default:
+		return errors.New(message)
+	}
+
+	// Switch to binary mode
+	if _, _, err = c.cmd(StatusCommandOK, "TYPE I"); err != nil {
+		return err
+	}
+
+	// Switch to UTF-8
+	if err := c.setUTF8(); err != nil {
+		return err
+	}
+
+	return nil
+}
+
+// feat issues a FEAT FTP command to list the additional commands supported by
+// the remote FTP server.
+// FEAT is described in RFC 2389
+func (c *ServerConn) feat() error {
+	code, message, err := c.cmd(-1, "FEAT")
+	if err != nil {
+		return err
+	}
+
+	if code != StatusSystem {
+		// The server does not support the FEAT command. This is not an
+		// error: we consider that there is no additional feature.
+		return nil
+	}
+
+	lines := strings.Split(message, "\n")
+	for _, line := range lines {
+		if !strings.HasPrefix(line, " ") {
+			continue
+		}
+
+		line = strings.TrimSpace(line)
+		featureElements := strings.SplitN(line, " ", 2)
+
+		command := featureElements[0]
+
+		var commandDesc string
+		if len(featureElements) == 2 {
+			commandDesc = featureElements[1]
+		}
+
+		c.features[command] = commandDesc
+	}
+
+	return nil
+}
+
+// setUTF8 issues an "OPTS UTF8 ON" command.
+func (c *ServerConn) setUTF8() error {
+	if _, ok := c.features["UTF8"]; !ok {
+		return nil
+	}
+
+	code, message, err := c.cmd(-1, "OPTS UTF8 ON")
+	if err != nil {
+		return err
+	}
+
+	// The ftpd "filezilla-server" has FEAT support for UTF8, but always returns
+	// "202 UTF8 mode is always enabled. No need to send this command." when
+	// trying to use it. That's OK
+	if code == StatusCommandNotImplemented {
+		return nil
+	}
+
+	if code != StatusCommandOK {
+		return errors.New(message)
+	}
+
+	return nil
+}
+
+// epsv issues an "EPSV" command to get a port number for a data connection.
+func (c *ServerConn) epsv() (port int, err error) {
+	_, line, err := c.cmd(StatusExtendedPassiveMode, "EPSV")
+	if err != nil {
+		return
+	}
+
+	start := strings.Index(line, "|||")
+	end := strings.LastIndex(line, "|")
+	if start == -1 || end == -1 {
+		err = errors.New("Invalid EPSV response format")
+		return
+	}
+	port, err = strconv.Atoi(line[start+3 : end])
+	return
+}
+
+// pasv issues a "PASV" command to get a port number for a data connection.
+func (c *ServerConn) pasv() (port int, err error) {
+	_, line, err := c.cmd(StatusPassiveMode, "PASV")
+	if err != nil {
+		return
+	}
+
+	// PASV response format : 227 Entering Passive Mode (h1,h2,h3,h4,p1,p2).
+	start := strings.Index(line, "(")
+	end := strings.LastIndex(line, ")")
+	if start == -1 || end == -1 {
+		return 0, errors.New("Invalid PASV response format")
+	}
+
+	// We have to split the response string
+	pasvData := strings.Split(line[start+1:end], ",")
+
+	if len(pasvData) < 6 {
+		return 0, errors.New("Invalid PASV response format")
+	}
+
+	// Let's compute the port number
+	portPart1, err1 := strconv.Atoi(pasvData[4])
+	if err1 != nil {
+		err = err1
+		return
+	}
+
+	portPart2, err2 := strconv.Atoi(pasvData[5])
+	if err2 != nil {
+		err = err2
+		return
+	}
+
+	// Recompose port
+	port = portPart1*256 + portPart2
+	return
+}
+
+// getDataConnPort returns a port for a new data connection
+// it uses the best available method to do so
+func (c *ServerConn) getDataConnPort() (int, error) {
+	if !c.DisableEPSV {
+		if port, err := c.epsv(); err == nil {
+			return port, nil
+		}
+
+		// if there is an error, disable EPSV for the next attempts
+		c.DisableEPSV = true
+	}
+
+	return c.pasv()
+}
+
+// openDataConn creates a new FTP data connection.
+func (c *ServerConn) openDataConn() (net.Conn, error) {
+	port, err := c.getDataConnPort()
+	if err != nil {
+		return nil, err
+	}
+
+	return net.DialTimeout("tcp", net.JoinHostPort(c.host, strconv.Itoa(port)), c.timeout)
+}
+
+// cmd is a helper function to execute a command and check for the expected FTP
+// return code
+func (c *ServerConn) cmd(expected int, format string, args ...interface{}) (int, string, error) {
+	_, err := c.conn.Cmd(format, args...)
+	if err != nil {
+		return 0, "", err
+	}
+
+	return c.conn.ReadResponse(expected)
+}
+
+// cmdDataConnFrom executes a command which require a FTP data connection.
+// Issues a REST FTP command to specify the number of bytes to skip for the transfer.
+func (c *ServerConn) cmdDataConnFrom(offset uint64, format string, args ...interface{}) (net.Conn, error) {
+	conn, err := c.openDataConn()
+	if err != nil {
+		return nil, err
+	}
+
+	if offset != 0 {
+		_, _, err := c.cmd(StatusRequestFilePending, "REST %d", offset)
+		if err != nil {
+			conn.Close()
+			return nil, err
+		}
+	}
+
+	_, err = c.conn.Cmd(format, args...)
+	if err != nil {
+		conn.Close()
+		return nil, err
+	}
+
+	code, msg, err := c.conn.ReadResponse(-1)
+	if err != nil {
+		conn.Close()
+		return nil, err
+	}
+	if code != StatusAlreadyOpen && code != StatusAboutToSend {
+		conn.Close()
+		return nil, &textproto.Error{Code: code, Msg: msg}
+	}
+
+	return conn, nil
+}
+
+// NameList issues an NLST FTP command.
+func (c *ServerConn) NameList(path string) (entries []string, err error) {
+	conn, err := c.cmdDataConnFrom(0, "NLST %s", path)
+	if err != nil {
+		return
+	}
+
+	r := &Response{conn: conn, c: c}
+	defer r.Close()
+
+	scanner := bufio.NewScanner(r)
+	for scanner.Scan() {
+		entries = append(entries, scanner.Text())
+	}
+	if err = scanner.Err(); err != nil {
+		return entries, err
+	}
+	return
+}
+
+// List issues a LIST FTP command.
+func (c *ServerConn) List(path string) (entries []*Entry, err error) {
+	var cmd string
+	var parseFunc func(string) (*Entry, error)
+
+	if c.mlstSupported {
+		cmd = "MLSD"
+		parseFunc = parseRFC3659ListLine
+	} else {
+		cmd = "LIST"
+		parseFunc = parseListLine
+	}
+
+	conn, err := c.cmdDataConnFrom(0, "%s %s", cmd, path)
+	if err != nil {
+		return
+	}
+
+	r := &Response{conn: conn, c: c}
+	defer r.Close()
+
+	scanner := bufio.NewScanner(r)
+	for scanner.Scan() {
+		entry, err := parseFunc(scanner.Text())
+		if err == nil {
+			entries = append(entries, entry)
+		}
+	}
+	if err := scanner.Err(); err != nil {
+		return nil, err
+	}
+	return
+}
+
+// ChangeDir issues a CWD FTP command, which changes the current directory to
+// the specified path.
+func (c *ServerConn) ChangeDir(path string) error {
+	_, _, err := c.cmd(StatusRequestedFileActionOK, "CWD %s", path)
+	return err
+}
+
+// ChangeDirToParent issues a CDUP FTP command, which changes the current
+// directory to the parent directory.  This is similar to a call to ChangeDir
+// with a path set to "..".
+func (c *ServerConn) ChangeDirToParent() error {
+	_, _, err := c.cmd(StatusRequestedFileActionOK, "CDUP")
+	return err
+}
+
+// CurrentDir issues a PWD FTP command, which Returns the path of the current
+// directory.
+func (c *ServerConn) CurrentDir() (string, error) {
+	_, msg, err := c.cmd(StatusPathCreated, "PWD")
+	if err != nil {
+		return "", err
+	}
+
+	start := strings.Index(msg, "\"")
+	end := strings.LastIndex(msg, "\"")
+
+	if start == -1 || end == -1 {
+		return "", errors.New("Unsuported PWD response format")
+	}
+
+	return msg[start+1 : end], nil
+}
+
+// FileSize issues a SIZE FTP command, which Returns the size of the file
+func (c *ServerConn) FileSize(path string) (int64, error) {
+	_, msg, err := c.cmd(StatusFile, "SIZE %s", path)
+	if err != nil {
+		return 0, err
+	}
+
+	return strconv.ParseInt(msg, 10, 64)
+}
+
+// Retr issues a RETR FTP command to fetch the specified file from the remote
+// FTP server.
+//
+// The returned ReadCloser must be closed to cleanup the FTP data connection.
+func (c *ServerConn) Retr(path string) (*Response, error) {
+	return c.RetrFrom(path, 0)
+}
+
+// RetrFrom issues a RETR FTP command to fetch the specified file from the remote
+// FTP server, the server will not send the offset first bytes of the file.
+//
+// The returned ReadCloser must be closed to cleanup the FTP data connection.
+func (c *ServerConn) RetrFrom(path string, offset uint64) (*Response, error) {
+	conn, err := c.cmdDataConnFrom(offset, "RETR %s", path)
+	if err != nil {
+		return nil, err
+	}
+
+	return &Response{conn: conn, c: c}, nil
+}
+
+// Stor issues a STOR FTP command to store a file to the remote FTP server.
+// Stor creates the specified file with the content of the io.Reader.
+//
+// Hint: io.Pipe() can be used if an io.Writer is required.
+func (c *ServerConn) Stor(path string, r io.Reader) error {
+	return c.StorFrom(path, r, 0)
+}
+
+// StorFrom issues a STOR FTP command to store a file to the remote FTP server.
+// Stor creates the specified file with the content of the io.Reader, writing
+// on the server will start at the given file offset.
+//
+// Hint: io.Pipe() can be used if an io.Writer is required.
+func (c *ServerConn) StorFrom(path string, r io.Reader, offset uint64) error {
+	conn, err := c.cmdDataConnFrom(offset, "STOR %s", path)
+	if err != nil {
+		return err
+	}
+
+	_, err = io.Copy(conn, r)
+	conn.Close()
+	if err != nil {
+		return err
+	}
+
+	_, _, err = c.conn.ReadResponse(StatusClosingDataConnection)
+	return err
+}
+
+// Rename renames a file on the remote FTP server.
+func (c *ServerConn) Rename(from, to string) error {
+	_, _, err := c.cmd(StatusRequestFilePending, "RNFR %s", from)
+	if err != nil {
+		return err
+	}
+
+	_, _, err = c.cmd(StatusRequestedFileActionOK, "RNTO %s", to)
+	return err
+}
+
+// Delete issues a DELE FTP command to delete the specified file from the
+// remote FTP server.
+func (c *ServerConn) Delete(path string) error {
+	_, _, err := c.cmd(StatusRequestedFileActionOK, "DELE %s", path)
+	return err
+}
+
+// MakeDir issues a MKD FTP command to create the specified directory on the
+// remote FTP server.
+func (c *ServerConn) MakeDir(path string) error {
+	_, _, err := c.cmd(StatusPathCreated, "MKD %s", path)
+	return err
+}
+
+// RemoveDir issues a RMD FTP command to remove the specified directory from
+// the remote FTP server.
+func (c *ServerConn) RemoveDir(path string) error {
+	_, _, err := c.cmd(StatusRequestedFileActionOK, "RMD %s", path)
+	return err
+}
+
+// NoOp issues a NOOP FTP command.
+// NOOP has no effects and is usually used to prevent the remote FTP server to
+// close the otherwise idle connection.
+func (c *ServerConn) NoOp() error {
+	_, _, err := c.cmd(StatusCommandOK, "NOOP")
+	return err
+}
+
+// Logout issues a REIN FTP command to logout the current user.
+func (c *ServerConn) Logout() error {
+	_, _, err := c.cmd(StatusReady, "REIN")
+	return err
+}
+
+// Quit issues a QUIT FTP command to properly close the connection from the
+// remote FTP server.
+func (c *ServerConn) Quit() error {
+	c.conn.Cmd("QUIT")
+	return c.conn.Close()
+}
+
+// Read implements the io.Reader interface on a FTP data connection.
+func (r *Response) Read(buf []byte) (int, error) {
+	return r.conn.Read(buf)
+}
+
+// Close implements the io.Closer interface on a FTP data connection.
+// After the first call, Close will do nothing and return nil.
+func (r *Response) Close() error {
+	if r.closed {
+		return nil
+	}
+	err := r.conn.Close()
+	_, _, err2 := r.c.conn.ReadResponse(StatusClosingDataConnection)
+	if err2 != nil {
+		err = err2
+	}
+	r.closed = true
+	return err
+}
+
+// SetDeadline sets the deadlines associated with the connection.
+func (r *Response) SetDeadline(t time.Time) error {
+	return r.conn.SetDeadline(t)
+}
diff --git a/vendor/github.com/jlaffaye/ftp/parse.go b/vendor/github.com/jlaffaye/ftp/parse.go
new file mode 100644
index 000000000..076062a48
--- /dev/null
+++ b/vendor/github.com/jlaffaye/ftp/parse.go
@@ -0,0 +1,213 @@
+package ftp
+
+import (
+	"errors"
+	"strconv"
+	"strings"
+	"time"
+)
+
+var errUnsupportedListLine = errors.New("Unsupported LIST line")
+
+var listLineParsers = []func(line string) (*Entry, error){
+	parseRFC3659ListLine,
+	parseLsListLine,
+	parseDirListLine,
+}
+
+var dirTimeFormats = []string{
+	"01-02-06  03:04PM",
+	"2006-01-02  15:04",
+}
+
+// parseRFC3659ListLine parses the style of directory line defined in RFC 3659.
+func parseRFC3659ListLine(line string) (*Entry, error) {
+	iSemicolon := strings.Index(line, ";")
+	iWhitespace := strings.Index(line, " ")
+
+	if iSemicolon < 0 || iSemicolon > iWhitespace {
+		return nil, errUnsupportedListLine
+	}
+
+	e := &Entry{
+		Name: line[iWhitespace+1:],
+	}
+
+	for _, field := range strings.Split(line[:iWhitespace-1], ";") {
+		i := strings.Index(field, "=")
+		if i < 1 {
+			return nil, errUnsupportedListLine
+		}
+
+		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
+}
+
+// parseLsListLine parses a directory line in a format based on the output of
+// the UNIX ls command.
+func parseLsListLine(line string) (*Entry, error) {
+
+	// Has the first field a length of 10 bytes?
+	if strings.IndexByte(line, ' ') != 10 {
+		return nil, errUnsupportedListLine
+	}
+
+	scanner := newScanner(line)
+	fields := scanner.NextFields(6)
+
+	if len(fields) < 6 {
+		return nil, errUnsupportedListLine
+	}
+
+	if fields[1] == "folder" && fields[2] == "0" {
+		e := &Entry{
+			Type: EntryTypeFolder,
+			Name: scanner.Remaining(),
+		}
+		if err := e.setTime(fields[3:6]); err != nil {
+			return nil, err
+		}
+
+		return e, nil
+	}
+
+	if fields[1] == "0" {
+		fields = append(fields, scanner.Next())
+		e := &Entry{
+			Type: EntryTypeFile,
+			Name: scanner.Remaining(),
+		}
+
+		if err := e.setSize(fields[2]); err != nil {
+			return nil, err
+		}
+		if err := e.setTime(fields[4:7]); err != nil {
+			return nil, err
+		}
+
+		return e, nil
+	}
+
+	// Read two more fields
+	fields = append(fields, scanner.NextFields(2)...)
+	if len(fields) < 8 {
+		return nil, errUnsupportedListLine
+	}
+
+	e := &Entry{
+		Name: scanner.Remaining(),
+	}
+	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
+	}
+
+	return e, nil
+}
+
+// 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 {
+		if len(line) > len(format) {
+			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
+	}
+
+	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.TrimLeft(line, " ")
+	return e, nil
+}
+
+// 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 {
+			return e, err
+		}
+	}
+	return nil, errUnsupportedListLine
+}
+
+func (e *Entry) setSize(str string) (err error) {
+	e.Size, err = strconv.ParseUint(str, 0, 64)
+	return
+}
+
+func (e *Entry) setTime(fields []string) (err error) {
+	var timeStr string
+	if strings.Contains(fields[2], ":") { // this year
+		thisYear, _, _ := time.Now().Date()
+		timeStr = fields[1] + " " + fields[0] + " " + strconv.Itoa(thisYear)[2:4] + " " + fields[2] + " GMT"
+	} else { // not this year
+		if len(fields[2]) != 4 {
+			return errors.New("Invalid year format in time string")
+		}
+		timeStr = fields[1] + " " + fields[0] + " " + fields[2][2:4] + " 00:00 GMT"
+	}
+	e.Time, err = time.Parse("_2 Jan 06 15:04 MST", timeStr)
+	return
+}
diff --git a/vendor/github.com/jlaffaye/ftp/parse_test.go b/vendor/github.com/jlaffaye/ftp/parse_test.go
new file mode 100644
index 000000000..ab44c3848
--- /dev/null
+++ b/vendor/github.com/jlaffaye/ftp/parse_test.go
@@ -0,0 +1,104 @@
+package ftp
+
+import (
+	"testing"
+	"time"
+)
+
+var thisYear, _, _ = time.Now().Date()
+
+type line struct {
+	line      string
+	name      string
+	size      uint64
+	entryType EntryType
+	time      time.Time
+}
+
+type unsupportedLine struct {
+	line string
+	err  string
+}
+
+var listTests = []line{
+	// UNIX ls -l style
+	{"drwxr-xr-x    3 110      1002            3 Dec 02  2009 pub", "pub", 0, EntryTypeFolder, time.Date(2009, time.December, 2, 0, 0, 0, 0, time.UTC)},
+	{"drwxr-xr-x    3 110      1002            3 Dec 02  2009 p u b", "p u b", 0, EntryTypeFolder, time.Date(2009, time.December, 2, 0, 0, 0, 0, time.UTC)},
+	{"-rw-r--r--   1 marketwired marketwired    12016 Mar 16  2016 2016031611G087802-001.newsml", "2016031611G087802-001.newsml", 12016, EntryTypeFile, time.Date(2016, time.March, 16, 0, 0, 0, 0, time.UTC)},
+
+	{"-rwxr-xr-x    3 110      1002            1234567 Dec 02  2009 fileName", "fileName", 1234567, EntryTypeFile, time.Date(2009, time.December, 2, 0, 0, 0, 0, time.UTC)},
+	{"lrwxrwxrwx   1 root     other          7 Jan 25 00:17 bin -> usr/bin", "bin -> usr/bin", 0, EntryTypeLink, time.Date(thisYear, time.January, 25, 0, 17, 0, 0, time.UTC)},
+
+	// Another ls style
+	{"drwxr-xr-x               folder        0 Aug 15 05:49 !!!-Tipp des Haus!", "!!!-Tipp des Haus!", 0, EntryTypeFolder, time.Date(thisYear, time.August, 15, 5, 49, 0, 0, time.UTC)},
+	{"drwxrwxrwx               folder        0 Aug 11 20:32 P0RN", "P0RN", 0, EntryTypeFolder, time.Date(thisYear, time.August, 11, 20, 32, 0, 0, time.UTC)},
+	{"-rw-r--r--        0   18446744073709551615 18446744073709551615 Nov 16  2006 VIDEO_TS.VOB", "VIDEO_TS.VOB", 18446744073709551615, EntryTypeFile, time.Date(2006, time.November, 16, 0, 0, 0, 0, time.UTC)},
+
+	// Microsoft's FTP servers for Windows
+	{"----------   1 owner    group         1803128 Jul 10 10:18 ls-lR.Z", "ls-lR.Z", 1803128, EntryTypeFile, time.Date(thisYear, time.July, 10, 10, 18, 0, 0, time.UTC)},
+	{"d---------   1 owner    group               0 May  9 19:45 Softlib", "Softlib", 0, EntryTypeFolder, time.Date(thisYear, time.May, 9, 19, 45, 0, 0, time.UTC)},
+
+	// WFTPD for MSDOS
+	{"-rwxrwxrwx   1 noone    nogroup      322 Aug 19  1996 message.ftp", "message.ftp", 322, EntryTypeFile, time.Date(1996, time.August, 19, 0, 0, 0, 0, time.UTC)},
+
+	// RFC3659 format: https://tools.ietf.org/html/rfc3659#section-7
+	{"modify=20150813224845;perm=fle;type=cdir;unique=119FBB87U4;UNIX.group=0;UNIX.mode=0755;UNIX.owner=0; .", ".", 0, EntryTypeFolder, time.Date(2015, time.August, 13, 22, 48, 45, 0, time.UTC)},
+	{"modify=20150813224845;perm=fle;type=pdir;unique=119FBB87U4;UNIX.group=0;UNIX.mode=0755;UNIX.owner=0; ..", "..", 0, EntryTypeFolder, time.Date(2015, time.August, 13, 22, 48, 45, 0, time.UTC)},
+	{"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)},
+
+	// dir and file names that contain multiple spaces
+	{"drwxr-xr-x    3 110      1002            3 Dec 02  2009 spaces   dir   name", "spaces   dir   name", 0, EntryTypeFolder, time.Date(2009, time.December, 2, 0, 0, 0, 0, time.UTC)},
+	{"-rwxr-xr-x    3 110      1002            1234567 Dec 02  2009 file   name", "file   name", 1234567, EntryTypeFile, time.Date(2009, time.December, 2, 0, 0, 0, 0, time.UTC)},
+	{"-rwxr-xr-x    3 110      1002            1234567 Dec 02  2009  foo bar ", " foo bar ", 1234567, EntryTypeFile, time.Date(2009, time.December, 2, 0, 0, 0, 0, time.UTC)},
+}
+
+// Not supported, we expect a specific error message
+var listTestsFail = []unsupportedLine{
+	{"d [R----F--] supervisor            512       Jan 16 18:53 login", "Unsupported LIST line"},
+	{"- [R----F--] rhesus             214059       Oct 20 15:27 cx.exe", "Unsupported LIST line"},
+	{"drwxr-xr-x    3 110      1002            3 Dec 02  209 pub", "Invalid year format in time string"},
+	{"modify=20150806235817;invalid;UNIX.owner=0; movies", "Unsupported LIST line"},
+	{"Zrwxrwxrwx   1 root     other          7 Jan 25 00:17 bin -> usr/bin", "Unknown entry type"},
+	{"total 1", "Unsupported LIST line"},
+	{"", "Unsupported LIST line"},
+}
+
+func TestParseValidListLine(t *testing.T) {
+	for _, lt := range listTests {
+		entry, err := parseListLine(lt.line)
+		if err != nil {
+			t.Errorf("parseListLine(%v) returned err = %v", lt.line, err)
+			continue
+		}
+		if entry.Name != lt.name {
+			t.Errorf("parseListLine(%v).Name = '%v', want '%v'", lt.line, entry.Name, lt.name)
+		}
+		if entry.Type != lt.entryType {
+			t.Errorf("parseListLine(%v).EntryType = %v, want %v", lt.line, entry.Type, lt.entryType)
+		}
+		if entry.Size != lt.size {
+			t.Errorf("parseListLine(%v).Size = %v, want %v", lt.line, entry.Size, lt.size)
+		}
+		if entry.Time.Unix() != lt.time.Unix() {
+			t.Errorf("parseListLine(%v).Time = %v, want %v", lt.line, entry.Time, lt.time)
+		}
+	}
+}
+
+func TestParseUnsupportedListLine(t *testing.T) {
+	for _, lt := range listTestsFail {
+		_, err := parseListLine(lt.line)
+		if err == nil {
+			t.Errorf("parseListLine(%v) expected to fail", lt.line)
+		}
+		if err.Error() != lt.err {
+			t.Errorf("parseListLine(%v) expected to fail with error: '%s'; was: '%s'", lt.line, lt.err, err.Error())
+		}
+	}
+}
diff --git a/vendor/github.com/jlaffaye/ftp/scanner.go b/vendor/github.com/jlaffaye/ftp/scanner.go
new file mode 100644
index 000000000..0dcc8ae64
--- /dev/null
+++ b/vendor/github.com/jlaffaye/ftp/scanner.go
@@ -0,0 +1,58 @@
+package ftp
+
+// A scanner for fields delimited by one or more whitespace characters
+type scanner struct {
+	bytes    []byte
+	position int
+}
+
+// newScanner creates a new scanner
+func newScanner(str string) *scanner {
+	return &scanner{
+		bytes: []byte(str),
+	}
+}
+
+// NextFields returns the next `count` fields
+func (s *scanner) NextFields(count int) []string {
+	fields := make([]string, 0, count)
+	for i := 0; i < count; i++ {
+		if field := s.Next(); field != "" {
+			fields = append(fields, field)
+		} else {
+			break
+		}
+	}
+	return fields
+}
+
+// Next returns the next field
+func (s *scanner) Next() string {
+	sLen := len(s.bytes)
+
+	// skip trailing whitespace
+	for s.position < sLen {
+		if s.bytes[s.position] != ' ' {
+			break
+		}
+		s.position++
+	}
+
+	start := s.position
+
+	// skip non-whitespace
+	for s.position < sLen {
+		if s.bytes[s.position] == ' ' {
+			s.position++
+			return string(s.bytes[start : s.position-1])
+		}
+		s.position++
+	}
+
+	return string(s.bytes[start:s.position])
+}
+
+// Remaining returns the remaining string
+func (s *scanner) Remaining() string {
+	return string(s.bytes[s.position:len(s.bytes)])
+}
diff --git a/vendor/github.com/jlaffaye/ftp/scanner_test.go b/vendor/github.com/jlaffaye/ftp/scanner_test.go
new file mode 100644
index 000000000..86ff4bf86
--- /dev/null
+++ b/vendor/github.com/jlaffaye/ftp/scanner_test.go
@@ -0,0 +1,28 @@
+package ftp
+
+import "testing"
+import "github.com/stretchr/testify/assert"
+
+func TestScanner(t *testing.T) {
+	assert := assert.New(t)
+
+	s := newScanner("foo  bar x  y")
+	assert.Equal("foo", s.Next())
+	assert.Equal(" bar x  y", s.Remaining())
+	assert.Equal("bar", s.Next())
+	assert.Equal("x  y", s.Remaining())
+	assert.Equal("x", s.Next())
+	assert.Equal(" y", s.Remaining())
+	assert.Equal("y", s.Next())
+	assert.Equal("", s.Next())
+	assert.Equal("", s.Remaining())
+}
+
+func TestScannerEmpty(t *testing.T) {
+	assert := assert.New(t)
+
+	s := newScanner("")
+	assert.Equal("", s.Next())
+	assert.Equal("", s.Next())
+	assert.Equal("", s.Remaining())
+}
diff --git a/vendor/github.com/jlaffaye/ftp/status.go b/vendor/github.com/jlaffaye/ftp/status.go
new file mode 100644
index 000000000..e90ca6211
--- /dev/null
+++ b/vendor/github.com/jlaffaye/ftp/status.go
@@ -0,0 +1,106 @@
+package ftp
+
+// FTP status codes, defined in RFC 959
+const (
+	StatusInitiating    = 100
+	StatusRestartMarker = 110
+	StatusReadyMinute   = 120
+	StatusAlreadyOpen   = 125
+	StatusAboutToSend   = 150
+
+	StatusCommandOK             = 200
+	StatusCommandNotImplemented = 202
+	StatusSystem                = 211
+	StatusDirectory             = 212
+	StatusFile                  = 213
+	StatusHelp                  = 214
+	StatusName                  = 215
+	StatusReady                 = 220
+	StatusClosing               = 221
+	StatusDataConnectionOpen    = 225
+	StatusClosingDataConnection = 226
+	StatusPassiveMode           = 227
+	StatusLongPassiveMode       = 228
+	StatusExtendedPassiveMode   = 229
+	StatusLoggedIn              = 230
+	StatusLoggedOut             = 231
+	StatusLogoutAck             = 232
+	StatusRequestedFileActionOK = 250
+	StatusPathCreated           = 257
+
+	StatusUserOK             = 331
+	StatusLoginNeedAccount   = 332
+	StatusRequestFilePending = 350
+
+	StatusNotAvailable             = 421
+	StatusCanNotOpenDataConnection = 425
+	StatusTransfertAborted         = 426
+	StatusInvalidCredentials       = 430
+	StatusHostUnavailable          = 434
+	StatusFileActionIgnored        = 450
+	StatusActionAborted            = 451
+	Status452                      = 452
+
+	StatusBadCommand              = 500
+	StatusBadArguments            = 501
+	StatusNotImplemented          = 502
+	StatusBadSequence             = 503
+	StatusNotImplementedParameter = 504
+	StatusNotLoggedIn             = 530
+	StatusStorNeedAccount         = 532
+	StatusFileUnavailable         = 550
+	StatusPageTypeUnknown         = 551
+	StatusExceededStorage         = 552
+	StatusBadFileName             = 553
+)
+
+var statusText = map[int]string{
+	// 200
+	StatusCommandOK:             "Command okay.",
+	StatusCommandNotImplemented: "Command not implemented, superfluous at this site.",
+	StatusSystem:                "System status, or system help reply.",
+	StatusDirectory:             "Directory status.",
+	StatusFile:                  "File status.",
+	StatusHelp:                  "Help message.",
+	StatusName:                  "",
+	StatusReady:                 "Service ready for new user.",
+	StatusClosing:               "Service closing control connection.",
+	StatusDataConnectionOpen:    "Data connection open; no transfer in progress.",
+	StatusClosingDataConnection: "Closing data connection. Requested file action successful.",
+	StatusPassiveMode:           "Entering Passive Mode.",
+	StatusLongPassiveMode:       "Entering Long Passive Mode.",
+	StatusExtendedPassiveMode:   "Entering Extended Passive Mode.",
+	StatusLoggedIn:              "User logged in, proceed.",
+	StatusLoggedOut:             "User logged out; service terminated.",
+	StatusLogoutAck:             "Logout command noted, will complete when transfer done.",
+	StatusRequestedFileActionOK: "Requested file action okay, completed.",
+	StatusPathCreated:           "Path created.",
+
+	// 300
+	StatusUserOK:             "User name okay, need password.",
+	StatusLoginNeedAccount:   "Need account for login.",
+	StatusRequestFilePending: "Requested file action pending further information.",
+
+	// 400
+	StatusNotAvailable:             "Service not available, closing control connection.",
+	StatusCanNotOpenDataConnection: "Can't open data connection.",
+	StatusTransfertAborted:         "Connection closed; transfer aborted.",
+	StatusInvalidCredentials:       "Invalid username or password.",
+	StatusHostUnavailable:          "Requested host unavailable.",
+	StatusFileActionIgnored:        "Requested file action not taken.",
+	StatusActionAborted:            "Requested action aborted. Local error in processing.",
+	Status452:                      "Insufficient storage space in system.",
+
+	// 500
+	StatusBadCommand:              "Command unrecognized.",
+	StatusBadArguments:            "Syntax error in parameters or arguments.",
+	StatusNotImplemented:          "Command not implemented.",
+	StatusBadSequence:             "Bad sequence of commands.",
+	StatusNotImplementedParameter: "Command not implemented for that parameter.",
+	StatusNotLoggedIn:             "Not logged in.",
+	StatusStorNeedAccount:         "Need account for storing files.",
+	StatusFileUnavailable:         "File unavailable.",
+	StatusPageTypeUnknown:         "Page type unknown.",
+	StatusExceededStorage:         "Exceeded storage allocation.",
+	StatusBadFileName:             "File name not allowed.",
+}