Add time commands, related options, state getters and unit tests
This commit is contained in:
		
							parent
							
								
									5d41901190
								
							
						
					
					
						commit
						90b220bedb
					
				@ -319,3 +319,82 @@ func TestListCurrentDir(t *testing.T) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	mock.Wait()
 | 
						mock.Wait()
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestTimeUnsupported(t *testing.T) {
 | 
				
			||||||
 | 
						mock, c := openConnExt(t, "127.0.0.1", "no-time")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						assert.False(t, c.mdtmSupported, "MDTM must NOT be supported")
 | 
				
			||||||
 | 
						assert.False(t, c.mfmtSupported, "MFMT must NOT be supported")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						assert.False(t, c.IsGetTimeSupported(), "GetTime must NOT be supported")
 | 
				
			||||||
 | 
						assert.False(t, c.IsSetTimeSupported(), "SetTime must NOT be supported")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						_, err := c.GetTime("file1")
 | 
				
			||||||
 | 
						assert.NotNil(t, err)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						err = c.SetTime("file1", time.Now())
 | 
				
			||||||
 | 
						assert.NotNil(t, err)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						assert.NoError(t, c.Quit())
 | 
				
			||||||
 | 
						mock.Wait()
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestTimeStandard(t *testing.T) {
 | 
				
			||||||
 | 
						mock, c := openConnExt(t, "127.0.0.1", "std-time")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						assert.True(t, c.mdtmSupported, "MDTM must be supported")
 | 
				
			||||||
 | 
						assert.True(t, c.mfmtSupported, "MFMT must be supported")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						assert.True(t, c.IsGetTimeSupported(), "GetTime must be supported")
 | 
				
			||||||
 | 
						assert.True(t, c.IsSetTimeSupported(), "SetTime must be supported")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						tm, err := c.GetTime("file1")
 | 
				
			||||||
 | 
						assert.NoError(t, err)
 | 
				
			||||||
 | 
						assert.False(t, tm.IsZero(), "GetTime must return valid time")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						err = c.SetTime("file1", time.Now())
 | 
				
			||||||
 | 
						assert.NoError(t, err)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						assert.NoError(t, c.Quit())
 | 
				
			||||||
 | 
						mock.Wait()
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestTimeVsftpdPartial(t *testing.T) {
 | 
				
			||||||
 | 
						mock, c := openConnExt(t, "127.0.0.1", "vsftpd")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						assert.True(t, c.mdtmSupported, "MDTM must be supported")
 | 
				
			||||||
 | 
						assert.False(t, c.mfmtSupported, "MFMT must NOT be supported")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						assert.True(t, c.IsGetTimeSupported(), "GetTime must be supported")
 | 
				
			||||||
 | 
						assert.False(t, c.IsSetTimeSupported(), "SetTime must NOT be supported")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						tm, err := c.GetTime("file1")
 | 
				
			||||||
 | 
						assert.NoError(t, err)
 | 
				
			||||||
 | 
						assert.False(t, tm.IsZero(), "GetTime must return valid time")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						err = c.SetTime("file1", time.Now())
 | 
				
			||||||
 | 
						assert.NotNil(t, err)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						assert.NoError(t, c.Quit())
 | 
				
			||||||
 | 
						mock.Wait()
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestTimeVsftpdFull(t *testing.T) {
 | 
				
			||||||
 | 
						mock, c := openConnExt(t, "127.0.0.1", "vsftpd", DialWithWritingMDTM(true))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						assert.True(t, c.mdtmSupported, "MDTM must be supported")
 | 
				
			||||||
 | 
						assert.False(t, c.mfmtSupported, "MFMT must NOT be supported")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						assert.True(t, c.IsGetTimeSupported(), "GetTime must be supported")
 | 
				
			||||||
 | 
						assert.True(t, c.IsSetTimeSupported(), "SetTime must be supported")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						tm, err := c.GetTime("file1")
 | 
				
			||||||
 | 
						assert.NoError(t, err)
 | 
				
			||||||
 | 
						assert.False(t, tm.IsZero(), "GetTime must return valid time")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						err = c.SetTime("file1", time.Now())
 | 
				
			||||||
 | 
						assert.NoError(t, err)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						assert.NoError(t, c.Quit())
 | 
				
			||||||
 | 
						mock.Wait()
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										57
									
								
								conn_test.go
									
									
									
									
									
								
							
							
						
						
									
										57
									
								
								conn_test.go
									
									
									
									
									
								
							@ -11,10 +11,12 @@ import (
 | 
				
			|||||||
	"strings"
 | 
						"strings"
 | 
				
			||||||
	"sync"
 | 
						"sync"
 | 
				
			||||||
	"testing"
 | 
						"testing"
 | 
				
			||||||
 | 
						"time"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type ftpMock struct {
 | 
					type ftpMock struct {
 | 
				
			||||||
	address  string
 | 
						address  string
 | 
				
			||||||
 | 
						modtime  string // no-time, std-time, vsftpd
 | 
				
			||||||
	listener *net.TCPListener
 | 
						listener *net.TCPListener
 | 
				
			||||||
	proto    *textproto.Conn
 | 
						proto    *textproto.Conn
 | 
				
			||||||
	commands []string // list of received commands
 | 
						commands []string // list of received commands
 | 
				
			||||||
@ -28,8 +30,15 @@ type ftpMock struct {
 | 
				
			|||||||
// newFtpMock returns a mock implementation of a FTP server
 | 
					// newFtpMock returns a mock implementation of a FTP server
 | 
				
			||||||
// For simplication, a mock instance only accepts a signle connection and terminates afer
 | 
					// For simplication, a mock instance only accepts a signle connection and terminates afer
 | 
				
			||||||
func newFtpMock(t *testing.T, address string) (*ftpMock, error) {
 | 
					func newFtpMock(t *testing.T, address string) (*ftpMock, error) {
 | 
				
			||||||
 | 
						return newFtpMockExt(t, address, "no-time")
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func newFtpMockExt(t *testing.T, address, modtime string) (*ftpMock, error) {
 | 
				
			||||||
	var err error
 | 
						var err error
 | 
				
			||||||
	mock := &ftpMock{address: address}
 | 
						mock := &ftpMock{
 | 
				
			||||||
 | 
							address: address,
 | 
				
			||||||
 | 
							modtime: modtime,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	l, err := net.Listen("tcp", address+":0")
 | 
						l, err := net.Listen("tcp", address+":0")
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
@ -77,7 +86,15 @@ func (mock *ftpMock) listen(t *testing.T) {
 | 
				
			|||||||
		// 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":
 | 
				
			||||||
			mock.proto.Writer.PrintfLine("211-Features:\r\n FEAT\r\n PASV\r\n EPSV\r\n UTF8\r\n SIZE\r\n211 End")
 | 
								features := "211-Features:\r\n FEAT\r\n PASV\r\n EPSV\r\n UTF8\r\n SIZE\r\n"
 | 
				
			||||||
 | 
								switch mock.modtime {
 | 
				
			||||||
 | 
								case "std-time":
 | 
				
			||||||
 | 
									features += " MDTM\r\n MFMT\r\n"
 | 
				
			||||||
 | 
								case "vsftpd":
 | 
				
			||||||
 | 
									features += " MDTM\r\n"
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								features += "211 End"
 | 
				
			||||||
 | 
								_ = mock.proto.Writer.PrintfLine(features)
 | 
				
			||||||
		case "USER":
 | 
							case "USER":
 | 
				
			||||||
			if cmdParts[1] == "anonymous" {
 | 
								if cmdParts[1] == "anonymous" {
 | 
				
			||||||
				mock.proto.Writer.PrintfLine("331 Please send your password")
 | 
									mock.proto.Writer.PrintfLine("331 Please send your password")
 | 
				
			||||||
@ -196,6 +213,36 @@ func (mock *ftpMock) listen(t *testing.T) {
 | 
				
			|||||||
			}
 | 
								}
 | 
				
			||||||
			mock.rest = rest
 | 
								mock.rest = rest
 | 
				
			||||||
			mock.proto.Writer.PrintfLine("350 Restarting at %s. Send STORE or RETRIEVE to initiate transfer", cmdParts[1])
 | 
								mock.proto.Writer.PrintfLine("350 Restarting at %s. Send STORE or RETRIEVE to initiate transfer", cmdParts[1])
 | 
				
			||||||
 | 
							case "MDTM":
 | 
				
			||||||
 | 
								var answer string
 | 
				
			||||||
 | 
								switch {
 | 
				
			||||||
 | 
								case mock.modtime == "no-time":
 | 
				
			||||||
 | 
									answer = "500 Unknown command MDTM"
 | 
				
			||||||
 | 
								case len(cmdParts) == 3 && mock.modtime == "vsftpd":
 | 
				
			||||||
 | 
									answer = "213 UTIME OK"
 | 
				
			||||||
 | 
									_, err := time.ParseInLocation(timeFormat, cmdParts[1], time.UTC)
 | 
				
			||||||
 | 
									if err != nil {
 | 
				
			||||||
 | 
										answer = "501 Can't get a time stamp"
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								case len(cmdParts) == 2:
 | 
				
			||||||
 | 
									answer = "213 20201213202400"
 | 
				
			||||||
 | 
								default:
 | 
				
			||||||
 | 
									answer = "500 wrong number of arguments"
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								_ = mock.proto.Writer.PrintfLine(answer)
 | 
				
			||||||
 | 
							case "MFMT":
 | 
				
			||||||
 | 
								var answer string
 | 
				
			||||||
 | 
								switch {
 | 
				
			||||||
 | 
								case mock.modtime == "std-time" && len(cmdParts) == 3:
 | 
				
			||||||
 | 
									answer = "213 UTIME OK"
 | 
				
			||||||
 | 
									_, err := time.ParseInLocation(timeFormat, cmdParts[1], time.UTC)
 | 
				
			||||||
 | 
									if err != nil {
 | 
				
			||||||
 | 
										answer = "501 Can't get a time stamp"
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								default:
 | 
				
			||||||
 | 
									answer = "500 Unknown command MFMT"
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								_ = mock.proto.Writer.PrintfLine(answer)
 | 
				
			||||||
		case "NOOP":
 | 
							case "NOOP":
 | 
				
			||||||
			mock.proto.Writer.PrintfLine("200 NOOP ok.")
 | 
								mock.proto.Writer.PrintfLine("200 NOOP ok.")
 | 
				
			||||||
		case "OPTS":
 | 
							case "OPTS":
 | 
				
			||||||
@ -307,7 +354,11 @@ func (mock *ftpMock) Close() {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
// Helper to return a client connected to a mock server
 | 
					// Helper to return a client connected to a mock server
 | 
				
			||||||
func openConn(t *testing.T, addr string, options ...DialOption) (*ftpMock, *ServerConn) {
 | 
					func openConn(t *testing.T, addr string, options ...DialOption) (*ftpMock, *ServerConn) {
 | 
				
			||||||
	mock, err := newFtpMock(t, addr)
 | 
						return openConnExt(t, addr, "no-time", options...)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func openConnExt(t *testing.T, addr, modtime string, options ...DialOption) (*ftpMock, *ServerConn) {
 | 
				
			||||||
 | 
						mock, err := newFtpMockExt(t, addr, modtime)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		t.Fatal(err)
 | 
							t.Fatal(err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										76
									
								
								ftp.go
									
									
									
									
									
								
							
							
						
						
									
										76
									
								
								ftp.go
									
									
									
									
									
								
							@ -27,6 +27,9 @@ const (
 | 
				
			|||||||
	EntryTypeLink
 | 
						EntryTypeLink
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Time format used by the MDTM and MFMT commands
 | 
				
			||||||
 | 
					const timeFormat = "20060102150405"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// ServerConn represents the connection to a remote FTP server.
 | 
					// ServerConn represents the connection to a remote FTP server.
 | 
				
			||||||
// A single connection only supports one in-flight data connection.
 | 
					// A single connection only supports one in-flight data connection.
 | 
				
			||||||
// It is not safe to be called concurrently.
 | 
					// It is not safe to be called concurrently.
 | 
				
			||||||
@ -39,6 +42,9 @@ type ServerConn struct {
 | 
				
			|||||||
	features      map[string]string
 | 
						features      map[string]string
 | 
				
			||||||
	skipEPSV      bool
 | 
						skipEPSV      bool
 | 
				
			||||||
	mlstSupported bool
 | 
						mlstSupported bool
 | 
				
			||||||
 | 
						mfmtSupported bool
 | 
				
			||||||
 | 
						mdtmSupported bool
 | 
				
			||||||
 | 
						mdtmCanWrite  bool
 | 
				
			||||||
	usePRET       bool
 | 
						usePRET       bool
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -57,6 +63,7 @@ type dialOptions struct {
 | 
				
			|||||||
	disableEPSV bool
 | 
						disableEPSV bool
 | 
				
			||||||
	disableUTF8 bool
 | 
						disableUTF8 bool
 | 
				
			||||||
	disableMLSD bool
 | 
						disableMLSD bool
 | 
				
			||||||
 | 
						writingMDTM 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)
 | 
				
			||||||
@ -187,6 +194,18 @@ func DialWithDisabledMLSD(disabled bool) DialOption {
 | 
				
			|||||||
	}}
 | 
						}}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// DialWithWritingMDTM returns a DialOption making ServerConn use MDTM to set file time
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// This option addresses a quirk in the VsFtpd server which doesn't support
 | 
				
			||||||
 | 
					// the MFMT command for setting file time like other servers but by default
 | 
				
			||||||
 | 
					// uses the MDTM command with non-standard arguments for that.
 | 
				
			||||||
 | 
					// See "mdtm_write" in https://security.appspot.com/vsftpd/vsftpd_conf.html
 | 
				
			||||||
 | 
					func DialWithWritingMDTM(enabled bool) DialOption {
 | 
				
			||||||
 | 
						return DialOption{func(do *dialOptions) {
 | 
				
			||||||
 | 
							do.writingMDTM = 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 {
 | 
				
			||||||
@ -293,9 +312,11 @@ func (c *ServerConn) Login(user, password string) error {
 | 
				
			|||||||
	if _, mlstSupported := c.features["MLST"]; mlstSupported && !c.options.disableMLSD {
 | 
						if _, mlstSupported := c.features["MLST"]; mlstSupported && !c.options.disableMLSD {
 | 
				
			||||||
		c.mlstSupported = true
 | 
							c.mlstSupported = true
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	if _, usePRET := c.features["PRET"]; usePRET {
 | 
						_, c.usePRET = c.features["PRET"]
 | 
				
			||||||
		c.usePRET = true
 | 
					
 | 
				
			||||||
	}
 | 
						_, c.mfmtSupported = c.features["MFMT"]
 | 
				
			||||||
 | 
						_, c.mdtmSupported = c.features["MDTM"]
 | 
				
			||||||
 | 
						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.cmd(StatusCommandOK, "TYPE I"); err != nil {
 | 
				
			||||||
@ -613,6 +634,12 @@ func (c *ServerConn) List(path string) (entries []*Entry, err error) {
 | 
				
			|||||||
	return entries, err
 | 
						return entries, err
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// IsTimePreciseInList returns true if client and server support the MLSD
 | 
				
			||||||
 | 
					// command so List can return time with 1-second precision for all files.
 | 
				
			||||||
 | 
					func (c *ServerConn) IsTimePreciseInList() bool {
 | 
				
			||||||
 | 
						return c.mlstSupported
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// ChangeDir issues a CWD FTP command, which changes the current directory to
 | 
					// ChangeDir issues a CWD FTP command, which changes the current directory to
 | 
				
			||||||
// the specified path.
 | 
					// the specified path.
 | 
				
			||||||
func (c *ServerConn) ChangeDir(path string) error {
 | 
					func (c *ServerConn) ChangeDir(path string) error {
 | 
				
			||||||
@ -656,6 +683,49 @@ func (c *ServerConn) FileSize(path string) (int64, error) {
 | 
				
			|||||||
	return strconv.ParseInt(msg, 10, 64)
 | 
						return strconv.ParseInt(msg, 10, 64)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// GetTime issues the MDTM FTP command to obtain the file modification time.
 | 
				
			||||||
 | 
					// It returns a UTC time.
 | 
				
			||||||
 | 
					func (c *ServerConn) GetTime(path string) (time.Time, error) {
 | 
				
			||||||
 | 
						var t time.Time
 | 
				
			||||||
 | 
						if !c.mdtmSupported {
 | 
				
			||||||
 | 
							return t, errors.New("GetTime is not supported")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						_, msg, err := c.cmd(StatusFile, "MDTM %s", path)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return t, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return time.ParseInLocation(timeFormat, msg, time.UTC)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// IsGetTimeSupported allows library callers to check in advance that they
 | 
				
			||||||
 | 
					// can use GetTime to get file time.
 | 
				
			||||||
 | 
					func (c *ServerConn) IsGetTimeSupported() bool {
 | 
				
			||||||
 | 
						return c.mdtmSupported
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// SetTime issues the MFMT FTP command to set the file modification time.
 | 
				
			||||||
 | 
					// Also it can use a non-standard form of the MDTM command supported by
 | 
				
			||||||
 | 
					// the VsFtpd server instead of MFMT for the same purpose.
 | 
				
			||||||
 | 
					// See "mdtm_write" in https://security.appspot.com/vsftpd/vsftpd_conf.html
 | 
				
			||||||
 | 
					func (c *ServerConn) SetTime(path string, t time.Time) (err error) {
 | 
				
			||||||
 | 
						utime := t.In(time.UTC).Format(timeFormat)
 | 
				
			||||||
 | 
						switch {
 | 
				
			||||||
 | 
						case c.mfmtSupported:
 | 
				
			||||||
 | 
							_, _, err = c.cmd(StatusFile, "MFMT %s %s", utime, path)
 | 
				
			||||||
 | 
						case c.mdtmCanWrite:
 | 
				
			||||||
 | 
							_, _, err = c.cmd(StatusFile, "MDTM %s %s", utime, path)
 | 
				
			||||||
 | 
						default:
 | 
				
			||||||
 | 
							err = errors.New("SetTime is not supported")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// IsSetTimeSupported allows library callers to check in advance that they
 | 
				
			||||||
 | 
					// can use SetTime to set file time.
 | 
				
			||||||
 | 
					func (c *ServerConn) IsSetTimeSupported() bool {
 | 
				
			||||||
 | 
						return c.mfmtSupported || c.mdtmCanWrite
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Retr issues a RETR FTP command to fetch the specified file from the remote
 | 
					// Retr issues a RETR FTP command to fetch the specified file from the remote
 | 
				
			||||||
// FTP server.
 | 
					// FTP server.
 | 
				
			||||||
//
 | 
					//
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user