File: net/ftp.rb

Overview
Module Structure
Class Hierarchy
Code

Overview

Module Structure

  module: <Toplevel Module>
  module: Net#20
  class: FTPError#23
inherits from
  StandardError ( Builtin-Module )
  class: FTPReplyError#24
inherits from
  FTPError ( Net )
  class: FTPTempError#25
inherits from
  FTPError ( Net )
  class: FTPPermError#26
inherits from
  FTPError ( Net )
  class: FTPProtoError#27
inherits from
  FTPError ( Net )
  class: FTP#72
includes
  MonitorMixin   
inherits from
  Object ( Builtin-Module )
has properties
constant: FTP_PORT #76
constant: CRLF #77
constant: DEFAULT_BLOCKSIZE #78
attribute: binary [RW] #82
attribute: passive [RW] #85
attribute: debug_mode [RW] #89
attribute: resume [RW] #93
attribute: welcome [R] #96
attribute: last_response_code [R] #99
alias: lastresp last_response_code #100
attribute: last_response [R] #103
class method: open / 4 #111
method: initialize / 4 #129
method: return_code #144
method: return_code= / 1 #150
method: open_socket / 2 #154
method: connect / 2 #170
method: set_socket / 2 #183
method: sanitize / 1 #192
method: putline / 1 #201
method: getline #210
method: getmultiline #220
method: getresp #234
method: voidresp #250
method: sendcmd / 1 #261
method: voidcmd / 1 #271
method: sendport / 2 #278
method: makeport #291
method: makepasv #300
method: transfercmd / 2 #311
method: getaddress #348
method: login / 3 #372
method: retrbinary / 3 #401
method: retrlines / 1 #421
method: storbinary / 5 #446
method: storlines / 3 #470
method: getbinaryfile / 2 #493
method: gettextfile / 2 #518
method: get / 2 #534
method: putbinaryfile / 2 #548
method: puttextfile / 2 #573
method: put / 2 #586
method: acct / 1 #598
method: nlst / 1 #606
method: list / 2 #622
alias: ls list #637
alias: dir list #638
method: rename / 2 #643
method: delete / 1 #654
method: chdir / 1 #668
method: size / 1 #686
constant: MDTM_REGEXP #695
method: mtime / 2 #701
method: mkdir / 1 #710
method: rmdir / 1 #718
method: pwd #725
alias: getdir pwd #729
method: system #734
method: abort #745
method: status #759
method: mdtm / 1 #769
method: help / 1 #779
method: quit #790
method: noop #797
method: site / 1 #804
method: close #813
method: closed? #820
method: parse227 / 1 #824
method: parse228 / 1 #843
method: parse229 / 1 #875
method: parse257 / 1 #894

Class Hierarchy

Object ( Builtin-Module )
Exception ( Builtin-Module )
StandardError ( Builtin-Module )
FTPError ( Net ) — #23
  FTPReplyError    #24
  FTPTempError    #25
  FTPPermError    #26
  FTPProtoError    #27
FTP ( Net ) — #72

Code

   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 
  17  require "socket"
  18  require "monitor"
  19 
  20  module Net
  21 
  22    # :stopdoc:
  23    class FTPError < StandardError; end
  24    class FTPReplyError < FTPError; end
  25    class FTPTempError < FTPError; end 
  26    class FTPPermError < FTPError; end 
  27    class FTPProtoError < FTPError; end
  28    # :startdoc:
  29 
  30    #
  31    # This class implements the File Transfer Protocol.  If you have used a
  32    # command-line FTP program, and are familiar with the commands, you will be
  33    # able to use this class easily.  Some extra features are included to take
  34    # advantage of Ruby's style and strengths.
  35    #
  36    # == Example
  37    # 
  38    #   require 'net/ftp'
  39    #
  40    # === Example 1
  41    #  
  42    #   ftp = Net::FTP.new('ftp.netlab.co.jp')
  43    #   ftp.login
  44    #   files = ftp.chdir('pub/lang/ruby/contrib')
  45    #   files = ftp.list('n*')
  46    #   ftp.getbinaryfile('nif.rb-0.91.gz', 'nif.gz', 1024)
  47    #   ftp.close
  48    #
  49    # === Example 2
  50    #
  51    #   Net::FTP.open('ftp.netlab.co.jp') do |ftp|
  52    #     ftp.login
  53    #     files = ftp.chdir('pub/lang/ruby/contrib')
  54    #     files = ftp.list('n*')
  55    #     ftp.getbinaryfile('nif.rb-0.91.gz', 'nif.gz', 1024)
  56    #   end
  57    #
  58    # == Major Methods
  59    #
  60    # The following are the methods most likely to be useful to users:
  61    # - FTP.open
  62    # - #getbinaryfile
  63    # - #gettextfile
  64    # - #putbinaryfile
  65    # - #puttextfile
  66    # - #chdir
  67    # - #nlst
  68    # - #size
  69    # - #rename
  70    # - #delete
  71    #
  72    class FTP
  73      include MonitorMixin
  74      
  75      # :stopdoc:
  76      FTP_PORT = 21
  77      CRLF = "\r\n"
  78      DEFAULT_BLOCKSIZE = 4096
  79      # :startdoc:
  80      
  81      # When +true+, transfers are performed in binary mode.  Default: +true+.
  82      attr_accessor :binary
  83 
  84      # When +true+, the connection is in passive mode.  Default: +false+.
  85      attr_accessor :passive
  86 
  87      # When +true+, all traffic to and from the server is written
  88      # to +$stdout+.  Default: +false+.
  89      attr_accessor :debug_mode
  90 
  91      # Sets or retrieves the +resume+ status, which decides whether incomplete
  92      # transfers are resumed or restarted.  Default: +false+.
  93      attr_accessor :resume
  94 
  95      # The server's welcome message.
  96      attr_reader :welcome
  97 
  98      # The server's last response code.
  99      attr_reader :last_response_code
 100      alias lastresp last_response_code
 101 
 102      # The server's last response.
 103      attr_reader :last_response
 104      
 105      #
 106      # A synonym for <tt>FTP.new</tt>, but with a mandatory host parameter.
 107      #
 108      # If a block is given, it is passed the +FTP+ object, which will be closed
 109      # when the block finishes, or when an exception is raised.
 110      #
 111      def FTP.open(host, user = nil, passwd = nil, acct = nil)
 112        if block_given?
 113          ftp = new(host, user, passwd, acct)
 114          begin
 115            yield ftp
 116          ensure
 117            ftp.close
 118          end
 119        else
 120          new(host, user, passwd, acct)
 121        end
 122      end
 123      
 124      #
 125      # Creates and returns a new +FTP+ object. If a +host+ is given, a connection
 126      # is made. Additionally, if the +user+ is given, the given user name,
 127      # password, and (optionally) account are used to log in.  See #login.
 128      #
 129      def initialize(host = nil, user = nil, passwd = nil, acct = nil)
 130        super()
 131        @binary = true
 132        @passive = false
 133        @debug_mode = false
 134        @resume = false
 135        if host
 136          connect(host)
 137          if user
 138            login(user, passwd, acct)
 139          end
 140        end
 141      end
 142 
 143      # Obsolete
 144      def return_code
 145        $stderr.puts("warning: Net::FTP#return_code is obsolete and do nothing")
 146        return "\n"
 147      end
 148 
 149      # Obsolete
 150      def return_code=(s)
 151        $stderr.puts("warning: Net::FTP#return_code= is obsolete and do nothing")
 152      end
 153 
 154      def open_socket(host, port)
 155        if defined? SOCKSSocket and ENV["SOCKS_SERVER"]
 156          @passive = true
 157          return SOCKSSocket.open(host, port)
 158        else
 159          return TCPSocket.open(host, port)
 160        end
 161      end
 162      private :open_socket
 163      
 164      #
 165      # Establishes an FTP connection to host, optionally overriding the default
 166      # port. If the environment variable +SOCKS_SERVER+ is set, sets up the
 167      # connection through a SOCKS proxy. Raises an exception (typically
 168      # <tt>Errno::ECONNREFUSED</tt>) if the connection cannot be established.
 169      #
 170      def connect(host, port = FTP_PORT)
 171        if @debug_mode
 172          print "connect: ", host, ", ", port, "\n"
 173        end
 174        synchronize do
 175          @sock = open_socket(host, port)
 176          voidresp
 177        end
 178      end
 179 
 180      #
 181      # WRITEME or make private
 182      #
 183      def set_socket(sock, get_greeting = true)
 184        synchronize do
 185          @sock = sock
 186          if get_greeting
 187            voidresp
 188          end
 189        end
 190      end
 191 
 192      def sanitize(s)
 193        if s =~ /^PASS /i
 194          return s[0, 5] + "*" * (s.length - 5)
 195        else
 196          return s
 197        end
 198      end
 199      private :sanitize
 200      
 201      def putline(line)
 202        if @debug_mode
 203          print "put: ", sanitize(line), "\n"
 204        end
 205        line = line + CRLF
 206        @sock.write(line)
 207      end
 208      private :putline
 209      
 210      def getline
 211        line = @sock.readline # if get EOF, raise EOFError
 212        line.sub!(/(\r\n|\n|\r)\z/n, "")
 213        if @debug_mode
 214          print "get: ", sanitize(line), "\n"
 215        end
 216        return line
 217      end
 218      private :getline
 219      
 220      def getmultiline
 221        line = getline
 222        buff = line
 223        if line[3] == ?-
 224            code = line[0, 3]
 225          begin
 226            line = getline
 227            buff << "\n" << line
 228          end until line[0, 3] == code and line[3] != ?-
 229        end
 230        return buff << "\n"
 231      end
 232      private :getmultiline
 233      
 234      def getresp
 235        @last_response = getmultiline
 236        @last_response_code = @last_response[0, 3]
 237        case @last_response_code
 238        when /\A[123]/
 239          return @last_response
 240        when /\A4/
 241          raise FTPTempError, @last_response
 242        when /\A5/
 243          raise FTPPermError, @last_response
 244        else
 245          raise FTPProtoError, @last_response
 246        end
 247      end
 248      private :getresp
 249      
 250      def voidresp
 251        resp = getresp
 252        if resp[0] != ?2
 253          raise FTPReplyError, resp
 254        end
 255      end
 256      private :voidresp
 257      
 258      #
 259      # Sends a command and returns the response.
 260      #
 261      def sendcmd(cmd)
 262        synchronize do
 263          putline(cmd)
 264          return getresp
 265        end
 266      end
 267      
 268      #
 269      # Sends a command and expect a response beginning with '2'.
 270      #
 271      def voidcmd(cmd)
 272        synchronize do
 273          putline(cmd)
 274          voidresp
 275        end
 276      end
 277      
 278      def sendport(host, port)
 279        af = (@sock.peeraddr)[0]
 280        if af == "AF_INET"
 281          cmd = "PORT " + (host.split(".") + port.divmod(256)).join(",")
 282        elsif af == "AF_INET6"
 283          cmd = sprintf("EPRT |2|%s|%d|", host, port)
 284        else
 285          raise FTPProtoError, host
 286        end
 287        voidcmd(cmd)
 288      end
 289      private :sendport
 290      
 291      def makeport
 292        sock = TCPServer.open(@sock.addr[3], 0)
 293        port = sock.addr[1]
 294        host = sock.addr[3]
 295        resp = sendport(host, port)
 296        return sock
 297      end
 298      private :makeport
 299      
 300      def makepasv
 301        if @sock.peeraddr[0] == "AF_INET"
 302          host, port = parse227(sendcmd("PASV"))
 303        else
 304          host, port = parse229(sendcmd("EPSV"))
 305          #     host, port = parse228(sendcmd("LPSV"))
 306        end
 307        return host, port
 308      end
 309      private :makepasv
 310      
 311      def transfercmd(cmd, rest_offset = nil)
 312        if @passive
 313          host, port = makepasv
 314          conn = open_socket(host, port)
 315          if @resume and rest_offset
 316            resp = sendcmd("REST " + rest_offset.to_s) 
 317            if resp[0] != ?3
 318              raise FTPReplyError, resp
 319            end
 320          end
 321          resp = sendcmd(cmd)
 322          # skip 2XX for some ftp servers
 323          resp = getresp if resp[0] == ?2
 324          if resp[0] != ?1
 325            raise FTPReplyError, resp
 326          end
 327        else
 328          sock = makeport
 329          if @resume and rest_offset
 330            resp = sendcmd("REST " + rest_offset.to_s) 
 331            if resp[0] != ?3
 332              raise FTPReplyError, resp
 333            end
 334          end
 335          resp = sendcmd(cmd)
 336          # skip 2XX for some ftp servers
 337          resp = getresp if resp[0] == ?2
 338          if resp[0] != ?1
 339            raise FTPReplyError, resp
 340          end
 341          conn = sock.accept
 342          sock.close
 343        end
 344        return conn
 345      end
 346      private :transfercmd
 347      
 348      def getaddress
 349        thishost = Socket.gethostname rescue ""
 350        if not thishost.index(".")
 351          thishost = Socket.gethostbyname(thishost)[0] rescue ""
 352        end
 353        if ENV.has_key?("LOGNAME")
 354          realuser = ENV["LOGNAME"]
 355        elsif ENV.has_key?("USER")
 356          realuser = ENV["USER"]
 357        else
 358          realuser = "anonymous"
 359        end
 360        return realuser + "@" + thishost
 361      end
 362      private :getaddress
 363      
 364      #
 365      # Logs in to the remote host. The session must have been previously
 366      # connected.  If +user+ is the string "anonymous" and the +password+ is
 367      # +nil+, a password of <tt>user@host</tt> is synthesized. If the +acct+
 368      # parameter is not +nil+, an FTP ACCT command is sent following the
 369      # successful login.  Raises an exception on error (typically
 370      # <tt>Net::FTPPermError</tt>).
 371      #
 372      def login(user = "anonymous", passwd = nil, acct = nil)
 373        if user == "anonymous" and passwd == nil
 374          passwd = getaddress
 375        end
 376        
 377        resp = ""
 378        synchronize do
 379          resp = sendcmd('USER ' + user)
 380          if resp[0] == ?3
 381            raise FTPReplyError, resp if passwd.nil?
 382            resp = sendcmd('PASS ' + passwd)
 383          end
 384          if resp[0] == ?3
 385            raise FTPReplyError, resp if acct.nil?
 386            resp = sendcmd('ACCT ' + acct)
 387          end
 388        end
 389        if resp[0] != ?2
 390          raise FTPReplyError, resp
 391        end
 392        @welcome = resp
 393      end
 394      
 395      #
 396      # Puts the connection into binary (image) mode, issues the given command,
 397      # and fetches the data returned, passing it to the associated block in
 398      # chunks of +blocksize+ characters. Note that +cmd+ is a server command
 399      # (such as "RETR myfile").
 400      #
 401      def retrbinary(cmd, blocksize, rest_offset = nil) # :yield: data
 402        synchronize do
 403          voidcmd("TYPE I")
 404          conn = transfercmd(cmd, rest_offset)
 405          loop do
 406            data = conn.read(blocksize)
 407            break if data == nil
 408            yield(data)
 409          end
 410          conn.close
 411          voidresp
 412        end
 413      end
 414      
 415      #
 416      # Puts the connection into ASCII (text) mode, issues the given command, and
 417      # passes the resulting data, one line at a time, to the associated block. If
 418      # no block is given, prints the lines. Note that +cmd+ is a server command
 419      # (such as "RETR myfile").
 420      #
 421      def retrlines(cmd) # :yield: line
 422        synchronize do
 423          voidcmd("TYPE A")
 424          conn = transfercmd(cmd)
 425          loop do
 426            line = conn.gets
 427            break if line == nil
 428            if line[-2, 2] == CRLF
 429              line = line[0 .. -3]
 430            elsif line[-1] == ?\n
 431              line = line[0 .. -2]
 432            end
 433            yield(line)
 434          end
 435          conn.close
 436          voidresp
 437        end
 438      end
 439      
 440      #
 441      # Puts the connection into binary (image) mode, issues the given server-side
 442      # command (such as "STOR myfile"), and sends the contents of the file named
 443      # +file+ to the server. If the optional block is given, it also passes it
 444      # the data, in chunks of +blocksize+ characters.
 445      #
 446      def storbinary(cmd, file, blocksize, rest_offset = nil, &block) # :yield: data
 447        if rest_offset
 448          file.seek(rest_offset, IO::SEEK_SET)
 449        end
 450        synchronize do
 451          voidcmd("TYPE I")
 452          conn = transfercmd(cmd, rest_offset)
 453          loop do
 454            buf = file.read(blocksize)
 455            break if buf == nil
 456            conn.write(buf)
 457            yield(buf) if block
 458          end
 459          conn.close
 460          voidresp
 461        end
 462      end
 463      
 464      #
 465      # Puts the connection into ASCII (text) mode, issues the given server-side
 466      # command (such as "STOR myfile"), and sends the contents of the file
 467      # named +file+ to the server, one line at a time. If the optional block is
 468      # given, it also passes it the lines.
 469      #
 470      def storlines(cmd, file, &block) # :yield: line
 471        synchronize do
 472          voidcmd("TYPE A")
 473          conn = transfercmd(cmd)
 474          loop do
 475            buf = file.gets
 476            break if buf == nil
 477            if buf[-2, 2] != CRLF
 478              buf = buf.chomp + CRLF
 479            end
 480            conn.write(buf)
 481            yield(buf) if block
 482          end
 483          conn.close
 484          voidresp
 485        end
 486      end
 487 
 488      #
 489      # Retrieves +remotefile+ in binary mode, storing the result in +localfile+.
 490      # If a block is supplied, it is passed the retrieved data in +blocksize+
 491      # chunks.
 492      #
 493      def getbinaryfile(remotefile, localfile = File.basename(remotefile),
 494                        blocksize = DEFAULT_BLOCKSIZE, &block) # :yield: data
 495        if @resume
 496          rest_offset = File.size?(localfile)
 497          f = open(localfile, "a")
 498        else
 499          rest_offset = nil
 500          f = open(localfile, "w")
 501        end
 502        begin
 503          f.binmode
 504          retrbinary("RETR " + remotefile, blocksize, rest_offset) do |data|
 505            f.write(data)
 506            yield(data) if block
 507          end
 508        ensure
 509          f.close
 510        end
 511      end
 512      
 513      #
 514      # Retrieves +remotefile+ in ASCII (text) mode, storing the result in
 515      # +localfile+. If a block is supplied, it is passed the retrieved data one
 516      # line at a time.
 517      #
 518      def gettextfile(remotefile, localfile = File.basename(remotefile), &block) # :yield: line
 519        f = open(localfile, "w")
 520        begin
 521          retrlines("RETR " + remotefile) do |line|
 522            f.puts(line)
 523            yield(line) if block
 524          end
 525        ensure
 526          f.close
 527        end
 528      end
 529 
 530      #
 531      # Retrieves +remotefile+ in whatever mode the session is set (text or
 532      # binary).  See #gettextfile and #getbinaryfile.
 533      #
 534      def get(remotefile, localfile = File.basename(remotefile),
 535              blocksize = DEFAULT_BLOCKSIZE, &block) # :yield: data
 536        unless @binary
 537          gettextfile(remotefile, localfile, &block)
 538        else
 539          getbinaryfile(remotefile, localfile, blocksize, &block)
 540        end
 541      end
 542      
 543      #
 544      # Transfers +localfile+ to the server in binary mode, storing the result in
 545      # +remotefile+. If a block is supplied, calls it, passing in the transmitted
 546      # data in +blocksize+ chunks.
 547      #
 548      def putbinaryfile(localfile, remotefile = File.basename(localfile),
 549                        blocksize = DEFAULT_BLOCKSIZE, &block) # :yield: data
 550        if @resume
 551          begin
 552            rest_offset = size(remotefile)
 553          rescue Net::FTPPermError
 554            rest_offset = nil
 555          end
 556        else
 557          rest_offset = nil
 558        end
 559        f = open(localfile)
 560        begin
 561          f.binmode
 562          storbinary("STOR " + remotefile, f, blocksize, rest_offset, &block)
 563        ensure
 564          f.close
 565        end
 566      end
 567      
 568      #
 569      # Transfers +localfile+ to the server in ASCII (text) mode, storing the result
 570      # in +remotefile+. If callback or an associated block is supplied, calls it,
 571      # passing in the transmitted data one line at a time.
 572      #
 573      def puttextfile(localfile, remotefile = File.basename(localfile), &block) # :yield: line
 574        f = open(localfile)
 575        begin
 576          storlines("STOR " + remotefile, f, &block)
 577        ensure
 578          f.close
 579        end
 580      end
 581 
 582      #
 583      # Transfers +localfile+ to the server in whatever mode the session is set
 584      # (text or binary).  See #puttextfile and #putbinaryfile.
 585      #
 586      def put(localfile, remotefile = File.basename(localfile),
 587              blocksize = DEFAULT_BLOCKSIZE, &block)
 588        unless @binary
 589          puttextfile(localfile, remotefile, &block)
 590        else
 591          putbinaryfile(localfile, remotefile, blocksize, &block)
 592        end
 593      end
 594 
 595      #
 596      # Sends the ACCT command.  TODO: more info.
 597      #
 598      def acct(account)
 599        cmd = "ACCT " + account
 600        voidcmd(cmd)
 601      end
 602      
 603      #
 604      # Returns an array of filenames in the remote directory.
 605      #
 606      def nlst(dir = nil)
 607        cmd = "NLST"
 608        if dir
 609          cmd = cmd + " " + dir
 610        end
 611        files = []
 612        retrlines(cmd) do |line|
 613          files.push(line)
 614        end
 615        return files
 616      end
 617      
 618      #
 619      # Returns an array of file information in the directory (the output is like
 620      # `ls -l`).  If a block is given, it iterates through the listing.
 621      #
 622      def list(*args, &block) # :yield: line
 623        cmd = "LIST"
 624        args.each do |arg|
 625          cmd = cmd + " " + arg
 626        end
 627        if block
 628          retrlines(cmd, &block)
 629        else
 630          lines = []
 631          retrlines(cmd) do |line|
 632            lines << line
 633          end
 634          return lines
 635        end
 636      end
 637      alias ls list
 638      alias dir list
 639      
 640      #
 641      # Renames a file on the server.
 642      #
 643      def rename(fromname, toname)
 644        resp = sendcmd("RNFR " + fromname)
 645        if resp[0] != ?3
 646          raise FTPReplyError, resp
 647        end
 648        voidcmd("RNTO " + toname)
 649      end
 650      
 651      #
 652      # Deletes a file on the server.
 653      #
 654      def delete(filename)
 655        resp = sendcmd("DELE " + filename)
 656        if resp[0, 3] == "250"
 657          return
 658        elsif resp[0] == ?5
 659          raise FTPPermError, resp
 660        else
 661          raise FTPReplyError, resp
 662        end
 663      end
 664      
 665      #
 666      # Changes the (remote) directory.
 667      #
 668      def chdir(dirname)
 669        if dirname == ".."
 670          begin
 671            voidcmd("CDUP")
 672            return
 673          rescue FTPPermError => e
 674            if e.message[0, 3] != "500"
 675              raise e
 676            end
 677          end
 678        end
 679        cmd = "CWD " + dirname
 680        voidcmd(cmd)
 681      end
 682      
 683      #
 684      # Returns the size of the given (remote) filename.
 685      #
 686      def size(filename)
 687        voidcmd("TYPE I")
 688        resp = sendcmd("SIZE " + filename)
 689        if resp[0, 3] != "213" 
 690          raise FTPReplyError, resp
 691        end
 692        return resp[3..-1].strip.to_i
 693      end
 694      
 695      MDTM_REGEXP = /^(\d\d\d\d)(\d\d)(\d\d)(\d\d)(\d\d)(\d\d)/  # :nodoc:
 696      
 697      #
 698      # Returns the last modification time of the (remote) file.  If +local+ is
 699      # +true+, it is returned as a local time, otherwise it's a UTC time.
 700      #
 701      def mtime(filename, local = false)
 702        str = mdtm(filename)
 703        ary = str.scan(MDTM_REGEXP)[0].collect {|i| i.to_i}
 704        return local ? Time.local(*ary) : Time.gm(*ary)
 705      end
 706      
 707      #
 708      # Creates a remote directory.
 709      #
 710      def mkdir(dirname)
 711        resp = sendcmd("MKD " + dirname)
 712        return parse257(resp)
 713      end
 714      
 715      #
 716      # Removes a remote directory.
 717      #
 718      def rmdir(dirname)
 719        voidcmd("RMD " + dirname)
 720      end
 721      
 722      #
 723      # Returns the current remote directory.
 724      #
 725      def pwd
 726        resp = sendcmd("PWD")
 727        return parse257(resp)
 728      end
 729      alias getdir pwd
 730      
 731      #
 732      # Returns system information.
 733      #
 734      def system
 735        resp = sendcmd("SYST")
 736        if resp[0, 3] != "215"
 737          raise FTPReplyError, resp
 738        end
 739        return resp[4 .. -1]
 740      end
 741      
 742      #
 743      # Aborts the previous command (ABOR command).
 744      #
 745      def abort
 746        line = "ABOR" + CRLF
 747        print "put: ABOR\n" if @debug_mode
 748        @sock.send(line, Socket::MSG_OOB)
 749        resp = getmultiline
 750        unless ["426", "226", "225"].include?(resp[0, 3])
 751          raise FTPProtoError, resp
 752        end
 753        return resp
 754      end
 755      
 756      #
 757      # Returns the status (STAT command).
 758      #
 759      def status
 760        line = "STAT" + CRLF
 761        print "put: STAT\n" if @debug_mode
 762        @sock.send(line, Socket::MSG_OOB)
 763        return getresp
 764      end
 765      
 766      #
 767      # Issues the MDTM command.  TODO: more info.
 768      #
 769      def mdtm(filename)
 770        resp = sendcmd("MDTM " + filename)
 771        if resp[0, 3] == "213"
 772          return resp[3 .. -1].strip
 773        end
 774      end
 775      
 776      #
 777      # Issues the HELP command.
 778      #
 779      def help(arg = nil)
 780        cmd = "HELP"
 781        if arg
 782          cmd = cmd + " " + arg
 783        end
 784        sendcmd(cmd)
 785      end
 786      
 787      #
 788      # Exits the FTP session.
 789      #
 790      def quit
 791        voidcmd("QUIT")
 792      end
 793 
 794      #
 795      # Issues a NOOP command.
 796      #
 797      def noop
 798        voidcmd("NOOP")
 799      end
 800 
 801      #
 802      # Issues a SITE command.
 803      #
 804      def site(arg)
 805        cmd = "SITE " + arg
 806        voidcmd(cmd)
 807      end
 808      
 809      #
 810      # Closes the connection.  Further operations are impossible until you open
 811      # a new connection with #connect.
 812      #
 813      def close
 814        @sock.close if @sock and not @sock.closed?
 815      end
 816      
 817      #
 818      # Returns +true+ iff the connection is closed.
 819      #
 820      def closed?
 821        @sock == nil or @sock.closed?
 822      end
 823      
 824      def parse227(resp)
 825        if resp[0, 3] != "227"
 826          raise FTPReplyError, resp
 827        end
 828        left = resp.index("(")
 829        right = resp.index(")")
 830        if left == nil or right == nil
 831          raise FTPProtoError, resp
 832        end
 833        numbers = resp[left + 1 .. right - 1].split(",")
 834        if numbers.length != 6
 835          raise FTPProtoError, resp
 836        end
 837        host = numbers[0, 4].join(".")
 838        port = (numbers[4].to_i << 8) + numbers[5].to_i
 839        return host, port
 840      end
 841      private :parse227
 842      
 843      def parse228(resp)
 844        if resp[0, 3] != "228"
 845          raise FTPReplyError, resp
 846        end
 847        left = resp.index("(")
 848        right = resp.index(")")
 849        if left == nil or right == nil
 850          raise FTPProtoError, resp
 851        end
 852        numbers = resp[left + 1 .. right - 1].split(",")
 853        if numbers[0] == "4"
 854          if numbers.length != 9 || numbers[1] != "4" || numbers[2 + 4] != "2"
 855            raise FTPProtoError, resp
 856          end
 857          host = numbers[2, 4].join(".")
 858          port = (numbers[7].to_i << 8) + numbers[8].to_i
 859        elsif numbers[0] == "6"
 860          if numbers.length != 21 || numbers[1] != "16" || numbers[2 + 16] != "2"
 861            raise FTPProtoError, resp
 862          end
 863          v6 = ["", "", "", "", "", "", "", ""]
 864          for i in 0 .. 7
 865            v6[i] = sprintf("%02x%02x", numbers[(i * 2) + 2].to_i,
 866                            numbers[(i * 2) + 3].to_i)
 867          end
 868          host = v6[0, 8].join(":")
 869          port = (numbers[19].to_i << 8) + numbers[20].to_i
 870        end 
 871        return host, port
 872      end
 873      private :parse228
 874      
 875      def parse229(resp)
 876        if resp[0, 3] != "229"
 877          raise FTPReplyError, resp
 878        end
 879        left = resp.index("(")
 880        right = resp.index(")")
 881        if left == nil or right == nil
 882          raise FTPProtoError, resp
 883        end
 884        numbers = resp[left + 1 .. right - 1].split(resp[left + 1, 1])
 885        if numbers.length != 4
 886          raise FTPProtoError, resp
 887        end
 888        port = numbers[3].to_i
 889        host = (@sock.peeraddr())[3]
 890        return host, port
 891      end
 892      private :parse229
 893      
 894      def parse257(resp)
 895        if resp[0, 3] != "257"
 896          raise FTPReplyError, resp
 897        end
 898        if resp[3, 2] != ' "'
 899          return ""
 900        end
 901        dirname = ""
 902        i = 5
 903        n = resp.length
 904        while i < n
 905          c = resp[i, 1]
 906          i = i + 1
 907          if c == '"'
 908            if i > n or resp[i, 1] != '"'
 909              break
 910            end
 911            i = i + 1
 912          end
 913          dirname = dirname + c
 914        end
 915        return dirname
 916      end
 917      private :parse257
 918    end
 919 
 920  end
 921 
 922 
 923  # Documentation comments:
 924  #  - sourced from pickaxe and nutshell, with improvements (hopefully)
 925  #  - three methods should be private (search WRITEME)
 926  #  - two methods need more information (search TODO)