diff --git a/client_test.go b/client_test.go new file mode 100644 index 0000000..8778488 --- /dev/null +++ b/client_test.go @@ -0,0 +1,55 @@ +package ftp + +import ( + "bytes" + "io/ioutil" + "testing" +) + +const ( + testData = "Just some text" +) + +func TestConn(t *testing.T) { + c, err := ConnectAnonymous("localhost:21") + if err != nil { + t.Fatal(err) + } + + err = c.NoOp() + if err != nil { + t.Error(err) + } + + data := bytes.NewBufferString(testData) + err = c.Stor("test", data) + if err != nil { + t.Error(err) + } + + _, err = c.List(".") + if err != nil { + t.Error(err) + } + + err = c.Rename("test", "tset") + if err != nil { + t.Error(err) + } + + r, err := c.Retr("tset") + if err != nil { + t.Error(err) + } else { + buf, err := ioutil.ReadAll(r) + if err != nil { + t.Error(err) + } + if string(buf) != testData { + t.Errorf("'%s'", buf) + } + r.Close() + } + + c.Quit() +} diff --git a/ftp.go b/ftp.go index 8353715..1c9af27 100644 --- a/ftp.go +++ b/ftp.go @@ -2,6 +2,7 @@ package ftp import ( "bufio" + "io" "net" "net/textproto" "os" @@ -10,8 +11,10 @@ import ( "strings" ) +type EntryType int + const ( - EntryTypeFile = iota + EntryTypeFile EntryType = iota EntryTypeFolder EntryTypeLink ) @@ -21,17 +24,17 @@ type ServerConn struct { host string } -type Response struct { - conn net.Conn - c *ServerConn -} - type Entry struct { Name string - EntryType int + Type EntryType Size uint64 } +type response struct { + conn net.Conn + c *ServerConn +} + // Connect to a ftp server and returns a ServerConn handler. func Connect(host, user, password string) (*ServerConn, os.Error) { conn, err := textproto.Dial("tcp", host) @@ -39,26 +42,26 @@ func Connect(host, user, password string) (*ServerConn, os.Error) { return nil, err } - a := strings.Split(host, ":", 2) + a := strings.SplitN(host, ":", 2) c := &ServerConn{conn, a[0]} _, _, err = c.conn.ReadCodeLine(StatusReady) if err != nil { - c.Close() + c.Quit() return nil, err } c.conn.Cmd("USER %s", user) _, _, err = c.conn.ReadCodeLine(StatusUserOK) if err != nil { - c.Close() + c.Quit() return nil, err } c.conn.Cmd("PASS %s", password) _, _, err = c.conn.ReadCodeLine(StatusLoggedIn) if err != nil { - c.Close() + c.Quit() return nil, err } @@ -88,10 +91,10 @@ func (c *ServerConn) epsv() (port int, err os.Error) { } // Open a new data connection using extended passive mode -func (c *ServerConn) openDataConnection() (r *Response, err os.Error) { +func (c *ServerConn) openDataConnection() (net.Conn, os.Error) { port, err := c.epsv() if err != nil { - return + return nil, err } // Build the new net address string @@ -99,11 +102,25 @@ func (c *ServerConn) openDataConnection() (r *Response, err os.Error) { conn, err := net.Dial("tcp", addr) if err != nil { - return + return nil, err } - r = &Response{conn, c} - return + return conn, nil +} + +// Helper function to check if the last command succeeded and if it will +// send the data to the data connection. +// This is needed because some servers return StatusAboutToSend (150) +// and some StatusAlreadyOpen (125) +func (c *ServerConn) checkDataConn() os.Error { + code, msg, err := c.conn.ReadCodeLine(-1) + if err != nil { + return err + } + if code != StatusAlreadyOpen && code != StatusAboutToSend { + return os.NewError(fmt.Sprintf("%d %s", code, msg)) + } + return nil } func parseListLine(line string) (*Entry, os.Error) { @@ -114,14 +131,14 @@ func parseListLine(line string) (*Entry, os.Error) { e := &Entry{} switch fields[0][0] { - case '-': - e.EntryType = EntryTypeFile - case 'd': - e.EntryType = EntryTypeFolder - case 'l': - e.EntryType = EntryTypeLink - default: - return nil, os.NewError("Unknown entry type") + case '-': + e.Type = EntryTypeFile + case 'd': + e.Type = EntryTypeFolder + case 'l': + e.Type = EntryTypeLink + default: + return nil, os.NewError("Unknown entry type") } e.Name = strings.Join(fields[8:], " ") @@ -129,14 +146,20 @@ func parseListLine(line string) (*Entry, os.Error) { } func (c *ServerConn) List(path string) (entries []*Entry, err os.Error) { - r, err := c.openDataConnection() + conn, err := c.openDataConnection() if err != nil { return } + + r := &response{conn, c} defer r.Close() - c.conn.Cmd("LIST %s", path) - _, _, err = c.conn.ReadCodeLine(StatusAboutToSend) + _, err = c.conn.Cmd("LIST %s", path) + if err != nil { + return + } + + err = c.checkDataConn() if err != nil { return } @@ -158,28 +181,109 @@ func (c *ServerConn) List(path string) (entries []*Entry, err os.Error) { } func (c *ServerConn) ChangeDir(path string) (err os.Error) { - c.conn.Cmd("CWD %s", path); - _, _, err = c.conn.ReadCodeLine(StatusRequestedFileActionOK) + _, err = c.conn.Cmd("CWD %s", path) + if err == nil { + _, _, err = c.conn.ReadCodeLine(StatusRequestedFileActionOK) + } return } -func (c *ServerConn) Get(path string) (r *Response, err os.Error) { - r, err = c.openDataConnection() +// Retrieves a remote file +func (c *ServerConn) Retr(path string) (io.ReadCloser, os.Error) { + conn, err := c.openDataConnection() if err != nil { - return + return nil, err } - c.conn.Cmd("RETR %s", path) - _, _, err = c.conn.ReadCodeLine(StatusAboutToSend) - return + _, err = c.conn.Cmd("RETR %s", path) + if err != nil { + conn.Close() + return nil, err + } + + err = c.checkDataConn() + if err != nil { + conn.Close() + return nil, err + } + + r := &response{conn, c} + return r, nil } -func (c *ServerConn) Close() { +func (c *ServerConn) Stor(name string, r io.Reader) os.Error { + conn, err := c.openDataConnection() + if err != nil { + return err + } + + _, err = c.conn.Cmd("STOR %s", name) + if err != nil { + conn.Close() + return err + } + + err = c.checkDataConn() + if err != nil { + conn.Close() + return err + } + + _, err = io.Copy(conn, r) + conn.Close() + if err != nil { + return err + } + + _, _, err = c.conn.ReadCodeLine(StatusClosingDataConnection) + return err +} + +func (c *ServerConn) Rename(from, to string) os.Error { + _, err := c.conn.Cmd("RNFR %s", from) + if err != nil { + return err + } + + _, _, err = c.conn.ReadCodeLine(StatusRequestFilePending) + if err != nil { + return err + } + + _, err = c.conn.Cmd("RNTO %s", to) + if err != nil { + return err + } + + _, _, err = c.conn.ReadCodeLine(StatusRequestedFileActionOK) + return err +} + +func (c *ServerConn) MakeDir(name string) os.Error { + // todo + return nil +} + +func (c *ServerConn) RemoveDir(name string) os.Error { + return nil +} + +// Sends a NOOP command. Usualy used to prevent timeouts. +func (c *ServerConn) NoOp() os.Error { + _, err := c.conn.Cmd("NOOP") + if err != nil { + return err + } + _, _, err = c.conn.ReadCodeLine(StatusCommandOK) + return err +} + +func (c *ServerConn) Quit() os.Error { c.conn.Cmd("QUIT") - c.conn.Close() + return c.conn.Close() } -func (r *Response) Read(buf []byte) (int, os.Error) { +func (r *response) Read(buf []byte) (int, os.Error) { n, err := r.conn.Read(buf) if err == os.EOF { _, _, err2 := r.c.conn.ReadCodeLine(StatusClosingDataConnection) @@ -190,6 +294,6 @@ func (r *Response) Read(buf []byte) (int, os.Error) { return n, err } -func (r *Response) Close() os.Error { +func (r *response) Close() os.Error { return r.conn.Close() } diff --git a/parse_test.go b/parse_test.go index 8fd86ff..8cca1d6 100644 --- a/parse_test.go +++ b/parse_test.go @@ -5,7 +5,7 @@ import "testing" type line struct { line string name string - entryType int + entryType EntryType } var listTests = []line { @@ -37,8 +37,8 @@ func TestParseListLine(t *testing.T) { if entry.Name != lt.name { t.Errorf("parseListLine(%v).Name = '%v', want '%v'", lt.line, entry.Name, lt.name) } - if entry.EntryType != lt.entryType { - t.Errorf("parseListLine(%v).EntryType = %v, want %v", lt.line, entry.EntryType, lt.entryType,) + if entry.Type != lt.entryType { + t.Errorf("parseListLine(%v).EntryType = %v, want %v", lt.line, entry.Type, lt.entryType,) } } for _, lt := range listTestsFail { diff --git a/status.go b/status.go index 70b0fbe..468a00b 100644 --- a/status.go +++ b/status.go @@ -4,6 +4,7 @@ const ( StatusInitiating = 100 StatusRestartMarker = 110 StatusReadyMinute = 120 + StatusAlreadyOpen = 125 StatusAboutToSend = 150 StatusCommandOK = 200 @@ -28,7 +29,7 @@ const ( StatusUserOK = 331 StatusLoginNeedAccount = 332 - Status350 = 350 + StatusRequestFilePending = 350 StatusNotAvailable = 421 StatusCanNotOpenDataConnection = 425 @@ -75,7 +76,7 @@ var statusText = map[int]string{ StatusUserOK: "", StatusLoginNeedAccount: "", - Status350: "", + StatusRequestFilePending: "", StatusNotAvailable: "", StatusCanNotOpenDataConnection: "",