Mercurial > projects > ldc
comparison tango/tango/net/ftp/FtpClient.d @ 132:1700239cab2e trunk
[svn r136] MAJOR UNSTABLE UPDATE!!!
Initial commit after moving to Tango instead of Phobos.
Lots of bugfixes...
This build is not suitable for most things.
author | lindquist |
---|---|
date | Fri, 11 Jan 2008 17:57:40 +0100 |
parents | |
children |
comparison
equal
deleted
inserted
replaced
131:5825d48b27d1 | 132:1700239cab2e |
---|---|
1 /******************************************************************************* | |
2 | |
3 copyright: Copyright (c) 2006 UWB. All rights reserved | |
4 | |
5 license: BSD style: $(LICENSE) | |
6 | |
7 version: Initial release: June 2006 | |
8 | |
9 author: UWB | |
10 | |
11 *******************************************************************************/ | |
12 | |
13 module tango.net.ftp.FtpClient; | |
14 | |
15 private import tango.net.Socket; | |
16 | |
17 private import tango.net.ftp.Telnet; | |
18 | |
19 private import tango.time.Clock; | |
20 | |
21 private import tango.io.Conduit, | |
22 tango.io.GrowBuffer, | |
23 tango.io.FileConduit; | |
24 | |
25 private import tango.time.chrono.Gregorian; | |
26 | |
27 private import Text = tango.text.Util; | |
28 | |
29 private import Ascii = tango.text.Ascii; | |
30 | |
31 private import Regex = tango.text.Regex; | |
32 | |
33 private import Integer = tango.text.convert.Integer; | |
34 | |
35 private import Timestamp = tango.text.convert.TimeStamp; | |
36 | |
37 | |
38 /// An FTP progress delegate. | |
39 /// | |
40 /// You may need to add the restart position to this, and use SIZE to determine | |
41 /// percentage completion. This only represents the number of bytes | |
42 /// transferred. | |
43 /// | |
44 /// Params: | |
45 /// pos = the current offset into the stream | |
46 alias void delegate(in size_t pos) FtpProgress; | |
47 | |
48 /// The format of data transfer. | |
49 enum FtpFormat | |
50 { | |
51 /// Indicates ASCII NON PRINT format (line ending conversion to CRLF.) | |
52 ascii, | |
53 /// Indicates IMAGE format (8 bit binary octets.) | |
54 image, | |
55 } | |
56 | |
57 /// A server response, consisting of a code and a potentially multi-line message. | |
58 struct FtpResponse | |
59 { | |
60 /// The response code. | |
61 /// | |
62 /// The digits in the response code can be used to determine status | |
63 /// programatically. | |
64 /// | |
65 /// First Digit (status): | |
66 /// 1xx = a positive, but preliminary, reply | |
67 /// 2xx = a positive reply indicating completion | |
68 /// 3xx = a positive reply indicating incomplete status | |
69 /// 4xx = a temporary negative reply | |
70 /// 5xx = a permanent negative reply | |
71 /// | |
72 /// Second Digit (subject): | |
73 /// x0x = condition based on syntax | |
74 /// x1x = informational | |
75 /// x2x = connection | |
76 /// x3x = authentication/process | |
77 /// x5x = file system | |
78 char[3] code = "000"; | |
79 | |
80 /// The message from the server. | |
81 /// | |
82 /// With some responses, the message may contain parseable information. | |
83 /// For example, this is true of the 257 response. | |
84 char[] message = null; | |
85 } | |
86 | |
87 /// Active or passive connection mode. | |
88 enum FtpConnectionType | |
89 { | |
90 /// Active - server connects to client on open port. | |
91 active, | |
92 /// Passive - server listens for a connection from the client. | |
93 passive, | |
94 } | |
95 | |
96 /// Detail about the data connection. | |
97 /// | |
98 /// This is used to properly send PORT and PASV commands. | |
99 struct FtpConnectionDetail | |
100 { | |
101 /// The type to be used. | |
102 FtpConnectionType type = FtpConnectionType.passive; | |
103 | |
104 /// The address to give the server. | |
105 Address address = null; | |
106 | |
107 /// The address to actually listen on. | |
108 Address listen = null; | |
109 } | |
110 | |
111 /// A supported feature of an FTP server. | |
112 struct FtpFeature | |
113 { | |
114 /// The command which is supported, e.g. SIZE. | |
115 char[] command = null; | |
116 /// Parameters for this command; e.g. facts for MLST. | |
117 char[] params = null; | |
118 } | |
119 | |
120 /// The type of a file in an FTP listing. | |
121 enum FtpFileType | |
122 { | |
123 /// An unknown file or type (no type fact.) | |
124 unknown, | |
125 /// A regular file, or similar. | |
126 file, | |
127 /// The current directory (e.g. ., but not necessarily.) | |
128 cdir, | |
129 /// A parent directory (usually "..".) | |
130 pdir, | |
131 /// Any other type of directory. | |
132 dir, | |
133 /// Another type of file. Consult the "type" fact. | |
134 other, | |
135 } | |
136 | |
137 /// Information about a file in an FTP listing. | |
138 struct FtpFileInfo | |
139 { | |
140 /// The filename. | |
141 char[] name = null; | |
142 /// Its type. | |
143 FtpFileType type = FtpFileType.unknown; | |
144 /// Size in bytes (8 bit octets), or -1 if not available. | |
145 long size = -1; | |
146 /// Modification time, if available. | |
147 Time modify = Time.max; | |
148 /// Creation time, if available (not often.) | |
149 Time create = Time.max; | |
150 /// The file's mime type, if known. | |
151 char[] mime = null; | |
152 /// An associative array of all facts returned by the server, lowercased. | |
153 char[][char[]] facts; | |
154 } | |
155 | |
156 /// A connection to an FTP server. | |
157 /// | |
158 /// Example: | |
159 /// ---------- | |
160 /// auto ftp = new FTPConnection("hostname", "user", "pass",21); | |
161 /// | |
162 /// ftp.mkdir("test"); | |
163 /// ftp.close(); | |
164 /// ---------- | |
165 /// | |
166 /// Standards: RFC 959, RFC 2228, RFC 2389, RFC 2428 | |
167 /// | |
168 /// Bugs: | |
169 /// Does not support several uncommon FTP commands and responses. | |
170 | |
171 | |
172 class FTPConnection : Telnet | |
173 { | |
174 /// Supported features (if known.) | |
175 /// | |
176 /// This will be empty if not known, or else contain at least FEAT. | |
177 public FtpFeature[] supported_features = null; | |
178 | |
179 /// Data connection information. | |
180 protected FtpConnectionDetail data_info; | |
181 | |
182 /// The last-set restart position. | |
183 /// | |
184 /// This is only used when a local file is used for a RETR or STOR. | |
185 protected size_t restart_pos = 0; | |
186 | |
187 /// error handler | |
188 protected void exception (char[] msg) | |
189 { | |
190 throw new FTPException ("Exception: " ~ msg); | |
191 } | |
192 | |
193 /// ditto | |
194 protected void exception (FtpResponse r) | |
195 { | |
196 throw new FTPException (r); | |
197 } | |
198 | |
199 /// Construct an FTPConnection without connecting immediately. | |
200 public this() | |
201 { | |
202 } | |
203 | |
204 /// Connect to an FTP server with a username and password. | |
205 /// | |
206 /// Params: | |
207 /// hostname = the hostname or IP address to connect to | |
208 /// port = the port number to connect to | |
209 /// username = username to be sent | |
210 /// password = password to be sent, if requested | |
211 public this(char[] hostname, char[] username, char[] password, int port = 21) | |
212 { | |
213 this.connect(hostname, username, password,port); | |
214 } | |
215 | |
216 /// Connect to an FTP server with a username and password. | |
217 /// | |
218 /// Params: | |
219 /// hostname = the hostname or IP address to connect to | |
220 /// port = the port number to connect to | |
221 /// username = username to be sent | |
222 /// password = password to be sent, if requested | |
223 public void connect(char[] hostname, char[] username, char[] password, int port = 21) | |
224 in | |
225 { | |
226 // We definitely need a hostname and port. | |
227 assert (hostname.length > 0); | |
228 assert (port > 0); | |
229 } | |
230 body | |
231 { | |
232 // Close any active connection. | |
233 | |
234 if (this.socket !is null) | |
235 this.close(); | |
236 | |
237 | |
238 // Connect to whichever FTP server responds first. | |
239 this.findAvailableServer(hostname, port); | |
240 | |
241 this.socket.blocking = false; | |
242 | |
243 scope (failure) | |
244 { | |
245 this.close(); | |
246 } | |
247 | |
248 // The welcome message should always be a 220. 120 and 421 are considered errors. | |
249 this.readResponse("220"); | |
250 | |
251 if (username.length == 0) | |
252 return; | |
253 | |
254 // Send the username. Anything but 230, 331, or 332 is basically an error. | |
255 this.sendCommand("USER", username); | |
256 auto response = this.readResponse(); | |
257 | |
258 // 331 means username okay, please proceed with password. | |
259 if (response.code == "331") | |
260 { | |
261 this.sendCommand("PASS", password); | |
262 response = this.readResponse(); | |
263 } | |
264 | |
265 // We don't support ACCT (332) so we should get a 230 here. | |
266 if (response.code != "230" && response.code != "202") | |
267 { | |
268 | |
269 exception (response); | |
270 } | |
271 | |
272 } | |
273 | |
274 /// Close the connection to the server. | |
275 public void close() | |
276 { | |
277 assert (this.socket !is null); | |
278 | |
279 // Don't even try to close it if it's not open. | |
280 if (this.socket !is null) | |
281 { | |
282 try | |
283 { | |
284 this.sendCommand("QUIT"); | |
285 this.readResponse("221"); | |
286 } | |
287 // Ignore if the above could not be completed. | |
288 catch (FTPException) | |
289 { | |
290 } | |
291 | |
292 // Shutdown the socket... | |
293 this.socket.shutdown(SocketShutdown.BOTH); | |
294 this.socket.detach(); | |
295 | |
296 // Clear out everything. | |
297 delete this.supported_features; | |
298 delete this.socket; | |
299 } | |
300 } | |
301 | |
302 /// Set the connection to use passive mode for data tranfers. | |
303 /// | |
304 /// This is the default. | |
305 public void setPassive() | |
306 { | |
307 this.data_info.type = FtpConnectionType.passive; | |
308 | |
309 delete this.data_info.address; | |
310 delete this.data_info.listen; | |
311 } | |
312 | |
313 /// Set the connection to use active mode for data transfers. | |
314 /// | |
315 /// This may not work behind firewalls. | |
316 /// | |
317 /// Params: | |
318 /// ip = the ip address to use | |
319 /// port = the port to use | |
320 /// listen_ip = the ip to listen on, or null for any | |
321 /// listen_port = the port to listen on, or 0 for the same port | |
322 public void setActive(char[] ip, ushort port, char[] listen_ip = null, ushort listen_port = 0) | |
323 in | |
324 { | |
325 assert (ip.length > 0); | |
326 assert (port > 0); | |
327 } | |
328 body | |
329 { | |
330 this.data_info.type = FtpConnectionType.active; | |
331 this.data_info.address = new IPv4Address(ip, port); | |
332 | |
333 // A local-side port? | |
334 if (listen_port == 0) | |
335 listen_port = port; | |
336 | |
337 // Any specific IP to listen on? | |
338 if (listen_ip == null) | |
339 this.data_info.listen = new IPv4Address(IPv4Address.ADDR_ANY, listen_port); | |
340 else | |
341 this.data_info.listen = new IPv4Address(listen_ip, listen_port); | |
342 } | |
343 | |
344 | |
345 /// Change to the specified directory. | |
346 public void cd(char[] dir) | |
347 in | |
348 { | |
349 assert (dir.length > 0); | |
350 } | |
351 body | |
352 { | |
353 this.sendCommand("CWD", dir); | |
354 this.readResponse("250"); | |
355 } | |
356 | |
357 /// Change to the parent of this directory. | |
358 public void cdup() | |
359 { | |
360 this.sendCommand("CDUP"); | |
361 this.readResponse("200"); | |
362 } | |
363 | |
364 /// Determine the current directory. | |
365 /// | |
366 /// Returns: the current working directory | |
367 public char[] cwd() | |
368 { | |
369 this.sendCommand("PWD"); | |
370 auto response = this.readResponse("257"); | |
371 | |
372 return this.parse257(response); | |
373 } | |
374 | |
375 /// Change the permissions of a file. | |
376 /// | |
377 /// This is a popular feature of most FTP servers, but not explicitly outlined | |
378 /// in the spec. It does not work on, for example, Windows servers. | |
379 /// | |
380 /// Params: | |
381 /// path = the path to the file to chmod | |
382 /// mode = the desired mode; expected in octal (0777, 0644, etc.) | |
383 public void chmod(char[] path, int mode) | |
384 in | |
385 { | |
386 assert (path.length > 0); | |
387 assert (mode >= 0 && (mode >> 16) == 0); | |
388 } | |
389 body | |
390 { | |
391 char[] tmp = "000"; | |
392 // Convert our octal parameter to a string. | |
393 Integer.format(tmp, cast(long) mode, Integer.Style.Octal); | |
394 this.sendCommand("SITE CHMOD", tmp, path); | |
395 this.readResponse("200"); | |
396 } | |
397 | |
398 /// Remove a file or directory. | |
399 /// | |
400 /// Params: | |
401 /// path = the path to the file or directory to delete | |
402 public void del(char[] path) | |
403 in | |
404 { | |
405 assert (path.length > 0); | |
406 } | |
407 body | |
408 { | |
409 this.sendCommand("DELE", path); | |
410 auto response = this.readResponse(); | |
411 | |
412 // Try it as a directory, then...? | |
413 if (response.code != "250") | |
414 this.rm(path); | |
415 } | |
416 | |
417 /// Remove a directory. | |
418 /// | |
419 /// Params: | |
420 /// path = the directory to delete | |
421 public void rm(char[] path) | |
422 in | |
423 { | |
424 assert (path.length > 0); | |
425 } | |
426 body | |
427 { | |
428 this.sendCommand("RMD", path); | |
429 this.readResponse("250"); | |
430 } | |
431 | |
432 /// Rename/move a file or directory. | |
433 /// | |
434 /// Params: | |
435 /// old_path = the current path to the file | |
436 /// new_path = the new desired path | |
437 public void rename(char[] old_path, char[] new_path) | |
438 in | |
439 { | |
440 assert (old_path.length > 0); | |
441 assert (new_path.length > 0); | |
442 } | |
443 body | |
444 { | |
445 // Rename from... rename to. Pretty simple. | |
446 this.sendCommand("RNFR", old_path); | |
447 this.readResponse("350"); | |
448 | |
449 this.sendCommand("RNTO", new_path); | |
450 this.readResponse("250"); | |
451 } | |
452 | |
453 /// Determine the size in bytes of a file. | |
454 /// | |
455 /// This size is dependent on the current type (ASCII or IMAGE.) | |
456 /// | |
457 /// Params: | |
458 /// path = the file to retrieve the size of | |
459 /// format = what format the size is desired in | |
460 public size_t size(char[] path, FtpFormat format = FtpFormat.image) | |
461 in | |
462 { | |
463 assert (path.length > 0); | |
464 } | |
465 body | |
466 { | |
467 this.type(format); | |
468 | |
469 this.sendCommand("SIZE", path); | |
470 auto response = this.readResponse("213"); | |
471 | |
472 // Only try to parse the numeric bytes of the response. | |
473 size_t end_pos = 0; | |
474 while (end_pos < response.message.length) | |
475 { | |
476 if (response.message[end_pos] < '0' || response.message[end_pos] > '9') | |
477 break; | |
478 end_pos++; | |
479 } | |
480 | |
481 return toInt(response.message[0 .. end_pos]); | |
482 } | |
483 | |
484 /// Send a command and process the data socket. | |
485 /// | |
486 /// This opens the data connection and checks for the appropriate response. | |
487 /// | |
488 /// Params: | |
489 /// command = the command to send (e.g. STOR) | |
490 /// parameters = any arguments to send | |
491 /// | |
492 /// Returns: the data socket | |
493 public Socket processDataCommand(char[] command, char[][] parameters ...) | |
494 { | |
495 // Create a connection. | |
496 Socket data = this.getDataSocket(); | |
497 scope (failure) | |
498 { | |
499 // Close the socket, whether we were listening or not. | |
500 data.shutdown(SocketShutdown.BOTH); | |
501 data.detach(); | |
502 } | |
503 | |
504 // Tell the server about it. | |
505 this.sendCommand(command, parameters); | |
506 | |
507 // We should always get a 150/125 response. | |
508 auto response = this.readResponse(); | |
509 if (response.code != "150" && response.code != "125") | |
510 exception (response); | |
511 | |
512 // We might need to do this for active connections. | |
513 this.prepareDataSocket(data); | |
514 | |
515 return data; | |
516 } | |
517 | |
518 /// Clean up after the data socket and process the response. | |
519 /// | |
520 /// This closes the socket and reads the 226 response. | |
521 /// | |
522 /// Params: | |
523 /// data = the data socket | |
524 public void finishDataCommand(Socket data) | |
525 { | |
526 // Close the socket. This tells the server we're done (EOF.) | |
527 data.shutdown(SocketShutdown.BOTH); | |
528 data.detach(); | |
529 | |
530 // We shouldn't get a 250 in STREAM mode. | |
531 this.readResponse("226"); | |
532 } | |
533 | |
534 /// Get a data socket from the server. | |
535 /// | |
536 /// This sends PASV/PORT as necessary. | |
537 /// | |
538 /// Returns: the data socket or a listener | |
539 protected Socket getDataSocket() | |
540 { | |
541 // What type are we using? | |
542 switch (this.data_info.type) | |
543 { | |
544 default: | |
545 exception ("unknown connection type"); | |
546 | |
547 // Passive is complicated. Handle it in another member. | |
548 case FtpConnectionType.passive: | |
549 return this.connectPassive(); | |
550 | |
551 // Active is simpler, but not as fool-proof. | |
552 case FtpConnectionType.active: | |
553 IPv4Address data_addr = cast(IPv4Address) this.data_info.address; | |
554 | |
555 // Start listening. | |
556 Socket listener = new Socket(AddressFamily.INET, SocketType.STREAM, ProtocolType.TCP); | |
557 listener.bind(this.data_info.listen); | |
558 listener.listen(32); | |
559 | |
560 // Use EPRT if we know it's supported. | |
561 if (this.is_supported("EPRT")) | |
562 { | |
563 char[64] tmp = void; | |
564 | |
565 this.sendCommand("EPRT", Text.layout(tmp, "|1|%0|%1|", data_addr.toAddrString, data_addr.toPortString)); | |
566 // this.sendCommand("EPRT", format("|1|%s|%s|", data_addr.toAddrString(), data_addr.toPortString())); | |
567 this.readResponse("200"); | |
568 } | |
569 else | |
570 { | |
571 int h1, h2, h3, h4, p1, p2; | |
572 h1 = (data_addr.addr() >> 24) % 256; | |
573 h2 = (data_addr.addr() >> 16) % 256; | |
574 h3 = (data_addr.addr() >> 8_) % 256; | |
575 h4 = (data_addr.addr() >> 0_) % 256; | |
576 p1 = (data_addr.port() >> 8_) % 256; | |
577 p2 = (data_addr.port() >> 0_) % 256; | |
578 | |
579 // low overhead method to format a numerical string | |
580 char[64] tmp = void; | |
581 char[20] foo = void; | |
582 auto str = Text.layout (tmp, "%0,%1,%2,%3,%4,%5", | |
583 Integer.format(foo[0..3], h1), | |
584 Integer.format(foo[3..6], h2), | |
585 Integer.format(foo[6..9], h3), | |
586 Integer.format(foo[9..12], h4), | |
587 Integer.format(foo[12..15], p1), | |
588 Integer.format(foo[15..18], p2)); | |
589 | |
590 // This formatting is weird. | |
591 // this.sendCommand("PORT", format("%d,%d,%d,%d,%d,%d", h1, h2, h3, h4, p1, p2)); | |
592 | |
593 this.sendCommand("PORT", str); | |
594 this.readResponse("200"); | |
595 } | |
596 | |
597 return listener; | |
598 } | |
599 assert (false); | |
600 } | |
601 | |
602 /// Prepare a data socket for use. | |
603 /// | |
604 /// This modifies the socket in some cases. | |
605 /// | |
606 /// Params: | |
607 /// data = the data listener socket | |
608 protected void prepareDataSocket(inout Socket data) | |
609 { | |
610 switch (this.data_info.type) | |
611 { | |
612 default: | |
613 exception ("unknown connection type"); | |
614 | |
615 case FtpConnectionType.active: | |
616 Socket new_data = null; | |
617 | |
618 SocketSet set = new SocketSet(); | |
619 scope (exit) | |
620 delete set; | |
621 | |
622 // At end_time, we bail. | |
623 Time end_time = Clock.now + this.timeout; | |
624 | |
625 while (Clock.now < end_time) | |
626 { | |
627 set.reset(); | |
628 set.add(data); | |
629 | |
630 // Can we accept yet? | |
631 int code = Socket.select(set, null, null, this.timeout); | |
632 if (code == -1 || code == 0) | |
633 break; | |
634 | |
635 new_data = data.accept(); | |
636 break; | |
637 } | |
638 | |
639 if (new_data is null) | |
640 throw new FTPException("CLIENT: No connection from server", "420"); | |
641 | |
642 // We don't need the listener anymore. | |
643 data.shutdown(SocketShutdown.BOTH); | |
644 data.detach(); | |
645 | |
646 // This is the actual socket. | |
647 data = new_data; | |
648 break; | |
649 | |
650 case FtpConnectionType.passive: | |
651 break; | |
652 } | |
653 } | |
654 | |
655 /// Send a PASV and initiate a connection. | |
656 /// | |
657 /// Returns: a connected socket | |
658 public Socket connectPassive() | |
659 { | |
660 Address connect_to = null; | |
661 | |
662 // SPSV, which is just a port number. | |
663 if (this.is_supported("SPSV")) | |
664 { | |
665 this.sendCommand("SPSV"); | |
666 auto response = this.readResponse("227"); | |
667 | |
668 // Connecting to the same host. | |
669 IPv4Address remote = cast(IPv4Address) this.socket.remoteAddress(); | |
670 assert (remote !is null); | |
671 | |
672 uint address = remote.addr(); | |
673 uint port = toInt(response.message); | |
674 | |
675 connect_to = new IPv4Address(address, cast(ushort) port); | |
676 } | |
677 // Extended passive mode (IP v6, etc.) | |
678 else if (this.is_supported("EPSV")) | |
679 { | |
680 this.sendCommand("EPSV"); | |
681 auto response = this.readResponse("229"); | |
682 | |
683 // Try to pull out the (possibly not parenthesized) address. | |
684 auto r = Regex.search(response.message, `\([^0-9][^0-9][^0-9](\d+)[^0-9]\)`); | |
685 if (r is null) | |
686 throw new FTPException("CLIENT: Unable to parse address", "501"); | |
687 | |
688 IPv4Address remote = cast(IPv4Address) this.socket.remoteAddress(); | |
689 assert (remote !is null); | |
690 | |
691 uint address = remote.addr(); | |
692 uint port = toInt(r.match(1)); | |
693 | |
694 connect_to = new IPv4Address(address, cast(ushort) port); | |
695 } | |
696 else | |
697 { | |
698 this.sendCommand("PASV"); | |
699 auto response = this.readResponse("227"); | |
700 | |
701 // Try to pull out the (possibly not parenthesized) address. | |
702 auto r = Regex.search(response.message, `(\d+),\s*(\d+),\s*(\d+),\s*(\d+),\s*(\d+)(,\s*(\d+))?`); | |
703 if (r is null) | |
704 throw new FTPException("CLIENT: Unable to parse address", "501"); | |
705 | |
706 // Now put it into something std.socket will understand. | |
707 char[] address = r.match(1)~"."~r.match(2)~"."~r.match(3)~"."~r.match(4); | |
708 uint port = (toInt(r.match(5)) << 8) + (r.match(7).length > 0 ? toInt(r.match(7)) : 0); | |
709 | |
710 // Okay, we've got it! | |
711 connect_to = new IPv4Address(address, port); | |
712 } | |
713 | |
714 scope (exit) | |
715 delete connect_to; | |
716 | |
717 // This will throw an exception if it cannot connect. | |
718 auto sock = new Socket(AddressFamily.INET, SocketType.STREAM, ProtocolType.TCP); | |
719 sock.connect (connect_to); | |
720 return sock; | |
721 } | |
722 | |
723 /// Change the type of data transfer. | |
724 /// | |
725 /// ASCII mode implies that line ending conversion should be made. | |
726 /// Only NON PRINT is supported. | |
727 /// | |
728 /// Params: | |
729 /// type = FtpFormat.ascii or FtpFormat.image | |
730 public void type(FtpFormat format) | |
731 { | |
732 if (format == FtpFormat.ascii) | |
733 this.sendCommand("TYPE", "A"); | |
734 else | |
735 this.sendCommand("TYPE", "I"); | |
736 | |
737 this.readResponse("200"); | |
738 } | |
739 | |
740 /// Store a local file on the server. | |
741 /// | |
742 /// Calling this function will change the current data transfer format. | |
743 /// | |
744 /// Params: | |
745 /// path = the path to the remote file | |
746 /// local_file = the path to the local file | |
747 /// progress = a delegate to call with progress information | |
748 /// format = what format to send the data in | |
749 | |
750 public void put(char[] path, char[] local_file, FtpProgress progress = null, FtpFormat format = FtpFormat.image) | |
751 in | |
752 { | |
753 assert (path.length > 0); | |
754 assert (local_file.length > 0); | |
755 } | |
756 body | |
757 { | |
758 // Open the file for reading... | |
759 auto file = new FileConduit(local_file); | |
760 scope (exit) | |
761 { | |
762 file.detach(); | |
763 delete file; | |
764 } | |
765 | |
766 // Seek to the correct place, if specified. | |
767 if (this.restart_pos > 0) | |
768 { | |
769 file.seek(this.restart_pos); | |
770 this.restart_pos = 0; | |
771 } | |
772 else | |
773 { | |
774 // Allocate space for the file, if we need to. | |
775 this.allocate(file.length); | |
776 } | |
777 | |
778 // Now that it's open, we do what we always do. | |
779 this.put(path, file, progress, format); | |
780 } | |
781 | |
782 /// Store data from a stream on the server. | |
783 /// | |
784 /// Calling this function will change the current data transfer format. | |
785 /// | |
786 /// Params: | |
787 /// path = the path to the remote file | |
788 /// stream = data to store, or null for a blank file | |
789 /// progress = a delegate to call with progress information | |
790 /// format = what format to send the data in | |
791 public void put(char[] path, InputStream stream = null, FtpProgress progress = null, FtpFormat format = FtpFormat.image) | |
792 in | |
793 { | |
794 assert (path.length > 0); | |
795 } | |
796 body | |
797 { | |
798 // Change to the specified format. | |
799 this.type(format); | |
800 | |
801 // Okay server, we want to store something... | |
802 Socket data = this.processDataCommand("STOR", path); | |
803 | |
804 // Send the stream over the socket! | |
805 if (stream !is null) | |
806 this.sendStream(data, stream, progress); | |
807 | |
808 this.finishDataCommand(data); | |
809 } | |
810 | |
811 /// Append data to a file on the server. | |
812 /// | |
813 /// Calling this function will change the current data transfer format. | |
814 /// | |
815 /// Params: | |
816 /// path = the path to the remote file | |
817 /// stream = data to append to the file | |
818 /// progress = a delegate to call with progress information | |
819 /// format = what format to send the data in | |
820 public void append(char[] path, InputStream stream, FtpProgress progress = null, FtpFormat format = FtpFormat.image) | |
821 in | |
822 { | |
823 assert (path.length > 0); | |
824 assert (stream !is null); | |
825 } | |
826 body | |
827 { | |
828 // Change to the specified format. | |
829 this.type(format); | |
830 | |
831 // Okay server, we want to store something... | |
832 Socket data = this.processDataCommand("APPE", path); | |
833 | |
834 // Send the stream over the socket! | |
835 this.sendStream(data, stream, progress); | |
836 | |
837 this.finishDataCommand(data); | |
838 } | |
839 | |
840 /// Seek to a byte offset for the next transfer. | |
841 /// | |
842 /// Params: | |
843 /// offset = the number of bytes to seek forward | |
844 public void restartSeek(size_t offset) | |
845 { | |
846 char[16] tmp; | |
847 this.sendCommand("REST", Integer.format (tmp, cast(long) offset)); | |
848 this.readResponse("350"); | |
849 | |
850 // Set this for later use. | |
851 this.restart_pos = offset; | |
852 } | |
853 | |
854 /// Allocate space for a file. | |
855 /// | |
856 /// After calling this, append() or put() should be the next command. | |
857 /// | |
858 /// Params: | |
859 /// bytes = the number of bytes to allocate | |
860 public void allocate(long bytes) | |
861 in | |
862 { | |
863 assert (bytes > 0); | |
864 } | |
865 body | |
866 { | |
867 char[16] tmp; | |
868 this.sendCommand("ALLO", Integer.format(tmp, bytes)); | |
869 auto response = this.readResponse(); | |
870 | |
871 // For our purposes 200 and 202 are both fine. | |
872 if (response.code != "200" && response.code != "202") | |
873 exception (response); | |
874 } | |
875 | |
876 /// Retrieve a remote file's contents into a local file. | |
877 /// | |
878 /// Calling this function will change the current data transfer format. | |
879 /// | |
880 /// Params: | |
881 /// path = the path to the remote file | |
882 /// local_file = the path to the local file | |
883 /// progress = a delegate to call with progress information | |
884 /// format = what format to read the data in | |
885 public void get(char[] path, char[] local_file, FtpProgress progress = null, FtpFormat format = FtpFormat.image) | |
886 in | |
887 { | |
888 assert (path.length > 0); | |
889 assert (local_file.length > 0); | |
890 } | |
891 body | |
892 { | |
893 FileConduit file = null; | |
894 | |
895 // We may either create a new file... | |
896 if (this.restart_pos == 0) | |
897 file = new FileConduit (local_file, FileConduit.ReadWriteCreate); | |
898 // Or open an existing file, and seek to the specified position (read: not end, necessarily.) | |
899 else | |
900 { | |
901 file = new FileConduit (local_file, FileConduit.ReadWriteExisting); | |
902 file.seek(this.restart_pos); | |
903 | |
904 this.restart_pos = 0; | |
905 } | |
906 | |
907 scope (exit) | |
908 { | |
909 file.detach(); | |
910 delete file; | |
911 } | |
912 | |
913 // Now that it's open, we do what we always do. | |
914 this.get(path, file, progress, format); | |
915 } | |
916 | |
917 /// Retrieve a remote file's contents into a local file. | |
918 /// | |
919 /// Calling this function will change the current data transfer format. | |
920 /// | |
921 /// Params: | |
922 /// path = the path to the remote file | |
923 /// stream = stream to write the data to | |
924 /// progress = a delegate to call with progress information | |
925 /// format = what format to read the data in | |
926 public void get(char[] path, OutputStream stream, FtpProgress progress = null, FtpFormat format = FtpFormat.image) | |
927 in | |
928 { | |
929 assert (path.length > 0); | |
930 assert (stream !is null); | |
931 } | |
932 body | |
933 { | |
934 // Change to the specified format. | |
935 this.type(format); | |
936 | |
937 // Okay server, we want to get this file... | |
938 Socket data = this.processDataCommand("RETR", path); | |
939 | |
940 // Read the stream in from the socket! | |
941 this.readStream(data, stream, progress); | |
942 | |
943 this.finishDataCommand(data); | |
944 } | |
945 | |
946 /// Get information about a single file. | |
947 /// | |
948 /// Return an FtpFileInfo struct about the specified path. | |
949 /// This may not work consistently on directories (but should.) | |
950 /// | |
951 /// Params: | |
952 /// path = the file or directory to get information about | |
953 /// | |
954 /// Returns: the file information | |
955 public FtpFileInfo getFileInfo(char[] path) | |
956 in | |
957 { | |
958 assert (path.length > 0); | |
959 } | |
960 body | |
961 { | |
962 // Start assuming the MLST didn't work. | |
963 bool mlst_success = false; | |
964 FtpResponse response; | |
965 | |
966 // Check if MLST might be supported... | |
967 if (this.isSupported("MLST")) | |
968 { | |
969 this.sendCommand("MLST", path); | |
970 response = this.readResponse(); | |
971 | |
972 // If we know it was supported for sure, this is an error. | |
973 if (this.is_supported("MLST")) | |
974 exception (response); | |
975 // Otherwise, it probably means we need to try a LIST. | |
976 else | |
977 mlst_success = response.code == "250"; | |
978 } | |
979 | |
980 // Okay, we got the MLST response... parse it. | |
981 if (mlst_success) | |
982 { | |
983 char[][] lines = Text.splitLines (response.message); | |
984 | |
985 // We need at least 3 lines - first and last and header/footer lines. | |
986 // Note that more than 3 could be returned; e.g. multiple lines about the one file. | |
987 if (lines.length <= 2) | |
988 throw new FTPException("CLIENT: Bad MLST response from server", "501"); | |
989 | |
990 // Return the first line's information. | |
991 return parseMlstLine(lines[1]); | |
992 } | |
993 else | |
994 { | |
995 // Send a list command. This may list the contents of a directory, even. | |
996 FtpFileInfo[] temp = this.sendListCommand(path); | |
997 | |
998 // If there wasn't at least one line, the file didn't exist? | |
999 // We should have already handled that. | |
1000 if (temp.length < 1) | |
1001 throw new FTPException("CLIENT: Bad LIST response from server", "501"); | |
1002 | |
1003 // If there are multiple lines, try to return the correct one. | |
1004 if (temp.length != 1) | |
1005 foreach (FtpFileInfo info; temp) | |
1006 { | |
1007 if (info.type == FtpFileType.cdir) | |
1008 return info; | |
1009 } | |
1010 | |
1011 // Okay then, the first line. Best we can do? | |
1012 return temp[0]; | |
1013 } | |
1014 } | |
1015 | |
1016 /// Get a listing of a directory's contents. | |
1017 /// | |
1018 /// Don't end path in a /. Blank means the current directory. | |
1019 /// | |
1020 /// Params: | |
1021 /// path = the directory to list | |
1022 /// | |
1023 /// Returns: an array of the contents | |
1024 public FtpFileInfo[] ls(char[] path = "") // default to current dir | |
1025 in | |
1026 { | |
1027 assert (path.length == 0 || path[path.length - 1] != '/'); | |
1028 } | |
1029 body | |
1030 { | |
1031 FtpFileInfo[] dir; | |
1032 | |
1033 // We'll try MLSD (which is so much better) first... but it may fail. | |
1034 bool mlsd_success = false; | |
1035 Socket data = null; | |
1036 | |
1037 // Try it if it could/might/maybe is supported. | |
1038 if (this.isSupported("MLST")) | |
1039 { | |
1040 mlsd_success = true; | |
1041 | |
1042 // Since this is a data command, processDataCommand handles | |
1043 // checking the response... just catch its Exception. | |
1044 try | |
1045 { | |
1046 if (path.length > 0) | |
1047 data = this.processDataCommand("MLSD", path); | |
1048 else | |
1049 data = this.processDataCommand("MLSD"); | |
1050 } | |
1051 catch (FTPException) | |
1052 mlsd_success = false; | |
1053 } | |
1054 | |
1055 // If it passed, parse away! | |
1056 if (mlsd_success) | |
1057 { | |
1058 auto listing = new GrowBuffer; | |
1059 this.readStream(data, listing); | |
1060 this.finishDataCommand(data); | |
1061 | |
1062 // Each line is something in that directory. | |
1063 char[][] lines = Text.splitLines (cast(char[]) listing.slice()); | |
1064 scope (exit) | |
1065 delete lines; | |
1066 | |
1067 foreach (char[] line; lines) | |
1068 { | |
1069 // Parse each line exactly like MLST does. | |
1070 FtpFileInfo info = this.parseMlstLine(line); | |
1071 if (info.name.length > 0) | |
1072 dir ~= info; | |
1073 } | |
1074 | |
1075 return dir; | |
1076 } | |
1077 // Fall back to LIST. | |
1078 else | |
1079 return this.sendListCommand(path); | |
1080 } | |
1081 | |
1082 /// Send a LIST command to determine a directory's content. | |
1083 /// | |
1084 /// The format of a LIST response is not guaranteed. If available, | |
1085 /// MLSD should be used instead. | |
1086 /// | |
1087 /// Params: | |
1088 /// path = the file or directory to list | |
1089 /// | |
1090 /// Returns: an array of the contents | |
1091 protected FtpFileInfo[] sendListCommand(char[] path) | |
1092 { | |
1093 FtpFileInfo[] dir; | |
1094 Socket data = null; | |
1095 | |
1096 if (path.length > 0) | |
1097 data = this.processDataCommand("LIST", path); | |
1098 else | |
1099 data = this.processDataCommand("LIST"); | |
1100 | |
1101 // Read in the stupid non-standardized response. | |
1102 auto listing = new GrowBuffer; | |
1103 this.readStream(data, listing); | |
1104 this.finishDataCommand(data); | |
1105 | |
1106 // Split out the lines. Most of the time, it's one-to-one. | |
1107 char[][] lines = Text.splitLines (cast(char[]) listing.slice()); | |
1108 scope (exit) | |
1109 delete lines; | |
1110 | |
1111 foreach (char[] line; lines) | |
1112 { | |
1113 // If there are no spaces, or if there's only one... skip the line. | |
1114 // This is probably like a "total 8" line. | |
1115 if (Text.locate(line, ' ') == Text.locatePrior(line, ' ')) | |
1116 continue; | |
1117 | |
1118 // Now parse the line, or try to. | |
1119 FtpFileInfo info = this.parseListLine(line); | |
1120 if (info.name.length > 0) | |
1121 dir ~= info; | |
1122 } | |
1123 | |
1124 return dir; | |
1125 } | |
1126 | |
1127 /// Parse a LIST response line. | |
1128 /// | |
1129 /// The format here isn't even specified, so we have to try to detect | |
1130 /// commmon ones. | |
1131 /// | |
1132 /// Params: | |
1133 /// line = the line to parse | |
1134 /// | |
1135 /// Returns: information about the file | |
1136 protected FtpFileInfo parseListLine(char[] line) | |
1137 { | |
1138 FtpFileInfo info; | |
1139 size_t pos = 0; | |
1140 | |
1141 // Convenience function to parse a word from the line. | |
1142 char[] parse_word() | |
1143 { | |
1144 size_t start = 0, end = 0; | |
1145 | |
1146 // Skip whitespace before. | |
1147 while (pos < line.length && line[pos] == ' ') | |
1148 pos++; | |
1149 | |
1150 start = pos; | |
1151 while (pos < line.length && line[pos] != ' ') | |
1152 pos++; | |
1153 end = pos; | |
1154 | |
1155 // Skip whitespace after. | |
1156 while (pos < line.length && line[pos] == ' ') | |
1157 pos++; | |
1158 | |
1159 return line[start .. end]; | |
1160 } | |
1161 | |
1162 // We have to sniff this... :/. | |
1163 switch (! Text.contains ("0123456789", line[0])) | |
1164 { | |
1165 // Not a number; this is UNIX format. | |
1166 case true: | |
1167 // The line must be at least 20 characters long. | |
1168 if (line.length < 20) | |
1169 return info; | |
1170 | |
1171 // The first character tells us what it is. | |
1172 if (line[0] == 'd') | |
1173 info.type = FtpFileType.dir; | |
1174 else if (line[0] == '-') | |
1175 info.type = FtpFileType.file; | |
1176 else | |
1177 info.type = FtpFileType.unknown; | |
1178 | |
1179 // Parse out the mode... rwxrwxrwx = 777. | |
1180 char[] unix_mode = "0000".dup; | |
1181 void read_mode(int digit) | |
1182 { | |
1183 for (pos = 1 + digit * 3; pos <= 3 + digit * 3; pos++) | |
1184 { | |
1185 if (line[pos] == 'r') | |
1186 unix_mode[digit + 1] |= 4; | |
1187 else if (line[pos] == 'w') | |
1188 unix_mode[digit + 1] |= 2; | |
1189 else if (line[pos] == 'x') | |
1190 unix_mode[digit + 1] |= 1; | |
1191 } | |
1192 } | |
1193 | |
1194 // This makes it easier, huh? | |
1195 read_mode(0); | |
1196 read_mode(1); | |
1197 read_mode(2); | |
1198 | |
1199 info.facts["UNIX.mode"] = unix_mode; | |
1200 | |
1201 // Links, owner, group. These are hard to translate to MLST facts. | |
1202 parse_word(); | |
1203 parse_word(); | |
1204 parse_word(); | |
1205 | |
1206 // Size in bytes, this one is good. | |
1207 info.size = toLong(parse_word()); | |
1208 | |
1209 // Make sure we still have enough space. | |
1210 if (pos + 13 >= line.length) | |
1211 return info; | |
1212 | |
1213 // Not parsing date for now. It's too weird (last 12 months, etc.) | |
1214 pos += 13; | |
1215 | |
1216 info.name = line[pos .. line.length]; | |
1217 break; | |
1218 | |
1219 // A number; this is DOS format. | |
1220 case false: | |
1221 // We need some data here, to parse. | |
1222 if (line.length < 18) | |
1223 return info; | |
1224 | |
1225 // The order is 1 MM, 2 DD, 3 YY, 4 HH, 5 MM, 6 P | |
1226 auto r = Regex.search(line, `(\d\d)-(\d\d)-(\d\d)\s+(\d\d):(\d\d)(A|P)M`); | |
1227 if (r is null) | |
1228 return info; | |
1229 | |
1230 if (Timestamp.dostime (r.match(0), info.modify) is 0) | |
1231 info.modify = Time.max; | |
1232 | |
1233 pos = r.match(0).length; | |
1234 delete r; | |
1235 | |
1236 // This will either be <DIR>, or a number. | |
1237 char[] dir_or_size = parse_word(); | |
1238 | |
1239 if (dir_or_size.length < 0) | |
1240 return info; | |
1241 else if (dir_or_size[0] == '<') | |
1242 info.type = FtpFileType.dir; | |
1243 else | |
1244 info.size = toLong(dir_or_size); | |
1245 | |
1246 info.name = line[pos .. line.length]; | |
1247 break; | |
1248 | |
1249 // Something else, not supported. | |
1250 default: | |
1251 throw new FTPException("CLIENT: Unsupported LIST format", "501"); | |
1252 } | |
1253 | |
1254 // Try to fix the type? | |
1255 if (info.name == ".") | |
1256 info.type = FtpFileType.cdir; | |
1257 else if (info.name == "..") | |
1258 info.type = FtpFileType.pdir; | |
1259 | |
1260 return info; | |
1261 } | |
1262 | |
1263 /// Parse an MLST/MLSD response line. | |
1264 /// | |
1265 /// The format here is very rigid, and has facts followed by a filename. | |
1266 /// | |
1267 /// Params: | |
1268 /// line = the line to parse | |
1269 /// | |
1270 /// Returns: information about the file | |
1271 protected FtpFileInfo parseMlstLine(char[] line) | |
1272 { | |
1273 FtpFileInfo info; | |
1274 | |
1275 // After this loop, filename_pos will be location of space + 1. | |
1276 size_t filename_pos = 0; | |
1277 while (filename_pos < line.length && line[filename_pos++] != ' ') | |
1278 continue; | |
1279 | |
1280 if (filename_pos == line.length) | |
1281 throw new FTPException("CLIENT: Bad syntax in MLSx response", "501"); | |
1282 | |
1283 info.name = line[filename_pos .. line.length]; | |
1284 | |
1285 // Everything else is frosting on top. | |
1286 if (filename_pos > 1) | |
1287 { | |
1288 char[][] temp_facts = Text.delimit(line[0 .. filename_pos - 1], ";"); | |
1289 | |
1290 // Go through each fact and parse them into the array. | |
1291 foreach (char[] fact; temp_facts) | |
1292 { | |
1293 int pos = Text.locate(fact, '='); | |
1294 if (pos == fact.length) | |
1295 continue; | |
1296 | |
1297 info.facts[Ascii.toLower(fact[0 .. pos])] = fact[pos + 1 .. fact.length]; | |
1298 } | |
1299 | |
1300 // Do we have a type? | |
1301 if ("type" in info.facts) | |
1302 { | |
1303 // Some reflection might be nice here. | |
1304 switch (Ascii.toLower(info.facts["type"])) | |
1305 { | |
1306 case "file": | |
1307 info.type = FtpFileType.file; | |
1308 break; | |
1309 | |
1310 case "cdir": | |
1311 info.type = FtpFileType.cdir; | |
1312 break; | |
1313 | |
1314 case "pdir": | |
1315 info.type = FtpFileType.pdir; | |
1316 break; | |
1317 | |
1318 case "dir": | |
1319 info.type = FtpFileType.dir; | |
1320 break; | |
1321 | |
1322 default: | |
1323 info.type = FtpFileType.other; | |
1324 } | |
1325 } | |
1326 | |
1327 // Size, mime, etc... | |
1328 if ("size" in info.facts) | |
1329 info.size = toLong(info.facts["size"]); | |
1330 if ("media-type" in info.facts) | |
1331 info.mime = info.facts["media-type"]; | |
1332 | |
1333 // And the two dates. | |
1334 if ("modify" in info.facts) | |
1335 info.modify = this.parseTimeval(info.facts["modify"]); | |
1336 if ("create" in info.facts) | |
1337 info.create = this.parseTimeval(info.facts["create"]); | |
1338 } | |
1339 | |
1340 return info; | |
1341 } | |
1342 | |
1343 /// Parse a timeval from an FTP response. | |
1344 /// | |
1345 /// This is basically an ISO 8601 date, but even more rigid. | |
1346 /// | |
1347 /// Params: | |
1348 /// timeval = the YYYYMMDDHHMMSS date | |
1349 /// | |
1350 /// Returns: a d_time representing the same date | |
1351 | |
1352 protected Time parseTimeval(char[] timeval) | |
1353 { | |
1354 if (timeval.length < 14) | |
1355 throw new FTPException("CLIENT: Unable to parse timeval", "501"); | |
1356 | |
1357 return Gregorian.generic.toTime (Integer.atoi (timeval[0..4]), | |
1358 Integer.atoi (timeval[4..6]), | |
1359 Integer.atoi (timeval[6..8]), | |
1360 Integer.atoi (timeval[8..10]), | |
1361 Integer.atoi (timeval[10..12]), | |
1362 Integer.atoi (timeval[12..14])); | |
1363 } | |
1364 | |
1365 /// Get the modification time of a file. | |
1366 /// | |
1367 /// Not supported by a lot of servers. | |
1368 /// | |
1369 /// Params: | |
1370 /// path = the file or directory in question | |
1371 /// | |
1372 /// Returns: a d_time representing the mtime | |
1373 public Time filemtime(char[] path) | |
1374 in | |
1375 { | |
1376 assert (path.length > 0); | |
1377 } | |
1378 body | |
1379 { | |
1380 this.sendCommand("MDTM", path); | |
1381 auto response = this.readResponse("213"); | |
1382 | |
1383 // The whole response should be a timeval. | |
1384 return this.parseTimeval(response.message); | |
1385 } | |
1386 | |
1387 /// Create a directory. | |
1388 /// | |
1389 /// Depending on server model, a cwd with the same path may not work. | |
1390 /// Use the return value instead to escape this problem. | |
1391 /// | |
1392 /// Params: | |
1393 /// path = the directory to create | |
1394 /// | |
1395 /// Returns: the path to the directory created | |
1396 public char[] mkdir(char[] path) | |
1397 in | |
1398 { | |
1399 assert (path.length > 0); | |
1400 } | |
1401 body | |
1402 { | |
1403 this.sendCommand("MKD", path); | |
1404 auto response = this.readResponse("257"); | |
1405 | |
1406 return this.parse257(response); | |
1407 } | |
1408 | |
1409 /// Get supported features from the server. | |
1410 /// | |
1411 /// This may not be supported, in which case the list will remain empty. | |
1412 /// Otherwise, it will contain at least FEAT. | |
1413 public void getFeatures() | |
1414 { | |
1415 this.sendCommand("FEAT"); | |
1416 auto response = this.readResponse(); | |
1417 | |
1418 // 221 means FEAT is supported, and a list follows. Otherwise we don't know... | |
1419 if (response.code != "211") | |
1420 delete this.supported_features; | |
1421 else | |
1422 { | |
1423 char[][] lines = Text.splitLines (response.message); | |
1424 | |
1425 // There are two more lines than features, but we also have FEAT. | |
1426 this.supported_features = new FtpFeature[lines.length - 1]; | |
1427 this.supported_features[0].command = "FEAT"; | |
1428 | |
1429 for (size_t i = 1; i < lines.length - 1; i++) | |
1430 { | |
1431 size_t pos = Text.locate(lines[i], ' '); | |
1432 | |
1433 this.supported_features[i].command = lines[i][0 .. pos]; | |
1434 if (pos < lines[i].length - 1) | |
1435 this.supported_features[i].params = lines[i][pos + 1 .. lines[i].length]; | |
1436 } | |
1437 | |
1438 delete lines; | |
1439 } | |
1440 } | |
1441 | |
1442 /// Check if a specific feature might be supported. | |
1443 /// | |
1444 /// Example: | |
1445 /// ---------- | |
1446 /// if (ftp.isSupported("SIZE")) | |
1447 /// size = ftp.size("example.txt"); | |
1448 /// ---------- | |
1449 /// | |
1450 /// Params: | |
1451 /// command = the command in question | |
1452 public bool isSupported(char[] command) | |
1453 in | |
1454 { | |
1455 assert (command.length > 0); | |
1456 } | |
1457 body | |
1458 { | |
1459 if (this.supported_features.length == 0) | |
1460 return true; | |
1461 | |
1462 // Search through the list for the feature. | |
1463 foreach (FtpFeature feat; this.supported_features) | |
1464 { | |
1465 if (Ascii.icompare(feat.command, command) == 0) | |
1466 return true; | |
1467 } | |
1468 | |
1469 return false; | |
1470 } | |
1471 | |
1472 /// Check if a specific feature is known to be supported. | |
1473 /// | |
1474 /// Example: | |
1475 /// ---------- | |
1476 /// if (ftp.is_supported("SIZE")) | |
1477 /// size = ftp.size("example.txt"); | |
1478 /// ---------- | |
1479 /// | |
1480 /// Params: | |
1481 /// command = the command in question | |
1482 public bool is_supported(char[] command) | |
1483 { | |
1484 if (this.supported_features.length == 0) | |
1485 return false; | |
1486 | |
1487 return this.isSupported(command); | |
1488 } | |
1489 | |
1490 /// Send a site-specific command. | |
1491 /// | |
1492 /// The command might be WHO, for example, returning a list of users online. | |
1493 /// These are typically heavily server-specific. | |
1494 /// | |
1495 /// Params: | |
1496 /// command = the command to send (after SITE) | |
1497 /// parameters = any additional parameters to send | |
1498 /// (each will be prefixed by a space) | |
1499 public FtpResponse siteCommand(char[] command, char[][] parameters ...) | |
1500 in | |
1501 { | |
1502 assert (command.length > 0); | |
1503 } | |
1504 body | |
1505 { | |
1506 // Because of the way sendCommand() works, we have to tweak this a bit. | |
1507 char[][] temp_params = new char[][parameters.length + 1]; | |
1508 temp_params[0] = command; | |
1509 temp_params[1 .. temp_params.length][] = parameters; | |
1510 | |
1511 this.sendCommand("SITE", temp_params); | |
1512 auto response = this.readResponse(); | |
1513 | |
1514 // Check to make sure it didn't fail. | |
1515 if (response.code[0] != '2') | |
1516 exception (response); | |
1517 | |
1518 return response; | |
1519 } | |
1520 | |
1521 /// Send a NOOP, typically used to keep the connection alive. | |
1522 public void noop() | |
1523 { | |
1524 this.sendCommand("NOOP"); | |
1525 this.readResponse("200"); | |
1526 } | |
1527 | |
1528 /// Send the stream to the server. | |
1529 /// | |
1530 /// Params: | |
1531 /// data = the socket to write to | |
1532 /// stream = the stream to read from | |
1533 /// progress = a delegate to call with progress information | |
1534 | |
1535 protected void sendStream(Socket data, InputStream stream, FtpProgress progress = null) | |
1536 in | |
1537 { | |
1538 assert (data !is null); | |
1539 assert (stream !is null); | |
1540 } | |
1541 body | |
1542 { | |
1543 // Set up a SocketSet so we can use select() - it's pretty efficient. | |
1544 SocketSet set = new SocketSet(); | |
1545 scope (exit) | |
1546 delete set; | |
1547 | |
1548 // At end_time, we bail. | |
1549 Time end_time = Clock.now + this.timeout; | |
1550 | |
1551 // This is the buffer the stream data is stored in. | |
1552 ubyte[8 * 1024] buf; | |
1553 size_t buf_size = 0, buf_pos = 0; | |
1554 int delta = 0; | |
1555 | |
1556 size_t pos = 0; | |
1557 bool completed = false; | |
1558 while (!completed && Clock.now < end_time) | |
1559 { | |
1560 set.reset(); | |
1561 set.add(data); | |
1562 | |
1563 // Can we write yet, can we write yet? | |
1564 int code = Socket.select(null, set, null, this.timeout); | |
1565 if (code == -1 || code == 0) | |
1566 break; | |
1567 | |
1568 if (buf_size - buf_pos <= 0) | |
1569 { | |
1570 if ((buf_size = stream.read(buf)) is stream.Eof) | |
1571 buf_size = 0, completed = true; | |
1572 buf_pos = 0; | |
1573 } | |
1574 | |
1575 // Send the chunk (or as much of it as possible!) | |
1576 delta = data.send(buf[buf_pos .. buf_size]); | |
1577 if (delta == data.ERROR) | |
1578 break; | |
1579 | |
1580 buf_pos += delta; | |
1581 | |
1582 pos += delta; | |
1583 if (progress !is null) | |
1584 progress(pos); | |
1585 | |
1586 // Give it more time as long as data is going through. | |
1587 if (delta != 0) | |
1588 end_time = Clock.now + this.timeout; | |
1589 } | |
1590 | |
1591 // Did all the data get sent? | |
1592 if (!completed) | |
1593 throw new FTPException("CLIENT: Timeout when sending data", "420"); | |
1594 } | |
1595 | |
1596 /// Reads from the server to a stream until EOF. | |
1597 /// | |
1598 /// Params: | |
1599 /// data = the socket to read from | |
1600 /// stream = the stream to write to | |
1601 /// progress = a delegate to call with progress information | |
1602 protected void readStream(Socket data, OutputStream stream, FtpProgress progress = null) | |
1603 in | |
1604 { | |
1605 assert (data !is null); | |
1606 assert (stream !is null); | |
1607 } | |
1608 body | |
1609 { | |
1610 // Set up a SocketSet so we can use select() - it's pretty efficient. | |
1611 SocketSet set = new SocketSet(); | |
1612 scope (exit) | |
1613 delete set; | |
1614 | |
1615 // At end_time, we bail. | |
1616 Time end_time = Clock.now + this.timeout; | |
1617 | |
1618 // This is the buffer the stream data is stored in. | |
1619 ubyte[8 * 1024] buf; | |
1620 int buf_size = 0; | |
1621 | |
1622 bool completed = false; | |
1623 size_t pos; | |
1624 while (Clock.now < end_time) | |
1625 { | |
1626 set.reset(); | |
1627 set.add(data); | |
1628 | |
1629 // Can we read yet, can we read yet? | |
1630 int code = Socket.select(set, null, null, this.timeout); | |
1631 if (code == -1 || code == 0) | |
1632 break; | |
1633 | |
1634 buf_size = data.receive(buf); | |
1635 if (buf_size == data.ERROR) | |
1636 break; | |
1637 | |
1638 if (buf_size == 0) | |
1639 { | |
1640 completed = true; | |
1641 break; | |
1642 } | |
1643 | |
1644 stream.write(buf[0 .. buf_size]); | |
1645 | |
1646 pos += buf_size; | |
1647 if (progress !is null) | |
1648 progress(pos); | |
1649 | |
1650 // Give it more time as long as data is going through. | |
1651 end_time = Clock.now + this.timeout; | |
1652 } | |
1653 | |
1654 // Did all the data get received? | |
1655 if (!completed) | |
1656 throw new FTPException("CLIENT: Timeout when reading data", "420"); | |
1657 } | |
1658 | |
1659 /// Parse a 257 response (which begins with a quoted path.) | |
1660 /// | |
1661 /// Params: | |
1662 /// response = the response to parse | |
1663 /// | |
1664 /// Returns: the path in the response | |
1665 | |
1666 protected char[] parse257(FtpResponse response) | |
1667 { | |
1668 char[] path = new char[response.message.length]; | |
1669 size_t pos = 1, len = 0; | |
1670 | |
1671 // Since it should be quoted, it has to be at least 3 characters in length. | |
1672 if (response.message.length <= 2) | |
1673 exception (response); | |
1674 | |
1675 assert (response.message[0] == '"'); | |
1676 | |
1677 // Trapse through the response... | |
1678 while (pos < response.message.length) | |
1679 { | |
1680 if (response.message[pos] == '"') | |
1681 { | |
1682 // An escaped quote, keep going. False alarm. | |
1683 if (response.message[++pos] == '"') | |
1684 path[len++] = response.message[pos]; | |
1685 else | |
1686 break; | |
1687 } | |
1688 else | |
1689 path[len++] = response.message[pos]; | |
1690 | |
1691 pos++; | |
1692 } | |
1693 | |
1694 // Okay, done! That wasn't too hard. | |
1695 path.length = len; | |
1696 return path; | |
1697 } | |
1698 | |
1699 /// Send a command to the FTP server. | |
1700 /// | |
1701 /// Does not get/wait for the response. | |
1702 /// | |
1703 /// Params: | |
1704 /// command = the command to send | |
1705 /// ... = additional parameters to send (a space will be prepended to each) | |
1706 public void sendCommand(char[] command, char[][] parameters ...) | |
1707 { | |
1708 assert (this.socket !is null); | |
1709 | |
1710 | |
1711 char [] socketCommand = command ; | |
1712 | |
1713 // Send the command, parameters, and then a CRLF. | |
1714 | |
1715 foreach (char[] param; parameters) | |
1716 { | |
1717 socketCommand ~= " " ~ param; | |
1718 | |
1719 } | |
1720 | |
1721 socketCommand ~= "\r\n"; | |
1722 | |
1723 debug(FtpDebug) | |
1724 { | |
1725 Stdout.formatln("[sendCommand] Sending command '{0}'",socketCommand ); | |
1726 } | |
1727 this.sendData(socketCommand); | |
1728 } | |
1729 | |
1730 /// Read in response lines from the server, expecting a certain code. | |
1731 /// | |
1732 /// Params: | |
1733 /// expected_code = the code expected from the server | |
1734 /// | |
1735 /// Returns: the response from the server | |
1736 /// | |
1737 /// Throws: FTPException if code does not match | |
1738 public FtpResponse readResponse(char[] expected_code) | |
1739 { | |
1740 debug (FtpDebug ) { Stdout.formatln("[readResponse] Expected Response {0}",expected_code )(); } | |
1741 auto response = this.readResponse(); | |
1742 debug (FtpDebug ) { Stdout.formatln("[readResponse] Actual Response {0}",response.code)(); } | |
1743 | |
1744 if (response.code != expected_code) | |
1745 exception (response); | |
1746 | |
1747 | |
1748 | |
1749 return response; | |
1750 } | |
1751 | |
1752 /// Read in the response line(s) from the server. | |
1753 /// | |
1754 /// Returns: the response from the server | |
1755 public FtpResponse readResponse() | |
1756 { | |
1757 assert (this.socket !is null); | |
1758 | |
1759 // Pick a time at which we stop reading. It can't take too long, but it could take a bit for the whole response. | |
1760 Time end_time = Clock.now + this.timeout * 10; | |
1761 | |
1762 FtpResponse response; | |
1763 char[] single_line = null; | |
1764 | |
1765 // Danger, Will Robinson, don't fall into an endless loop from a malicious server. | |
1766 while (Clock.now < end_time) | |
1767 { | |
1768 single_line = this.readLine(); | |
1769 | |
1770 | |
1771 // This is the first line. | |
1772 if (response.message.length == 0) | |
1773 { | |
1774 // The first line must have a code and then a space or hyphen. | |
1775 if (single_line.length <= 4) | |
1776 { | |
1777 response.code[] = "500"; | |
1778 break; | |
1779 } | |
1780 | |
1781 // The code is the first three characters. | |
1782 response.code[] = single_line[0 .. 3]; | |
1783 response.message = single_line[4 .. single_line.length]; | |
1784 } | |
1785 // This is either an extra line, or the last line. | |
1786 else | |
1787 { | |
1788 response.message ~= "\n"; | |
1789 | |
1790 // If the line starts like "123-", that is not part of the response message. | |
1791 if (single_line.length > 4 && single_line[0 .. 3] == response.code) | |
1792 response.message ~= single_line[4 .. single_line.length]; | |
1793 // If it starts with a space, that isn't either. | |
1794 else if (single_line.length > 2 && single_line[0] == ' ') | |
1795 response.message ~= single_line[1 .. single_line.length]; | |
1796 else | |
1797 response.message ~= single_line; | |
1798 } | |
1799 | |
1800 // We're done if the line starts like "123 ". Otherwise we're not. | |
1801 if (single_line.length > 4 && single_line[0 .. 3] == response.code && single_line[3] == ' ') | |
1802 break; | |
1803 } | |
1804 | |
1805 return response; | |
1806 } | |
1807 | |
1808 /// convert text to integer | |
1809 private int toInt (char[] s) | |
1810 { | |
1811 return cast(int) toLong (s); | |
1812 } | |
1813 | |
1814 /// convert text to integer | |
1815 private long toLong (char[] s) | |
1816 { | |
1817 return Integer.parse (s); | |
1818 } | |
1819 } | |
1820 | |
1821 | |
1822 | |
1823 /// An exception caused by an unexpected FTP response. | |
1824 /// | |
1825 /// Even after such an exception, the connection may be in a usable state. | |
1826 /// Use the response code to determine more information about the error. | |
1827 /// | |
1828 /// Standards: RFC 959, RFC 2228, RFC 2389, RFC 2428 | |
1829 class FTPException: Exception | |
1830 { | |
1831 /// The three byte response code. | |
1832 char[3] response_code = "000"; | |
1833 | |
1834 /// Construct an FTPException based on a message and code. | |
1835 /// | |
1836 /// Params: | |
1837 /// message = the exception message | |
1838 /// code = the code (5xx for fatal errors) | |
1839 this (char[] message, char[3] code = "420") | |
1840 { | |
1841 this.response_code[] = code; | |
1842 super(message); | |
1843 } | |
1844 | |
1845 /// Construct an FTPException based on a response. | |
1846 /// | |
1847 /// Params: | |
1848 /// r = the server response | |
1849 this (FtpResponse r) | |
1850 { | |
1851 this.response_code[] = r.code; | |
1852 super(r.message); | |
1853 } | |
1854 | |
1855 /// A string representation of the error. | |
1856 char[] toString() | |
1857 { | |
1858 char[] buffer = new char[this.msg.length + 4]; | |
1859 | |
1860 buffer[0 .. 3] = this.response_code; | |
1861 buffer[3] = ' '; | |
1862 buffer[4 .. buffer.length] = this.msg; | |
1863 | |
1864 return buffer; | |
1865 } | |
1866 } | |
1867 | |
1868 | |
1869 debug (UnitTest ) | |
1870 { | |
1871 import tango.io.Stdout; | |
1872 | |
1873 unittest | |
1874 { | |
1875 | |
1876 try | |
1877 { | |
1878 /+ | |
1879 + TODO: Fix this | |
1880 + | |
1881 auto ftp = new FTPConnection("ftp.gnu.org","anonymous","anonymous"); | |
1882 auto dirList = ftp.ls(); // get list for current dir | |
1883 | |
1884 foreach ( entry;dirList ) | |
1885 { | |
1886 | |
1887 Stdout("File :")(entry.name)("\tSize :")(entry.size).newline; | |
1888 | |
1889 } | |
1890 | |
1891 ftp.cd("gnu/windows/emacs"); | |
1892 | |
1893 | |
1894 dirList = ftp.ls(); | |
1895 | |
1896 foreach ( entry;dirList ) | |
1897 { | |
1898 | |
1899 Stdout("File :")(entry.name)("\tSize :")(entry.size).newline; | |
1900 | |
1901 } | |
1902 | |
1903 | |
1904 size_t size = ftp.size("emacs-21.3-barebin-i386.tar.gz"); | |
1905 | |
1906 void progress( size_t pos ) | |
1907 { | |
1908 | |
1909 Stdout.formatln("Byte {0} of {1}",pos,size); | |
1910 | |
1911 } | |
1912 | |
1913 | |
1914 ftp.get("emacs-21.3-barebin-i386.tar.gz","emacs.tgz", &progress); | |
1915 +/ | |
1916 } | |
1917 catch( Object o ) | |
1918 { | |
1919 assert( false ); | |
1920 } | |
1921 } | |
1922 } |