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