├── COPYRIGHT
├── LICENSE
├── README.md
├── client.go
├── client_test.go
├── clientproto.go
├── clientutil.go
├── status.go
└── test
├── subdir
├── test6310.jpg
├── test6313.jpg
└── testsubdir.jpg
├── test.jpg
├── test.txt
├── test6352.jpg
├── test6353.jpg
└── test6361.jpg
/COPYRIGHT:
--------------------------------------------------------------------------------
1 | Copyright (c) 2013, Enrico Mezzato
2 | All rights reserved.
3 |
4 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
5 |
6 | * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
7 | * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
8 | * Neither the name of goconf nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
9 |
10 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
11 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "{}"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright 2017 Alexander Trost
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | This is an FTP client started as a port of the standard Python FTP client library
2 | Forked form code.google.com/p/ftp4go and change some dependenes of the package
3 |
4 | # Installation
5 |
6 | go get github.com/shenshouer/ftp4go
7 |
8 | # How to use it
9 | Import the library in your code and call the methods exposed by the FTP structure, for instance:
10 |
11 | package main 12 | import ( 13 | "fmt" 14 | "os" 15 | ftp4go "github.com/shenshouer/ftp4go" 16 | ) 17 | func main() { 18 | ftpClient := ftp4go.NewFTP(0) // 1 for debugging 19 | //set time out 20 | ftpClient.SetFTPTimeout(20 * time.Second) 21 | //connect 22 | _, err := ftpClient.Connect("myFtpAddress", ftp4go.DefaultFtpPort) 23 | if err != nil { 24 | fmt.Println("The connection failed") 25 | os.Exit(1) 26 | } 27 | defer ftpClient.Quit() 28 | _, err = ftpClient.Login("myUsername", "myPassword", "") 29 | if err != nil { 30 | fmt.Println("The login failed") 31 | os.Exit(1) 32 | } 33 | //Print the current working directory 34 | var cwd string 35 | cwd, err = ftpClient.Pwd() 36 | if err != nil { 37 | fmt.Println("The Pwd command failed") 38 | os.Exit(1) 39 | } 40 | fmt.Println("The current folder is", cwd) 41 | } 42 |43 | 44 | # 断点续传示例 45 |
46 | package main 47 | 48 | import ( 49 | ftp4go "github.com/shenshouer/ftp4go" 50 | "fmt" 51 | "os" 52 | ) 53 | 54 | var( 55 | downloadFileName = "DockerToolbox-1.8.2a.pkg" 56 | BASE_FTP_PATH = "/home/bob/" // base data path in ftp server 57 | ) 58 | 59 | func main() { 60 | ftpClient := ftp4go.NewFTP(0) // 1 for debugging 61 | 62 | //connect 63 | _, err := ftpClient.Connect("172.8.4.101", ftp4go.DefaultFtpPort, "") 64 | if err != nil { 65 | fmt.Println("The connection failed") 66 | os.Exit(1) 67 | } 68 | defer ftpClient.Quit() 69 | 70 | _, err = ftpClient.Login("bob", "p@ssw0rd", "") 71 | if err != nil { 72 | fmt.Println("The login failed") 73 | os.Exit(1) 74 | } 75 | 76 | //Print the current working directory 77 | var cwd string 78 | cwd, err = ftpClient.Pwd() 79 | if err != nil { 80 | fmt.Println("The Pwd command failed") 81 | os.Exit(1) 82 | } 83 | fmt.Println("The current folder is", cwd) 84 | 85 | 86 | // get the remote file size 87 | size, err := ftpClient.Size("/home/bob/"+downloadFileName) 88 | if err != nil { 89 | fmt.Println("The Pwd command failed") 90 | os.Exit(1) 91 | } 92 | fmt.Println("size ", size) 93 | 94 | // start resume file download 95 | if err = ftpClient.DownloadResumeFile("/home/bob/"+downloadFileName, "/Users/goyoo/ftptest/"+downloadFileName, false); err != nil{ 96 | panic(err) 97 | } 98 | 99 | } 100 |101 | 102 | 103 | # More on the code 104 | Being a port of a Python library, the original Python version is probably the best reference. 105 | Python ftplib 106 | 107 | ## Differences to the original version 108 | Some new methods have been implemented to upload and download files, recursively in a folder as well. 109 | 110 | ## TODOs and unsupported functionality 111 | * TLS is not supported yet 112 | * add multi goroutine for one download task support -------------------------------------------------------------------------------- /client.go: -------------------------------------------------------------------------------- 1 | // Package ftp implements an FTP client. 2 | package ftp4go 3 | 4 | import ( 5 | "bufio" 6 | "errors" 7 | "fmt" 8 | "golang.org/x/net/proxy" 9 | "io" 10 | "log" 11 | "net" 12 | "net/textproto" 13 | "net/url" 14 | "os" 15 | "strconv" 16 | "strings" 17 | "time" 18 | ) 19 | 20 | // The default constants 21 | const ( 22 | DefaultFtpPort = 21 23 | DefaultTimeoutInMsec = 20 * time.Second 24 | CRLF = "\r\n" 25 | BLOCK_SIZE = 8192 26 | ) 27 | 28 | // FTP command strings 29 | type FtpCmd int 30 | 31 | const ( 32 | NONE_FTP_CMD FtpCmd = 0 33 | USER_FTP_CMD FtpCmd = 1 34 | PASSWORD_FTP_CMD FtpCmd = 2 35 | ACCT_FTP_CMD FtpCmd = 3 36 | ABORT_FTP_CMD FtpCmd = 4 37 | PORT_FTP_CMD FtpCmd = 5 38 | PASV_FTP_CMD FtpCmd = 6 39 | TYPE_A_FTP_CMD FtpCmd = 7 40 | NLST_FTP_CMD FtpCmd = 8 41 | LIST_FTP_CMD FtpCmd = 9 42 | FEAT_FTP_CMD FtpCmd = 10 43 | OPTS_FTP_CMD FtpCmd = 11 44 | RETR_FTP_CMD FtpCmd = 12 45 | TYPE_I_FTP_CMD FtpCmd = 13 46 | STORE_FTP_CMD FtpCmd = 14 47 | RENAMEFROM_FTP_CMD FtpCmd = 15 48 | RENAMETO_FTP_CMD FtpCmd = 16 49 | DELETE_FTP_CMD FtpCmd = 17 50 | CWD_FTP_CMD FtpCmd = 18 51 | SIZE_FTP_CMD FtpCmd = 19 52 | MKDIR_FTP_CMD FtpCmd = 20 53 | RMDIR_FTP_CMD FtpCmd = 21 54 | PWDIR_FTP_CMD FtpCmd = 22 55 | CDUP_FTP_CMD FtpCmd = 23 56 | QUIT_FTP_CMD FtpCmd = 24 57 | MLSD_FTP_CMD FtpCmd = 25 58 | REST_FTP_CMD FtpCmd = 26 59 | ) 60 | 61 | const MSG_OOB = 0x1 //Process data out of band 62 | 63 | var ftpCmdStrings = map[FtpCmd]string{ 64 | NONE_FTP_CMD: "", 65 | USER_FTP_CMD: "USER", 66 | PASSWORD_FTP_CMD: "PASS", 67 | ACCT_FTP_CMD: "ACCT", 68 | ABORT_FTP_CMD: "ABOR", 69 | PORT_FTP_CMD: "PORT", 70 | PASV_FTP_CMD: "PASV", 71 | TYPE_A_FTP_CMD: "TYPE A", 72 | NLST_FTP_CMD: "NLST", 73 | LIST_FTP_CMD: "LIST", 74 | MLSD_FTP_CMD: "MLSD", 75 | FEAT_FTP_CMD: "FEAT", 76 | OPTS_FTP_CMD: "OPTS", 77 | RETR_FTP_CMD: "RETR", 78 | TYPE_I_FTP_CMD: "TYPE I", 79 | STORE_FTP_CMD: "STOR", 80 | RENAMEFROM_FTP_CMD: "RNFR", 81 | RENAMETO_FTP_CMD: "RNTO", 82 | DELETE_FTP_CMD: "DELE", 83 | CWD_FTP_CMD: "CWD", 84 | SIZE_FTP_CMD: "SIZE", 85 | MKDIR_FTP_CMD: "MKD", 86 | RMDIR_FTP_CMD: "RMD", 87 | PWDIR_FTP_CMD: "PWD", 88 | CDUP_FTP_CMD: "CDUP", 89 | QUIT_FTP_CMD: "QUIT", 90 | REST_FTP_CMD: "REST", 91 | } 92 | 93 | // The FTP client structure containing: 94 | // - host, user, password, acct, timeout 95 | type FTP struct { 96 | debugging int 97 | Host string 98 | Port int 99 | file string 100 | welcome string 101 | passiveserver bool 102 | logger *log.Logger 103 | timeoutInMsec time.Duration 104 | textprotoConn *textproto.Conn 105 | dialer proxy.Dialer 106 | conn net.Conn 107 | encoding string 108 | stop chan bool 109 | } 110 | 111 | type NameFactsLine struct { 112 | Name string 113 | Facts map[string]string 114 | } 115 | 116 | func getTimeoutInMsec(msec int64) time.Time { 117 | return time.Now().Add(time.Duration(msec) * time.Millisecond) 118 | } 119 | 120 | func (i FtpCmd) String() string { 121 | if cmd, ok := ftpCmdStrings[i]; ok { 122 | return cmd 123 | } 124 | panic("No cmd found") 125 | } 126 | 127 | func (i FtpCmd) AppendParameters(pars ...string) string { 128 | allPars := make([]string, len(pars)+1) 129 | allPars[0] = i.String() 130 | var k int = 1 131 | for _, par := range pars { 132 | if p := strings.TrimSpace(par); len(p) > 0 { 133 | allPars[k] = p 134 | k++ 135 | } 136 | } 137 | return strings.Join(allPars[:k], " ") 138 | } 139 | 140 | func (ftp *FTP) writeInfo(params ...interface{}) { 141 | if ftp.debugging >= 1 { 142 | log.Println(params...) 143 | } 144 | } 145 | 146 | func (ftp *FTP) Stop() { 147 | ftp.stop = make(chan bool) 148 | ftp.stop <- true 149 | } 150 | 151 | // NewFTP creates a new FTP client using a debug level, default is 0, which is disabled. 152 | // The FTP server uses the passive tranfer mode by default. 153 | // 154 | // Debuglevel: 155 | // 0 -> disabled 156 | // 1 -> information 157 | // 2 -> verbose 158 | // 159 | func NewFTP(debuglevel int) *FTP { 160 | logger := log.New(os.Stdout, "", log.LstdFlags) //syslog.NewLogger(syslog.LOG_ERR, 999) 161 | ftp := &FTP{ 162 | debugging: debuglevel, 163 | Port: DefaultFtpPort, 164 | logger: logger, 165 | //timeoutInMsec: DefaultTimeoutInMsec, 166 | passiveserver: true, 167 | } 168 | return ftp 169 | } 170 | 171 | //Set ftp dial timeout 172 | func (ftp *FTP) SetFTPTimeout(timeout time.Duration) error { 173 | if ftp == nil { 174 | return errors.New("ftp client is nil") 175 | } 176 | if timeout <= 0 { 177 | return errors.New("timeout must be greater than 0") 178 | } 179 | ftp.timeoutInMsec = timeout 180 | return nil 181 | } 182 | 183 | // Connect connects to the host by using the specified port or the default one if the value is <=0. 184 | func (ftp *FTP) Connect(host string, port int, socks5ProxyUrl string) (resp *Response, err error) { 185 | 186 | if len(host) == 0 { 187 | return nil, errors.New("The host must be specified") 188 | } 189 | ftp.Host = host 190 | 191 | if port <= 0 { 192 | port = DefaultFtpPort 193 | } else { 194 | ftp.Port = port 195 | } 196 | 197 | addr := fmt.Sprintf("%s:%d", ftp.Host, ftp.Port) 198 | 199 | // use the system proxy if emtpy 200 | if socks5ProxyUrl == "" { 201 | ftp.writeInfo("using environment proxy, url: ", os.Getenv("all_proxy")) 202 | ftp.dialer = proxy.FromEnvironment() 203 | } else { 204 | ftp.dialer = proxy.Direct 205 | 206 | if u, err1 := url.Parse(socks5ProxyUrl); err1 == nil { 207 | p, err2 := proxy.FromURL(u, proxy.Direct) 208 | if err2 == nil { 209 | ftp.dialer = p 210 | } 211 | } 212 | 213 | } 214 | 215 | err = ftp.NewConn(addr) 216 | if err != nil { 217 | return 218 | } 219 | 220 | ftp.writeInfo("host:", ftp.Host, " port:", strconv.Itoa(ftp.Port), " proxy enabled:", ftp.dialer != proxy.Direct) 221 | 222 | // NOTE: this is an absolute time that needs refreshing after each READ/WRITE net operation 223 | //ftp.conn.conn.SetDeadline(getTimeoutInMsec(ftp.timeoutInMsec)) 224 | 225 | if resp, err = ftp.Read(NONE_FTP_CMD); err != nil { 226 | return 227 | } 228 | ftp.welcome = resp.Message 229 | ftp.writeInfo("Successfully connected on local address:", ftp.conn.LocalAddr()) 230 | return 231 | } 232 | 233 | // SetPassive sets the mode to passive or active for data transfers. 234 | // With a false statement use the normal PORT mode. 235 | // With a true statement use the PASV command. 236 | func (ftp *FTP) SetPassive(ispassive bool) { 237 | ftp.passiveserver = ispassive 238 | } 239 | 240 | // Login logs on to the server. 241 | func (ftp *FTP) Login(username, password string, acct string) (response *Response, err error) { 242 | 243 | //Login, default anonymous. 244 | if len(username) == 0 { 245 | username = "anonymous" 246 | } 247 | if len(password) == 0 { 248 | password = "" 249 | } 250 | 251 | if username == "anonymous" && len(password) == 0 { 252 | // If there is no anonymous ftp password specified 253 | // then we'll just use anonymous@ 254 | // We don't send any other thing because: 255 | // - We want to remain anonymous 256 | // - We want to stop SPAM 257 | // - We don't want to let ftp sites to discriminate by the user, 258 | // host or country. 259 | password = password + "anonymous@" 260 | } 261 | 262 | ftp.writeInfo("username:", username) 263 | tempResponse, err := ftp.SendAndRead(USER_FTP_CMD, username) 264 | if err != nil { 265 | return 266 | } 267 | 268 | // if tempResponse.getFirstChar() == "3" { 269 | if tempResponse.Code == StatusUserOK { 270 | tempResponse, err = ftp.SendAndRead(PASSWORD_FTP_CMD, password) 271 | if err != nil { 272 | return 273 | } 274 | } 275 | if tempResponse.getFirstChar() == "3" { 276 | tempResponse, err = ftp.SendAndRead(ACCT_FTP_CMD, acct) 277 | if err != nil { 278 | return 279 | } 280 | } 281 | // if tempResponse.getFirstChar() != "2" { 282 | if tempResponse.Code != StatusLoggedIn { 283 | err = NewErrReply(errors.New(tempResponse.Message)) 284 | return 285 | } 286 | return tempResponse, err 287 | } 288 | 289 | // Abort interrupts a file transfer, which uses out-of-band data. 290 | // This does not follow the procedure from the RFC to send Telnet IP and Synch; 291 | // that does not seem to work with all servers. Instead just send the ABOR command as OOB data. 292 | func (ftp *FTP) Abort() (response *Response, err error) { 293 | return ftp.SendAndRead(ABORT_FTP_CMD) 294 | } 295 | 296 | // SendPort sends a PORT command with the current host and given port number 297 | func (ftp *FTP) SendPort(host string, port int) (response *Response, err error) { 298 | hbytes := strings.Split(host, ".") // return all substrings 299 | pbytes := []string{strconv.Itoa(port / 256), strconv.Itoa(port % 256)} 300 | bytes := strings.Join(append(hbytes, pbytes...), ",") 301 | return ftp.SendAndRead(PORT_FTP_CMD, bytes) 302 | } 303 | 304 | // makePasv sends a PASV command and returns the host and port number to be used for the data transfer connection. 305 | func (ftp *FTP) makePasv() (host string, port int, err error) { 306 | var resp *Response 307 | resp, err = ftp.SendAndRead(PASV_FTP_CMD) 308 | if err != nil { 309 | return 310 | } 311 | return parse227(resp) 312 | } 313 | 314 | // Acct sends an ACCT command. 315 | func (ftp *FTP) Acct() (response *Response, err error) { 316 | return ftp.SendAndRead(ACCT_FTP_CMD) 317 | } 318 | 319 | // Mlsd lists a directory in a standardized format by using MLSD 320 | // command (RFC-3659). If path is omitted the current directory 321 | // is assumed. "facts" is a list of strings representing the type 322 | // of information desired (e.g. ["type", "size", "perm"]). 323 | // Return a generator object yielding a tuple of two elements 324 | // for every file found in path. 325 | // First element is the file name, the second one is a dictionary 326 | // including a variable number of "facts" depending on the server 327 | // and whether "facts" argument has been provided. 328 | func (ftp *FTP) Mlsd(path string, facts []string) (ls []*NameFactsLine, err error) { 329 | 330 | if len(facts) > 0 { 331 | if _, err = ftp.Opts("MLST", strings.Join(facts, ";")+";"); err != nil { 332 | return nil, err 333 | } 334 | } 335 | 336 | sw := &stringSliceWriter{make([]string, 0, 50)} 337 | if err = ftp.GetLines(MLSD_FTP_CMD, sw, path); err != nil { 338 | return nil, err 339 | } 340 | 341 | ls = make([]*NameFactsLine, len(sw.s)) 342 | for _, l := range sw.s { 343 | tkns := strings.Split(strings.TrimSpace(l), " ") 344 | name := tkns[0] 345 | facts := strings.Split(tkns[1], ";") 346 | ftp.writeInfo("Found facts:", facts) 347 | vals := make(map[string]string, len(facts)-1) 348 | for i := 0; i < len(facts)-1; i++ { 349 | fpair := strings.Split(facts[i], "=") 350 | vals[fpair[0]] = fpair[1] 351 | } 352 | ls = append(ls, &NameFactsLine{strings.ToLower(name), vals}) 353 | } 354 | return 355 | } 356 | 357 | // Feat lists all new FTP features that the server supports beyond those described in RFC 959. 358 | func (ftp *FTP) Feat(params ...string) (fts []string, err error) { 359 | var r *Response 360 | if r, err = ftp.SendAndRead(FEAT_FTP_CMD); err != nil { 361 | return 362 | } 363 | 364 | return parse211(r) 365 | } 366 | 367 | // Nlst returns a list of file in a directory, by default the current. 368 | func (ftp *FTP) Nlst(params ...string) (filelist []string, err error) { 369 | return ftp.getList(NLST_FTP_CMD, params...) 370 | } 371 | 372 | // Dir returns a list of file in a directory in long form, by default the current. 373 | func (ftp *FTP) Dir(params ...string) (filelist []string, err error) { 374 | return ftp.getList(LIST_FTP_CMD, params...) 375 | } 376 | 377 | func (ftp *FTP) getList(cmd FtpCmd, params ...string) (filelist []string, err error) { 378 | files := make([]string, 0, 50) 379 | sw := &stringSliceWriter{files} 380 | if err = ftp.GetLines(cmd, sw, params...); err != nil { 381 | return nil, err 382 | } 383 | return sw.s, nil 384 | } 385 | 386 | // Rename renames a file. 387 | func (ftp *FTP) Rename(fromname string, toname string) (response *Response, err error) { 388 | tempResponse, err := ftp.SendAndRead(RENAMEFROM_FTP_CMD, fromname) 389 | if err != nil { 390 | return nil, err 391 | } 392 | if tempResponse.getFirstChar() != "3" { 393 | err = NewErrReply(errors.New(tempResponse.Message)) 394 | return nil, err 395 | } 396 | return ftp.SendAndRead(RENAMETO_FTP_CMD, toname) 397 | } 398 | 399 | // Delete deletes a file. 400 | func (ftp *FTP) Delete(filename string) (response *Response, err error) { 401 | tempResponse, err := ftp.SendAndRead(DELETE_FTP_CMD, filename) 402 | if err != nil { 403 | return nil, err 404 | } 405 | if c := tempResponse.Code; c == StatusRequestedFileActionOK || c == StatusCommandOK { 406 | return tempResponse, nil 407 | } else { 408 | return nil, NewErrReply(errors.New(tempResponse.Message)) 409 | } 410 | return 411 | } 412 | 413 | // Cwd changes to current directory. 414 | func (ftp *FTP) Cwd(dirname string) (response *Response, err error) { 415 | if dirname == ".." { 416 | return ftp.SendAndRead(CDUP_FTP_CMD) 417 | } else if dirname == "" { 418 | dirname = "." 419 | } 420 | return ftp.SendAndRead(CWD_FTP_CMD, dirname) 421 | } 422 | 423 | // Size retrieves the size of a file. 424 | func (ftp *FTP) Size(filename string) (size int, err error) { 425 | response, err := ftp.SendAndRead(SIZE_FTP_CMD, filename) 426 | if err != nil { 427 | return 428 | } 429 | if response.Code == StatusFile { 430 | //size, _ = strconv.Atoi(strings.TrimSpace(response.Message[3:])) 431 | size, _ = strconv.Atoi(response.Message) 432 | return size, err 433 | } 434 | return 435 | } 436 | 437 | // Mkd creates a directory and returns its full pathname. 438 | func (ftp *FTP) Mkd(dirname string) (dname string, err error) { 439 | var response *Response 440 | response, err = ftp.SendAndRead(MKDIR_FTP_CMD, dirname) 441 | if err != nil { 442 | return 443 | } 444 | // fix around non-compliant implementations such as IIS shipped 445 | // with Windows server 2003 446 | if response.Code != StatusPathCreated { 447 | return "", nil 448 | } 449 | return parse257(response) 450 | } 451 | 452 | // Rmd removes a directory. 453 | func (ftp *FTP) Rmd(dirname string) (response *Response, err error) { 454 | return ftp.SendAndRead(RMDIR_FTP_CMD, dirname) 455 | } 456 | 457 | // Pwd returns the current working directory. 458 | func (ftp *FTP) Pwd() (dirname string, err error) { 459 | response, err := ftp.SendAndRead(PWDIR_FTP_CMD) 460 | // fix around non-compliant implementations such as IIS shipped 461 | // with Windows server 2003 462 | if err != nil { 463 | return "", err 464 | } 465 | if response.Code != 257 { 466 | return "", nil 467 | } 468 | return parse257(response) 469 | } 470 | 471 | // Quits sends a QUIT command and closes the connection. 472 | func (ftp *FTP) Quit() (response *Response, err error) { 473 | response, err = ftp.SendAndRead(QUIT_FTP_CMD) 474 | if ftp.conn != nil { 475 | ftp.conn.Close() 476 | ftp.conn = nil 477 | } 478 | 479 | return 480 | } 481 | 482 | // DownloadFile downloads a file and stores it locally. 483 | // There are two modes: 484 | // - binary, useLineMode = false 485 | // - line by line (text), useLineMode = true 486 | func (ftp *FTP) DownloadFile(remotename string, localpath string, useLineMode bool) (err error) { 487 | // remove local file 488 | os.Remove(localpath) 489 | var f *os.File 490 | f, err = os.OpenFile(localpath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644) 491 | defer f.Close() 492 | 493 | if err != nil { 494 | return 495 | } 496 | 497 | if useLineMode { 498 | w := newTextFileWriter(f) 499 | defer w.bw.Flush() // remember to flush 500 | if err = ftp.GetLines(RETR_FTP_CMD, w, remotename); err != nil { 501 | return err 502 | } 503 | } else { 504 | if err = ftp.GetBytes(RETR_FTP_CMD, f, BLOCK_SIZE, remotename); err != nil { 505 | return err 506 | } 507 | } 508 | 509 | return err 510 | } 511 | 512 | // UploadFile uploads a file from a local path to the current folder (see Cwd too) on the FTP server. 513 | // A remotename needs to be specified. 514 | // There are two modes set via the useLineMode flag: 515 | // - binary, useLineMode = false 516 | // - line by line (text), useLineMode = true 517 | func (ftp *FTP) UploadFile(remotename string, localpath string, useLineMode bool, callback Callback) (err error) { 518 | var f *os.File 519 | f, err = os.Open(localpath) 520 | defer f.Close() 521 | 522 | if err != nil { 523 | return 524 | } 525 | 526 | if useLineMode { 527 | if err = ftp.StoreLines(STORE_FTP_CMD, f, remotename, localpath, callback); err != nil { 528 | return err 529 | } 530 | } else { 531 | if err = ftp.StoreBytes(STORE_FTP_CMD, f, BLOCK_SIZE, remotename, localpath, callback); err != nil { 532 | return err 533 | } 534 | } 535 | 536 | return err 537 | } 538 | 539 | // Opts returns a list of file in a directory in long form, by default the current. 540 | func (ftp *FTP) Opts(params ...string) (response *Response, err error) { 541 | return ftp.SendAndRead(OPTS_FTP_CMD, params...) 542 | } 543 | 544 | // GetLines retrieves data in line mode. 545 | // Args: 546 | // cmd: A RETR, LIST, NLST, or MLSD command. 547 | // writer: of interface type io.Writer that is called for each line with the trailing CRLF stripped. 548 | // 549 | // returns: 550 | // The response code. 551 | func (ftp *FTP) GetLines(cmd FtpCmd, writer io.Writer, params ...string) (err error) { 552 | var conn net.Conn 553 | if _, err = ftp.SendAndRead(TYPE_A_FTP_CMD); err != nil { 554 | return 555 | } 556 | 557 | // wrap this code up to guarantee the connection disposal via a defer 558 | separateCall := func() error { 559 | if conn, _, err = ftp.transferCmd(cmd, params...); err != nil { 560 | return err 561 | } 562 | defer conn.Close() // close the connection on exit 563 | 564 | ftpReader := textproto.NewConn(conn) 565 | ftp.writeInfo("Try and get lines via connection for remote address:", conn.RemoteAddr().String()) 566 | 567 | for { 568 | line, err := ftpReader.ReadLineBytes() 569 | 570 | if err != nil { 571 | if err == io.EOF { 572 | ftp.writeInfo("Reached end of buffer with line:", line) 573 | break 574 | } 575 | return err 576 | } 577 | 578 | if _, err1 := writer.Write(line); err1 != nil { 579 | return err1 580 | } 581 | 582 | } 583 | return nil 584 | 585 | } 586 | 587 | if err := separateCall(); err != nil { 588 | return err 589 | } 590 | 591 | ftp.writeInfo("Reading final empty line") 592 | _, err = ftp.Read(cmd) 593 | return 594 | 595 | } 596 | 597 | // GetBytes retrieves data in binary mode. 598 | // Args: 599 | // cmd: A RETR command. 600 | // callback: A single parameter callable to be called on each 601 | // block of data read. 602 | // blocksize: The maximum number of bytes to read from the 603 | // socket at one time. [default: 8192] 604 | // 605 | //Returns: 606 | // The response code. 607 | func (ftp *FTP) GetBytes(cmd FtpCmd, writer io.Writer, blocksize int, params ...string) (err error) { 608 | var conn net.Conn 609 | if _, err = ftp.SendAndRead(TYPE_I_FTP_CMD); err != nil { 610 | return 611 | } 612 | 613 | // wrap this code up to guarantee the connection disposal via a defer 614 | separateCall := func() error { 615 | if conn, _, err = ftp.transferCmd(cmd, params...); err != nil { 616 | return err 617 | } 618 | defer conn.Close() // close the connection on exit 619 | 620 | bufReader := bufio.NewReaderSize(conn, blocksize) 621 | 622 | ftp.writeInfo("Try and get bytes via connection for remote address:", conn.RemoteAddr().String()) 623 | 624 | s := make([]byte, blocksize) 625 | var n int 626 | 627 | for { 628 | 629 | n, err = bufReader.Read(s) 630 | ftp.writeInfo("GETBYTES: Number of bytes read:", n) 631 | if _, err1 := writer.Write(s[:n]); err1 != nil { 632 | return err1 633 | } 634 | 635 | if tmpfile, ok := writer.(*os.File); ok { 636 | tmpfile.Sync() 637 | } 638 | 639 | if err != nil { 640 | if err == io.EOF { 641 | break 642 | } 643 | return err 644 | } 645 | 646 | } 647 | 648 | return nil 649 | } 650 | 651 | if err := separateCall(); err != nil { 652 | return err 653 | } 654 | 655 | _, err = ftp.Read(cmd) 656 | return 657 | } 658 | 659 | func (ftp *FTP) DownloadResumeFile(remotename string, localpath string, useLineMode bool) (err error) { 660 | // remove local file 661 | // os.Remove(localpath) 662 | var f *os.File 663 | f, err = os.OpenFile(localpath, os.O_WRONLY|os.O_CREATE, 0644) 664 | defer f.Close() 665 | 666 | if err != nil { 667 | return 668 | } 669 | 670 | if useLineMode { 671 | w := newTextFileWriter(f) 672 | defer w.bw.Flush() // remember to flush 673 | if err = ftp.GetLines(RETR_FTP_CMD, w, remotename); err != nil { 674 | return err 675 | } 676 | } else { 677 | var stat os.FileInfo 678 | stat, err = f.Stat() 679 | if err != nil { 680 | return 681 | } 682 | 683 | offset := stat.Size() 684 | if err = ftp.ResumeFile(RETR_FTP_CMD, f, offset, BLOCK_SIZE, remotename); err != nil { 685 | return err 686 | } 687 | } 688 | 689 | return err 690 | } 691 | 692 | func (ftp *FTP) ResumeFile(cmd FtpCmd, writer *os.File, offset int64, blocksize int, params ...string) (err error) { 693 | var conn net.Conn 694 | if _, err = ftp.SendAndRead(TYPE_I_FTP_CMD); err != nil { 695 | return 696 | } 697 | 698 | // wrap this code up to guarantee the connection disposal via a defer 699 | separateCall := func() error { 700 | 701 | if offset != 0 { 702 | if res, err := ftp.SendAndRead(REST_FTP_CMD, fmt.Sprintf("%d", offset)); err != nil { 703 | return err 704 | } else { 705 | fmt.Println(res) 706 | } 707 | 708 | } 709 | 710 | if conn, _, err = ftp.transferCmd(cmd, params...); err != nil { 711 | return err 712 | } 713 | defer conn.Close() // close the connection on exit 714 | 715 | bufReader := bufio.NewReaderSize(conn, blocksize) 716 | 717 | ftp.writeInfo("Try and get bytes via connection for remote address:", conn.RemoteAddr().String()) 718 | 719 | s := make([]byte, blocksize) 720 | var n int 721 | 722 | for { 723 | if ftp.stop != nil { 724 | select { 725 | case <-ftp.stop: 726 | return NewErrStop 727 | default: 728 | n, err = bufReader.Read(s) 729 | ftp.writeInfo("GETBYTES: Number of bytes read:", n) 730 | 731 | if _, err1 := writer.WriteAt(s[:n], offset); err1 != nil { 732 | return err1 733 | } 734 | if err2 := writer.Sync(); err2 != nil { 735 | return err2 736 | } 737 | 738 | offset += int64(n) 739 | if err != nil { 740 | if err == io.EOF { 741 | break 742 | } 743 | return err 744 | } 745 | } 746 | } else { 747 | n, err = bufReader.Read(s) 748 | ftp.writeInfo("GETBYTES: Number of bytes read:", n) 749 | 750 | if _, err1 := writer.WriteAt(s[:n], offset); err1 != nil { 751 | return err1 752 | } 753 | if err2 := writer.Sync(); err2 != nil { 754 | return err2 755 | } 756 | 757 | offset += int64(n) 758 | if err != nil { 759 | if err == io.EOF { 760 | break 761 | } 762 | return err 763 | } 764 | } 765 | 766 | } 767 | 768 | return nil 769 | } 770 | 771 | if err := separateCall(); err != nil { 772 | return err 773 | } 774 | 775 | _, err = ftp.Read(cmd) 776 | return 777 | } 778 | 779 | // StoreLines stores a file in line mode. 780 | // 781 | // Args: 782 | // cmd: A STOR command. 783 | // reader: A reader object with a ReadLine() method. 784 | // callback: An optional single parameter callable that is called on 785 | // on each line after it is sent. [default: None] 786 | // 787 | // Returns: 788 | // The response code. 789 | func (ftp *FTP) StoreLines(cmd FtpCmd, reader io.Reader, remotename string, filename string, callback Callback) (err error) { 790 | var conn net.Conn 791 | if _, err = ftp.SendAndRead(TYPE_A_FTP_CMD); err != nil { 792 | return 793 | } 794 | 795 | // wrap this code up to guarantee the connection disposal via a defer 796 | separateCall := func() error { 797 | if conn, _, err = ftp.transferCmd(cmd, remotename); err != nil { 798 | return err 799 | } 800 | defer conn.Close() // close the connection on exit 801 | 802 | ftp.writeInfo("Try and write lines via connection for remote address:", conn.RemoteAddr().String()) 803 | 804 | //lineReader := bufio.NewReader(reader) 805 | lineReader := bufio.NewReader(reader) 806 | 807 | var tot int64 808 | 809 | for { 810 | var n int 811 | var eof bool 812 | line, _, err := lineReader.ReadLine() 813 | if err != nil { 814 | eof = err == io.EOF 815 | if !eof { 816 | return err 817 | } 818 | } 819 | 820 | // !Remember to convert to string (UTF-8 encoding) 821 | if !eof { 822 | n, err = fmt.Fprintln(conn, string(line)) 823 | if err != nil { 824 | return err 825 | } 826 | } 827 | if callback != nil { 828 | tot += int64(n) 829 | callback(&CallbackInfo{remotename, filename, tot, eof}) 830 | } 831 | 832 | if eof { 833 | break 834 | } 835 | } 836 | return nil 837 | 838 | } 839 | 840 | if err := separateCall(); err != nil { 841 | return err 842 | } 843 | 844 | ftp.writeInfo("Reading final empty line") 845 | 846 | _, err = ftp.Read(cmd) 847 | return 848 | 849 | } 850 | 851 | // StoreBytes uploads bytes in chunks defined by the blocksize parameter. 852 | // It uses an io.Reader to read the input data. 853 | func (ftp *FTP) StoreBytes(cmd FtpCmd, reader io.Reader, blocksize int, remotename string, filename string, callback Callback) (err error) { 854 | var conn net.Conn 855 | if _, err = ftp.SendAndRead(TYPE_I_FTP_CMD); err != nil { 856 | return 857 | } 858 | 859 | // wrap this code up to guarantee the connection disposal via a defer 860 | separateCall := func() error { 861 | if conn, _, err = ftp.transferCmd(cmd, remotename); err != nil { 862 | return err 863 | } 864 | defer conn.Close() // close the connection on exit 865 | 866 | bufReader := bufio.NewReaderSize(reader, blocksize) 867 | 868 | ftp.writeInfo("Try and store bytes via connection for remote address:", conn.RemoteAddr().String()) 869 | 870 | s := make([]byte, blocksize) 871 | 872 | var tot int64 873 | 874 | for { 875 | var nr, nw int 876 | var eof bool 877 | 878 | nr, err = bufReader.Read(s) 879 | 880 | eof = err == io.EOF 881 | if err != nil && !eof { 882 | return err 883 | } 884 | 885 | if nw, err = conn.Write(s[:nr]); err != nil { 886 | return err 887 | } 888 | 889 | if callback != nil { 890 | tot += int64(nw) 891 | callback(&CallbackInfo{remotename, filename, tot, eof}) 892 | } 893 | 894 | if eof { 895 | break 896 | } 897 | } 898 | return nil 899 | } 900 | 901 | if err := separateCall(); err != nil { 902 | return err 903 | } 904 | 905 | _, err = ftp.Read(cmd) 906 | return 907 | } 908 | 909 | // transferCmd initializes a tranfer over the data connection. 910 | // 911 | // If the transfer is active, send a port command and the tranfer command 912 | // then accept the connection. If the server is passive, send a pasv command, connect to it 913 | // and start the tranfer command. Either way return the connection and the expected size of the transfer. 914 | // The expected size may be none if it could be not be determined. 915 | func (ftp *FTP) transferCmd(cmd FtpCmd, params ...string) (conn net.Conn, size int, err error) { 916 | 917 | var listener net.Listener 918 | 919 | ftp.writeInfo("Server is passive:", ftp.passiveserver) 920 | if ftp.passiveserver { 921 | host, port, error := ftp.makePasv() 922 | if ftp.conn.LocalAddr().Network() != host { 923 | ftp.writeInfo("The remote server answered with a different host address, which is", host, ", using the orginal host instead:", ftp.Host) 924 | host = ftp.Host 925 | } 926 | if error != nil { 927 | return nil, -1, error 928 | } 929 | 930 | addr := fmt.Sprintf("%s:%d", host, port) 931 | if ftp.timeoutInMsec > 0 { 932 | if conn, err = net.DialTimeout("tcp", addr, ftp.timeoutInMsec); err != nil { 933 | ftp.writeInfo("Dial error, address:", addr, "error:", err) 934 | return 935 | } 936 | } else { 937 | if conn, err = ftp.dialer.Dial("tcp", addr); err != nil { 938 | ftp.writeInfo("Dial error, address:", addr, "error:", err, "proxy enabled:", ftp.dialer != proxy.Direct) 939 | return 940 | } 941 | } 942 | 943 | } else { 944 | if listener, err = ftp.makePort(); err != nil { 945 | return 946 | } 947 | ftp.writeInfo("Listener created for non-passive mode") 948 | 949 | } 950 | 951 | var resp *Response 952 | if resp, err = ftp.SendAndRead(cmd, params...); err != nil { 953 | resp = nil 954 | return 955 | } 956 | 957 | // Some servers apparently send a 200 reply to 958 | // a LIST or STOR command, before the 150 reply 959 | // (and way before the 226 reply). This seems to 960 | // be in violation of the protocol (which only allows 961 | // 1xx or error messages for LIST), so we just discard 962 | // this response. 963 | if resp.getFirstChar() == "2" { 964 | resp, err = ftp.Read(cmd) 965 | } 966 | if resp.getFirstChar() != "1" { 967 | err = NewErrReply(errors.New(resp.Message)) 968 | return 969 | } 970 | 971 | // not passive, open connection and close it then 972 | if listener != nil { 973 | ftp.writeInfo("Preparing to listen for non-passive mode.") 974 | if conn, err = listener.Accept(); err != nil { 975 | conn = nil 976 | return 977 | } 978 | ftp.writeInfo("Trying to communicate with local host: ", conn.LocalAddr()) 979 | defer listener.Close() // close after getting the connection 980 | } 981 | 982 | if resp.Code == 150 { 983 | // this is conditional in case we received a 125 984 | ftp.writeInfo("Parsing return code 150") 985 | size, err = parse150ForSize(resp) 986 | } 987 | return conn, size, err 988 | } 989 | 990 | // makePort creates a new communication port and return a listener for this. 991 | func (ftp *FTP) makePort() (listener net.Listener, err error) { 992 | 993 | tcpAddr := ftp.conn.LocalAddr() 994 | network := tcpAddr.Network() 995 | 996 | var la *net.TCPAddr 997 | if la, err = net.ResolveTCPAddr(network, tcpAddr.String()); err != nil { 998 | return 999 | } 1000 | // get the new address 1001 | newad := la.IP.String() + ":0" // any available port 1002 | 1003 | ftp.writeInfo("The new local address in makePort is:", newad) 1004 | 1005 | listening := runServer(newad, network) 1006 | list := <-listening // wait for server to start and accept 1007 | if list == nil { 1008 | return nil, errors.New("Unable to create listener") 1009 | } 1010 | 1011 | la, _ = net.ResolveTCPAddr(list.Addr().Network(), list.Addr().String()) 1012 | ftp.writeInfo("Trying to listen locally at: ", la.IP.String(), " on new port:", la.Port) 1013 | 1014 | _, err = ftp.SendPort(la.IP.String(), la.Port) 1015 | 1016 | return list, err 1017 | } 1018 | 1019 | func runServer(laddr string, network string) chan net.Listener { 1020 | listening := make(chan net.Listener) 1021 | go func() { 1022 | l, err := net.Listen(network, laddr) 1023 | if err != nil { 1024 | log.Fatalf("net.Listen(%q, %q) = _, %v", network, laddr, err) 1025 | listening <- nil 1026 | return 1027 | } 1028 | listening <- l 1029 | }() 1030 | return listening 1031 | } 1032 | -------------------------------------------------------------------------------- /client_test.go: -------------------------------------------------------------------------------- 1 | package ftp4go 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | "strings" 8 | "testing" 9 | "time" 10 | ) 11 | 12 | type connPars struct { 13 | ftpAddress string 14 | ftpPort int 15 | username string 16 | password string 17 | homefolder string 18 | debugFtp bool 19 | } 20 | 21 | var allpars = []*connPars{ 22 | &connPars{ftpAddress: "ftp.drivehq.com", ftpPort: 21, username: "goftptest", password: "g0ftpt3st", homefolder: "/publicFolder", debugFtp: false}, 23 | &connPars{ftpAddress: "ftp.fileserve.com", ftpPort: 21, username: "ftp4go", password: "52fe56bc", homefolder: "/", debugFtp: true}, 24 | &connPars{ftpAddress: "www.0catch.com", ftpPort: 21, username: "ftp4go.0catch.com", password: "g0ftpt3st", homefolder: "/", debugFtp: true}, 25 | } 26 | 27 | var pars = allpars[0] 28 | 29 | func NewFtpConn(t *testing.T) (ftpClient *FTP, err error) { 30 | 31 | var logl int 32 | if pars.debugFtp { 33 | logl = 1 34 | } 35 | 36 | ftpClient = NewFTP(logl) // 1 for debugging 37 | 38 | ftpClient.SetPassive(true) 39 | 40 | // connect 41 | _, err = ftpClient.Connect(pars.ftpAddress, pars.ftpPort, "") 42 | if err != nil { 43 | t.Fatalf("The FTP connection could not be established, error: %v", err.Error()) 44 | } 45 | 46 | t.Logf("Connecting with username: %s and password %s", pars.username, pars.password) 47 | _, err = ftpClient.Login(pars.username, pars.password, "") 48 | if err != nil { 49 | t.Fatalf("The user could not be logged in, error: %s", err.Error()) 50 | } 51 | 52 | return 53 | } 54 | 55 | type asciiTestSet struct { 56 | fname string 57 | isascii bool 58 | } 59 | 60 | func _TestServerAsciiMode(t *testing.T) { 61 | 62 | ftpClient, err := NewFtpConn(t) 63 | defer ftpClient.Quit() 64 | 65 | if err != nil { 66 | return 67 | } 68 | 69 | _, err = ftpClient.Cwd(pars.homefolder) // home 70 | if err != nil { 71 | t.Fatalf("error: ", err) 72 | } 73 | 74 | fstochk := []*asciiTestSet{ 75 | &asciiTestSet{"test/test.txt", true}, 76 | &asciiTestSet{"test/test.txt", false}, 77 | } 78 | 79 | var getPrefixedName = func(fn string, textmode bool) string { 80 | f := filepath.Base(fn) 81 | prefix := "remote_binary_" 82 | if textmode { 83 | prefix = "remote_ascii_" 84 | } 85 | return prefix + f 86 | } 87 | 88 | for _, entry := range fstochk { 89 | r_filename := getPrefixedName(entry.fname, entry.isascii) 90 | fmt.Printf("Uploading file %s\n", r_filename) 91 | if err = ftpClient.UploadFile(r_filename, entry.fname, entry.isascii, nil); err != nil { 92 | t.Fatalf("error: ", err) 93 | } 94 | t.Logf("Uploaded %s file in ASCII mode.\n", r_filename) 95 | defer ftpClient.Delete(r_filename) 96 | } 97 | 98 | check := func(remotename string, localpath string, istext bool) { 99 | s1, s2, tempFilePath := checkintegrityWithPaths(ftpClient, remotename, localpath, istext, false, t) 100 | defer os.Remove(tempFilePath) 101 | t.Logf("\n---Check results\nMode is text: %v.\nDownloaded %s file to local file%s.\n", istext, remotename, tempFilePath) 102 | 103 | if s1 != s2 { 104 | t.Logf("The size of real file %s and the downloaded copy %s differ, size local: %d, size remote: %d\n", localpath, remotename, s1, s2) 105 | } else { 106 | t.Logf("The size of real file %s and the downloaded copy %s are the same, size: %d\n", localpath, remotename, s1) 107 | } 108 | 109 | if istext && s1 != s2 { 110 | t.Fatalf("The size of the file uploaded in ASCII mode should be the same as the downloaded in ASCII mode.") 111 | } 112 | } 113 | 114 | for _, entry := range fstochk { 115 | fn := getPrefixedName(entry.fname, entry.isascii) 116 | check(fn, entry.fname, true) 117 | check(fn, entry.fname, false) 118 | } 119 | 120 | } 121 | 122 | func TestFeatures(t *testing.T) { 123 | 124 | ftpClient, err := NewFtpConn(t) 125 | defer ftpClient.Quit() 126 | 127 | if err != nil { 128 | return 129 | } 130 | 131 | homefolder := pars.homefolder 132 | fmt.Println("The home folder is:", homefolder) 133 | 134 | fts, err := ftpClient.Feat() 135 | if err != nil { 136 | t.Fatalf("error: ", err) 137 | } 138 | 139 | fmt.Printf("Supported feats\n") 140 | for _, ft := range fts { 141 | fmt.Printf("%s\n", ft) 142 | } 143 | 144 | fmt.Printf("Use UTF8\n") 145 | _, err = ftpClient.Opts("UTF8 ON") 146 | if err != nil { 147 | t.Logf("UTF8 ON error: %v", err) 148 | } 149 | 150 | var cwd string 151 | 152 | _, err = ftpClient.Cwd(homefolder) // home 153 | if err != nil { 154 | t.Fatalf("error: %v", err) 155 | } 156 | 157 | cwd, err = ftpClient.Pwd() 158 | if err != nil { 159 | t.Fatalf("error: ", err) 160 | } 161 | t.Log("The current folder is", cwd) 162 | 163 | t.Log("Testings Mlsd") 164 | //ls, err := ftpClient.Mlsd(".", []string{"type", "size"}) 165 | ls, err := ftpClient.Mlsd("", nil) 166 | 167 | if err != nil { 168 | t.Logf("The ftp command MLSD does not work or is not supported, error: %s", err.Error()) 169 | } else { 170 | for _, l := range ls { 171 | //t.Logf("\nMlsd entry: %s, facts: %v", l.Name, l.Facts) 172 | t.Logf("\nMlsd entry and facts: %v", l) 173 | } 174 | } 175 | 176 | t.Logf("Testing upload\n") 177 | test_f := "test" 178 | maxSimultaneousConns := 1 179 | 180 | t.Log("Cleaning up before testing") 181 | var cleanup = func() error { return cleanupFolderTree(ftpClient, test_f, homefolder, t) } 182 | cleanup() 183 | defer cleanup() // at the end again 184 | 185 | var n int 186 | 187 | n, err = ftpClient.UploadDirTree(test_f, homefolder, maxSimultaneousConns, nil, nil) 188 | if err != nil { 189 | t.Fatalf("Error uploading folder tree %s, error: %v\n", test_f, err) 190 | } 191 | t.Logf("Uploaded %d files.\n", n) 192 | 193 | t.Log("Checking download integrity by downloading the uploaded files and comparing the sizes") 194 | 195 | check := func(fi string, istext bool) { 196 | s1, s2 := checkintegrity(ftpClient, fi, istext, t) 197 | 198 | if s1 != s2 { 199 | t.Errorf("The size of real file %s and the downloaded copy differ, size local: %d, size remote: %d", fi, s1, s2) 200 | } 201 | } 202 | 203 | ftpClient.Cwd(homefolder) 204 | 205 | fstochk := map[string]bool{"test/test.txt": true, "test/test.jpg": false} 206 | for s, v := range fstochk { 207 | check(s, v) 208 | } 209 | 210 | } 211 | 212 | func _TestRecursion(t *testing.T) { 213 | 214 | ftpClient, err := NewFtpConn(t) 215 | defer ftpClient.Quit() 216 | 217 | if err != nil { 218 | return 219 | } 220 | 221 | test_f := "test" 222 | noiterations := 1 223 | 224 | maxSimultaneousConns := 1 225 | homefolder := pars.homefolder 226 | 227 | t.Log("Cleaning up before testing") 228 | 229 | var cleanup = func() error { return cleanupFolderTree(ftpClient, test_f, homefolder, t) } 230 | 231 | var check = func(f string) error { return checkFolder(ftpClient, f, homefolder, t) } 232 | 233 | defer cleanup() // at the end again 234 | 235 | stats, fileUploaded, _ := startStats() 236 | var collector = func(info *CallbackInfo) { 237 | if info.Eof { 238 | stats <- info // pipe in for stats 239 | } 240 | } // do not block the call 241 | 242 | var n int 243 | for i := 0; i < noiterations; i++ { 244 | t.Logf("\n -- Uploading folder tree: %s, iteration %d\n", filepath.Base(test_f), i+1) 245 | 246 | cleanup() 247 | t.Logf("Sleeping a second\n") 248 | time.Sleep(1e9) 249 | 250 | n, err = ftpClient.UploadDirTree(test_f, homefolder, maxSimultaneousConns, nil, collector) 251 | if err != nil { 252 | t.Fatalf("Error uploading folder tree %s, error:\n", test_f, err) 253 | } 254 | 255 | t.Logf("Uploaded %d files.\n", n) 256 | // wait for all stats to finish 257 | for k := 0; k < n; k++ { 258 | <-fileUploaded 259 | } 260 | 261 | check("test") 262 | check("test/subdir") 263 | } 264 | 265 | } 266 | 267 | // FTP routine utils 268 | 269 | func checkFolder(ftpClient *FTP, f string, homefolder string, t *testing.T) (err error) { 270 | 271 | _, err = ftpClient.Cwd(homefolder) 272 | if err != nil { 273 | t.Fatalf("Error in Cwd for folder %s:", homefolder, err.Error()) 274 | } 275 | 276 | defer ftpClient.Cwd(homefolder) //back to home at the end 277 | 278 | t.Logf("Checking subfolder %s", f) 279 | dirs := filepath.SplitList(f) 280 | for _, d := range dirs { 281 | _, err = ftpClient.Cwd(d) 282 | if err != nil { 283 | t.Fatalf("The folder %s was not created.", f) 284 | } 285 | ftpClient.Cwd("..") 286 | } 287 | 288 | var filelist []string 289 | if filelist, err = ftpClient.Nlst(); err != nil { 290 | t.Fatalf("No files in folder %s on the ftp server", f) 291 | } 292 | 293 | dir, _ := os.Open(f) 294 | files, _ := dir.Readdirnames(-1) 295 | fno := len(files) 296 | t.Logf("No of files in local folder %s is: %d", f, fno) 297 | 298 | for _, locF := range files { 299 | t.Logf("Checking local file or folder %s", locF) 300 | fi, err := os.Stat(locF) 301 | if err == nil && !fi.IsDir() { 302 | var found bool 303 | for _, remF := range filelist { 304 | if strings.Contains(strings.ToLower(remF), strings.ToLower(locF)) { 305 | found = true 306 | break 307 | } 308 | } 309 | if !found { 310 | t.Fatalf("The local file %s could not be found at the server", locF) 311 | } 312 | } 313 | } 314 | 315 | return 316 | 317 | } 318 | 319 | func cleanupFolderTree(ftpClient *FTP, test_f string, homefolder string, t *testing.T) (err error) { 320 | 321 | _, err = ftpClient.Cwd(homefolder) 322 | if err != nil { 323 | t.Fatalf("Error in Cwd for folder %s:", homefolder, err.Error()) 324 | } 325 | 326 | defer ftpClient.Cwd(homefolder) //back to home at the end 327 | 328 | t.Logf("Removing directory tree %s.", test_f) 329 | 330 | if err := ftpClient.RemoveRemoteDirTree(test_f); err != nil { 331 | if err != DIRECTORY_NON_EXISTENT { 332 | t.Fatalf("Error: %v", err) 333 | } 334 | } 335 | 336 | return 337 | } 338 | 339 | func checkintegrity(ftpClient *FTP, remotename string, istext bool, t *testing.T) (sizeOriginal int64, sizeOnServer int64) { 340 | sizeOriginal, sizeOnServer, _ = checkintegrityWithPaths(ftpClient, remotename, remotename, istext, true, t) 341 | return 342 | } 343 | 344 | func checkintegrityWithPaths(ftpClient *FTP, remotename string, localpath string, istext bool, deleteTemporaryFile bool, t *testing.T) (sizeOriginal int64, sizeOnServer int64, tempFilePath string) { 345 | t.Logf("Checking download integrity of remote file %s\n", remotename) 346 | tkns := strings.Split(localpath, "/") 347 | tempFilePath = "ftptest_" + tkns[len(tkns)-1] 348 | 349 | fmt.Printf("Downloading file %s to temporary file %s\n", remotename, tempFilePath) 350 | err := ftpClient.DownloadFile(remotename, tempFilePath, istext) 351 | if err != nil { 352 | t.Fatalf("Error downloading file %s, error: %s", remotename, err) 353 | } 354 | 355 | // delete if required 356 | if deleteTemporaryFile { 357 | defer os.Remove(tempFilePath) 358 | } 359 | 360 | var ofi, oficp *os.File 361 | var e error 362 | 363 | if ofi, e = os.Open(localpath); e != nil { 364 | t.Fatalf("Error opening file %s, error: %s", localpath, e) 365 | } 366 | defer ofi.Close() 367 | 368 | if oficp, e = os.Open(tempFilePath); e != nil { 369 | t.Fatalf("Error opening file %s, error: %s", oficp, e) 370 | } 371 | 372 | defer oficp.Close() 373 | 374 | s1, _ := ofi.Stat() 375 | s2, _ := oficp.Stat() 376 | 377 | return s1.Size(), s2.Size(), tempFilePath 378 | 379 | } 380 | 381 | func startStats() (stats chan *CallbackInfo, fileUploaded chan bool, quit chan bool) { 382 | stats = make(chan *CallbackInfo, 100) 383 | quit = make(chan bool) 384 | fileUploaded = make(chan bool, 100) 385 | 386 | files := make(map[string][2]int64, 100) 387 | 388 | go func() { 389 | for { 390 | select { 391 | case st := <-stats: 392 | // do not wait here, the buffered request channel is the barrier 393 | 394 | go func() { 395 | pair, ok := files[st.Resourcename] 396 | var pos, size int64 397 | if !ok { 398 | fi, _ := os.Stat(st.Filename) 399 | 400 | files[st.Resourcename] = [2]int64{fi.Size(), pos} 401 | size = fi.Size() 402 | } else { 403 | pos = pair[1] // position correctly for writing 404 | size = pair[0] 405 | } 406 | 407 | mo := int((float32(st.BytesTransmitted)/float32(size))*100) / 10 408 | msg := fmt.Sprintf("File: %s - received: %d percent\n", st.Resourcename, mo*10) 409 | if st.Eof { 410 | fmt.Println("Uploaded (reached EOF) file:", st.Resourcename) 411 | fileUploaded <- true // done here 412 | } else { 413 | fmt.Print(msg) 414 | } 415 | /* 416 | if size <= st.BytesTransmitted { 417 | fileUploaded <- true // done here 418 | } 419 | */ 420 | }() 421 | case <-quit: 422 | fmt.Println("Stopping workers") 423 | return // get out 424 | } 425 | } 426 | }() 427 | return 428 | } 429 | -------------------------------------------------------------------------------- /clientproto.go: -------------------------------------------------------------------------------- 1 | package ftp4go 2 | 3 | import ( 4 | "bufio" 5 | "errors" 6 | "fmt" 7 | "io" 8 | "net" 9 | "net/textproto" 10 | "os" 11 | "regexp" 12 | "strconv" 13 | "strings" 14 | ) 15 | 16 | const ( 17 | BYTE_BLK = 1024 18 | ) 19 | 20 | var ( 21 | NewErrReply = func(error error) error { return errors.New("Reply error: " + error.Error()) } 22 | NewErrTemp = func(error error) error { return errors.New("Temporary error: " + error.Error()) } 23 | NewErrPerm = func(error error) error { return errors.New("Permanent error: " + error.Error()) } 24 | NewErrProto = func(error error) error { return errors.New("Protocol error: " + error.Error()) } 25 | NewErrStop = fmt.Errorf("Stop by human behavior: call FTP.Stop()") 26 | ) 27 | 28 | // string writer 29 | type stringSliceWriter struct { 30 | s []string 31 | } 32 | 33 | // utility string writer 34 | func (sw *stringSliceWriter) Write(p []byte) (n int, err error) { 35 | sw.s = append(sw.s, string(p)) 36 | n = len(p) 37 | return 38 | } 39 | 40 | // string writer 41 | type textFileWriter struct { 42 | //file *os.File 43 | bw *bufio.Writer 44 | } 45 | 46 | func newTextFileWriter(f *os.File) *textFileWriter { 47 | return &textFileWriter{bufio.NewWriter(f)} 48 | } 49 | 50 | // utility string writer 51 | func (tfw *textFileWriter) Write(p []byte) (n int, err error) { 52 | //return fmt.Fprintln(tfw.f, string(p)) 53 | n, err = tfw.bw.Write(p) 54 | if err != nil { 55 | return 56 | } 57 | 58 | n1, err1 := tfw.bw.WriteRune('\n') // always add a new line 59 | return n + n1, err1 60 | } 61 | 62 | type CallbackInfo struct { 63 | Resourcename string 64 | Filename string 65 | BytesTransmitted int64 66 | Eof bool 67 | } 68 | 69 | type Callback func(info *CallbackInfo) 70 | 71 | type Response struct { 72 | Code int 73 | Message string 74 | Stream []byte 75 | } 76 | 77 | func (r *Response) getFirstChar() string { 78 | if r == nil { 79 | return "" 80 | } 81 | return strconv.Itoa(r.Code)[0:1] 82 | } 83 | 84 | var re227, re150 *regexp.Regexp 85 | 86 | func init() { 87 | re227, _ = regexp.Compile("([0-9]+),([0-9]+),([0-9]+),([0-9]+),([0-9]+),([0-9]+)") 88 | re150, _ = regexp.Compile("150 .* \\(([0-9]+) bytes\\)") 89 | } 90 | 91 | // Dial connects to the given address on the given network using net.Dial 92 | // and then returns a new Conn for the connection. 93 | func Dial(network, addr string) (net.Conn, error) { 94 | c, err := net.Dial(network, addr) 95 | if err != nil { 96 | return nil, err 97 | } 98 | return c, nil 99 | } 100 | 101 | func (ftp *FTP) NewConn(addr string) error { 102 | 103 | var ( 104 | c net.Conn 105 | err error 106 | ) 107 | if ftp.timeoutInMsec > 0 { 108 | c, err = net.DialTimeout("tcp", addr, ftp.timeoutInMsec) 109 | 110 | if err != nil { 111 | return err 112 | } 113 | } else { 114 | c, err = ftp.dialer.Dial("tcp", addr) 115 | 116 | if err != nil { 117 | return err 118 | } 119 | } 120 | 121 | // use textproto for parsing 122 | ftp.conn = c 123 | ftp.textprotoConn = textproto.NewConn(c) 124 | return nil 125 | } 126 | 127 | // SendAndRead sends a command to the server and reads the response. 128 | func (ftp *FTP) SendAndRead(cmd FtpCmd, params ...string) (response *Response, err error) { 129 | if err = ftp.Send(cmd, params...); err != nil { 130 | return nil, err 131 | } 132 | return ftp.Read(cmd) 133 | } 134 | 135 | // Send sends a command to the server. 136 | func (ftp *FTP) Send(cmd FtpCmd, params ...string) (err error) { 137 | fullCmd := cmd.String() 138 | //ftp.writeInfo(fmt.Sprintf("Sending to server partial command '%s'", fullCmd)) 139 | if len(params) > 0 { 140 | fullCmd = cmd.AppendParameters(params...) 141 | } 142 | 143 | ftp.writeInfo(fmt.Sprintf("Sending to server command '%s'", fullCmd)) 144 | //_, err = ftp.textprotoConn.Cmd(fullCmd) 145 | err = ftp.textprotoConn.PrintfLine(fullCmd) 146 | 147 | return 148 | } 149 | 150 | // Read reads the response along with the response code from the server 151 | func (ftp *FTP) Read(cmd FtpCmd) (resp *Response, err error) { 152 | 153 | var msg string 154 | var code int 155 | 156 | if code, msg, err = ftp.textprotoConn.ReadResponse(-1); err != nil { 157 | return nil, err 158 | } 159 | 160 | ftp.writeInfo(fmt.Sprintf("The message returned by the server was: code=%d, message=%s", code, msg)) 161 | 162 | c := strconv.Itoa(code)[0:1] 163 | 164 | switch { 165 | //valid 166 | case strings.IndexAny(c, "123") >= 0: 167 | return &Response{Code: code, Message: msg}, nil 168 | //wrong 169 | case c == "4": 170 | err = errors.New("Temporary error: " + msg) 171 | case c == "5": 172 | err = errors.New("Permanent error: " + msg) 173 | default: 174 | err = errors.New("Protocol error: " + msg) 175 | } 176 | 177 | ftp.writeInfo("Response error") 178 | return nil, err 179 | } 180 | 181 | // parse227 parses the 227 response for PASV request. 182 | // Raises a protocol error if it does not contain {h1,h2,h3,h4,p1,p2}. 183 | // Returns the host and port. 184 | func parse227(resp *Response) (host string, port int, err error) { 185 | if resp.Code != 227 { 186 | err = NewErrProto(errors.New(resp.Message)) 187 | return 188 | } 189 | 190 | matches := re227.FindStringSubmatch(resp.Message) 191 | if matches == nil { 192 | err = NewErrProto(errors.New("No matching pattern for message:" + resp.Message)) 193 | return 194 | } 195 | numbers := matches[1:] // get the groups 196 | host = strings.Join(numbers[:4], ".") 197 | p1, _ := strconv.Atoi(numbers[4]) 198 | p2, _ := strconv.Atoi(numbers[5]) 199 | port = (p1 << 8) + p2 200 | return 201 | } 202 | 203 | // parse150ForSize parses the '150' response for a RETR request. 204 | // Returns the expected transfer size or None; size is not guaranteed to 205 | // be present in the 150 message. 206 | func parse150ForSize(resp *Response) (int, error) { 207 | if resp.Code != 150 { 208 | return -1, NewErrReply(errors.New(resp.Message)) 209 | } 210 | 211 | matches := re150.FindStringSubmatch(resp.Message) 212 | if len(matches) < 2 { 213 | return -1, nil 214 | } 215 | 216 | return strconv.Atoi(string(matches[1])) 217 | 218 | } 219 | 220 | // parse257 parses the 257 response for a MKD or PWD request, the response is a directory name. 221 | // Return the directory name in the 257 reply. 222 | func parse257(resp *Response) (dirname string, err error) { 223 | if resp.Code != 257 { 224 | err = NewErrProto(errors.New(resp.Message)) 225 | return "", err 226 | } 227 | if resp.Message[0:1] != "\"" { 228 | return "", nil // Not compliant to RFC 959, but UNIX ftpd does this 229 | } 230 | dirname = "" 231 | i := 1 232 | n := len(resp.Message) 233 | for i < n { 234 | c := resp.Message[i] 235 | i++ 236 | if c == '"' { 237 | if i >= n || resp.Message[i] != '"' { 238 | break 239 | } 240 | i++ 241 | } 242 | dirname = dirname + string(c) 243 | } 244 | return dirname, nil 245 | } 246 | 247 | // parse211 parses the 211 response for a FEAT command. 248 | // Return the list of feats. 249 | func parse211(resp *Response) (list []string, err error) { 250 | if resp.Code != 211 { 251 | err = NewErrProto(errors.New(resp.Message)) 252 | return nil, err 253 | } 254 | 255 | list = make([]string, 0, 20) 256 | var no int 257 | 258 | r := bufio.NewReader(strings.NewReader(resp.Message)) 259 | 260 | for { 261 | line, _, err1 := r.ReadLine() 262 | 263 | if err1 != nil { 264 | if err1 == io.EOF { 265 | break 266 | } 267 | return list, err1 268 | } 269 | 270 | l := strings.TrimSpace(string(line)) 271 | 272 | if !strings.HasPrefix(l, strconv.Itoa(resp.Code)) && len(l) > 0 { 273 | list = append(list, l) 274 | no++ 275 | } 276 | } 277 | return list[:no], nil 278 | 279 | } 280 | 281 | // TrimString returns s without leading and trailing ASCII space. 282 | func TrimString(s string) string { 283 | for len(s) > 0 && isASCIISpace(s[0]) { 284 | s = s[1:] 285 | } 286 | for len(s) > 0 && isASCIISpace(s[len(s)-1]) { 287 | s = s[:len(s)-1] 288 | } 289 | return s 290 | } 291 | 292 | // TrimBytes returns b without leading and trailing ASCII space. 293 | func TrimBytes(b []byte) []byte { 294 | for len(b) > 0 && isASCIISpace(b[0]) { 295 | b = b[1:] 296 | } 297 | for len(b) > 0 && isASCIISpace(b[len(b)-1]) { 298 | b = b[:len(b)-1] 299 | } 300 | return b 301 | } 302 | 303 | func isASCIISpace(b byte) bool { 304 | return b == ' ' || b == '\t' || b == '\n' || b == '\r' 305 | } 306 | 307 | func isASCIILetter(b byte) bool { 308 | b |= 0x20 // make lower case 309 | return 'a' <= b && b <= 'z' 310 | } 311 | 312 | // An Error represents a numeric error response from a server. 313 | type Error struct { 314 | Code int 315 | Msg string 316 | } 317 | 318 | func (e *Error) Error() string { 319 | return fmt.Sprintf("%03d %s", e.Code, e.Msg) 320 | } 321 | 322 | // A ProtocolError describes a protocol violation such 323 | // as an invalid response or a hung-up connection. 324 | type ProtocolError string 325 | 326 | func (p ProtocolError) Error() string { 327 | return string(p) 328 | } 329 | -------------------------------------------------------------------------------- /clientutil.go: -------------------------------------------------------------------------------- 1 | // Package ftp implements an FTP client. 2 | package ftp4go 3 | 4 | import ( 5 | "errors" 6 | "fmt" 7 | "os" 8 | "path/filepath" 9 | "sort" 10 | "strings" 11 | ) 12 | 13 | var DIRECTORY_NON_EXISTENT = errors.New("The folder does not exist and can not be removed") 14 | 15 | // RemoveRemoteDirTree removes a remote folder and all of its subfolders recursively. 16 | // The current directory is then set to the orginal one before the operation or to the root of the deleted folder if it fails. 17 | func (ftp *FTP) RemoveRemoteDirTree(remoteDir string) (err error) { 18 | 19 | var pwd string 20 | if pwd, err = ftp.Pwd(); err != nil { 21 | return 22 | } 23 | 24 | // go back to original wd in a separate routine, if this fails stay where we are, onle level before the folder 25 | defer ftp.Cwd(pwd) 26 | 27 | return ftp.removeRemoteDirTree(remoteDir) 28 | } 29 | 30 | // removeRemoteDirTree removes a remote folder and all of its subfolders recursively. 31 | // The error DIRECTORY_NON_EXISTENT of type os.Error is thrown if the FTP folder does not exist. 32 | func (ftp *FTP) removeRemoteDirTree(remoteDir string) (err error) { 33 | ftp.writeInfo("Changing working remote dir to:", remoteDir) 34 | 35 | if _, err = ftp.Cwd(remoteDir); err != nil { 36 | return DIRECTORY_NON_EXISTENT 37 | } 38 | 39 | ftp.writeInfo("Cleaning up remote folder:", remoteDir) 40 | 41 | var filelist []string 42 | if filelist, err = ftp.Dir(); err != nil { 43 | return err 44 | } 45 | 46 | for _, s := range filelist { 47 | subStrings := strings.Fields(s) // split on whitespace 48 | perm := subStrings[0] 49 | fname := subStrings[len(subStrings)-1] // file name, assume 'drw... ... filename' 50 | switch { 51 | case fname == "." || fname == "..": 52 | continue 53 | case perm[0] != 'd': 54 | // file, delete 55 | ftp.Delete(fname) 56 | case perm[0] == 'd': // directory 57 | if err = ftp.RemoveRemoteDirTree(fname); err != nil { 58 | return err 59 | } 60 | } 61 | } 62 | ftp.Cwd("..") 63 | if _, err = ftp.Rmd(remoteDir); err != nil { 64 | return err 65 | } 66 | return nil 67 | 68 | } 69 | 70 | // UploadDirTree uploads a local directory and all of its subfolders 71 | // localDir -> path to the local folder to upload along with all of its subfolders. 72 | // remoteRootDir -> the root folder on the FTP server where to store the localDir tree. 73 | // excludedDirs -> a slice of folder names to exclude from the uploaded directory tree. 74 | // callback -> a callback function, which is called synchronously. Do remember to collect data in a go routine for instance if you do not want the upload to block. 75 | // Returns the number of files uploaded and an error if any. 76 | // 77 | // The current workding directory is set back to the initial value at the end. 78 | func (ftp *FTP) UploadDirTree(localDir string, remoteRootDir string, maxSimultaneousConns int, excludedDirs []string, callback Callback) (n int, err error) { 79 | 80 | if len(remoteRootDir) == 0 { 81 | return n, errors.New("A valid remote root folder with write permission needs specifying.") 82 | } 83 | 84 | var pwd string 85 | if pwd, err = ftp.Pwd(); err != nil { 86 | return 87 | } 88 | 89 | if _, err = ftp.Cwd(remoteRootDir); err != nil { 90 | return n, nil 91 | } 92 | //go back to original wd 93 | defer ftp.Cwd(pwd) 94 | 95 | //all lower case 96 | var exDirs sort.StringSlice 97 | if len(excludedDirs) > 0 { 98 | exDirs = sort.StringSlice(excludedDirs) 99 | for _, v := range exDirs { 100 | v = strings.ToLower(v) 101 | } 102 | exDirs.Sort() 103 | } 104 | 105 | err = ftp.uploadDirTree(localDir, exDirs, callback, &n) 106 | if err != nil { 107 | ftp.writeInfo(fmt.Sprintf("An error while uploading the folder %s occurred.", localDir)) 108 | } 109 | 110 | return n, err 111 | } 112 | 113 | func (ftp *FTP) uploadDirTree(localDir string, excludedDirs sort.StringSlice, callback Callback, n *int) (err error) { 114 | 115 | _, dir := filepath.Split(localDir) 116 | ftp.writeInfo("The directory where to upload is:", dir) 117 | if _, err = ftp.Mkd(dir); err != nil { 118 | return 119 | } 120 | 121 | _, err = ftp.Cwd(dir) 122 | if err != nil { 123 | ftp.writeInfo(fmt.Sprintf("An error occurred while CWD, err: %s.", err)) 124 | return 125 | } 126 | defer ftp.Cwd("..") 127 | globSearch := filepath.Join(localDir, "*") 128 | ftp.writeInfo("Looking up files in", globSearch) 129 | var files []string 130 | files, err = filepath.Glob(globSearch) // find all files in folder 131 | if err != nil { 132 | return 133 | } 134 | ftp.writeInfo("Found", len(files), "files") 135 | sort.Strings(files) // sort by name 136 | 137 | for _, s := range files { 138 | _, fname := filepath.Split(s) // find file name 139 | localPath := filepath.Join(localDir, fname) 140 | ftp.writeInfo("Uploading file or dir:", localPath) 141 | var f os.FileInfo 142 | if f, err = os.Stat(localPath); err != nil { 143 | return 144 | } 145 | if !f.IsDir() { 146 | err = ftp.UploadFile(fname, localPath, false, callback) // always binary upload 147 | if err != nil { 148 | return 149 | } 150 | *n += 1 // increment 151 | } else { 152 | if len(excludedDirs) > 0 { 153 | ftp.writeInfo("Checking folder name:", fname) 154 | lfname := strings.ToLower(fname) 155 | idx := sort.SearchStrings(excludedDirs, lfname) 156 | if idx < len(excludedDirs) && excludedDirs[idx] == lfname { 157 | ftp.writeInfo("Excluding folder:", s) 158 | continue 159 | } 160 | } 161 | if err = ftp.uploadDirTree(localPath, excludedDirs, callback, n); err != nil { 162 | return 163 | } 164 | } 165 | 166 | } 167 | 168 | return 169 | } 170 | -------------------------------------------------------------------------------- /status.go: -------------------------------------------------------------------------------- 1 | package ftp4go 2 | 3 | // FTP status codes, defined in RFC 959 4 | const ( 5 | StatusInitiating = 100 6 | StatusRestartMarker = 110 7 | StatusReadyMinute = 120 8 | StatusAlreadyOpen = 125 9 | StatusAboutToSend = 150 10 | 11 | StatusCommandOK = 200 12 | StatusCommandNotImplemented = 202 13 | StatusSystem = 211 14 | StatusDirectory = 212 15 | StatusFile = 213 16 | StatusHelp = 214 17 | StatusName = 215 18 | StatusReady = 220 19 | StatusClosing = 221 20 | StatusDataConnectionOpen = 225 21 | StatusClosingDataConnection = 226 22 | StatusPassiveMode = 227 23 | StatusLongPassiveMode = 228 24 | StatusExtendedPassiveMode = 229 25 | StatusLoggedIn = 230 26 | StatusLoggedOut = 231 27 | StatusLogoutAck = 232 28 | StatusRequestedFileActionOK = 250 29 | StatusPathCreated = 257 30 | 31 | StatusUserOK = 331 32 | StatusLoginNeedAccount = 332 33 | StatusRequestFilePending = 350 34 | 35 | StatusNotAvailable = 421 36 | StatusCanNotOpenDataConnection = 425 37 | StatusTransfertAborted = 426 38 | StatusInvalidCredentials = 430 39 | StatusHostUnavailable = 434 40 | StatusFileActionIgnored = 450 41 | StatusActionAborted = 451 42 | Status452 = 452 43 | 44 | StatusBadCommand = 500 45 | StatusBadArguments = 501 46 | StatusNotImplemented = 502 47 | StatusBadSequence = 503 48 | StatusNotImplementedParameter = 504 49 | StatusNotLoggedIn = 530 50 | StatusStorNeedAccount = 532 51 | StatusFileUnavailable = 550 52 | StatusPageTypeUnknown = 551 53 | StatusExceededStorage = 552 54 | StatusBadFileName = 553 55 | ) 56 | 57 | var statusText = map[int]string{ 58 | // 200 59 | StatusCommandOK: "Command okay.", 60 | StatusCommandNotImplemented: "Command not implemented, superfluous at this site.", 61 | StatusSystem: "System status, or system help reply.", 62 | StatusDirectory: "Directory status.", 63 | StatusFile: "File status.", 64 | StatusHelp: "Help message.", 65 | StatusName: "", 66 | StatusReady: "Service ready for new user.", 67 | StatusClosing: "Service closing control connection.", 68 | StatusDataConnectionOpen: "Data connection open; no transfer in progress.", 69 | StatusClosingDataConnection: "Closing data connection. Requested file action successful.", 70 | StatusPassiveMode: "Entering Passive Mode.", 71 | StatusLongPassiveMode: "Entering Long Passive Mode.", 72 | StatusExtendedPassiveMode: "Entering Extended Passive Mode.", 73 | StatusLoggedIn: "User logged in, proceed.", 74 | StatusLoggedOut: "User logged out; service terminated.", 75 | StatusLogoutAck: "Logout command noted, will complete when transfer done.", 76 | StatusRequestedFileActionOK: "Requested file action okay, completed.", 77 | StatusPathCreated: "Path created.", 78 | 79 | // 300 80 | StatusUserOK: "User name okay, need password.", 81 | StatusLoginNeedAccount: "Need account for login.", 82 | StatusRequestFilePending: "Requested file action pending further information.", 83 | 84 | // 400 85 | StatusNotAvailable: "Service not available, closing control connection.", 86 | StatusCanNotOpenDataConnection: "Can't open data connection.", 87 | StatusTransfertAborted: "Connection closed; transfer aborted.", 88 | StatusInvalidCredentials: "Invalid username or password.", 89 | StatusHostUnavailable: "Requested host unavailable.", 90 | StatusFileActionIgnored: "Requested file action not taken.", 91 | StatusActionAborted: "Requested action aborted. Local error in processing.", 92 | Status452: "Insufficient storage space in system.", 93 | 94 | // 500 95 | StatusBadCommand: "Command unrecognized.", 96 | StatusBadArguments: "Syntax error in parameters or arguments.", 97 | StatusNotImplemented: "Command not implemented.", 98 | StatusBadSequence: "Bad sequence of commands.", 99 | StatusNotImplementedParameter: "Command not implemented for that parameter.", 100 | StatusNotLoggedIn: "Not logged in.", 101 | StatusStorNeedAccount: "Need account for storing files.", 102 | StatusFileUnavailable: "File unavailable.", 103 | StatusPageTypeUnknown: "Page type unknown.", 104 | StatusExceededStorage: "Exceeded storage allocation.", 105 | StatusBadFileName: "File name not allowed.", 106 | } 107 | -------------------------------------------------------------------------------- /test/subdir/test6310.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shenshouer/ftp4go/421554c4a04168e2e35517d18ab179b59e372e61/test/subdir/test6310.jpg -------------------------------------------------------------------------------- /test/subdir/test6313.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shenshouer/ftp4go/421554c4a04168e2e35517d18ab179b59e372e61/test/subdir/test6313.jpg -------------------------------------------------------------------------------- /test/subdir/testsubdir.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shenshouer/ftp4go/421554c4a04168e2e35517d18ab179b59e372e61/test/subdir/testsubdir.jpg -------------------------------------------------------------------------------- /test/test.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shenshouer/ftp4go/421554c4a04168e2e35517d18ab179b59e372e61/test/test.jpg -------------------------------------------------------------------------------- /test/test.txt: -------------------------------------------------------------------------------- 1 | This is a weird character (unicode) 2 | "日" 3 | This is a new line 4 | This is the last line 5 | 6 | 7 | -------------------------------------------------------------------------------- /test/test6352.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shenshouer/ftp4go/421554c4a04168e2e35517d18ab179b59e372e61/test/test6352.jpg -------------------------------------------------------------------------------- /test/test6353.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shenshouer/ftp4go/421554c4a04168e2e35517d18ab179b59e372e61/test/test6353.jpg -------------------------------------------------------------------------------- /test/test6361.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shenshouer/ftp4go/421554c4a04168e2e35517d18ab179b59e372e61/test/test6361.jpg --------------------------------------------------------------------------------