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 }