Vendor github.com/jlaffaye/ftp for ftp backend

This commit is contained in:
Nick Craig-Wood 2017-05-16 18:01:02 +01:00
parent 35c210d36f
commit af043eda15
15 changed files with 1537 additions and 1 deletions

8
Gopkg.lock generated
View File

@ -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 = ["."]

15
vendor/github.com/jlaffaye/ftp/.travis.yml generated vendored Normal file
View File

@ -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

18
vendor/github.com/jlaffaye/ftp/.travis/prepare.sh generated vendored Executable file
View File

@ -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"

9
vendor/github.com/jlaffaye/ftp/.travis/proftpd.conf generated vendored Normal file
View File

@ -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>

15
vendor/github.com/jlaffaye/ftp/.travis/vsftpd.conf generated vendored Normal file
View File

@ -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

13
vendor/github.com/jlaffaye/ftp/LICENSE generated vendored Normal file
View File

@ -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.

17
vendor/github.com/jlaffaye/ftp/README.md generated vendored Normal file
View File

@ -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

107
vendor/github.com/jlaffaye/ftp/client_multiline_test.go generated vendored Normal file
View File

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

270
vendor/github.com/jlaffaye/ftp/client_test.go generated vendored Normal file
View File

@ -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")
}
}

557
vendor/github.com/jlaffaye/ftp/ftp.go generated vendored Normal file
View File

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

213
vendor/github.com/jlaffaye/ftp/parse.go generated vendored Normal file
View File

@ -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
}

104
vendor/github.com/jlaffaye/ftp/parse_test.go generated vendored Normal file
View File

@ -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())
}
}
}

58
vendor/github.com/jlaffaye/ftp/scanner.go generated vendored Normal file
View File

@ -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)])
}

28
vendor/github.com/jlaffaye/ftp/scanner_test.go generated vendored Normal file
View File

@ -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())
}

106
vendor/github.com/jlaffaye/ftp/status.go generated vendored Normal file
View File

@ -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.",
}