1# 2# = net/ftp.rb - FTP Client Library 3# 4# Written by Shugo Maeda <shugo@ruby-lang.org>. 5# 6# Documentation by Gavin Sinclair, sourced from "Programming Ruby" (Hunt/Thomas) 7# and "Ruby In a Nutshell" (Matsumoto), used with permission. 8# 9# This library is distributed under the terms of the Ruby license. 10# You can freely distribute/modify this library. 11# 12# It is included in the Ruby standard library. 13# 14# See the Net::FTP class for an overview. 15# 16 17require "socket" 18require "monitor" 19require "net/protocol" 20 21module Net 22 23 # :stopdoc: 24 class FTPError < StandardError; end 25 class FTPReplyError < FTPError; end 26 class FTPTempError < FTPError; end 27 class FTPPermError < FTPError; end 28 class FTPProtoError < FTPError; end 29 class FTPConnectionError < FTPError; end 30 # :startdoc: 31 32 # 33 # This class implements the File Transfer Protocol. If you have used a 34 # command-line FTP program, and are familiar with the commands, you will be 35 # able to use this class easily. Some extra features are included to take 36 # advantage of Ruby's style and strengths. 37 # 38 # == Example 39 # 40 # require 'net/ftp' 41 # 42 # === Example 1 43 # 44 # ftp = Net::FTP.new('example.com') 45 # ftp.login 46 # files = ftp.chdir('pub/lang/ruby/contrib') 47 # files = ftp.list('n*') 48 # ftp.getbinaryfile('nif.rb-0.91.gz', 'nif.gz', 1024) 49 # ftp.close 50 # 51 # === Example 2 52 # 53 # Net::FTP.open('example.com') do |ftp| 54 # ftp.login 55 # files = ftp.chdir('pub/lang/ruby/contrib') 56 # files = ftp.list('n*') 57 # ftp.getbinaryfile('nif.rb-0.91.gz', 'nif.gz', 1024) 58 # end 59 # 60 # == Major Methods 61 # 62 # The following are the methods most likely to be useful to users: 63 # - FTP.open 64 # - #getbinaryfile 65 # - #gettextfile 66 # - #putbinaryfile 67 # - #puttextfile 68 # - #chdir 69 # - #nlst 70 # - #size 71 # - #rename 72 # - #delete 73 # 74 class FTP 75 include MonitorMixin 76 77 # :stopdoc: 78 FTP_PORT = 21 79 CRLF = "\r\n" 80 DEFAULT_BLOCKSIZE = BufferedIO::BUFSIZE 81 # :startdoc: 82 83 # When +true+, transfers are performed in binary mode. Default: +true+. 84 attr_reader :binary 85 86 # When +true+, the connection is in passive mode. Default: +false+. 87 attr_accessor :passive 88 89 # When +true+, all traffic to and from the server is written 90 # to +$stdout+. Default: +false+. 91 attr_accessor :debug_mode 92 93 # Sets or retrieves the +resume+ status, which decides whether incomplete 94 # transfers are resumed or restarted. Default: +false+. 95 attr_accessor :resume 96 97 # Number of seconds to wait for the connection to open. Any number 98 # may be used, including Floats for fractional seconds. If the FTP 99 # object cannot open a connection in this many seconds, it raises a 100 # Net::OpenTimeout exception. The default value is +nil+. 101 attr_accessor :open_timeout 102 103 # Number of seconds to wait for one block to be read (via one read(2) 104 # call). Any number may be used, including Floats for fractional 105 # seconds. If the FTP object cannot read data in this many seconds, 106 # it raises a TimeoutError exception. The default value is 60 seconds. 107 attr_reader :read_timeout 108 109 # Setter for the read_timeout attribute. 110 def read_timeout=(sec) 111 @sock.read_timeout = sec 112 @read_timeout = sec 113 end 114 115 # The server's welcome message. 116 attr_reader :welcome 117 118 # The server's last response code. 119 attr_reader :last_response_code 120 alias lastresp last_response_code 121 122 # The server's last response. 123 attr_reader :last_response 124 125 # 126 # A synonym for <tt>FTP.new</tt>, but with a mandatory host parameter. 127 # 128 # If a block is given, it is passed the +FTP+ object, which will be closed 129 # when the block finishes, or when an exception is raised. 130 # 131 def FTP.open(host, user = nil, passwd = nil, acct = nil) 132 if block_given? 133 ftp = new(host, user, passwd, acct) 134 begin 135 yield ftp 136 ensure 137 ftp.close 138 end 139 else 140 new(host, user, passwd, acct) 141 end 142 end 143 144 # 145 # Creates and returns a new +FTP+ object. If a +host+ is given, a connection 146 # is made. Additionally, if the +user+ is given, the given user name, 147 # password, and (optionally) account are used to log in. See #login. 148 # 149 def initialize(host = nil, user = nil, passwd = nil, acct = nil) 150 super() 151 @binary = true 152 @passive = false 153 @debug_mode = false 154 @resume = false 155 @sock = NullSocket.new 156 @logged_in = false 157 @open_timeout = nil 158 @read_timeout = 60 159 if host 160 connect(host) 161 if user 162 login(user, passwd, acct) 163 end 164 end 165 end 166 167 # A setter to toggle transfers in binary mode. 168 # +newmode+ is either +true+ or +false+ 169 def binary=(newmode) 170 if newmode != @binary 171 @binary = newmode 172 send_type_command if @logged_in 173 end 174 end 175 176 # Sends a command to destination host, with the current binary sendmode 177 # type. 178 # 179 # If binary mode is +true+, then "TYPE I" (image) is sent, otherwise "TYPE 180 # A" (ascii) is sent. 181 def send_type_command # :nodoc: 182 if @binary 183 voidcmd("TYPE I") 184 else 185 voidcmd("TYPE A") 186 end 187 end 188 private :send_type_command 189 190 # Toggles transfers in binary mode and yields to a block. 191 # This preserves your current binary send mode, but allows a temporary 192 # transaction with binary sendmode of +newmode+. 193 # 194 # +newmode+ is either +true+ or +false+ 195 def with_binary(newmode) # :nodoc: 196 oldmode = binary 197 self.binary = newmode 198 begin 199 yield 200 ensure 201 self.binary = oldmode 202 end 203 end 204 private :with_binary 205 206 # Obsolete 207 def return_code # :nodoc: 208 $stderr.puts("warning: Net::FTP#return_code is obsolete and do nothing") 209 return "\n" 210 end 211 212 # Obsolete 213 def return_code=(s) # :nodoc: 214 $stderr.puts("warning: Net::FTP#return_code= is obsolete and do nothing") 215 end 216 217 # Contructs a socket with +host+ and +port+. 218 # 219 # If SOCKSSocket is defined and the environment (ENV) defines 220 # SOCKS_SERVER, then a SOCKSSocket is returned, else a TCPSocket is 221 # returned. 222 def open_socket(host, port) # :nodoc: 223 return Timeout.timeout(@open_timeout, Net::OpenTimeout) { 224 if defined? SOCKSSocket and ENV["SOCKS_SERVER"] 225 @passive = true 226 sock = SOCKSSocket.open(host, port) 227 else 228 sock = TCPSocket.open(host, port) 229 end 230 io = BufferedSocket.new(sock) 231 io.read_timeout = @read_timeout 232 io 233 } 234 end 235 private :open_socket 236 237 # 238 # Establishes an FTP connection to host, optionally overriding the default 239 # port. If the environment variable +SOCKS_SERVER+ is set, sets up the 240 # connection through a SOCKS proxy. Raises an exception (typically 241 # <tt>Errno::ECONNREFUSED</tt>) if the connection cannot be established. 242 # 243 def connect(host, port = FTP_PORT) 244 if @debug_mode 245 print "connect: ", host, ", ", port, "\n" 246 end 247 synchronize do 248 @sock = open_socket(host, port) 249 voidresp 250 end 251 end 252 253 # 254 # WRITEME or make private 255 # 256 def set_socket(sock, get_greeting = true) 257 synchronize do 258 @sock = sock 259 if get_greeting 260 voidresp 261 end 262 end 263 end 264 265 # If string +s+ includes the PASS command (password), then the contents of 266 # the password are cleaned from the string using "*" 267 def sanitize(s) # :nodoc: 268 if s =~ /^PASS /i 269 return s[0, 5] + "*" * (s.length - 5) 270 else 271 return s 272 end 273 end 274 private :sanitize 275 276 # Ensures that +line+ has a control return / line feed (CRLF) and writes 277 # it to the socket. 278 def putline(line) # :nodoc: 279 if @debug_mode 280 print "put: ", sanitize(line), "\n" 281 end 282 line = line + CRLF 283 @sock.write(line) 284 end 285 private :putline 286 287 # Reads a line from the sock. If EOF, then it will raise EOFError 288 def getline # :nodoc: 289 line = @sock.readline # if get EOF, raise EOFError 290 line.sub!(/(\r\n|\n|\r)\z/n, "") 291 if @debug_mode 292 print "get: ", sanitize(line), "\n" 293 end 294 return line 295 end 296 private :getline 297 298 # Receive a section of lines until the response code's match. 299 def getmultiline # :nodoc: 300 line = getline 301 buff = line 302 if line[3] == ?- 303 code = line[0, 3] 304 begin 305 line = getline 306 buff << "\n" << line 307 end until line[0, 3] == code and line[3] != ?- 308 end 309 return buff << "\n" 310 end 311 private :getmultiline 312 313 # Recieves a response from the destination host. 314 # 315 # Returns the response code or raises FTPTempError, FTPPermError, or 316 # FTPProtoError 317 def getresp # :nodoc: 318 @last_response = getmultiline 319 @last_response_code = @last_response[0, 3] 320 case @last_response_code 321 when /\A[123]/ 322 return @last_response 323 when /\A4/ 324 raise FTPTempError, @last_response 325 when /\A5/ 326 raise FTPPermError, @last_response 327 else 328 raise FTPProtoError, @last_response 329 end 330 end 331 private :getresp 332 333 # Recieves a response. 334 # 335 # Raises FTPReplyError if the first position of the response code is not 336 # equal 2. 337 def voidresp # :nodoc: 338 resp = getresp 339 if resp[0] != ?2 340 raise FTPReplyError, resp 341 end 342 end 343 private :voidresp 344 345 # 346 # Sends a command and returns the response. 347 # 348 def sendcmd(cmd) 349 synchronize do 350 putline(cmd) 351 return getresp 352 end 353 end 354 355 # 356 # Sends a command and expect a response beginning with '2'. 357 # 358 def voidcmd(cmd) 359 synchronize do 360 putline(cmd) 361 voidresp 362 end 363 end 364 365 # Constructs and send the appropriate PORT (or EPRT) command 366 def sendport(host, port) # :nodoc: 367 af = (@sock.peeraddr)[0] 368 if af == "AF_INET" 369 cmd = "PORT " + (host.split(".") + port.divmod(256)).join(",") 370 elsif af == "AF_INET6" 371 cmd = sprintf("EPRT |2|%s|%d|", host, port) 372 else 373 raise FTPProtoError, host 374 end 375 voidcmd(cmd) 376 end 377 private :sendport 378 379 # Constructs a TCPServer socket, and sends it the PORT command 380 # 381 # Returns the constructed TCPServer socket 382 def makeport # :nodoc: 383 sock = TCPServer.open(@sock.addr[3], 0) 384 port = sock.addr[1] 385 host = sock.addr[3] 386 sendport(host, port) 387 return sock 388 end 389 private :makeport 390 391 # sends the appropriate command to enable a passive connection 392 def makepasv # :nodoc: 393 if @sock.peeraddr[0] == "AF_INET" 394 host, port = parse227(sendcmd("PASV")) 395 else 396 host, port = parse229(sendcmd("EPSV")) 397 # host, port = parse228(sendcmd("LPSV")) 398 end 399 return host, port 400 end 401 private :makepasv 402 403 # Constructs a connection for transferring data 404 def transfercmd(cmd, rest_offset = nil) # :nodoc: 405 if @passive 406 host, port = makepasv 407 conn = open_socket(host, port) 408 if @resume and rest_offset 409 resp = sendcmd("REST " + rest_offset.to_s) 410 if resp[0] != ?3 411 raise FTPReplyError, resp 412 end 413 end 414 resp = sendcmd(cmd) 415 # skip 2XX for some ftp servers 416 resp = getresp if resp[0] == ?2 417 if resp[0] != ?1 418 raise FTPReplyError, resp 419 end 420 else 421 sock = makeport 422 if @resume and rest_offset 423 resp = sendcmd("REST " + rest_offset.to_s) 424 if resp[0] != ?3 425 raise FTPReplyError, resp 426 end 427 end 428 resp = sendcmd(cmd) 429 # skip 2XX for some ftp servers 430 resp = getresp if resp[0] == ?2 431 if resp[0] != ?1 432 raise FTPReplyError, resp 433 end 434 conn = BufferedSocket.new(sock.accept) 435 conn.read_timeout = @read_timeout 436 sock.shutdown(Socket::SHUT_WR) rescue nil 437 sock.read rescue nil 438 sock.close 439 end 440 return conn 441 end 442 private :transfercmd 443 444 # 445 # Logs in to the remote host. The session must have been previously 446 # connected. If +user+ is the string "anonymous" and the +password+ is 447 # +nil+, a password of <tt>user@host</tt> is synthesized. If the +acct+ 448 # parameter is not +nil+, an FTP ACCT command is sent following the 449 # successful login. Raises an exception on error (typically 450 # <tt>Net::FTPPermError</tt>). 451 # 452 def login(user = "anonymous", passwd = nil, acct = nil) 453 if user == "anonymous" and passwd == nil 454 passwd = "anonymous@" 455 end 456 457 resp = "" 458 synchronize do 459 resp = sendcmd('USER ' + user) 460 if resp[0] == ?3 461 raise FTPReplyError, resp if passwd.nil? 462 resp = sendcmd('PASS ' + passwd) 463 end 464 if resp[0] == ?3 465 raise FTPReplyError, resp if acct.nil? 466 resp = sendcmd('ACCT ' + acct) 467 end 468 end 469 if resp[0] != ?2 470 raise FTPReplyError, resp 471 end 472 @welcome = resp 473 send_type_command 474 @logged_in = true 475 end 476 477 # 478 # Puts the connection into binary (image) mode, issues the given command, 479 # and fetches the data returned, passing it to the associated block in 480 # chunks of +blocksize+ characters. Note that +cmd+ is a server command 481 # (such as "RETR myfile"). 482 # 483 def retrbinary(cmd, blocksize, rest_offset = nil) # :yield: data 484 synchronize do 485 with_binary(true) do 486 begin 487 conn = transfercmd(cmd, rest_offset) 488 loop do 489 data = conn.read(blocksize) 490 break if data == nil 491 yield(data) 492 end 493 conn.shutdown(Socket::SHUT_WR) 494 conn.read_timeout = 1 495 conn.read 496 ensure 497 conn.close if conn 498 end 499 voidresp 500 end 501 end 502 end 503 504 # 505 # Puts the connection into ASCII (text) mode, issues the given command, and 506 # passes the resulting data, one line at a time, to the associated block. If 507 # no block is given, prints the lines. Note that +cmd+ is a server command 508 # (such as "RETR myfile"). 509 # 510 def retrlines(cmd) # :yield: line 511 synchronize do 512 with_binary(false) do 513 begin 514 conn = transfercmd(cmd) 515 loop do 516 line = conn.gets 517 break if line == nil 518 yield(line.sub(/\r?\n\z/, ""), !line.match(/\n\z/).nil?) 519 end 520 conn.shutdown(Socket::SHUT_WR) 521 conn.read_timeout = 1 522 conn.read 523 ensure 524 conn.close if conn 525 end 526 voidresp 527 end 528 end 529 end 530 531 # 532 # Puts the connection into binary (image) mode, issues the given server-side 533 # command (such as "STOR myfile"), and sends the contents of the file named 534 # +file+ to the server. If the optional block is given, it also passes it 535 # the data, in chunks of +blocksize+ characters. 536 # 537 def storbinary(cmd, file, blocksize, rest_offset = nil) # :yield: data 538 if rest_offset 539 file.seek(rest_offset, IO::SEEK_SET) 540 end 541 synchronize do 542 with_binary(true) do 543 conn = transfercmd(cmd) 544 loop do 545 buf = file.read(blocksize) 546 break if buf == nil 547 conn.write(buf) 548 yield(buf) if block_given? 549 end 550 conn.close 551 voidresp 552 end 553 end 554 rescue Errno::EPIPE 555 # EPIPE, in this case, means that the data connection was unexpectedly 556 # terminated. Rather than just raising EPIPE to the caller, check the 557 # response on the control connection. If getresp doesn't raise a more 558 # appropriate exception, re-raise the original exception. 559 getresp 560 raise 561 end 562 563 # 564 # Puts the connection into ASCII (text) mode, issues the given server-side 565 # command (such as "STOR myfile"), and sends the contents of the file 566 # named +file+ to the server, one line at a time. If the optional block is 567 # given, it also passes it the lines. 568 # 569 def storlines(cmd, file) # :yield: line 570 synchronize do 571 with_binary(false) do 572 conn = transfercmd(cmd) 573 loop do 574 buf = file.gets 575 break if buf == nil 576 if buf[-2, 2] != CRLF 577 buf = buf.chomp + CRLF 578 end 579 conn.write(buf) 580 yield(buf) if block_given? 581 end 582 conn.close 583 voidresp 584 end 585 end 586 rescue Errno::EPIPE 587 # EPIPE, in this case, means that the data connection was unexpectedly 588 # terminated. Rather than just raising EPIPE to the caller, check the 589 # response on the control connection. If getresp doesn't raise a more 590 # appropriate exception, re-raise the original exception. 591 getresp 592 raise 593 end 594 595 # 596 # Retrieves +remotefile+ in binary mode, storing the result in +localfile+. 597 # If +localfile+ is nil, returns retrieved data. 598 # If a block is supplied, it is passed the retrieved data in +blocksize+ 599 # chunks. 600 # 601 def getbinaryfile(remotefile, localfile = File.basename(remotefile), 602 blocksize = DEFAULT_BLOCKSIZE) # :yield: data 603 result = nil 604 if localfile 605 if @resume 606 rest_offset = File.size?(localfile) 607 f = open(localfile, "a") 608 else 609 rest_offset = nil 610 f = open(localfile, "w") 611 end 612 elsif !block_given? 613 result = "" 614 end 615 begin 616 f.binmode if localfile 617 retrbinary("RETR " + remotefile.to_s, blocksize, rest_offset) do |data| 618 f.write(data) if localfile 619 yield(data) if block_given? 620 result.concat(data) if result 621 end 622 return result 623 ensure 624 f.close if localfile 625 end 626 end 627 628 # 629 # Retrieves +remotefile+ in ASCII (text) mode, storing the result in 630 # +localfile+. 631 # If +localfile+ is nil, returns retrieved data. 632 # If a block is supplied, it is passed the retrieved data one 633 # line at a time. 634 # 635 def gettextfile(remotefile, localfile = File.basename(remotefile)) # :yield: line 636 result = nil 637 if localfile 638 f = open(localfile, "w") 639 elsif !block_given? 640 result = "" 641 end 642 begin 643 retrlines("RETR " + remotefile) do |line, newline| 644 l = newline ? line + "\n" : line 645 f.print(l) if localfile 646 yield(line, newline) if block_given? 647 result.concat(l) if result 648 end 649 return result 650 ensure 651 f.close if localfile 652 end 653 end 654 655 # 656 # Retrieves +remotefile+ in whatever mode the session is set (text or 657 # binary). See #gettextfile and #getbinaryfile. 658 # 659 def get(remotefile, localfile = File.basename(remotefile), 660 blocksize = DEFAULT_BLOCKSIZE, &block) # :yield: data 661 if @binary 662 getbinaryfile(remotefile, localfile, blocksize, &block) 663 else 664 gettextfile(remotefile, localfile, &block) 665 end 666 end 667 668 # 669 # Transfers +localfile+ to the server in binary mode, storing the result in 670 # +remotefile+. If a block is supplied, calls it, passing in the transmitted 671 # data in +blocksize+ chunks. 672 # 673 def putbinaryfile(localfile, remotefile = File.basename(localfile), 674 blocksize = DEFAULT_BLOCKSIZE, &block) # :yield: data 675 if @resume 676 begin 677 rest_offset = size(remotefile) 678 rescue Net::FTPPermError 679 rest_offset = nil 680 end 681 else 682 rest_offset = nil 683 end 684 f = open(localfile) 685 begin 686 f.binmode 687 if rest_offset 688 storbinary("APPE " + remotefile, f, blocksize, rest_offset, &block) 689 else 690 storbinary("STOR " + remotefile, f, blocksize, rest_offset, &block) 691 end 692 ensure 693 f.close 694 end 695 end 696 697 # 698 # Transfers +localfile+ to the server in ASCII (text) mode, storing the result 699 # in +remotefile+. If callback or an associated block is supplied, calls it, 700 # passing in the transmitted data one line at a time. 701 # 702 def puttextfile(localfile, remotefile = File.basename(localfile), &block) # :yield: line 703 f = open(localfile) 704 begin 705 storlines("STOR " + remotefile, f, &block) 706 ensure 707 f.close 708 end 709 end 710 711 # 712 # Transfers +localfile+ to the server in whatever mode the session is set 713 # (text or binary). See #puttextfile and #putbinaryfile. 714 # 715 def put(localfile, remotefile = File.basename(localfile), 716 blocksize = DEFAULT_BLOCKSIZE, &block) 717 if @binary 718 putbinaryfile(localfile, remotefile, blocksize, &block) 719 else 720 puttextfile(localfile, remotefile, &block) 721 end 722 end 723 724 # 725 # Sends the ACCT command. 726 # 727 # This is a less common FTP command, to send account 728 # information if the destination host requires it. 729 # 730 def acct(account) 731 cmd = "ACCT " + account 732 voidcmd(cmd) 733 end 734 735 # 736 # Returns an array of filenames in the remote directory. 737 # 738 def nlst(dir = nil) 739 cmd = "NLST" 740 if dir 741 cmd = cmd + " " + dir 742 end 743 files = [] 744 retrlines(cmd) do |line| 745 files.push(line) 746 end 747 return files 748 end 749 750 # 751 # Returns an array of file information in the directory (the output is like 752 # `ls -l`). If a block is given, it iterates through the listing. 753 # 754 def list(*args, &block) # :yield: line 755 cmd = "LIST" 756 args.each do |arg| 757 cmd = cmd + " " + arg.to_s 758 end 759 if block 760 retrlines(cmd, &block) 761 else 762 lines = [] 763 retrlines(cmd) do |line| 764 lines << line 765 end 766 return lines 767 end 768 end 769 alias ls list 770 alias dir list 771 772 # 773 # Renames a file on the server. 774 # 775 def rename(fromname, toname) 776 resp = sendcmd("RNFR " + fromname) 777 if resp[0] != ?3 778 raise FTPReplyError, resp 779 end 780 voidcmd("RNTO " + toname) 781 end 782 783 # 784 # Deletes a file on the server. 785 # 786 def delete(filename) 787 resp = sendcmd("DELE " + filename) 788 if resp[0, 3] == "250" 789 return 790 elsif resp[0] == ?5 791 raise FTPPermError, resp 792 else 793 raise FTPReplyError, resp 794 end 795 end 796 797 # 798 # Changes the (remote) directory. 799 # 800 def chdir(dirname) 801 if dirname == ".." 802 begin 803 voidcmd("CDUP") 804 return 805 rescue FTPPermError => e 806 if e.message[0, 3] != "500" 807 raise e 808 end 809 end 810 end 811 cmd = "CWD " + dirname 812 voidcmd(cmd) 813 end 814 815 # 816 # Returns the size of the given (remote) filename. 817 # 818 def size(filename) 819 with_binary(true) do 820 resp = sendcmd("SIZE " + filename) 821 if resp[0, 3] != "213" 822 raise FTPReplyError, resp 823 end 824 return resp[3..-1].strip.to_i 825 end 826 end 827 828 MDTM_REGEXP = /^(\d\d\d\d)(\d\d)(\d\d)(\d\d)(\d\d)(\d\d)/ # :nodoc: 829 830 # 831 # Returns the last modification time of the (remote) file. If +local+ is 832 # +true+, it is returned as a local time, otherwise it's a UTC time. 833 # 834 def mtime(filename, local = false) 835 str = mdtm(filename) 836 ary = str.scan(MDTM_REGEXP)[0].collect {|i| i.to_i} 837 return local ? Time.local(*ary) : Time.gm(*ary) 838 end 839 840 # 841 # Creates a remote directory. 842 # 843 def mkdir(dirname) 844 resp = sendcmd("MKD " + dirname) 845 return parse257(resp) 846 end 847 848 # 849 # Removes a remote directory. 850 # 851 def rmdir(dirname) 852 voidcmd("RMD " + dirname) 853 end 854 855 # 856 # Returns the current remote directory. 857 # 858 def pwd 859 resp = sendcmd("PWD") 860 return parse257(resp) 861 end 862 alias getdir pwd 863 864 # 865 # Returns system information. 866 # 867 def system 868 resp = sendcmd("SYST") 869 if resp[0, 3] != "215" 870 raise FTPReplyError, resp 871 end 872 return resp[4 .. -1] 873 end 874 875 # 876 # Aborts the previous command (ABOR command). 877 # 878 def abort 879 line = "ABOR" + CRLF 880 print "put: ABOR\n" if @debug_mode 881 @sock.send(line, Socket::MSG_OOB) 882 resp = getmultiline 883 unless ["426", "226", "225"].include?(resp[0, 3]) 884 raise FTPProtoError, resp 885 end 886 return resp 887 end 888 889 # 890 # Returns the status (STAT command). 891 # 892 def status 893 line = "STAT" + CRLF 894 print "put: STAT\n" if @debug_mode 895 @sock.send(line, Socket::MSG_OOB) 896 return getresp 897 end 898 899 # 900 # Issues the MDTM command. TODO: more info. 901 # 902 def mdtm(filename) 903 resp = sendcmd("MDTM " + filename) 904 if resp[0, 3] == "213" 905 return resp[3 .. -1].strip 906 end 907 end 908 909 # 910 # Issues the HELP command. 911 # 912 def help(arg = nil) 913 cmd = "HELP" 914 if arg 915 cmd = cmd + " " + arg 916 end 917 sendcmd(cmd) 918 end 919 920 # 921 # Exits the FTP session. 922 # 923 def quit 924 voidcmd("QUIT") 925 end 926 927 # 928 # Issues a NOOP command. 929 # 930 # Does nothing except return a response. 931 # 932 def noop 933 voidcmd("NOOP") 934 end 935 936 # 937 # Issues a SITE command. 938 # 939 def site(arg) 940 cmd = "SITE " + arg 941 voidcmd(cmd) 942 end 943 944 # 945 # Closes the connection. Further operations are impossible until you open 946 # a new connection with #connect. 947 # 948 def close 949 if @sock and not @sock.closed? 950 begin 951 @sock.shutdown(Socket::SHUT_WR) rescue nil 952 orig, self.read_timeout = self.read_timeout, 3 953 @sock.read rescue nil 954 ensure 955 @sock.close 956 self.read_timeout = orig 957 end 958 end 959 end 960 961 # 962 # Returns +true+ iff the connection is closed. 963 # 964 def closed? 965 @sock == nil or @sock.closed? 966 end 967 968 # handler for response code 227 969 # (Entering Passive Mode (h1,h2,h3,h4,p1,p2)) 970 # 971 # Returns host and port. 972 def parse227(resp) # :nodoc: 973 if resp[0, 3] != "227" 974 raise FTPReplyError, resp 975 end 976 if m = /\((?<host>\d+(,\d+){3}),(?<port>\d+,\d+)\)/.match(resp) 977 return parse_pasv_ipv4_host(m["host"]), parse_pasv_port(m["port"]) 978 else 979 raise FTPProtoError, resp 980 end 981 end 982 private :parse227 983 984 # handler for response code 228 985 # (Entering Long Passive Mode) 986 # 987 # Returns host and port. 988 def parse228(resp) # :nodoc: 989 if resp[0, 3] != "228" 990 raise FTPReplyError, resp 991 end 992 if m = /\(4,4,(?<host>\d+(,\d+){3}),2,(?<port>\d+,\d+)\)/.match(resp) 993 return parse_pasv_ipv4_host(m["host"]), parse_pasv_port(m["port"]) 994 elsif m = /\(6,16,(?<host>\d+(,(\d+)){15}),2,(?<port>\d+,\d+)\)/.match(resp) 995 return parse_pasv_ipv6_host(m["host"]), parse_pasv_port(m["port"]) 996 else 997 raise FTPProtoError, resp 998 end 999 return host, port 1000 end 1001 private :parse228 1002 1003 def parse_pasv_ipv4_host(s) 1004 return s.tr(",", ".") 1005 end 1006 private :parse_pasv_ipv4_host 1007 1008 def parse_pasv_ipv6_host(s) 1009 return s.split(/,/).map { |i| 1010 "%02x" % i.to_i 1011 }.each_slice(2).map(&:join).join(":") 1012 end 1013 private :parse_pasv_ipv6_host 1014 1015 def parse_pasv_port(s) 1016 return s.split(/,/).map(&:to_i).inject { |x, y| 1017 (x << 8) + y 1018 } 1019 end 1020 private :parse_pasv_port 1021 1022 # handler for response code 229 1023 # (Extended Passive Mode Entered) 1024 # 1025 # Returns host and port. 1026 def parse229(resp) # :nodoc: 1027 if resp[0, 3] != "229" 1028 raise FTPReplyError, resp 1029 end 1030 if m = /\((?<d>[!-~])\k<d>\k<d>(?<port>\d+)\k<d>\)/.match(resp) 1031 return @sock.peeraddr[3], m["port"].to_i 1032 else 1033 raise FTPProtoError, resp 1034 end 1035 end 1036 private :parse229 1037 1038 # handler for response code 257 1039 # ("PATHNAME" created) 1040 # 1041 # Returns host and port. 1042 def parse257(resp) # :nodoc: 1043 if resp[0, 3] != "257" 1044 raise FTPReplyError, resp 1045 end 1046 if resp[3, 2] != ' "' 1047 return "" 1048 end 1049 dirname = "" 1050 i = 5 1051 n = resp.length 1052 while i < n 1053 c = resp[i, 1] 1054 i = i + 1 1055 if c == '"' 1056 if i > n or resp[i, 1] != '"' 1057 break 1058 end 1059 i = i + 1 1060 end 1061 dirname = dirname + c 1062 end 1063 return dirname 1064 end 1065 private :parse257 1066 1067 # :stopdoc: 1068 class NullSocket 1069 def read_timeout=(sec) 1070 end 1071 1072 def close 1073 end 1074 1075 def method_missing(mid, *args) 1076 raise FTPConnectionError, "not connected" 1077 end 1078 end 1079 1080 class BufferedSocket < BufferedIO 1081 [:addr, :peeraddr, :send, :shutdown].each do |method| 1082 define_method(method) { |*args| 1083 @io.__send__(method, *args) 1084 } 1085 end 1086 1087 def read(len = nil) 1088 if len 1089 s = super(len, "", true) 1090 return s.empty? ? nil : s 1091 else 1092 result = "" 1093 while s = super(DEFAULT_BLOCKSIZE, "", true) 1094 break if s.empty? 1095 result << s 1096 end 1097 return result 1098 end 1099 end 1100 1101 def gets 1102 return readuntil("\n") 1103 rescue EOFError 1104 return nil 1105 end 1106 1107 def readline 1108 return readuntil("\n") 1109 end 1110 end 1111 # :startdoc: 1112 end 1113end 1114 1115 1116# Documentation comments: 1117# - sourced from pickaxe and nutshell, with improvements (hopefully) 1118# - three methods should be private (search WRITEME) 1119# - two methods need more information (search TODO) 1120