Compare commits
19 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
99be0634ab | ||
|
|
b85cf1edcc | ||
|
|
0aeb8660a7 | ||
|
|
4d1d644cf1 | ||
|
|
5a2fd50da8 | ||
|
|
9cda78131d | ||
|
|
39592b91e4 | ||
|
|
fa83b53d0e | ||
|
|
45482d097e | ||
|
|
560423fa8a | ||
|
|
11536801d1 | ||
|
|
6c74f63d3c | ||
|
|
60a941566c | ||
|
|
712e6cb8bc | ||
|
|
6696131620 | ||
|
|
dfa1e758f3 | ||
|
|
b29e1f6c62 | ||
|
|
190f39e8b2 | ||
|
|
d2c44e311e |
13
.github/dependabot.yml
vendored
Normal file
13
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
# To get started with Dependabot version updates, you'll need to specify which
|
||||||
|
# package ecosystems to update and where the package manifests are located.
|
||||||
|
# Please see the documentation for all configuration options:
|
||||||
|
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
|
||||||
|
|
||||||
|
version: 2
|
||||||
|
updates:
|
||||||
|
- package-ecosystem: "gomod" # See documentation for possible values
|
||||||
|
directory: "/" # Location of package manifests
|
||||||
|
schedule:
|
||||||
|
interval: "daily"
|
||||||
|
assignees:
|
||||||
|
- "jlaffaye"
|
||||||
191
client_test.go
191
client_test.go
@@ -3,10 +3,8 @@ package ftp
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
"net/textproto"
|
|
||||||
"strings"
|
|
||||||
"syscall"
|
"syscall"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
@@ -28,174 +26,145 @@ func TestConnEPSV(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func testConn(t *testing.T, disableEPSV bool) {
|
func testConn(t *testing.T, disableEPSV bool) {
|
||||||
|
assert := assert.New(t)
|
||||||
mock, c := openConn(t, "127.0.0.1", DialWithTimeout(5*time.Second), DialWithDisabledEPSV(disableEPSV))
|
mock, c := openConn(t, "127.0.0.1", DialWithTimeout(5*time.Second), DialWithDisabledEPSV(disableEPSV))
|
||||||
|
|
||||||
err := c.Login("anonymous", "anonymous")
|
err := c.Login("anonymous", "anonymous")
|
||||||
if err != nil {
|
assert.NoError(err)
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = c.NoOp()
|
err = c.NoOp()
|
||||||
if err != nil {
|
assert.NoError(err)
|
||||||
t.Error(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = c.ChangeDir("incoming")
|
err = c.ChangeDir("incoming")
|
||||||
if err != nil {
|
assert.NoError(err)
|
||||||
t.Error(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
dir, err := c.CurrentDir()
|
dir, err := c.CurrentDir()
|
||||||
if err != nil {
|
if assert.NoError(err) {
|
||||||
t.Error(err)
|
assert.Equal("/incoming", dir)
|
||||||
} else {
|
|
||||||
if dir != "/incoming" {
|
|
||||||
t.Error("Wrong dir: " + dir)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
data := bytes.NewBufferString(testData)
|
data := bytes.NewBufferString(testData)
|
||||||
err = c.Stor("test", data)
|
err = c.Stor("test", data)
|
||||||
if err != nil {
|
assert.NoError(err)
|
||||||
t.Error(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = c.List(".")
|
_, err = c.List(".")
|
||||||
if err != nil {
|
assert.NoError(err)
|
||||||
t.Error(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = c.Rename("test", "tset")
|
err = c.Rename("test", "tset")
|
||||||
if err != nil {
|
assert.NoError(err)
|
||||||
t.Error(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read without deadline
|
// Read without deadline
|
||||||
r, err := c.Retr("tset")
|
r, err := c.Retr("tset")
|
||||||
if err != nil {
|
if assert.NoError(err) {
|
||||||
t.Error(err)
|
buf, err := io.ReadAll(r)
|
||||||
} else {
|
if assert.NoError(err) {
|
||||||
buf, errRead := ioutil.ReadAll(r)
|
assert.Equal(testData, string(buf))
|
||||||
if err != nil {
|
|
||||||
t.Error(errRead)
|
|
||||||
}
|
|
||||||
if string(buf) != testData {
|
|
||||||
t.Errorf("'%s'", buf)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
r.Close()
|
r.Close()
|
||||||
r.Close() // test we can close two times
|
r.Close() // test we can close two times
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read with deadline
|
// Read with deadline
|
||||||
r, err = c.Retr("tset")
|
r, err = c.Retr("tset")
|
||||||
if err != nil {
|
if assert.NoError(err) {
|
||||||
t.Error(err)
|
|
||||||
} else {
|
|
||||||
if err := r.SetDeadline(time.Now()); err != nil {
|
if err := r.SetDeadline(time.Now()); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
_, err = ioutil.ReadAll(r)
|
_, err = io.ReadAll(r)
|
||||||
if err == nil {
|
assert.ErrorContains(err, "i/o timeout")
|
||||||
t.Error("deadline should have caused error")
|
|
||||||
} else if !strings.HasSuffix(err.Error(), "i/o timeout") {
|
|
||||||
t.Error(err)
|
|
||||||
}
|
|
||||||
r.Close()
|
r.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read with offset
|
// Read with offset
|
||||||
r, err = c.RetrFrom("tset", 5)
|
r, err = c.RetrFrom("tset", 5)
|
||||||
if err != nil {
|
if assert.NoError(err) {
|
||||||
t.Error(err)
|
buf, err := io.ReadAll(r)
|
||||||
} else {
|
if assert.NoError(err) {
|
||||||
buf, errRead := ioutil.ReadAll(r)
|
|
||||||
if errRead != nil {
|
|
||||||
t.Error(errRead)
|
|
||||||
}
|
|
||||||
expected := testData[5:]
|
expected := testData[5:]
|
||||||
if string(buf) != expected {
|
assert.Equal(expected, string(buf))
|
||||||
t.Errorf("read %q, expected %q", buf, expected)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
r.Close()
|
r.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
data2 := bytes.NewBufferString(testData)
|
data2 := bytes.NewBufferString(testData)
|
||||||
err = c.Append("tset", data2)
|
err = c.Append("tset", data2)
|
||||||
if err != nil {
|
assert.NoError(err)
|
||||||
t.Error(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read without deadline, after append
|
// Read without deadline, after append
|
||||||
r, err = c.Retr("tset")
|
r, err = c.Retr("tset")
|
||||||
if err != nil {
|
if assert.NoError(err) {
|
||||||
t.Error(err)
|
buf, err := io.ReadAll(r)
|
||||||
} else {
|
if assert.NoError(err) {
|
||||||
buf, errRead := ioutil.ReadAll(r)
|
assert.Equal(testData+testData, string(buf))
|
||||||
if err != nil {
|
|
||||||
t.Error(errRead)
|
|
||||||
}
|
|
||||||
if string(buf) != testData+testData {
|
|
||||||
t.Errorf("'%s'", buf)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
r.Close()
|
r.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
fileSize, err := c.FileSize("magic-file")
|
fileSize, err := c.FileSize("magic-file")
|
||||||
|
assert.NoError(err)
|
||||||
|
assert.Equal(int64(42), fileSize)
|
||||||
|
|
||||||
|
_, err = c.FileSize("not-found")
|
||||||
|
assert.Error(err)
|
||||||
|
|
||||||
|
entry, err := c.GetEntry("magic-file")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Error(err)
|
t.Error(err)
|
||||||
}
|
}
|
||||||
if fileSize != 42 {
|
if entry == nil {
|
||||||
t.Errorf("file size %q, expected %q", fileSize, 42)
|
t.Fatal("expected entry, got nil")
|
||||||
|
}
|
||||||
|
if entry.Size != 42 {
|
||||||
|
t.Errorf("entry size %q, expected %q", entry.Size, 42)
|
||||||
|
}
|
||||||
|
if entry.Type != EntryTypeFile {
|
||||||
|
t.Errorf("entry type %q, expected %q", entry.Type, EntryTypeFile)
|
||||||
|
}
|
||||||
|
if entry.Name != "magic-file" {
|
||||||
|
t.Errorf("entry name %q, expected %q", entry.Name, "magic-file")
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = c.FileSize("not-found")
|
entry, err = c.GetEntry("multiline-dir")
|
||||||
if err == nil {
|
if err != nil {
|
||||||
t.Fatal("expected error, got nil")
|
t.Error(err)
|
||||||
|
}
|
||||||
|
if entry == nil {
|
||||||
|
t.Fatal("expected entry, got nil")
|
||||||
|
}
|
||||||
|
if entry.Size != 0 {
|
||||||
|
t.Errorf("entry size %q, expected %q", entry.Size, 0)
|
||||||
|
}
|
||||||
|
if entry.Type != EntryTypeFolder {
|
||||||
|
t.Errorf("entry type %q, expected %q", entry.Type, EntryTypeFolder)
|
||||||
|
}
|
||||||
|
if entry.Name != "multiline-dir" {
|
||||||
|
t.Errorf("entry name %q, expected %q", entry.Name, "multiline-dir")
|
||||||
}
|
}
|
||||||
|
|
||||||
err = c.Delete("tset")
|
err = c.Delete("tset")
|
||||||
if err != nil {
|
assert.NoError(err)
|
||||||
t.Error(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = c.MakeDir(testDir)
|
err = c.MakeDir(testDir)
|
||||||
if err != nil {
|
assert.NoError(err)
|
||||||
t.Error(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = c.ChangeDir(testDir)
|
err = c.ChangeDir(testDir)
|
||||||
if err != nil {
|
assert.NoError(err)
|
||||||
t.Error(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = c.ChangeDirToParent()
|
err = c.ChangeDirToParent()
|
||||||
if err != nil {
|
assert.NoError(err)
|
||||||
t.Error(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
entries, err := c.NameList("/")
|
entries, err := c.NameList("/")
|
||||||
if err != nil {
|
assert.NoError(err)
|
||||||
t.Error(err)
|
assert.Equal([]string{"/incoming"}, entries)
|
||||||
}
|
|
||||||
if len(entries) != 1 || entries[0] != "/incoming" {
|
|
||||||
t.Errorf("Unexpected entries: %v", entries)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = c.RemoveDir(testDir)
|
err = c.RemoveDir(testDir)
|
||||||
if err != nil {
|
assert.NoError(err)
|
||||||
t.Error(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = c.Logout()
|
err = c.Logout()
|
||||||
if err != nil {
|
assert.NoError(err)
|
||||||
if protoErr := err.(*textproto.Error); protoErr != nil {
|
|
||||||
if protoErr.Code != StatusNotImplemented {
|
|
||||||
t.Error(err)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
t.Error(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = c.Quit(); err != nil {
|
if err = c.Quit(); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
@@ -205,9 +174,7 @@ func testConn(t *testing.T, disableEPSV bool) {
|
|||||||
mock.Wait()
|
mock.Wait()
|
||||||
|
|
||||||
err = c.NoOp()
|
err = c.NoOp()
|
||||||
if err == nil {
|
assert.Error(err, "should error on closed conn")
|
||||||
t.Error("Expected error")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestConnect tests the legacy Connect function
|
// TestConnect tests the legacy Connect function
|
||||||
@@ -312,7 +279,7 @@ func TestMissingFolderDeleteDirRecur(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestListCurrentDir(t *testing.T) {
|
func TestListCurrentDir(t *testing.T) {
|
||||||
mock, c := openConn(t, "127.0.0.1")
|
mock, c := openConnExt(t, "127.0.0.1", "no-time", DialWithDisabledMLSD(true))
|
||||||
|
|
||||||
_, err := c.List("")
|
_, err := c.List("")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
@@ -328,6 +295,20 @@ func TestListCurrentDir(t *testing.T) {
|
|||||||
mock.Wait()
|
mock.Wait()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestListCurrentDirWithForceListHidden(t *testing.T) {
|
||||||
|
mock, c := openConnExt(t, "127.0.0.1", "no-time", DialWithDisabledMLSD(true), DialWithForceListHidden(true))
|
||||||
|
|
||||||
|
assert.True(t, c.options.forceListHidden)
|
||||||
|
_, err := c.List("")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, "LIST -a", mock.lastFull, "LIST -a must not have a trailing whitespace")
|
||||||
|
|
||||||
|
err = c.Quit()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
mock.Wait()
|
||||||
|
}
|
||||||
|
|
||||||
func TestTimeUnsupported(t *testing.T) {
|
func TestTimeUnsupported(t *testing.T) {
|
||||||
mock, c := openConnExt(t, "127.0.0.1", "no-time")
|
mock, c := openConnExt(t, "127.0.0.1", "no-time")
|
||||||
|
|
||||||
|
|||||||
41
conn_test.go
41
conn_test.go
@@ -6,12 +6,14 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
"net/textproto"
|
"net/textproto"
|
||||||
"reflect"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ftpMock struct {
|
type ftpMock struct {
|
||||||
@@ -88,7 +90,7 @@ func (mock *ftpMock) listen() {
|
|||||||
// At least one command must have a multiline response
|
// At least one command must have a multiline response
|
||||||
switch cmdParts[0] {
|
switch cmdParts[0] {
|
||||||
case "FEAT":
|
case "FEAT":
|
||||||
features := "211-Features:\r\n FEAT\r\n PASV\r\n EPSV\r\n UTF8\r\n SIZE\r\n"
|
features := "211-Features:\r\n FEAT\r\n PASV\r\n EPSV\r\n UTF8\r\n SIZE\r\n MLST\r\n"
|
||||||
switch mock.modtime {
|
switch mock.modtime {
|
||||||
case "std-time":
|
case "std-time":
|
||||||
features += " MDTM\r\n MFMT\r\n"
|
features += " MDTM\r\n MFMT\r\n"
|
||||||
@@ -173,9 +175,26 @@ func (mock *ftpMock) listen() {
|
|||||||
|
|
||||||
mock.dataConn.Wait()
|
mock.dataConn.Wait()
|
||||||
mock.printfLine("150 Opening ASCII mode data connection for file list")
|
mock.printfLine("150 Opening ASCII mode data connection for file list")
|
||||||
mock.dataConn.write([]byte("-rw-r--r-- 1 ftp wheel 0 Jan 29 10:29 lo"))
|
mock.dataConn.write([]byte("-rw-r--r-- 1 ftp wheel 0 Jan 29 10:29 lo\r\ntotal 1"))
|
||||||
mock.printfLine("226 Transfer complete")
|
mock.printfLine("226 Transfer complete")
|
||||||
mock.closeDataConn()
|
mock.closeDataConn()
|
||||||
|
case "MLSD":
|
||||||
|
if mock.dataConn == nil {
|
||||||
|
mock.printfLine("425 Unable to build data connection: Connection refused")
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
mock.dataConn.Wait()
|
||||||
|
mock.printfLine("150 Opening data connection for file list")
|
||||||
|
mock.dataConn.write([]byte("Type=file;Size=0;Modify=20201213202400; lo\r\n"))
|
||||||
|
mock.printfLine("226 Transfer complete")
|
||||||
|
mock.closeDataConn()
|
||||||
|
case "MLST":
|
||||||
|
if cmdParts[1] == "multiline-dir" {
|
||||||
|
mock.printfLine("250-File data\r\n Type=dir;Size=0; multiline-dir\r\n Modify=20201213202400; multiline-dir\r\n250 End")
|
||||||
|
} else {
|
||||||
|
mock.printfLine("250-File data\r\n Type=file;Size=42;Modify=20201213202400; magic-file\r\n250 End")
|
||||||
|
}
|
||||||
case "NLST":
|
case "NLST":
|
||||||
if mock.dataConn == nil {
|
if mock.dataConn == nil {
|
||||||
mock.printfLine("425 Unable to build data connection: Connection refused")
|
mock.printfLine("425 Unable to build data connection: Connection refused")
|
||||||
@@ -386,20 +405,14 @@ func openConn(t *testing.T, addr string, options ...DialOption) (*ftpMock, *Serv
|
|||||||
|
|
||||||
func openConnExt(t *testing.T, addr, modtime string, options ...DialOption) (*ftpMock, *ServerConn) {
|
func openConnExt(t *testing.T, addr, modtime string, options ...DialOption) (*ftpMock, *ServerConn) {
|
||||||
mock, err := newFtpMockExt(t, addr, modtime)
|
mock, err := newFtpMockExt(t, addr, modtime)
|
||||||
if err != nil {
|
require.NoError(t, err)
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
defer mock.Close()
|
defer mock.Close()
|
||||||
|
|
||||||
c, err := Dial(mock.Addr(), options...)
|
c, err := Dial(mock.Addr(), options...)
|
||||||
if err != nil {
|
require.NoError(t, err)
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = c.Login("anonymous", "anonymous")
|
err = c.Login("anonymous", "anonymous")
|
||||||
if err != nil {
|
require.NoError(t, err)
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return mock, c
|
return mock, c
|
||||||
}
|
}
|
||||||
@@ -417,9 +430,7 @@ func closeConn(t *testing.T, mock *ftpMock, c *ServerConn, commands []string) {
|
|||||||
// Wait for the connection to close
|
// Wait for the connection to close
|
||||||
mock.Wait()
|
mock.Wait()
|
||||||
|
|
||||||
if !reflect.DeepEqual(mock.commands, expected) {
|
assert.Equal(t, expected, mock.commands, "unexpected sequence of commands")
|
||||||
t.Fatal("unexpected sequence of commands:", mock.commands, "expected:", expected)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestConn4(t *testing.T) {
|
func TestConn4(t *testing.T) {
|
||||||
|
|||||||
147
ftp.go
147
ftp.go
@@ -18,6 +18,12 @@ import (
|
|||||||
"github.com/hashicorp/go-multierror"
|
"github.com/hashicorp/go-multierror"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// 30 seconds was chosen as it's the
|
||||||
|
// same duration as http.DefaultTransport's timeout.
|
||||||
|
DefaultDialTimeout = 30 * time.Second
|
||||||
|
)
|
||||||
|
|
||||||
// EntryType describes the different types of an Entry.
|
// EntryType describes the different types of an Entry.
|
||||||
type EntryType int
|
type EntryType int
|
||||||
|
|
||||||
@@ -28,6 +34,15 @@ const (
|
|||||||
EntryTypeLink
|
EntryTypeLink
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// TransferType denotes the formats for transferring Entries.
|
||||||
|
type TransferType string
|
||||||
|
|
||||||
|
// The different transfer types
|
||||||
|
const (
|
||||||
|
TransferTypeBinary = TransferType("I")
|
||||||
|
TransferTypeASCII = TransferType("A")
|
||||||
|
)
|
||||||
|
|
||||||
// Time format used by the MDTM and MFMT commands
|
// Time format used by the MDTM and MFMT commands
|
||||||
const timeFormat = "20060102150405"
|
const timeFormat = "20060102150405"
|
||||||
|
|
||||||
@@ -61,11 +76,11 @@ type dialOptions struct {
|
|||||||
dialer net.Dialer
|
dialer net.Dialer
|
||||||
tlsConfig *tls.Config
|
tlsConfig *tls.Config
|
||||||
explicitTLS bool
|
explicitTLS bool
|
||||||
conn net.Conn
|
|
||||||
disableEPSV bool
|
disableEPSV bool
|
||||||
disableUTF8 bool
|
disableUTF8 bool
|
||||||
disableMLSD bool
|
disableMLSD bool
|
||||||
writingMDTM bool
|
writingMDTM bool
|
||||||
|
forceListHidden bool
|
||||||
location *time.Location
|
location *time.Location
|
||||||
debugOutput io.Writer
|
debugOutput io.Writer
|
||||||
dialFunc func(network, address string) (net.Conn, error)
|
dialFunc func(network, address string) (net.Conn, error)
|
||||||
@@ -99,28 +114,40 @@ func Dial(addr string, options ...DialOption) (*ServerConn, error) {
|
|||||||
do.location = time.UTC
|
do.location = time.UTC
|
||||||
}
|
}
|
||||||
|
|
||||||
tconn := do.conn
|
dialFunc := do.dialFunc
|
||||||
if tconn == nil {
|
|
||||||
var err error
|
|
||||||
|
|
||||||
if do.dialFunc != nil {
|
if dialFunc == nil {
|
||||||
tconn, err = do.dialFunc("tcp", addr)
|
|
||||||
} else if do.tlsConfig != nil && !do.explicitTLS {
|
|
||||||
tconn, err = tls.DialWithDialer(&do.dialer, "tcp", addr, do.tlsConfig)
|
|
||||||
} else {
|
|
||||||
ctx := do.context
|
ctx := do.context
|
||||||
|
|
||||||
if ctx == nil {
|
if ctx == nil {
|
||||||
ctx = context.Background()
|
ctx = context.Background()
|
||||||
}
|
}
|
||||||
|
if _, ok := ctx.Deadline(); !ok {
|
||||||
tconn, err = do.dialer.DialContext(ctx, "tcp", addr)
|
var cancel context.CancelFunc
|
||||||
|
ctx, cancel = context.WithTimeout(ctx, DefaultDialTimeout)
|
||||||
|
defer cancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if do.tlsConfig != nil && !do.explicitTLS {
|
||||||
|
dialFunc = func(network, address string) (net.Conn, error) {
|
||||||
|
tlsDialer := &tls.Dialer{
|
||||||
|
NetDialer: &do.dialer,
|
||||||
|
Config: do.tlsConfig,
|
||||||
|
}
|
||||||
|
return tlsDialer.DialContext(ctx, network, addr)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
|
||||||
|
dialFunc = func(network, address string) (net.Conn, error) {
|
||||||
|
return do.dialer.DialContext(ctx, network, addr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tconn, err := dialFunc("tcp", addr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Use the resolved IP address in case addr contains a domain name
|
// 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.
|
// If we use the domain name, we might not resolve to the same IP.
|
||||||
@@ -134,7 +161,7 @@ func Dial(addr string, options ...DialOption) (*ServerConn, error) {
|
|||||||
host: remoteAddr.IP.String(),
|
host: remoteAddr.IP.String(),
|
||||||
}
|
}
|
||||||
|
|
||||||
_, _, err := c.conn.ReadResponse(StatusReady)
|
_, _, err = c.conn.ReadResponse(StatusReady)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_ = c.Quit()
|
_ = c.Quit()
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -176,10 +203,12 @@ func DialWithDialer(dialer net.Dialer) DialOption {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// DialWithNetConn returns a DialOption that configures the ServerConn with the underlying net.Conn
|
// DialWithNetConn returns a DialOption that configures the ServerConn with the underlying net.Conn
|
||||||
|
//
|
||||||
|
// Deprecated: Use [DialWithDialFunc] instead
|
||||||
func DialWithNetConn(conn net.Conn) DialOption {
|
func DialWithNetConn(conn net.Conn) DialOption {
|
||||||
return DialOption{func(do *dialOptions) {
|
return DialWithDialFunc(func(network, address string) (net.Conn, error) {
|
||||||
do.conn = conn
|
return conn, nil
|
||||||
}}
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// DialWithDisabledEPSV returns a DialOption that configures the ServerConn with EPSV disabled
|
// DialWithDisabledEPSV returns a DialOption that configures the ServerConn with EPSV disabled
|
||||||
@@ -219,6 +248,16 @@ func DialWithWritingMDTM(enabled bool) DialOption {
|
|||||||
}}
|
}}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DialWithForceListHidden returns a DialOption making ServerConn use LIST -a to include hidden files and folders in directory listings
|
||||||
|
//
|
||||||
|
// This is useful for servers that do not do this by default, but it forces the use of the LIST command
|
||||||
|
// even if the server supports MLST.
|
||||||
|
func DialWithForceListHidden(enabled bool) DialOption {
|
||||||
|
return DialOption{func(do *dialOptions) {
|
||||||
|
do.forceListHidden = enabled
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
|
||||||
// DialWithLocation returns a DialOption that configures the ServerConn with specified time.Location
|
// DialWithLocation returns a DialOption that configures the ServerConn with specified time.Location
|
||||||
// The location is used to parse the dates sent by the server which are in server's timezone
|
// The location is used to parse the dates sent by the server which are in server's timezone
|
||||||
func DialWithLocation(location *time.Location) DialOption {
|
func DialWithLocation(location *time.Location) DialOption {
|
||||||
@@ -292,14 +331,15 @@ func (o *dialOptions) wrapStream(rd io.ReadCloser) io.ReadCloser {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Connect is an alias to Dial, for backward compatibility
|
// Connect is an alias to Dial, for backward compatibility
|
||||||
|
//
|
||||||
|
// Deprecated: Use [Dial] instead
|
||||||
func Connect(addr string) (*ServerConn, error) {
|
func Connect(addr string) (*ServerConn, error) {
|
||||||
return Dial(addr)
|
return Dial(addr)
|
||||||
}
|
}
|
||||||
|
|
||||||
// DialTimeout initializes the connection to the specified ftp server address.
|
// DialTimeout initializes the connection to the specified ftp server address.
|
||||||
//
|
//
|
||||||
// It is generally followed by a call to Login() as most FTP commands require
|
// Deprecated: Use [Dial] with [DialWithTimeout] option instead
|
||||||
// an authenticated user.
|
|
||||||
func DialTimeout(addr string, timeout time.Duration) (*ServerConn, error) {
|
func DialTimeout(addr string, timeout time.Duration) (*ServerConn, error) {
|
||||||
return Dial(addr, DialWithTimeout(timeout))
|
return Dial(addr, DialWithTimeout(timeout))
|
||||||
}
|
}
|
||||||
@@ -340,7 +380,7 @@ func (c *ServerConn) Login(user, password string) error {
|
|||||||
c.mdtmCanWrite = c.mdtmSupported && c.options.writingMDTM
|
c.mdtmCanWrite = c.mdtmSupported && c.options.writingMDTM
|
||||||
|
|
||||||
// Switch to binary mode
|
// Switch to binary mode
|
||||||
if _, _, err = c.cmd(StatusCommandOK, "TYPE I"); err != nil {
|
if err = c.Type(TransferTypeBinary); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -580,6 +620,12 @@ func (c *ServerConn) cmdDataConnFrom(offset uint64, format string, args ...inter
|
|||||||
return conn, nil
|
return conn, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Type switches the transfer mode for the connection.
|
||||||
|
func (c *ServerConn) Type(transferType TransferType) (err error) {
|
||||||
|
_, _, err = c.cmd(StatusCommandOK, "TYPE "+string(transferType))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
// NameList issues an NLST FTP command.
|
// NameList issues an NLST FTP command.
|
||||||
func (c *ServerConn) NameList(path string) (entries []string, err error) {
|
func (c *ServerConn) NameList(path string) (entries []string, err error) {
|
||||||
space := " "
|
space := " "
|
||||||
@@ -615,11 +661,14 @@ func (c *ServerConn) List(path string) (entries []*Entry, err error) {
|
|||||||
var cmd string
|
var cmd string
|
||||||
var parser parseFunc
|
var parser parseFunc
|
||||||
|
|
||||||
if c.mlstSupported {
|
if c.mlstSupported && !c.options.forceListHidden {
|
||||||
cmd = "MLSD"
|
cmd = "MLSD"
|
||||||
parser = parseRFC3659ListLine
|
parser = parseRFC3659ListLine
|
||||||
} else {
|
} else {
|
||||||
cmd = "LIST"
|
cmd = "LIST"
|
||||||
|
if c.options.forceListHidden {
|
||||||
|
cmd += " -a"
|
||||||
|
}
|
||||||
parser = parseListLine
|
parser = parseListLine
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -640,9 +689,7 @@ func (c *ServerConn) List(path string) (entries []*Entry, err error) {
|
|||||||
now := time.Now()
|
now := time.Now()
|
||||||
for scanner.Scan() {
|
for scanner.Scan() {
|
||||||
entry, errParse := parser(scanner.Text(), now, c.options.location)
|
entry, errParse := parser(scanner.Text(), now, c.options.location)
|
||||||
if errParse != nil {
|
if errParse == nil {
|
||||||
errs = multierror.Append(errs, errParse)
|
|
||||||
} else {
|
|
||||||
entries = append(entries, entry)
|
entries = append(entries, entry)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -657,6 +704,58 @@ func (c *ServerConn) List(path string) (entries []*Entry, err error) {
|
|||||||
return entries, errs.ErrorOrNil()
|
return entries, errs.ErrorOrNil()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetEntry issues a MLST FTP command which retrieves one single Entry using the
|
||||||
|
// control connection. The returnedEntry will describe the current directory
|
||||||
|
// when no path is given.
|
||||||
|
func (c *ServerConn) GetEntry(path string) (entry *Entry, err error) {
|
||||||
|
if !c.mlstSupported {
|
||||||
|
return nil, &textproto.Error{Code: StatusNotImplemented, Msg: StatusText(StatusNotImplemented)}
|
||||||
|
}
|
||||||
|
space := " "
|
||||||
|
if path == "" {
|
||||||
|
space = ""
|
||||||
|
}
|
||||||
|
_, msg, err := c.cmd(StatusRequestedFileActionOK, "%s%s%s", "MLST", space, path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// The expected reply will look something like:
|
||||||
|
//
|
||||||
|
// 250-File details
|
||||||
|
// Type=file;Size=1024;Modify=20220813133357; path
|
||||||
|
// 250 End
|
||||||
|
//
|
||||||
|
// Multiple lines are allowed though, so it can also be in the form:
|
||||||
|
//
|
||||||
|
// 250-File details
|
||||||
|
// Type=file;Size=1024; path
|
||||||
|
// Modify=20220813133357; path
|
||||||
|
// 250 End
|
||||||
|
lines := strings.Split(msg, "\n")
|
||||||
|
lc := len(lines)
|
||||||
|
|
||||||
|
// lines must be a multi-line message with a length of 3 or more, and we
|
||||||
|
// don't care about the first and last line
|
||||||
|
if lc < 3 {
|
||||||
|
return nil, errors.New("invalid response")
|
||||||
|
}
|
||||||
|
|
||||||
|
e := &Entry{}
|
||||||
|
for _, l := range lines[1 : lc-1] {
|
||||||
|
// According to RFC 3659, the entry lines must start with a space when passed over the
|
||||||
|
// control connection. Some servers don't seem to add that space though. Both forms are
|
||||||
|
// accepted here.
|
||||||
|
if len(l) > 0 && l[0] == ' ' {
|
||||||
|
l = l[1:]
|
||||||
|
}
|
||||||
|
if e, err = parseNextRFC3659ListLine(l, c.options.location, e); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return e, nil
|
||||||
|
}
|
||||||
|
|
||||||
// IsTimePreciseInList returns true if client and server support the MLSD
|
// IsTimePreciseInList returns true if client and server support the MLSD
|
||||||
// command so List can return time with 1-second precision for all files.
|
// command so List can return time with 1-second precision for all files.
|
||||||
func (c *ServerConn) IsTimePreciseInList() bool {
|
func (c *ServerConn) IsTimePreciseInList() bool {
|
||||||
@@ -928,7 +1027,7 @@ func (c *ServerConn) RemoveDir(path string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
//Walk prepares the internal walk function so that the caller can begin traversing the directory
|
// Walk prepares the internal walk function so that the caller can begin traversing the directory
|
||||||
func (c *ServerConn) Walk(root string) *Walker {
|
func (c *ServerConn) Walk(root string) *Walker {
|
||||||
w := new(Walker)
|
w := new(Walker)
|
||||||
w.serverConn = c
|
w.serverConn = c
|
||||||
|
|||||||
6
go.mod
6
go.mod
@@ -4,12 +4,12 @@ go 1.17
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/hashicorp/go-multierror v1.1.1
|
github.com/hashicorp/go-multierror v1.1.1
|
||||||
github.com/stretchr/testify v1.6.1
|
github.com/stretchr/testify v1.8.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/davecgh/go-spew v1.1.0 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/hashicorp/errwrap v1.0.0 // indirect
|
github.com/hashicorp/errwrap v1.0.0 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
12
go.sum
12
go.sum
@@ -1,5 +1,6 @@
|
|||||||
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
|
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA=
|
github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA=
|
||||||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||||
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
|
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
|
||||||
@@ -7,9 +8,12 @@ github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9
|
|||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
|
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
|
||||||
|
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
|
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
|||||||
14
parse.go
14
parse.go
@@ -27,7 +27,11 @@ var dirTimeFormats = []string{
|
|||||||
}
|
}
|
||||||
|
|
||||||
// parseRFC3659ListLine parses the style of directory line defined in RFC 3659.
|
// parseRFC3659ListLine parses the style of directory line defined in RFC 3659.
|
||||||
func parseRFC3659ListLine(line string, now time.Time, loc *time.Location) (*Entry, error) {
|
func parseRFC3659ListLine(line string, _ time.Time, loc *time.Location) (*Entry, error) {
|
||||||
|
return parseNextRFC3659ListLine(line, loc, &Entry{})
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseNextRFC3659ListLine(line string, loc *time.Location, e *Entry) (*Entry, error) {
|
||||||
iSemicolon := strings.Index(line, ";")
|
iSemicolon := strings.Index(line, ";")
|
||||||
iWhitespace := strings.Index(line, " ")
|
iWhitespace := strings.Index(line, " ")
|
||||||
|
|
||||||
@@ -35,8 +39,12 @@ func parseRFC3659ListLine(line string, now time.Time, loc *time.Location) (*Entr
|
|||||||
return nil, errUnsupportedListLine
|
return nil, errUnsupportedListLine
|
||||||
}
|
}
|
||||||
|
|
||||||
e := &Entry{
|
name := line[iWhitespace+1:]
|
||||||
Name: line[iWhitespace+1:],
|
if e.Name == "" {
|
||||||
|
e.Name = name
|
||||||
|
} else if e.Name != name {
|
||||||
|
// All lines must have the same name
|
||||||
|
return nil, errUnsupportedListLine
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, field := range strings.Split(line[:iWhitespace-1], ";") {
|
for _, field := range strings.Split(line[:iWhitespace-1], ";") {
|
||||||
|
|||||||
Reference in New Issue
Block a user