// Copyright 2015 Muir Manders. All rights reserved. // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. package goftp import ( "bufio" "fmt" "os" "path/filepath" "regexp" "strconv" "strings" "time" ) // time.Parse format string for parsing file mtimes. const timeFormat = "20060102150405" // Delete deletes the file "path". func (c *Client) Delete(path string) error { pconn, err := c.getIdleConn() if err != nil { return err } defer c.returnConn(pconn) return pconn.sendCommandExpected(replyFileActionOkay, "DELE %s", path) } // Rename renames file "from" to "to". func (c *Client) Rename(from, to string) error { pconn, err := c.getIdleConn() if err != nil { return err } defer c.returnConn(pconn) err = pconn.sendCommandExpected(replyFileActionPending, "RNFR %s", from) if err != nil { return err } return pconn.sendCommandExpected(replyFileActionOkay, "RNTO %s", to) } // Mkdir creates directory "path". The returned string is how the client // should refer to the created directory. func (c *Client) Mkdir(path string) (string, error) { pconn, err := c.getIdleConn() if err != nil { return "", err } defer c.returnConn(pconn) code, msg, err := pconn.sendCommand("MKD %s", path) if err != nil { return "", err } if code != replyDirCreated { return "", ftpError{code: code, msg: msg} } dir, err := extractDirName(msg) if err != nil { return "", err } return dir, nil } // Rmdir removes directory "path". func (c *Client) Rmdir(path string) error { pconn, err := c.getIdleConn() if err != nil { return err } defer c.returnConn(pconn) return pconn.sendCommandExpected(replyFileActionOkay, "RMD %s", path) } // Getwd returns the current working directory. func (c *Client) Getwd() (string, error) { pconn, err := c.getIdleConn() if err != nil { return "", err } defer c.returnConn(pconn) code, msg, err := pconn.sendCommand("PWD") if err != nil { return "", err } if code != replyDirCreated { return "", ftpError{code: code, msg: msg} } dir, err := extractDirName(msg) if err != nil { return "", err } return dir, nil } func commandNotSupporterdError(err error) bool { respCode := err.(ftpError).Code() return respCode == replyCommandSyntaxError || respCode == replyCommandNotImplemented } // ReadDir fetches the contents of a directory, returning a list of // os.FileInfo's which are relatively easy to work with programatically. It // will not return entries corresponding to the current directory or parent // directories. The os.FileInfo's fields may be incomplete depending on what // the server supports. If the server does not support "MLSD", "LIST" will // be used. You may have to set ServerLocation in your config to get (more) // accurate ModTimes in this case. func (c *Client) ReadDir(path string) ([]os.FileInfo, error) { entries, err := c.dataStringList("MLSD %s", path) parser := parseMLST if err != nil { if !commandNotSupporterdError(err) { return nil, err } entries, err = c.dataStringList("LIST %s", path) if err != nil { return nil, err } parser = func(entry string, skipSelfParent bool) (os.FileInfo, error) { return parseLIST(entry, c.config.ServerLocation, skipSelfParent) } } var ret []os.FileInfo for _, entry := range entries { info, err := parser(entry, true) if err != nil { c.debug("error in ReadDir: %s", err) return nil, err } if info == nil { continue } ret = append(ret, info) } return ret, nil } // Stat fetches details for a particular file. The os.FileInfo's fields may // be incomplete depending on what the server supports. If the server doesn't // support "MLST", "LIST" will be attempted, but "LIST" will not work if path // is a directory. You may have to set ServerLocation in your config to get // (more) accurate ModTimes when using "LIST". func (c *Client) Stat(path string) (os.FileInfo, error) { lines, err := c.controlStringList("MLST %s", path) if err != nil { if commandNotSupporterdError(err) { lines, err = c.dataStringList("LIST %s", path) if err != nil { return nil, err } if len(lines) != 1 { return nil, ftpError{err: fmt.Errorf("unexpected LIST response: %v", lines)} } return parseLIST(lines[0], c.config.ServerLocation, false) } return nil, err } if len(lines) != 3 { return nil, ftpError{err: fmt.Errorf("unexpected MLST response: %v", lines)} } return parseMLST(strings.TrimLeft(lines[1], " "), false) } func extractDirName(msg string) (string, error) { openQuote := strings.Index(msg, "\"") closeQuote := strings.LastIndex(msg, "\"") if openQuote == -1 || len(msg) == openQuote+1 || closeQuote <= openQuote { return "", ftpError{ err: fmt.Errorf("failed parsing directory name: %s", msg), } } return strings.Replace(msg[openQuote+1:closeQuote], `""`, `"`, -1), nil } func (c *Client) controlStringList(f string, args ...interface{}) ([]string, error) { pconn, err := c.getIdleConn() if err != nil { return nil, err } defer c.returnConn(pconn) cmd := fmt.Sprintf(f, args...) code, msg, err := pconn.sendCommand(cmd) if !positiveCompletionReply(code) { pconn.debug("unexpected response to %s: %d-%s", cmd, code, msg) return nil, ftpError{code: code, msg: msg} } return strings.Split(msg, "\n"), nil } func (c *Client) dataStringList(f string, args ...interface{}) ([]string, error) { pconn, err := c.getIdleConn() if err != nil { return nil, err } defer c.returnConn(pconn) dcGetter, err := pconn.prepareDataConn() if err != nil { return nil, err } cmd := fmt.Sprintf(f, args...) err = pconn.sendCommandExpected(replyGroupPreliminaryReply, cmd) if err != nil { return nil, err } dc, err := dcGetter() if err != nil { return nil, err } // to catch early returns defer dc.Close() scanner := bufio.NewScanner(dc) scanner.Split(bufio.ScanLines) var res []string for scanner.Scan() { res = append(res, scanner.Text()) } var dataError error if err = scanner.Err(); err != nil { pconn.debug("error reading %s data: %s", cmd, err) dataError = ftpError{ err: fmt.Errorf("error reading %s data: %s", cmd, err), temporary: true, } } err = dc.Close() if err != nil { pconn.debug("error closing data connection: %s", err) } code, msg, err := pconn.readResponse() if err != nil { return nil, err } if !positiveCompletionReply(code) { pconn.debug("unexpected result: %d-%s", code, msg) return nil, ftpError{code: code, msg: msg} } if dataError != nil { return nil, dataError } return res, nil } type ftpFile struct { name string size int64 mode os.FileMode mtime time.Time raw string } func (f *ftpFile) Name() string { return f.name } func (f *ftpFile) Size() int64 { return f.size } func (f *ftpFile) Mode() os.FileMode { return f.mode } func (f *ftpFile) ModTime() time.Time { return f.mtime } func (f *ftpFile) IsDir() bool { return f.mode.IsDir() } func (f *ftpFile) Sys() interface{} { return f.raw } var lsRegex = regexp.MustCompile(`^\s*(\S)(\S{3})(\S{3})(\S{3})(?:\s+\S+){3}\s+(\d+)\s+(\w+\s+\d+)\s+([\d:]+)\s+(.+)$`) var lsRegexMS = regexp.MustCompile(`^(\d+-\d+-\d+)\s+(\d+:\d+[^ ]+)\s+([^ ]+)\s+(.*)$`) // total 404456 // drwxr-xr-x 8 goftp 20 272 Jul 28 05:03 git-ignored // or // 07-23-21 05:03PM