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