Download Reference Manual
The Developer's Library for D
About Wiki Forums Source Search Contact

root/trunk/tango/net/ftp/FtpClient.d

Revision 5700, 59.6 kB (checked in by Marenz, 13 years ago)

Not using package for import in socket, fixes #2105, thanks luca

  • Property svn:mime-type set to text/x-dsrc
  • Property svn:eol-style set to native
Line 
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(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                 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(Object o) {
111             return null;
112         }
113     }
114
115     char[] address;
116     char[] directory;
117     char[] user = "anonymous";
118     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     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     char[] command = null;
206     /**********************************************************************
207      Parameters for this command; e.g. facts for MLST.
208      **********************************************************************/
209     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     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     char[] mime = null;
273     /***********************************************************************
274      An associative array of all facts returned by the server, lowercased.
275      ***********************************************************************/
276     char[][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(char[] 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);
308     }
309
310     /***********************************************************************
311      A string representation of the error.
312      ***********************************************************************/
313     char[] 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;
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     char[] currFile_ = "";
335     Socket dataSocket_;
336     TimeSpan timeout_ = TimeSpan.fromMillis(5000);
337
338     /***********************************************************************
339      Added Since: 0.99.8
340      ***********************************************************************/
341     public TimeSpan timeout() {
342         return timeout_;
343     }
344
345     /***********************************************************************
346      Added Since: 0.99.8
347      ***********************************************************************/
348     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     void exception(char[] 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);
382     }
383
384     public this() {
385
386     }
387
388     public this(char[] hostname, char[] username = "anonymous",
389             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(char[] hostname, char[] username = "anonymous",
411             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             delete supportedFeatures_;
463             delete socket_;
464         }
465     }
466
467     public void setPassive() {
468         inf_.type = FtpConnectionType.passive;
469
470         delete inf_.address;
471         delete inf_.listen;
472     }
473
474     public void setActive(char[] ip, ushort port, 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(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 char[] cwd() {
514         sendCommand("PWD");
515         auto response = readResponse("257");
516
517         return parse257(response);
518     }
519
520     public void chmod(char[] path, int mode)
521     in {
522         assert(path.length > 0);
523         assert(mode >= 0 && (mode >> 16) == 0);
524     }
525     body {
526         char[] 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(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(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(char[] old_path, 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(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(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(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(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 char[] mkdir(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             delete supportedFeatures_;
669         else {
670             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             delete lines;
685         }
686     }
687
688     public void sendCommand(char[] command, char[][] parameters...) {
689
690         char[] socketCommand = command;
691
692         // Send the command, parameters, and then a CRLF.
693
694         foreach(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(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         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 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");
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     public Socket connectPassive() {
889         Address connect_to = null;
890
891         // SPSV, which is just a port number.
892         if(this.is_supported("SPSV")) {
893             this.sendCommand("SPSV");
894             auto response = this.readResponse("227");
895
896             // Connecting to the same host.
897             IPv4Address
898                     remote = cast(IPv4Address) this.socket_.socket.remoteAddress();
899             assert(remote !is null);
900
901             uint address = remote.addr();
902             uint port = cast(int) Integer.parse(((response.message)));
903
904             connect_to = new IPv4Address(address, cast(ushort) port);
905         }
906         // Extended passive mode (IP v6, etc.)
907         else if(this.is_supported("EPSV")) {
908             this.sendCommand("EPSV");
909             auto response = this.readResponse("229");
910
911             // Try to pull out the (possibly not parenthesized) address.
912             auto r = Regex(`\([^0-9][^0-9][^0-9](\d+)[^0-9]\)`);
913             if(!r.test(response.message[0 .. find(response.message, '\n')]))
914                 throw new FtpException("CLIENT: Unable to parse address", "501");
915
916             IPv4Address
917                     remote = cast(IPv4Address) this.socket_.socket.remoteAddress();
918             assert(remote !is null);
919
920             uint address = remote.addr();
921             uint port = cast(int) Integer.parse(((r.match(1))));
922
923             connect_to = new IPv4Address(address, cast(ushort) port);
924         } else {
925             this.sendCommand("PASV");
926             auto response = this.readResponse("227");
927
928             // Try to pull out the (possibly not parenthesized) address.
929             auto r = Regex(`(\d+),\s*(\d+),\s*(\d+),\s*(\d+),\s*(\d+)(,\s*(\d+))?`);
930             if(!r.test(response.message[0 .. find(response.message, '\n')]))
931                 throw new FtpException("CLIENT: Unable to parse address", "501");
932
933             // Now put it into something std.socket will understand.
934             char[] address = r.match(1) ~ "." ~ r.match(2) ~ "." ~ r.match(3) ~ "." ~ r.match(4);
935             ushort port = (((cast(int) Integer.parse(r.match(5))) << 8) + (r.match(7).
936                            length > 0 ? cast(ushort) Integer.parse(r.match(7)) : 0));
937
938             // Okay, we've got it!
939             connect_to = new IPv4Address(address, port);
940         }
941
942         scope(exit)
943             delete connect_to;
944
945         // This will throw an exception if it cannot connect.
946         auto sock = new Socket;
947         sock.connect(connect_to);
948         return sock;
949     }
950
951     /*
952      Socket sock = new Socket();
953      sock.connect(connect_to);
954      return sock;
955      */
956
957     public bool isSupported(char[] command)
958     in {
959         assert(command.length > 0);
960     }
961     body {
962         if(this.supportedFeatures_.length == 0)
963             return true;
964
965         // Search through the list for the feature.
966         foreach(FtpFeature feat; this.supportedFeatures_) {
967             if(Ascii.icompare(feat.command, command) == 0)
968                 return true;
969         }
970
971         return false;
972     }
973
974     public bool is_supported(char[] command) {
975         if(this.supportedFeatures_.length == 0)
976             return false;
977
978         return this.isSupported(command);
979     }
980
981     /*******************************************************************************
982      Prepare a data socket for use.
983     
984      This modifies the socket in some cases.
985     
986      Params:
987      data =            the data listener socket
988      Changed Since: 0.99.8
989      ********************************************************************************/
990     protected void prepareDataSocket(ref Socket data) {
991         switch(this.inf_.type) {
992             default:
993                 exception("unknown connection type");
994
995             case FtpConnectionType.active:
996                 Berkeley new_data;
997
998                 scope set = new SocketSet;
999
1000                 // At end_time, we bail.
1001                 Time end_time = Clock.now + this.timeout;
1002
1003                 while(Clock.now < end_time) {
1004                     set.reset();
1005                     set.add(data.socket);
1006
1007                     // Can we accept yet?
1008                     int code = set.select(set, null, null, timeout.micros);
1009                     if(code == -1 || code == 0)
1010                         break;
1011
1012                     data.socket.accept(new_data);
1013                     break;
1014                 }
1015
1016             if(new_data.sock is new_data.sock.init)
1017                throw new FtpException("CLIENT: No connection from server", "420");
1018
1019             // We don't need the listener anymore.
1020             data.shutdown.detach;
1021
1022             // This is the actual socket.
1023             data.socket.sock = new_data.sock;
1024             break;
1025
1026             case FtpConnectionType.passive:
1027             break;
1028         }
1029     }
1030
1031     /*****************************************************************************
1032      Changed Since: 0.99.8
1033      *****************************************************************************/
1034     public void finishDataCommand(Socket data) {
1035         // Close the socket.  This tells the server we're done (EOF.)
1036         data.close();
1037         data.detach();
1038
1039         // We shouldn't get a 250 in STREAM mode.
1040         FtpResponse r = readResponse();
1041         if(!(r.code == "226" || r.code == "420"))
1042             exception("Bad finish");
1043
1044     }
1045
1046     /*****************************************************************************
1047      Changed Since: 0.99.8
1048      *****************************************************************************/
1049     public Socket processDataCommand(char[] command, char[][] parameters...) {
1050         // Create a connection.
1051         Socket data = this.getDataSocket();
1052         scope(failure) {
1053             // Close the socket, whether we were listening or not.
1054             data.close();
1055         }
1056
1057         // Tell the server about it.
1058         this.sendCommand(command, parameters);
1059
1060         // We should always get a 150/125 response.
1061         auto response = this.readResponse();
1062         if(response.code != "150" && response.code != "125")
1063             exception(response);
1064
1065         // We might need to do this for active connections.
1066         this.prepareDataSocket(data);
1067
1068         return data;
1069     }
1070
1071     public FtpFileInfo[] ls(char[] path = "")
1072     // default to current dir
1073     in {
1074         assert(path.length == 0 || path[path.length - 1] != '/');
1075     }
1076     body {
1077         FtpFileInfo[] dir;
1078
1079         // We'll try MLSD (which is so much better) first... but it may fail.
1080         bool mlsd_success = false;
1081         Socket data = null;
1082
1083         // Try it if it could/might/maybe is supported.
1084         if(this.isSupported("MLST")) {
1085             mlsd_success = true;
1086
1087             // Since this is a data command, processDataCommand handles
1088             // checking the response... just catch its Exception.
1089             try {
1090                 if(path.length > 0)
1091                     data = this.processDataCommand("MLSD", path);
1092                 else
1093                     data = this.processDataCommand("MLSD");
1094             } catch(FtpException)
1095                 mlsd_success = false;
1096         }
1097
1098         // If it passed, parse away!
1099         if(mlsd_success) {
1100             auto listing = new Array(256, 65536);
1101             this.readStream(data, listing);
1102             this.finishDataCommand(data);
1103
1104             // Each line is something in that directory.
1105             char[][] lines = Text.splitLines(cast(char[]) listing.slice());
1106             scope(exit)
1107                 delete lines;
1108
1109             foreach(char[] line; lines) {
1110                 if(line.length == 0)
1111                     continue;
1112                 // Parse each line exactly like MLST does.
1113                 try {
1114                     FtpFileInfo info = this.parseMlstLine(line);
1115                     if(info.name.length > 0)
1116                         dir ~= info;
1117                 } catch(FtpException) {
1118                     return this.sendListCommand(path);
1119                 }
1120             }
1121
1122             return dir;
1123         }
1124         // Fall back to LIST.
1125         else
1126             return this.sendListCommand(path);
1127     }
1128
1129     /*****************************************************************************
1130      Changed Since: 0.99.8
1131      *****************************************************************************/
1132     protected void readStream(Socket data, OutputStream stream,
1133             FtpProgress progress = null)
1134     in {
1135         assert(data !is null);
1136         assert(stream !is null);
1137     }
1138     body {
1139         // Set up a SocketSet so we can use select() - it's pretty efficient.
1140         scope set = new SocketSet;
1141
1142         // At end_time, we bail.
1143         Time end_time = Clock.now + this.timeout;
1144
1145         // This is the buffer the stream data is stored in.
1146         ubyte[8 * 1024] buf;
1147         int buf_size = 0;
1148
1149         bool completed = false;
1150         size_t pos;
1151         while(Clock.now < end_time) {
1152             set.reset();
1153             set.add(data.socket);
1154
1155             // Can we read yet, can we read yet?
1156             int code = set.select(set, null, null, timeout.micros);
1157             if(code == -1 || code == 0)
1158                 break;
1159
1160             buf_size = data.socket.receive(buf);
1161             if(buf_size == data.socket.ERROR)
1162                 break;
1163
1164             if(buf_size == 0) {
1165                 completed = true;
1166                 break;
1167             }
1168
1169             stream.write(buf[0 .. buf_size]);
1170
1171             pos += buf_size;
1172             if(progress !is null)
1173                 progress(pos);
1174
1175             // Give it more time as long as data is going through.
1176             end_time = Clock.now + this.timeout;
1177         }
1178
1179         // Did all the data get received?
1180         if(!completed)
1181             throw new FtpException("CLIENT: Timeout when reading data", "420");
1182     }
1183
1184     /*****************************************************************************
1185      Changed Since: 0.99.8
1186      *****************************************************************************/
1187     protected void sendStream(Socket data, InputStream stream,
1188             FtpProgress progress = null)
1189     in {
1190         assert(data !is null);
1191         assert(stream !is null);
1192     }
1193     body {
1194         // Set up a SocketSet so we can use select() - it's pretty efficient.
1195         scope set = new SocketSet;
1196
1197         // At end_time, we bail.
1198         Time end_time = Clock.now + this.timeout;
1199
1200         // This is the buffer the stream data is stored in.
1201         ubyte[8 * 1024] buf;
1202         size_t buf_size = 0, buf_pos = 0;
1203         int delta = 0;
1204
1205         size_t pos = 0;
1206         bool completed = false;
1207         while(!completed && Clock.now < end_time) {
1208             set.reset();
1209             set.add(data.socket);
1210
1211             // Can we write yet, can we write yet?
1212             int code = set.select(null, set, null, timeout.micros);
1213             if(code == -1 || code == 0)
1214                 break;
1215
1216             if(buf_size - buf_pos <= 0) {
1217                 if((buf_size = stream.read(buf)) is stream.Eof)
1218                     buf_size = 0 , completed = true;
1219                 buf_pos = 0;
1220             }
1221
1222             // Send the chunk (or as much of it as possible!)
1223             delta = data.socket.send(buf[buf_pos .. buf_size]);
1224             if(delta == data.socket.ERROR)
1225                 break;
1226
1227             buf_pos += delta;
1228
1229             pos += delta;
1230             if(progress !is null)
1231                 progress(pos);
1232
1233             // Give it more time as long as data is going through.
1234             if(delta != 0)
1235                 end_time = Clock.now + this.timeout;
1236         }
1237
1238         // Did all the data get sent?
1239         if(!completed)
1240             throw new FtpException("CLIENT: Timeout when sending data", "420");
1241     }
1242
1243     protected FtpFileInfo[] sendListCommand(char[] path) {
1244         FtpFileInfo[] dir;
1245         Socket data = null;
1246
1247         if(path.length > 0)
1248             data = this.processDataCommand("LIST", path);
1249         else
1250             data = this.processDataCommand("LIST");
1251
1252         // Read in the stupid non-standardized response.
1253         auto listing = new Array(256, 65536);
1254         this.readStream(data, listing);
1255         this.finishDataCommand(data);
1256
1257         // Split out the lines.  Most of the time, it's one-to-one.
1258         char[][] lines = Text.splitLines(cast(char[]) listing.slice());
1259         scope(exit)
1260             delete lines;
1261
1262         foreach(char[] line; lines) {
1263             if(line.length == 0)
1264                 continue;
1265             // If there are no spaces, or if there's only one... skip the line.
1266             // This is probably like a "total 8" line.
1267             if(Text.locate(line, ' ') == Text.locatePrior(line, ' '))
1268                 continue;
1269
1270             // Now parse the line, or try to.
1271             FtpFileInfo info = this.parseListLine(line);
1272             if(info.name.length > 0)
1273                 dir ~= info;
1274         }
1275
1276         return dir;
1277     }
1278
1279     protected FtpFileInfo parseListLine(char[] line) {
1280         FtpFileInfo info;
1281         size_t pos = 0;
1282
1283         // Convenience function to parse a word from the line.
1284         char[] parse_word() {
1285             size_t start = 0, end = 0;
1286
1287             // Skip whitespace before.
1288             while(pos < line.length && line[pos] == ' ')
1289                 pos++;
1290
1291             start = pos;
1292             while(pos < line.length && line[pos] != ' ')
1293                 pos++;
1294             end = pos;
1295
1296             // Skip whitespace after.
1297             while(pos < line.length && line[pos] == ' ')
1298                 pos++;
1299
1300             return line[start .. end];
1301         }
1302
1303         // We have to sniff this... :/.
1304         switch(!Text.contains("0123456789", line[0])) {
1305             // Not a number; this is UNIX format.
1306             case true:
1307                 // The line must be at least 20 characters long.
1308                 if(line.length < 20)
1309                     return info;
1310
1311                 // The first character tells us what it is.
1312                 if(line[0] == 'd')
1313                     info.type = FtpFileType.dir;
1314                 // #3
1315                 // Might be a link entry - additional test down below
1316                 else if(line[0] == 'l')
1317                     info.type = FtpFileType.other;
1318                 else if(line[0] == '-')
1319                     info.type = FtpFileType.file;
1320                 else
1321                     info.type = FtpFileType.unknown;
1322
1323                 // Parse out the mode... rwxrwxrwx = 777.
1324                 char[] unix_mode = "0000".dup;
1325                 void read_mode(int digit) {
1326                     for(pos = 1 + digit * 3; pos <= 3 + digit * 3; pos++) {
1327                         if(line[pos] == 'r')
1328                             unix_mode[digit + 1] |= 4;
1329                         else if(line[pos] == 'w')
1330                             unix_mode[digit + 1] |= 2;
1331                         else if(line[pos] == 'x')
1332                             unix_mode[digit + 1] |= 1;
1333                     }
1334                 }
1335
1336                 // This makes it easier, huh?
1337                 read_mode(0);
1338                 read_mode(1);
1339                 read_mode(2);
1340
1341                 info.facts["UNIX.mode"] = unix_mode;
1342
1343                 // #4
1344                 // Not only parse lines like
1345                 //    drwxrwxr-x    2 10490    100          4096 May 20  2005 Acrobat
1346                 //    lrwxrwxrwx    1 root     other           7 Sep 21  2007 Broker.link -> Acrobat
1347                 //    -rwxrwxr-x    1 filelib  100           468 Nov  1  1999 Web_Users_Click_Here.html
1348                 // but also parse lines like
1349                 //    d--x--x--x   2 staff        512 Sep 24  2000 dev
1350                 // (see ftp.sunfreeware.com)
1351
1352                 // Links, owner.  These are hard to translate to MLST facts.
1353                 parse_word();
1354                 parse_word();
1355
1356                 // Group or size in bytes
1357                 char[] group_or_size = parse_word();
1358                 size_t oldpos = pos;
1359
1360                 // Size in bytes or month
1361                 char[] size_or_month = parse_word();
1362
1363                 if(!Text.contains("0123456789", size_or_month[0])) {
1364                     // Oops, no size here - go back to previous column
1365                     pos = oldpos;
1366                     info.size = cast(ulong) Integer.parse(group_or_size);
1367                 } else
1368                     info.size = cast(ulong) Integer.parse(size_or_month);
1369
1370                 // Make sure we still have enough space.
1371                 if(pos + 13 >= line.length)
1372                     return info;
1373
1374                 // Not parsing date for now.  It's too weird (last 12 months, etc.)
1375                 pos += 13;
1376
1377                 info.name = line[pos .. line.length];
1378                 // #3
1379                 // Might be a link entry - additional test here
1380                 if(info.type == FtpFileType.other) {
1381                     // Is name like 'name -> /some/other/path'?
1382                     size_t pos2 = Text.locatePattern(info.name, " -> ");
1383                     if(pos2 != info.name.length) {
1384                         // It is a link - split into target and name
1385                         info.facts["target"] = info.name[pos2 + 4 .. info.name.length];
1386                         info.name = info.name[0 .. pos2];
1387                         info.facts["type"] = "link";
1388                     }
1389                 }
1390             break;
1391
1392             // A number; this is DOS format.
1393             case false:
1394                 // We need some data here, to parse.
1395                 if(line.length < 18)
1396                     return info;
1397
1398                 // The order is 1 MM, 2 DD, 3 YY, 4 HH, 5 MM, 6 P
1399                 auto r = Regex(`(\d\d)-(\d\d)-(\d\d)\s+(\d\d):(\d\d)(A|P)M`);
1400                 // #5
1401                 // wrong test
1402                 if(!r.test(line))
1403                     return info;
1404
1405                 if(Timestamp.dostime(r.match(0), info.modify) is 0)
1406                     info.modify = Time.max;
1407
1408                 pos = r.match(0).length;
1409                 delete r;
1410
1411                 // This will either be <DIR>, or a number.
1412                 char[] dir_or_size = parse_word();
1413
1414                 if(dir_or_size.length < 0)
1415                     return info;
1416                 else if(dir_or_size[0] == '<')
1417                     info.type = FtpFileType.dir;
1418                 else {
1419                     // #5
1420                     // It is a file
1421                     info.size = cast(ulong) Integer.parse((dir_or_size));
1422                     info.type = FtpFileType.file;
1423                 }
1424
1425                 info.name = line[pos .. line.length];
1426             break;
1427
1428             // Something else, not supported.
1429             default:
1430                 throw new FtpException("CLIENT: Unsupported LIST format", "501");
1431         }
1432
1433         // Try to fix the type?
1434         if(info.name == ".")
1435             info.type = FtpFileType.cdir;
1436         else if(info.name == "..")
1437             info.type = FtpFileType.pdir;
1438
1439         return info;
1440     }
1441
1442     protected FtpFileInfo parseMlstLine(char[] line) {
1443         FtpFileInfo info;
1444
1445         // After this loop, filename_pos will be location of space + 1.
1446         size_t filename_pos = 0;
1447         while(filename_pos < line.length && line[filename_pos++] != ' ')
1448             continue;
1449
1450         if(filename_pos == line.length)
1451             throw new FtpException("CLIENT: Bad syntax in MLSx response", "501");
1452         /*{
1453          info.name = "";
1454          return info;
1455          }*/
1456
1457         info.name = line[filename_pos .. line.length];
1458
1459         // Everything else is frosting on top.
1460         if(filename_pos > 1) {
1461             char[][]
1462                     temp_facts = Text.delimit(line[0 .. filename_pos - 1], ";");
1463
1464             // Go through each fact and parse them into the array.
1465             foreach(char[] fact; temp_facts) {
1466                 int pos = Text.locate(fact, '=');
1467                 if(pos == fact.length)
1468                     continue;
1469
1470                 info.facts[Ascii.toLower(fact[0 .. pos])] = fact[pos + 1 .. fact.length];
1471             }
1472
1473             // Do we have a type?
1474             if("type" in info.facts) {
1475                 // Some reflection might be nice here.
1476                 switch(Ascii.toLower(info.facts["type"])) {
1477                     case "file":
1478                         info.type = FtpFileType.file;
1479                     break;
1480
1481                     case "cdir":
1482                         info.type = FtpFileType.cdir;
1483                     break;
1484
1485                     case "pdir":
1486                         info.type = FtpFileType.pdir;
1487                     break;
1488
1489                     case "dir":
1490                         info.type = FtpFileType.dir;
1491                     break;
1492
1493                     default:
1494                         info.type = FtpFileType.other;
1495                 }
1496             }
1497
1498             // Size, mime, etc...
1499             if("size" in info.facts)
1500                 info.size = cast(ulong) Integer.parse((info.facts["size"]));
1501             if("media-type" in info.facts)
1502                 info.mime = info.facts["media-type"];
1503
1504             // And the two dates.
1505             if("modify" in info.facts)
1506                 info.modify = this.parseTimeval(info.facts["modify"]);
1507             if("create" in info.facts)
1508                 info.create = this.parseTimeval(info.facts["create"]);
1509         }
1510
1511         return info;
1512     }
1513
1514     public FtpFileInfo getFileInfo(char[] path)
1515     in {
1516         assert(path.length > 0);
1517     }
1518     body {
1519         // Start assuming the MLST didn't work.
1520         bool mlst_success = false;
1521         FtpResponse response;
1522         auto inf = ls(path);
1523         if(inf.length == 1)
1524             return inf[0];
1525         else {
1526             debug(FtpUnitTest) {
1527                 Stdout("In getFileInfo.").newline.flush;
1528             }
1529             {
1530                 // Send a list command.  This may list the contents of a directory, even.
1531                 FtpFileInfo[] temp = this.sendListCommand(path);
1532
1533                 // If there wasn't at least one line, the file didn't exist?
1534                 // We should have already handled that.
1535                 if(temp.length < 1)
1536                     throw new FtpException(
1537                             "CLIENT: Bad LIST response from server", "501");
1538
1539                 // If there are multiple lines, try to return the correct one.
1540                 if(temp.length != 1)
1541                     foreach(FtpFileInfo info; temp) {
1542                         if(info.type == FtpFileType.cdir)
1543                             return info;
1544                     }
1545
1546                 // Okay then, the first line.  Best we can do?
1547                 return temp[0];
1548             }
1549         }
1550     }
1551
1552     public void put(char[] path, char[] local_file,
1553             FtpProgress progress = null, FtpFormat format = FtpFormat.image)
1554     in {
1555         assert(path.length > 0);
1556         assert(local_file.length > 0);
1557     }
1558     body {
1559         // Open the file for reading...
1560         auto file = new File(local_file);
1561         scope(exit) {
1562             file.detach();
1563             delete file;
1564         }
1565
1566         // Seek to the correct place, if specified.
1567         if(this.restartPos_ > 0) {
1568             file.seek(this.restartPos_);
1569             this.restartPos_ = 0;
1570         } else {
1571             // Allocate space for the file, if we need to.
1572             //this.allocate(file.length);
1573         }
1574
1575         // Now that it's open, we do what we always do.
1576         this.put(path, file, progress, format);
1577     }
1578
1579     /********************************************************************************
1580      Store data from a stream on the server.
1581     
1582      Calling this function will change the current data transfer format.
1583     
1584      Params:
1585      path =            the path to the remote file
1586      stream =          data to store, or null for a blank file
1587      progress =        a delegate to call with progress information
1588      format =          what format to send the data in
1589      ********************************************************************************/
1590     public void put(char[] path, InputStream stream = null,
1591             FtpProgress progress = null, FtpFormat format = FtpFormat.image)
1592     in {
1593         assert(path.length > 0);
1594     }
1595     body {
1596         // Change to the specified format.
1597         this.type(format);
1598
1599         // Okay server, we want to store something...
1600         Socket data = this.processDataCommand("STOR", path);
1601
1602         // Send the stream over the socket!
1603         if(stream !is null)
1604             this.sendStream(data, stream, progress);
1605
1606         this.finishDataCommand(data);
1607     }
1608
1609     /********************************************************************************
1610      Append data to a file on the server.
1611     
1612      Calling this function will change the current data transfer format.
1613     
1614      Params:
1615      path =            the path to the remote file
1616      stream =          data to append to the file
1617      progress =        a delegate to call with progress information
1618      format =          what format to send the data in
1619      ********************************************************************************/
1620     public void append(char[] path, InputStream stream,
1621             FtpProgress progress = null, FtpFormat format = FtpFormat.image)
1622     in {
1623         assert(path.length > 0);
1624         assert(stream !is null);
1625     }
1626     body {
1627         // Change to the specified format.
1628         this.type(format);
1629
1630         // Okay server, we want to store something...
1631         Socket data = this.processDataCommand("APPE", path);
1632
1633         // Send the stream over the socket!
1634         this.sendStream(data, stream, progress);
1635
1636         this.finishDataCommand(data);
1637     }
1638
1639     /*********************************************************************************
1640      Seek to a byte offset for the next transfer.
1641     
1642      Params:
1643      offset =          the number of bytes to seek forward
1644      **********************************************************************************/
1645     public void restartSeek(size_t offset) {
1646         char[16] tmp;
1647         this.sendCommand("REST", Integer.format(tmp, cast(long) offset));
1648         this.readResponse("350");
1649
1650         // Set this for later use.
1651         this.restartPos_ = offset;
1652     }
1653
1654     /**********************************************************************************
1655      Allocate space for a file.
1656     
1657      After calling this, append() or put() should be the next command.
1658     
1659      Params:
1660      bytes =           the number of bytes to allocate
1661      ***********************************************************************************/
1662     public void allocate(long bytes)
1663     in {
1664         assert(bytes > 0);
1665     }
1666     body {
1667         char[16] tmp;
1668         this.sendCommand("ALLO", Integer.format(tmp, bytes));
1669         auto response = this.readResponse();
1670
1671         // For our purposes 200 and 202 are both fine.
1672         if(response.code != "200" && response.code != "202")
1673             exception(response);
1674     }
1675
1676     /**********************************************************************************
1677      Retrieve a remote file's contents into a local file.
1678     
1679      Calling this function will change the current data transfer format.
1680     
1681      Params:
1682      path =            the path to the remote file
1683      local_file =      the path to the local file
1684      progress =        a delegate to call with progress information
1685      format =          what format to read the data in
1686      **********************************************************************************/
1687     public void get(char[] path, char[] local_file,
1688             FtpProgress progress = null, FtpFormat format = FtpFormat.image)
1689     in {
1690         assert(path.length > 0);
1691         assert(local_file.length > 0);
1692     }
1693     body {
1694         File file = null;
1695
1696         // We may either create a new file...
1697         if(this.restartPos_ == 0)
1698             file = new File(local_file, File.ReadWriteCreate);
1699         // Or open an existing file, and seek to the specified position (read: not end, necessarily.)
1700         else {
1701             file = new File(local_file, File.ReadWriteExisting);
1702             file.seek(this.restartPos_);
1703
1704             this.restartPos_ = 0;
1705         }
1706
1707         scope(exit) {
1708             file.detach();
1709             delete file;
1710         }
1711
1712         // Now that it's open, we do what we always do.
1713         this.get(path, file, progress, format);
1714     }
1715
1716     /*********************************************************************************
1717      Enable UTF8 on servers that don't use this as default. Might need some work
1718      *********************************************************************************/
1719     public void enableUTF8() {
1720         sendCommand("OPTS UTF8 ON");
1721         readResponse("200");
1722     }
1723
1724     /**********************************************************************************
1725      Retrieve a remote file's contents into a local file.
1726     
1727      Calling this function will change the current data transfer format.
1728     
1729      Params:
1730      path =            the path to the remote file
1731      stream =          stream to write the data to
1732      progress =        a delegate to call with progress information
1733      format =          what format to read the data in
1734      ***********************************************************************************/
1735     public void get(char[] path, OutputStream stream,
1736             FtpProgress progress = null, FtpFormat format = FtpFormat.image)
1737     in {
1738         assert(path.length > 0);
1739         assert(stream !is null);
1740     }
1741     body {
1742         // Change to the specified format.
1743         this.type(format);
1744
1745         // Okay server, we want to get this file...
1746         Socket data = this.processDataCommand("RETR", path);
1747
1748         // Read the stream in from the socket!
1749         this.readStream(data, stream, progress);
1750
1751         this.finishDataCommand(data);
1752     }
1753
1754     /*****************************************************************************
1755      Added Since: 0.99.8
1756      *****************************************************************************/
1757     public InputStream input(char[] path) {
1758         type(FtpFormat.image);
1759         dataSocket_ = this.processDataCommand("RETR", path);
1760         return dataSocket_;
1761     }
1762
1763     /*****************************************************************************
1764      Added Since: 0.99.8
1765      *****************************************************************************/
1766     public OutputStream output(char[] path) {
1767         type(FtpFormat.image);
1768         dataSocket_ = this.processDataCommand("STOR", path);
1769         return dataSocket_;
1770     }
1771 }
Note: See TracBrowser for help on using the browser.