[前][次][番号順一覧][スレッド一覧]

ruby-changes:23144

From: shugo <ko1@a...>
Date: Sat, 31 Mar 2012 13:44:46 +0900 (JST)
Subject: [ruby-changes:23144] shugo:r35194 (trunk): * lib/net/ftp.rb (read_timeout=, open_timeout=): supported timeout.

shugo	2012-03-31 13:44:36 +0900 (Sat, 31 Mar 2012)

  New Revision: 35194

  http://svn.ruby-lang.org/cgi-bin/viewvc.cgi?view=rev&revision=35194

  Log:
    * lib/net/ftp.rb (read_timeout=, open_timeout=): supported timeout.

  Modified files:
    trunk/ChangeLog
    trunk/lib/net/ftp.rb
    trunk/test/net/ftp/test_ftp.rb

Index: ChangeLog
===================================================================
--- ChangeLog	(revision 35193)
+++ ChangeLog	(revision 35194)
@@ -1,3 +1,7 @@
+Sat Mar 31 13:42:39 2012  Shugo Maeda  <shugo@r...>
+
+	* lib/net/ftp.rb (read_timeout=, open_timeout=): supported timeout.
+
 Sat Mar 31 13:20:40 2012  Sokolov Yura (funny-falcon)  <funny.falcon@g...>
 
 	* hash.c: remove unnecessary checks for Qundef in hash iterations.
Index: lib/net/ftp.rb
===================================================================
--- lib/net/ftp.rb	(revision 35193)
+++ lib/net/ftp.rb	(revision 35194)
@@ -16,6 +16,7 @@
 
 require "socket"
 require "monitor"
+require "net/protocol"
 
 module Net
 
@@ -76,7 +77,7 @@
     # :stopdoc:
     FTP_PORT = 21
     CRLF = "\r\n"
-    DEFAULT_BLOCKSIZE = 4096
+    DEFAULT_BLOCKSIZE = BufferedIO::BUFSIZE
     # :startdoc:
 
     # When +true+, transfers are performed in binary mode.  Default: +true+.
@@ -93,6 +94,24 @@
     # transfers are resumed or restarted.  Default: +false+.
     attr_accessor :resume
 
+    # Number of seconds to wait for the connection to open. Any number
+    # may be used, including Floats for fractional seconds. If the FTP
+    # object cannot open a connection in this many seconds, it raises a
+    # Net::OpenTimeout exception.
+    attr_accessor :open_timeout
+
+    # Number of seconds to wait for one block to be read (via one read(2)
+    # call). Any number may be used, including Floats for fractional
+    # seconds. If the FTP object cannot read data in this many seconds,
+    # it raises a TimeoutError exception.
+    attr_reader :read_timeout
+
+    # Setter for the read_timeout attribute.
+    def read_timeout=(sec)
+      @sock.read_timeout = sec
+      @read_timeout = sec
+    end
+
     # The server's welcome message.
     attr_reader :welcome
 
@@ -135,6 +154,8 @@
       @resume = false
       @sock = NullSocket.new
       @logged_in = false
+      @open_timeout = nil
+      @read_timeout = 60
       if host
         connect(host)
         if user
@@ -199,12 +220,17 @@
     # SOCKS_SERVER, then a SOCKSSocket is returned, else a TCPSocket is
     # returned.
     def open_socket(host, port) # :nodoc:
-      if defined? SOCKSSocket and ENV["SOCKS_SERVER"]
-        @passive = true
-        return SOCKSSocket.open(host, port)
-      else
-        return TCPSocket.open(host, port)
-      end
+      return Timeout.timeout(@open_timeout, Net::OpenTimeout) {
+        if defined? SOCKSSocket and ENV["SOCKS_SERVER"]
+          @passive = true
+          sock = SOCKSSocket.open(host, port)
+        else
+          sock = TCPSocket.open(host, port)
+        end
+        io = BufferedSocket.new(sock)
+        io.read_timeout = @read_timeout
+        io
+      }
     end
     private :open_socket
 
@@ -405,7 +431,8 @@
         if resp[0] != ?1
           raise FTPReplyError, resp
         end
-        conn = sock.accept
+        conn = BufferedSocket.new(sock.accept)
+        conn.read_timeout = @read_timeout
         sock.close
       end
       return conn
@@ -1016,10 +1043,47 @@
 
     # :stopdoc:
     class NullSocket
+      def read_timeout=(sec)
+      end
+
+      def close
+      end
+
       def method_missing(mid, *args)
         raise FTPConnectionError, "not connected"
       end
     end
+
+    class BufferedSocket < BufferedIO
+      [:addr, :peeraddr].each do |method|
+        define_method(method) { |*args|
+          @io.__send__(method, *args)
+        }
+      end
+
+      def read(len = nil)
+        if len
+          s = super(len, "", true)
+          return s.empty? ? nil : s
+        else
+          result = ""
+          while s = super(BUFSIZ, "", true)
+            result << s
+          end
+          return result
+        end
+      end
+
+      def gets
+        return readuntil("\n")
+      rescue EOFError
+        return nil
+      end
+
+      def readline
+        return readuntil("\n")
+      end
+    end
     # :startdoc:
   end
 end
Index: test/net/ftp/test_ftp.rb
===================================================================
--- test/net/ftp/test_ftp.rb	(revision 35193)
+++ test/net/ftp/test_ftp.rb	(revision 35194)
@@ -1,8 +1,11 @@
 require "net/ftp"
 require "test/unit"
 require "ostruct"
+require "stringio"
 
 class FTPTest < Test::Unit::TestCase
+  SERVER_ADDR = "127.0.0.1"
+
   def test_not_connected
     ftp = Net::FTP.new
     assert_raise(Net::FTPConnectionError) do
@@ -107,4 +110,430 @@
     assert_equal(795192, ftp.send(:parse_pasv_port, "12,34,56"))
     assert_equal(203569230, ftp.send(:parse_pasv_port, "12,34,56,78"))
   end
+
+  def test_login
+    commands = []
+    server = create_ftp_server { |sock|
+      sock.print("220 (test_ftp).\r\n")
+      commands.push(sock.gets)
+      sock.print("331 Please specify the password.\r\n")
+      commands.push(sock.gets)
+      sock.print("230 Login successful.\r\n")
+      commands.push(sock.gets)
+      sock.print("200 Switching to Binary mode.\r\n")
+    }
+    begin
+      begin
+        ftp = Net::FTP.new
+        ftp.connect(SERVER_ADDR, server.port)
+        ftp.login
+        assert_match(/\AUSER /, commands.shift)
+        assert_match(/\APASS /, commands.shift)
+        assert_equal("TYPE I\r\n", commands.shift)
+        assert_equal(nil, commands.shift)
+      ensure
+        ftp.close if ftp
+      end
+    ensure
+      server.close
+    end
+  end
+
+  # TODO: How can we test open_timeout?  sleep before accept cannot delay
+  # connections.
+  def _test_open_timeout_exceeded
+    commands = []
+    server = create_ftp_server(0.2) { |sock|
+      sock.print("220 (test_ftp).\r\n")
+      commands.push(sock.gets)
+      sock.print("331 Please specify the password.\r\n")
+      commands.push(sock.gets)
+      sock.print("230 Login successful.\r\n")
+      commands.push(sock.gets)
+      sock.print("200 Switching to Binary mode.\r\n")
+    }
+    begin
+      begin
+        ftp = Net::FTP.new
+        ftp.open_timeout = 0.1
+        ftp.connect(SERVER_ADDR, server.port)
+        assert_raise(Net::OpenTimeout) do
+          ftp.login
+        end
+        assert_match(/\AUSER /, commands.shift)
+        assert_match(/\APASS /, commands.shift)
+        assert_equal(nil, commands.shift)
+      ensure
+        ftp.close if ftp
+      end
+    ensure
+      server.close
+    end
+  end
+
+  def test_read_timeout_exceeded
+    commands = []
+    server = create_ftp_server { |sock|
+      sock.print("220 (test_ftp).\r\n")
+      commands.push(sock.gets)
+      sleep(0.1)
+      sock.print("331 Please specify the password.\r\n")
+      commands.push(sock.gets)
+      sleep(0.3)
+      sock.print("230 Login successful.\r\n")
+      commands.push(sock.gets)
+      sleep(0.1)
+      sock.print("200 Switching to Binary mode.\r\n")
+    }
+    begin
+      begin
+        ftp = Net::FTP.new
+        ftp.read_timeout = 0.2
+        ftp.connect(SERVER_ADDR, server.port)
+        assert_raise(Timeout::Error) do
+          ftp.login
+        end
+        assert_match(/\AUSER /, commands.shift)
+        assert_match(/\APASS /, commands.shift)
+        assert_equal(nil, commands.shift)
+      ensure
+        ftp.close if ftp
+      end
+    ensure
+      server.close
+    end
+  end
+
+  def test_read_timeout_not_exceeded
+    commands = []
+    server = create_ftp_server { |sock|
+      sock.print("220 (test_ftp).\r\n")
+      commands.push(sock.gets)
+      sleep(0.1)
+      sock.print("331 Please specify the password.\r\n")
+      commands.push(sock.gets)
+      sleep(0.1)
+      sock.print("230 Login successful.\r\n")
+      commands.push(sock.gets)
+      sleep(0.1)
+      sock.print("200 Switching to Binary mode.\r\n")
+    }
+    begin
+      begin
+        ftp = Net::FTP.new
+        ftp.read_timeout = 0.2
+        ftp.connect(SERVER_ADDR, server.port)
+        ftp.login
+        assert_match(/\AUSER /, commands.shift)
+        assert_match(/\APASS /, commands.shift)
+        assert_equal("TYPE I\r\n", commands.shift)
+        assert_equal(nil, commands.shift)
+      ensure
+        ftp.close if ftp
+      end
+    ensure
+      server.close
+    end
+  end
+
+  def test_list_read_timeout_exceeded
+    commands = []
+    list_lines = [
+      "-rw-r--r--    1 0        0               0 Mar 30 11:22 foo.txt",
+      "-rw-r--r--    1 0        0               0 Mar 30 11:22 bar.txt",
+      "-rw-r--r--    1 0        0               0 Mar 30 11:22 baz.txt"
+    ]
+    server = create_ftp_server { |sock|
+      sock.print("220 (test_ftp).\r\n")
+      commands.push(sock.gets)
+      sock.print("331 Please specify the password.\r\n")
+      commands.push(sock.gets)
+      sock.print("230 Login successful.\r\n")
+      commands.push(sock.gets)
+      sock.print("200 Switching to Binary mode.\r\n")
+      commands.push(sock.gets)
+      sock.print("200 Switching to ASCII mode.\r\n")
+      line = sock.gets
+      commands.push(line)
+      port_args = line.slice(/\APORT (.*)/, 1).split(/,/)
+      host = port_args[0, 4].join(".")
+      port = port_args[4, 2].map(&:to_i).inject {|x, y| (x << 8) + y}
+      sock.print("200 PORT command successful.\r\n")
+      commands.push(sock.gets)
+      sock.print("150 Here comes the directory listing.\r\n")
+      conn = TCPSocket.new(host, port)
+      list_lines.each_with_index do |l, i|
+        if i == 1
+          sleep(0.3)
+        else
+          sleep(0.1)
+        end
+        conn.print(l, "\r\n")
+      end
+      conn.close
+      sock.print("226 Directory send OK.\r\n")
+    }
+    begin
+      begin
+        ftp = Net::FTP.new
+        ftp.read_timeout = 0.2
+        ftp.connect(SERVER_ADDR, server.port)
+        ftp.login
+        assert_match(/\AUSER /, commands.shift)
+        assert_match(/\APASS /, commands.shift)
+        assert_equal("TYPE I\r\n", commands.shift)
+        assert_raise(Timeout::Error) do
+          ftp.list
+        end
+        assert_equal("TYPE A\r\n", commands.shift)
+        assert_match(/\APORT /, commands.shift)
+        assert_equal("LIST\r\n", commands.shift)
+        assert_equal(nil, commands.shift)
+      ensure
+        ftp.close if ftp
+      end
+    ensure
+      server.close
+    end
+  end
+
+  def test_list_read_timeout_not_exceeded
+    commands = []
+    list_lines = [
+      "-rw-r--r--    1 0        0               0 Mar 30 11:22 foo.txt",
+      "-rw-r--r--    1 0        0               0 Mar 30 11:22 bar.txt",
+      "-rw-r--r--    1 0        0               0 Mar 30 11:22 baz.txt"
+    ]
+    server = create_ftp_server { |sock|
+      sock.print("220 (test_ftp).\r\n")
+      commands.push(sock.gets)
+      sock.print("331 Please specify the password.\r\n")
+      commands.push(sock.gets)
+      sock.print("230 Login successful.\r\n")
+      commands.push(sock.gets)
+      sock.print("200 Switching to Binary mode.\r\n")
+      commands.push(sock.gets)
+      sock.print("200 Switching to ASCII mode.\r\n")
+      line = sock.gets
+      commands.push(line)
+      port_args = line.slice(/\APORT (.*)/, 1).split(/,/)
+      host = port_args[0, 4].join(".")
+      port = port_args[4, 2].map(&:to_i).inject {|x, y| (x << 8) + y}
+      sock.print("200 PORT command successful.\r\n")
+      commands.push(sock.gets)
+      sock.print("150 Here comes the directory listing.\r\n")
+      conn = TCPSocket.new(host, port)
+      list_lines.each do |l|
+        sleep(0.1)
+        conn.print(l, "\r\n")
+      end
+      conn.close
+      sock.print("226 Directory send OK.\r\n")
+      commands.push(sock.gets)
+      sock.print("200 Switching to Binary mode.\r\n")
+    }
+    begin
+      begin
+        ftp = Net::FTP.new
+        ftp.read_timeout = 0.2
+        ftp.connect(SERVER_ADDR, server.port)
+        ftp.login
+        assert_match(/\AUSER /, commands.shift)
+        assert_match(/\APASS /, commands.shift)
+        assert_equal("TYPE I\r\n", commands.shift)
+        assert_equal(list_lines, ftp.list)
+        assert_equal("TYPE A\r\n", commands.shift)
+        assert_match(/\APORT /, commands.shift)
+        assert_equal("LIST\r\n", commands.shift)
+        assert_equal("TYPE I\r\n", commands.shift)
+        assert_equal(nil, commands.shift)
+      ensure
+        ftp.close if ftp
+      end
+    ensure
+      server.close
+    end
+  end
+
+  def test_retrbinary_read_timeout_exceeded
+    commands = []
+    binary_data = (0..0xff).map {|i| i.chr}.join * 4 * 3
+    server = create_ftp_server { |sock|
+      sock.print("220 (test_ftp).\r\n")
+      commands.push(sock.gets)
+      sock.print("331 Please specify the password.\r\n")
+      commands.push(sock.gets)
+      sock.print("230 Login successful.\r\n")
+      commands.push(sock.gets)
+      sock.print("200 Switching to Binary mode.\r\n")
+      line = sock.gets
+      commands.push(line)
+      port_args = line.slice(/\APORT (.*)/, 1).split(/,/)
+      host = port_args[0, 4].join(".")
+      port = port_args[4, 2].map(&:to_i).inject {|x, y| (x << 8) + y}
+      sock.print("200 PORT command successful.\r\n")
+      commands.push(sock.gets)
+      sock.print("150 Opening BINARY mode data connection for foo (#{binary_data.size} bytes)\r\n")
+      conn = TCPSocket.new(host, port)
+      binary_data.scan(/.{1,1024}/nm).each_with_index do |s, i|
+        if i == 1
+          sleep(0.3)
+        else
+          sleep(0.1)
+        end
+        conn.print(s)
+      end
+      conn.close
+      sock.print("226 Transfer complete.\r\n")
+    }
+    begin
+      begin
+        ftp = Net::FTP.new
+        ftp.read_timeout = 0.2
+        ftp.connect(SERVER_ADDR, server.port)
+        ftp.login
+        assert_match(/\AUSER /, commands.shift)
+        assert_match(/\APASS /, commands.shift)
+        assert_equal("TYPE I\r\n", commands.shift)
+        buf = ""
+        assert_raise(Timeout::Error) do
+          ftp.retrbinary("RETR foo", 1024) do |s|
+            buf << s
+          end
+        end
+        assert_equal(binary_data[0, 1024], buf)
+        assert_match(/\APORT /, commands.shift)
+        assert_equal("RETR foo\r\n", commands.shift)
+        assert_equal(nil, commands.shift)
+      ensure
+        ftp.close if ftp
+      end
+    ensure
+      server.close
+    end
+  end
+
+  def test_retrbinary_read_timeout_not_exceeded
+    commands = []
+    binary_data = (0..0xff).map {|i| i.chr}.join * 4 * 3
+    server = create_ftp_server { |sock|
+      sock.print("220 (test_ftp).\r\n")
+      commands.push(sock.gets)
+      sock.print("331 Please specify the password.\r\n")
+      commands.push(sock.gets)
+      sock.print("230 Login successful.\r\n")
+      commands.push(sock.gets)
+      sock.print("200 Switching to Binary mode.\r\n")
+      line = sock.gets
+      commands.push(line)
+      port_args = line.slice(/\APORT (.*)/, 1).split(/,/)
+      host = port_args[0, 4].join(".")
+      port = port_args[4, 2].map(&:to_i).inject {|x, y| (x << 8) + y}
+      sock.print("200 PORT command successful.\r\n")
+      commands.push(sock.gets)
+      sock.print("150 Opening BINARY mode data connection for foo (#{binary_data.size} bytes)\r\n")
+      conn = TCPSocket.new(host, port)
+      binary_data.scan(/.{1,1024}/nm) do |s|
+        sleep(0.1)
+        conn.print(s)
+      end
+      conn.close
+      sock.print("226 Transfer complete.\r\n")
+    }
+    begin
+      begin
+        ftp = Net::FTP.new
+        ftp.read_timeout = 0.2
+        ftp.connect(SERVER_ADDR, server.port)
+        ftp.login
+        assert_match(/\AUSER /, commands.shift)
+        assert_match(/\APASS /, commands.shift)
+        assert_equal("TYPE I\r\n", commands.shift)
+        buf = ""
+        ftp.retrbinary("RETR foo", 1024) do |s|
+          buf << s
+        end
+        assert_equal(binary_data, buf)
+        assert_match(/\APORT /, commands.shift)
+        assert_equal("RETR foo\r\n", commands.shift)
+        assert_equal(nil, commands.shift)
+      ensure
+        ftp.close if ftp
+      end
+    ensure
+      server.close
+    end
+  end
+
+  def test_storbinary
+    commands = []
+    binary_data = (0..0xff).map {|i| i.chr}.join * 4 * 3
+    stored_data = nil
+    server = create_ftp_server { |sock|
+      sock.print("220 (test_ftp).\r\n")
+      commands.push(sock.gets)
+      sock.print("331 Please specify the password.\r\n")
+      commands.push(sock.gets)
+      sock.print("230 Login successful.\r\n")
+      commands.push(sock.gets)
+      sock.print("200 Switching to Binary mode.\r\n")
+      line = sock.gets
+      commands.push(line)
+      port_args = line.slice(/\APORT (.*)/, 1).split(/,/)
+      host = port_args[0, 4].join(".")
+      port = port_args[4, 2].map(&:to_i).inject {|x, y| (x << 8) + y}
+      sock.print("200 PORT command successful.\r\n")
+      commands.push(sock.gets)
+      sock.print("150 Opening BINARY mode data connection for foo\r\n")
+      conn = TCPSocket.new(host, port)
+      stored_data = conn.read
+      conn.close
+      sock.print("226 Transfer complete.\r\n")
+    }
+    begin
+      begin
+        ftp = Net::FTP.new
+        ftp.read_timeout = 0.2
+        ftp.connect(SERVER_ADDR, server.port)
+        ftp.login
+        assert_match(/\AUSER /, commands.shift)
+        assert_match(/\APASS /, commands.shift)
+        assert_equal("TYPE I\r\n", commands.shift)
+        ftp.storbinary("STOR foo", StringIO.new(binary_data), 1024)
+        assert_equal(binary_data, stored_data)
+        assert_match(/\APORT /, commands.shift)
+        assert_equal("STOR foo\r\n", commands.shift)
+        assert_equal(nil, commands.shift)
+      ensure
+        ftp.close if ftp
+      end
+    ensure
+      server.close
+    end
+  end
+
+  private
+
+
+  def create_ftp_server(sleep_time = nil)
+    server = TCPServer.new(SERVER_ADDR, 0)
+    Thread.start do
+      begin
+        if sleep_time
+          sleep(sleep_time)
+        end
+        sock = server.accept
+        begin
+          yield(sock)
+        ensure
+          sock.close
+        end
+      rescue
+      end
+    end
+    def server.port
+      addr[1]
+    end
+    return server
+  end
 end

--
ML: ruby-changes@q...
Info: http://www.atdot.net/~ko1/quickml/

[前][次][番号順一覧][スレッド一覧]