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 }