1// Package ftp implements a FTP client as described in RFC 959.
2//
3// A textproto.Error is returned for errors at the protocol level.
4package ftp
5
6import (
7	"bufio"
8	"context"
9	"crypto/tls"
10	"errors"
11	"io"
12	"net"
13	"net/textproto"
14	"strconv"
15	"strings"
16	"time"
17)
18
19// EntryType describes the different types of an Entry.
20type EntryType int
21
22// The differents types of an Entry
23const (
24	EntryTypeFile EntryType = iota
25	EntryTypeFolder
26	EntryTypeLink
27)
28
29// ServerConn represents the connection to a remote FTP server.
30// A single connection only supports one in-flight data connection.
31// It is not safe to be called concurrently.
32type ServerConn struct {
33	options *dialOptions
34	conn    *textproto.Conn
35	host    string
36
37	// Server capabilities discovered at runtime
38	features      map[string]string
39	skipEPSV      bool
40	mlstSupported bool
41	usePRET       bool
42}
43
44// DialOption represents an option to start a new connection with Dial
45type DialOption struct {
46	setup func(do *dialOptions)
47}
48
49// dialOptions contains all the options set by DialOption.setup
50type dialOptions struct {
51	context     context.Context
52	dialer      net.Dialer
53	tlsConfig   *tls.Config
54	explicitTLS bool
55	conn        net.Conn
56	disableEPSV bool
57	disableUTF8 bool
58	disableMLSD bool
59	location    *time.Location
60	debugOutput io.Writer
61	dialFunc    func(network, address string) (net.Conn, error)
62}
63
64// Entry describes a file and is returned by List().
65type Entry struct {
66	Name   string
67	Target string // target of symbolic link
68	Type   EntryType
69	Size   uint64
70	Time   time.Time
71}
72
73// Response represents a data-connection
74type Response struct {
75	conn   net.Conn
76	c      *ServerConn
77	closed bool
78}
79
80// Dial connects to the specified address with optional options
81func Dial(addr string, options ...DialOption) (*ServerConn, error) {
82	do := &dialOptions{}
83	for _, option := range options {
84		option.setup(do)
85	}
86
87	if do.location == nil {
88		do.location = time.UTC
89	}
90
91	tconn := do.conn
92	if tconn == nil {
93		var err error
94
95		if do.dialFunc != nil {
96			tconn, err = do.dialFunc("tcp", addr)
97		} else if do.tlsConfig != nil && !do.explicitTLS {
98			tconn, err = tls.DialWithDialer(&do.dialer, "tcp", addr, do.tlsConfig)
99		} else {
100			ctx := do.context
101
102			if ctx == nil {
103				ctx = context.Background()
104			}
105
106			tconn, err = do.dialer.DialContext(ctx, "tcp", addr)
107		}
108
109		if err != nil {
110			return nil, err
111		}
112	}
113
114	// Use the resolved IP address in case addr contains a domain name
115	// If we use the domain name, we might not resolve to the same IP.
116	remoteAddr := tconn.RemoteAddr().(*net.TCPAddr)
117
118	c := &ServerConn{
119		options:  do,
120		features: make(map[string]string),
121		conn:     textproto.NewConn(do.wrapConn(tconn)),
122		host:     remoteAddr.IP.String(),
123	}
124
125	_, _, err := c.conn.ReadResponse(StatusReady)
126	if err != nil {
127		c.Quit()
128		return nil, err
129	}
130
131	if do.explicitTLS {
132		if err := c.authTLS(); err != nil {
133			_ = c.Quit()
134			return nil, err
135		}
136		tconn = tls.Client(tconn, do.tlsConfig)
137		c.conn = textproto.NewConn(do.wrapConn(tconn))
138	}
139
140	return c, nil
141}
142
143// DialWithTimeout returns a DialOption that configures the ServerConn with specified timeout
144func DialWithTimeout(timeout time.Duration) DialOption {
145	return DialOption{func(do *dialOptions) {
146		do.dialer.Timeout = timeout
147	}}
148}
149
150// DialWithDialer returns a DialOption that configures the ServerConn with specified net.Dialer
151func DialWithDialer(dialer net.Dialer) DialOption {
152	return DialOption{func(do *dialOptions) {
153		do.dialer = dialer
154	}}
155}
156
157// DialWithNetConn returns a DialOption that configures the ServerConn with the underlying net.Conn
158func DialWithNetConn(conn net.Conn) DialOption {
159	return DialOption{func(do *dialOptions) {
160		do.conn = conn
161	}}
162}
163
164// DialWithDisabledEPSV returns a DialOption that configures the ServerConn with EPSV disabled
165// Note that EPSV is only used when advertised in the server features.
166func DialWithDisabledEPSV(disabled bool) DialOption {
167	return DialOption{func(do *dialOptions) {
168		do.disableEPSV = disabled
169	}}
170}
171
172// DialWithDisabledUTF8 returns a DialOption that configures the ServerConn with UTF8 option disabled
173func DialWithDisabledUTF8(disabled bool) DialOption {
174	return DialOption{func(do *dialOptions) {
175		do.disableUTF8 = disabled
176	}}
177}
178
179// DialWithDisabledMLSD returns a DialOption that configures the ServerConn with MLSD option disabled
180//
181// This is useful for servers which advertise MLSD (eg some versions
182// of Serv-U) but don't support it properly.
183func DialWithDisabledMLSD(disabled bool) DialOption {
184	return DialOption{func(do *dialOptions) {
185		do.disableMLSD = disabled
186	}}
187}
188
189// DialWithLocation returns a DialOption that configures the ServerConn with specified time.Location
190// The location is used to parse the dates sent by the server which are in server's timezone
191func DialWithLocation(location *time.Location) DialOption {
192	return DialOption{func(do *dialOptions) {
193		do.location = location
194	}}
195}
196
197// DialWithContext returns a DialOption that configures the ServerConn with specified context
198// The context will be used for the initial connection setup
199func DialWithContext(ctx context.Context) DialOption {
200	return DialOption{func(do *dialOptions) {
201		do.context = ctx
202	}}
203}
204
205// DialWithTLS returns a DialOption that configures the ServerConn with specified TLS config
206//
207// If called together with the DialWithDialFunc option, the DialWithDialFunc function
208// will be used when dialing new connections but regardless of the function,
209// the connection will be treated as a TLS connection.
210func DialWithTLS(tlsConfig *tls.Config) DialOption {
211	return DialOption{func(do *dialOptions) {
212		do.tlsConfig = tlsConfig
213	}}
214}
215
216// DialWithExplicitTLS returns a DialOption that configures the ServerConn to be upgraded to TLS
217// See DialWithTLS for general TLS documentation
218func DialWithExplicitTLS(tlsConfig *tls.Config) DialOption {
219	return DialOption{func(do *dialOptions) {
220		do.explicitTLS = true
221		do.tlsConfig = tlsConfig
222	}}
223}
224
225// DialWithDebugOutput returns a DialOption that configures the ServerConn to write to the Writer
226// everything it reads from the server
227func DialWithDebugOutput(w io.Writer) DialOption {
228	return DialOption{func(do *dialOptions) {
229		do.debugOutput = w
230	}}
231}
232
233// DialWithDialFunc returns a DialOption that configures the ServerConn to use the
234// specified function to establish both control and data connections
235//
236// If used together with the DialWithNetConn option, the DialWithNetConn
237// takes precedence for the control connection, while data connections will
238// be established using function specified with the DialWithDialFunc option
239func DialWithDialFunc(f func(network, address string) (net.Conn, error)) DialOption {
240	return DialOption{func(do *dialOptions) {
241		do.dialFunc = f
242	}}
243}
244
245func (o *dialOptions) wrapConn(netConn net.Conn) io.ReadWriteCloser {
246	if o.debugOutput == nil {
247		return netConn
248	}
249
250	return newDebugWrapper(netConn, o.debugOutput)
251}
252
253// Connect is an alias to Dial, for backward compatibility
254func Connect(addr string) (*ServerConn, error) {
255	return Dial(addr)
256}
257
258// DialTimeout initializes the connection to the specified ftp server address.
259//
260// It is generally followed by a call to Login() as most FTP commands require
261// an authenticated user.
262func DialTimeout(addr string, timeout time.Duration) (*ServerConn, error) {
263	return Dial(addr, DialWithTimeout(timeout))
264}
265
266// Login authenticates the client with specified user and password.
267//
268// "anonymous"/"anonymous" is a common user/password scheme for FTP servers
269// that allows anonymous read-only accounts.
270func (c *ServerConn) Login(user, password string) error {
271	code, message, err := c.cmd(-1, "USER %s", user)
272	if err != nil {
273		return err
274	}
275
276	switch code {
277	case StatusLoggedIn:
278	case StatusUserOK:
279		_, _, err = c.cmd(StatusLoggedIn, "PASS %s", password)
280		if err != nil {
281			return err
282		}
283	default:
284		return errors.New(message)
285	}
286
287	// Probe features
288	err = c.feat()
289	if err != nil {
290		return err
291	}
292	if _, mlstSupported := c.features["MLST"]; mlstSupported && !c.options.disableMLSD {
293		c.mlstSupported = true
294	}
295	if _, usePRET := c.features["PRET"]; usePRET {
296		c.usePRET = true
297	}
298
299	// Switch to binary mode
300	if _, _, err = c.cmd(StatusCommandOK, "TYPE I"); err != nil {
301		return err
302	}
303
304	// Switch to UTF-8
305	if !c.options.disableUTF8 {
306		err = c.setUTF8()
307	}
308
309	// If using implicit TLS, make data connections also use TLS
310	if c.options.tlsConfig != nil {
311		c.cmd(StatusCommandOK, "PBSZ 0")
312		c.cmd(StatusCommandOK, "PROT P")
313	}
314
315	return err
316}
317
318// authTLS upgrades the connection to use TLS
319func (c *ServerConn) authTLS() error {
320	_, _, err := c.cmd(StatusAuthOK, "AUTH TLS")
321	return err
322}
323
324// feat issues a FEAT FTP command to list the additional commands supported by
325// the remote FTP server.
326// FEAT is described in RFC 2389
327func (c *ServerConn) feat() error {
328	code, message, err := c.cmd(-1, "FEAT")
329	if err != nil {
330		return err
331	}
332
333	if code != StatusSystem {
334		// The server does not support the FEAT command. This is not an
335		// error: we consider that there is no additional feature.
336		return nil
337	}
338
339	lines := strings.Split(message, "\n")
340	for _, line := range lines {
341		if !strings.HasPrefix(line, " ") {
342			continue
343		}
344
345		line = strings.TrimSpace(line)
346		featureElements := strings.SplitN(line, " ", 2)
347
348		command := featureElements[0]
349
350		var commandDesc string
351		if len(featureElements) == 2 {
352			commandDesc = featureElements[1]
353		}
354
355		c.features[command] = commandDesc
356	}
357
358	return nil
359}
360
361// setUTF8 issues an "OPTS UTF8 ON" command.
362func (c *ServerConn) setUTF8() error {
363	if _, ok := c.features["UTF8"]; !ok {
364		return nil
365	}
366
367	code, message, err := c.cmd(-1, "OPTS UTF8 ON")
368	if err != nil {
369		return err
370	}
371
372	// Workaround for FTP servers, that does not support this option.
373	if code == StatusBadArguments || code == StatusNotImplementedParameter {
374		return nil
375	}
376
377	// The ftpd "filezilla-server" has FEAT support for UTF8, but always returns
378	// "202 UTF8 mode is always enabled. No need to send this command." when
379	// trying to use it. That's OK
380	if code == StatusCommandNotImplemented {
381		return nil
382	}
383
384	if code != StatusCommandOK {
385		return errors.New(message)
386	}
387
388	return nil
389}
390
391// epsv issues an "EPSV" command to get a port number for a data connection.
392func (c *ServerConn) epsv() (port int, err error) {
393	_, line, err := c.cmd(StatusExtendedPassiveMode, "EPSV")
394	if err != nil {
395		return 0, err
396	}
397
398	start := strings.Index(line, "|||")
399	end := strings.LastIndex(line, "|")
400	if start == -1 || end == -1 {
401		return 0, errors.New("invalid EPSV response format")
402	}
403	port, err = strconv.Atoi(line[start+3 : end])
404	return port, err
405}
406
407// pasv issues a "PASV" command to get a port number for a data connection.
408func (c *ServerConn) pasv() (host string, port int, err error) {
409	_, line, err := c.cmd(StatusPassiveMode, "PASV")
410	if err != nil {
411		return "", 0, err
412	}
413
414	// PASV response format : 227 Entering Passive Mode (h1,h2,h3,h4,p1,p2).
415	start := strings.Index(line, "(")
416	end := strings.LastIndex(line, ")")
417	if start == -1 || end == -1 {
418		return "", 0, errors.New("invalid PASV response format")
419	}
420
421	// We have to split the response string
422	pasvData := strings.Split(line[start+1:end], ",")
423
424	if len(pasvData) < 6 {
425		return "", 0, errors.New("invalid PASV response format")
426	}
427
428	// Let's compute the port number
429	portPart1, err := strconv.Atoi(pasvData[4])
430	if err != nil {
431		return "", 0, err
432	}
433
434	portPart2, err := strconv.Atoi(pasvData[5])
435	if err != nil {
436		return "", 0, err
437	}
438
439	// Recompose port
440	port = portPart1*256 + portPart2
441
442	// Make the IP address to connect to
443	host = strings.Join(pasvData[0:4], ".")
444	return host, port, nil
445}
446
447// getDataConnPort returns a host, port for a new data connection
448// it uses the best available method to do so
449func (c *ServerConn) getDataConnPort() (string, int, error) {
450	if !c.options.disableEPSV && !c.skipEPSV {
451		if port, err := c.epsv(); err == nil {
452			return c.host, port, nil
453		}
454
455		// if there is an error, skip EPSV for the next attempts
456		c.skipEPSV = true
457	}
458
459	return c.pasv()
460}
461
462// openDataConn creates a new FTP data connection.
463func (c *ServerConn) openDataConn() (net.Conn, error) {
464	host, port, err := c.getDataConnPort()
465	if err != nil {
466		return nil, err
467	}
468
469	addr := net.JoinHostPort(host, strconv.Itoa(port))
470	if c.options.dialFunc != nil {
471		return c.options.dialFunc("tcp", addr)
472	}
473
474	if c.options.tlsConfig != nil {
475		conn, err := c.options.dialer.Dial("tcp", addr)
476		if err != nil {
477			return nil, err
478		}
479		return tls.Client(conn, c.options.tlsConfig), err
480	}
481
482	return c.options.dialer.Dial("tcp", addr)
483}
484
485// cmd is a helper function to execute a command and check for the expected FTP
486// return code
487func (c *ServerConn) cmd(expected int, format string, args ...interface{}) (int, string, error) {
488	_, err := c.conn.Cmd(format, args...)
489	if err != nil {
490		return 0, "", err
491	}
492
493	return c.conn.ReadResponse(expected)
494}
495
496// cmdDataConnFrom executes a command which require a FTP data connection.
497// Issues a REST FTP command to specify the number of bytes to skip for the transfer.
498func (c *ServerConn) cmdDataConnFrom(offset uint64, format string, args ...interface{}) (net.Conn, error) {
499	// If server requires PRET send the PRET command to warm it up
500	// See: https://tools.ietf.org/html/draft-dd-pret-00
501	if c.usePRET {
502		_, _, err := c.cmd(-1, "PRET "+format, args...)
503		if err != nil {
504			return nil, err
505		}
506	}
507
508	conn, err := c.openDataConn()
509	if err != nil {
510		return nil, err
511	}
512
513	if offset != 0 {
514		_, _, err := c.cmd(StatusRequestFilePending, "REST %d", offset)
515		if err != nil {
516			conn.Close()
517			return nil, err
518		}
519	}
520
521	_, err = c.conn.Cmd(format, args...)
522	if err != nil {
523		conn.Close()
524		return nil, err
525	}
526
527	code, msg, err := c.conn.ReadResponse(-1)
528	if err != nil {
529		conn.Close()
530		return nil, err
531	}
532	if code != StatusAlreadyOpen && code != StatusAboutToSend {
533		conn.Close()
534		return nil, &textproto.Error{Code: code, Msg: msg}
535	}
536
537	return conn, nil
538}
539
540// NameList issues an NLST FTP command.
541func (c *ServerConn) NameList(path string) (entries []string, err error) {
542	conn, err := c.cmdDataConnFrom(0, "NLST %s", path)
543	if err != nil {
544		return nil, err
545	}
546
547	r := &Response{conn: conn, c: c}
548	defer r.Close()
549
550	scanner := bufio.NewScanner(r)
551	for scanner.Scan() {
552		entries = append(entries, scanner.Text())
553	}
554	if err = scanner.Err(); err != nil {
555		return entries, err
556	}
557	return entries, nil
558}
559
560// List issues a LIST FTP command.
561func (c *ServerConn) List(path string) (entries []*Entry, err error) {
562	var cmd string
563	var parser parseFunc
564
565	if c.mlstSupported {
566		cmd = "MLSD"
567		parser = parseRFC3659ListLine
568	} else {
569		cmd = "LIST"
570		parser = parseListLine
571	}
572
573	conn, err := c.cmdDataConnFrom(0, "%s %s", cmd, path)
574	if err != nil {
575		return nil, err
576	}
577
578	r := &Response{conn: conn, c: c}
579	defer r.Close()
580
581	scanner := bufio.NewScanner(r)
582	now := time.Now()
583	for scanner.Scan() {
584		entry, err := parser(scanner.Text(), now, c.options.location)
585		if err == nil {
586			entries = append(entries, entry)
587		}
588	}
589	if err := scanner.Err(); err != nil {
590		return nil, err
591	}
592	return entries, nil
593}
594
595// ChangeDir issues a CWD FTP command, which changes the current directory to
596// the specified path.
597func (c *ServerConn) ChangeDir(path string) error {
598	_, _, err := c.cmd(StatusRequestedFileActionOK, "CWD %s", path)
599	return err
600}
601
602// ChangeDirToParent issues a CDUP FTP command, which changes the current
603// directory to the parent directory.  This is similar to a call to ChangeDir
604// with a path set to "..".
605func (c *ServerConn) ChangeDirToParent() error {
606	_, _, err := c.cmd(StatusRequestedFileActionOK, "CDUP")
607	return err
608}
609
610// CurrentDir issues a PWD FTP command, which Returns the path of the current
611// directory.
612func (c *ServerConn) CurrentDir() (string, error) {
613	_, msg, err := c.cmd(StatusPathCreated, "PWD")
614	if err != nil {
615		return "", err
616	}
617
618	start := strings.Index(msg, "\"")
619	end := strings.LastIndex(msg, "\"")
620
621	if start == -1 || end == -1 {
622		return "", errors.New("unsuported PWD response format")
623	}
624
625	return msg[start+1 : end], nil
626}
627
628// FileSize issues a SIZE FTP command, which Returns the size of the file
629func (c *ServerConn) FileSize(path string) (int64, error) {
630	_, msg, err := c.cmd(StatusFile, "SIZE %s", path)
631	if err != nil {
632		return 0, err
633	}
634
635	return strconv.ParseInt(msg, 10, 64)
636}
637
638// Retr issues a RETR FTP command to fetch the specified file from the remote
639// FTP server.
640//
641// The returned ReadCloser must be closed to cleanup the FTP data connection.
642func (c *ServerConn) Retr(path string) (*Response, error) {
643	return c.RetrFrom(path, 0)
644}
645
646// RetrFrom issues a RETR FTP command to fetch the specified file from the remote
647// FTP server, the server will not send the offset first bytes of the file.
648//
649// The returned ReadCloser must be closed to cleanup the FTP data connection.
650func (c *ServerConn) RetrFrom(path string, offset uint64) (*Response, error) {
651	conn, err := c.cmdDataConnFrom(offset, "RETR %s", path)
652	if err != nil {
653		return nil, err
654	}
655
656	return &Response{conn: conn, c: c}, nil
657}
658
659// Stor issues a STOR FTP command to store a file to the remote FTP server.
660// Stor creates the specified file with the content of the io.Reader.
661//
662// Hint: io.Pipe() can be used if an io.Writer is required.
663func (c *ServerConn) Stor(path string, r io.Reader) error {
664	return c.StorFrom(path, r, 0)
665}
666
667// StorFrom issues a STOR FTP command to store a file to the remote FTP server.
668// Stor creates the specified file with the content of the io.Reader, writing
669// on the server will start at the given file offset.
670//
671// Hint: io.Pipe() can be used if an io.Writer is required.
672func (c *ServerConn) StorFrom(path string, r io.Reader, offset uint64) error {
673	conn, err := c.cmdDataConnFrom(offset, "STOR %s", path)
674	if err != nil {
675		return err
676	}
677
678	// if the upload fails we still need to try to read the server
679	// response otherwise if the failure is not due to a connection problem,
680	// for example the server denied the upload for quota limits, we miss
681	// the response and we cannot use the connection to send other commands.
682	// So we don't check io.Copy error and we return the error from
683	// ReadResponse so the user can see the real error
684	_, err = io.Copy(conn, r)
685	conn.Close()
686
687	_, _, respErr := c.conn.ReadResponse(StatusClosingDataConnection)
688	if respErr != nil {
689		err = respErr
690	}
691	return err
692}
693
694// Append issues a APPE FTP command to store a file to the remote FTP server.
695// If a file already exists with the given path, then the content of the
696// io.Reader is appended. Otherwise, a new file is created with that content.
697//
698// Hint: io.Pipe() can be used if an io.Writer is required.
699func (c *ServerConn) Append(path string, r io.Reader) error {
700	conn, err := c.cmdDataConnFrom(0, "APPE %s", path)
701	if err != nil {
702		return err
703	}
704
705	// see the comment for StorFrom above
706	_, err = io.Copy(conn, r)
707	conn.Close()
708
709	_, _, respErr := c.conn.ReadResponse(StatusClosingDataConnection)
710	if respErr != nil {
711		err = respErr
712	}
713	return err
714}
715
716// Rename renames a file on the remote FTP server.
717func (c *ServerConn) Rename(from, to string) error {
718	_, _, err := c.cmd(StatusRequestFilePending, "RNFR %s", from)
719	if err != nil {
720		return err
721	}
722
723	_, _, err = c.cmd(StatusRequestedFileActionOK, "RNTO %s", to)
724	return err
725}
726
727// Delete issues a DELE FTP command to delete the specified file from the
728// remote FTP server.
729func (c *ServerConn) Delete(path string) error {
730	_, _, err := c.cmd(StatusRequestedFileActionOK, "DELE %s", path)
731	return err
732}
733
734// RemoveDirRecur deletes a non-empty folder recursively using
735// RemoveDir and Delete
736func (c *ServerConn) RemoveDirRecur(path string) error {
737	err := c.ChangeDir(path)
738	if err != nil {
739		return err
740	}
741	currentDir, err := c.CurrentDir()
742	if err != nil {
743		return err
744	}
745
746	entries, err := c.List(currentDir)
747	if err != nil {
748		return err
749	}
750
751	for _, entry := range entries {
752		if entry.Name != ".." && entry.Name != "." {
753			if entry.Type == EntryTypeFolder {
754				err = c.RemoveDirRecur(currentDir + "/" + entry.Name)
755				if err != nil {
756					return err
757				}
758			} else {
759				err = c.Delete(entry.Name)
760				if err != nil {
761					return err
762				}
763			}
764		}
765	}
766	err = c.ChangeDirToParent()
767	if err != nil {
768		return err
769	}
770	err = c.RemoveDir(currentDir)
771	return err
772}
773
774// MakeDir issues a MKD FTP command to create the specified directory on the
775// remote FTP server.
776func (c *ServerConn) MakeDir(path string) error {
777	_, _, err := c.cmd(StatusPathCreated, "MKD %s", path)
778	return err
779}
780
781// RemoveDir issues a RMD FTP command to remove the specified directory from
782// the remote FTP server.
783func (c *ServerConn) RemoveDir(path string) error {
784	_, _, err := c.cmd(StatusRequestedFileActionOK, "RMD %s", path)
785	return err
786}
787
788//Walk prepares the internal walk function so that the caller can begin traversing the directory
789func (c *ServerConn) Walk(root string) *Walker {
790	w := new(Walker)
791	w.serverConn = c
792
793	if !strings.HasSuffix(root, "/") {
794		root += "/"
795	}
796
797	w.root = root
798	w.descend = true
799
800	return w
801}
802
803// NoOp issues a NOOP FTP command.
804// NOOP has no effects and is usually used to prevent the remote FTP server to
805// close the otherwise idle connection.
806func (c *ServerConn) NoOp() error {
807	_, _, err := c.cmd(StatusCommandOK, "NOOP")
808	return err
809}
810
811// Logout issues a REIN FTP command to logout the current user.
812func (c *ServerConn) Logout() error {
813	_, _, err := c.cmd(StatusReady, "REIN")
814	return err
815}
816
817// Quit issues a QUIT FTP command to properly close the connection from the
818// remote FTP server.
819func (c *ServerConn) Quit() error {
820	c.conn.Cmd("QUIT")
821	return c.conn.Close()
822}
823
824// Read implements the io.Reader interface on a FTP data connection.
825func (r *Response) Read(buf []byte) (int, error) {
826	return r.conn.Read(buf)
827}
828
829// Close implements the io.Closer interface on a FTP data connection.
830// After the first call, Close will do nothing and return nil.
831func (r *Response) Close() error {
832	if r.closed {
833		return nil
834	}
835	err := r.conn.Close()
836	_, _, err2 := r.c.conn.ReadResponse(StatusClosingDataConnection)
837	if err2 != nil {
838		err = err2
839	}
840	r.closed = true
841	return err
842}
843
844// SetDeadline sets the deadlines associated with the connection.
845func (r *Response) SetDeadline(t time.Time) error {
846	return r.conn.SetDeadline(t)
847}
848
849// String returns the string representation of EntryType t.
850func (t EntryType) String() string {
851	return [...]string{"file", "folder", "link"}[t]
852}
853