From d464704f111d211c1f1ff9ef23ef1d755054be00 Mon Sep 17 00:00:00 2001 From: shyouhei Date: Wed, 15 Aug 2007 19:08:43 +0000 Subject: add tag v1_8_5_54 git-svn-id: svn+ssh://ci.ruby-lang.org/ruby/tags/v1_8_5_54@12952 b2dd03c8-39d4-4d8f-98ff-823fe69b080e --- ruby_1_8_5/lib/net/ftp.rb | 923 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 923 insertions(+) create mode 100644 ruby_1_8_5/lib/net/ftp.rb (limited to 'ruby_1_8_5/lib/net/ftp.rb') diff --git a/ruby_1_8_5/lib/net/ftp.rb b/ruby_1_8_5/lib/net/ftp.rb new file mode 100644 index 0000000000..dfbcf1499f --- /dev/null +++ b/ruby_1_8_5/lib/net/ftp.rb @@ -0,0 +1,923 @@ +# +# = net/ftp.rb - FTP Client Library +# +# Written by Shugo Maeda . +# +# Documentation by Gavin Sinclair, sourced from "Programming Ruby" (Hunt/Thomas) +# and "Ruby In a Nutshell" (Matsumoto), used with permission. +# +# This library is distributed under the terms of the Ruby license. +# You can freely distribute/modify this library. +# +# It is included in the Ruby standard library. +# +# See the Net::FTP class for an overview. +# + +require "socket" +require "monitor" + +module Net + + # :stopdoc: + class FTPError < StandardError; end + class FTPReplyError < FTPError; end + class FTPTempError < FTPError; end + class FTPPermError < FTPError; end + class FTPProtoError < FTPError; end + # :startdoc: + + # + # This class implements the File Transfer Protocol. If you have used a + # command-line FTP program, and are familiar with the commands, you will be + # able to use this class easily. Some extra features are included to take + # advantage of Ruby's style and strengths. + # + # == Example + # + # require 'net/ftp' + # + # === Example 1 + # + # ftp = Net::FTP.new('ftp.netlab.co.jp') + # ftp.login + # files = ftp.chdir('pub/lang/ruby/contrib') + # files = ftp.list('n*') + # ftp.getbinaryfile('nif.rb-0.91.gz', 'nif.gz', 1024) + # ftp.close + # + # === Example 2 + # + # Net::FTP.open('ftp.netlab.co.jp') do |ftp| + # ftp.login + # files = ftp.chdir('pub/lang/ruby/contrib') + # files = ftp.list('n*') + # ftp.getbinaryfile('nif.rb-0.91.gz', 'nif.gz', 1024) + # end + # + # == Major Methods + # + # The following are the methods most likely to be useful to users: + # - FTP.open + # - #getbinaryfile + # - #gettextfile + # - #putbinaryfile + # - #puttextfile + # - #chdir + # - #nlst + # - #size + # - #rename + # - #delete + # + class FTP + include MonitorMixin + + # :stopdoc: + FTP_PORT = 21 + CRLF = "\r\n" + DEFAULT_BLOCKSIZE = 4096 + # :startdoc: + + # When +true+, transfers are performed in binary mode. Default: +true+. + attr_accessor :binary + + # When +true+, the connection is in passive mode. Default: +false+. + attr_accessor :passive + + # When +true+, all traffic to and from the server is written + # to +$stdout+. Default: +false+. + attr_accessor :debug_mode + + # Sets or retrieves the +resume+ status, which decides whether incomplete + # transfers are resumed or restarted. Default: +false+. + attr_accessor :resume + + # The server's welcome message. + attr_reader :welcome + + # The server's last response code. + attr_reader :last_response_code + alias lastresp last_response_code + + # The server's last response. + attr_reader :last_response + + # + # A synonym for FTP.new, but with a mandatory host parameter. + # + # If a block is given, it is passed the +FTP+ object, which will be closed + # when the block finishes, or when an exception is raised. + # + def FTP.open(host, user = nil, passwd = nil, acct = nil) + if block_given? + ftp = new(host, user, passwd, acct) + begin + yield ftp + ensure + ftp.close + end + else + new(host, user, passwd, acct) + end + end + + # + # Creates and returns a new +FTP+ object. If a +host+ is given, a connection + # is made. Additionally, if the +user+ is given, the given user name, + # password, and (optionally) account are used to log in. See #login. + # + def initialize(host = nil, user = nil, passwd = nil, acct = nil) + super() + @binary = true + @passive = false + @debug_mode = false + @resume = false + if host + connect(host) + if user + login(user, passwd, acct) + end + end + end + + # Obsolete + def return_code + $stderr.puts("warning: Net::FTP#return_code is obsolete and do nothing") + return "\n" + end + + # Obsolete + def return_code=(s) + $stderr.puts("warning: Net::FTP#return_code= is obsolete and do nothing") + end + + def open_socket(host, port) + if defined? SOCKSsocket and ENV["SOCKS_SERVER"] + @passive = true + return SOCKSsocket.open(host, port) + else + return TCPSocket.open(host, port) + end + end + private :open_socket + + # + # Establishes an FTP connection to host, optionally overriding the default + # port. If the environment variable +SOCKS_SERVER+ is set, sets up the + # connection through a SOCKS proxy. Raises an exception (typically + # Errno::ECONNREFUSED) if the connection cannot be established. + # + def connect(host, port = FTP_PORT) + if @debug_mode + print "connect: ", host, ", ", port, "\n" + end + synchronize do + @sock = open_socket(host, port) + voidresp + end + end + + # + # WRITEME or make private + # + def set_socket(sock, get_greeting = true) + synchronize do + @sock = sock + if get_greeting + voidresp + end + end + end + + def sanitize(s) + if s =~ /^PASS /i + return s[0, 5] + "*" * (s.length - 5) + else + return s + end + end + private :sanitize + + def putline(line) + if @debug_mode + print "put: ", sanitize(line), "\n" + end + line = line + CRLF + @sock.write(line) + end + private :putline + + def getline + line = @sock.readline # if get EOF, raise EOFError + line.sub!(/(\r\n|\n|\r)\z/n, "") + if @debug_mode + print "get: ", sanitize(line), "\n" + end + return line + end + private :getline + + def getmultiline + line = getline + buff = line + if line[3] == ?- + code = line[0, 3] + begin + line = getline + buff << "\n" << line + end until line[0, 3] == code and line[3] != ?- + end + return buff << "\n" + end + private :getmultiline + + def getresp + @last_response = getmultiline + @last_response_code = @last_response[0, 3] + case @last_response_code + when /\A[123]/ + return @last_response + when /\A4/ + raise FTPTempError, @last_response + when /\A5/ + raise FTPPermError, @last_response + else + raise FTPProtoError, @last_response + end + end + private :getresp + + def voidresp + resp = getresp + if resp[0] != ?2 + raise FTPReplyError, resp + end + end + private :voidresp + + # + # Sends a command and returns the response. + # + def sendcmd(cmd) + synchronize do + putline(cmd) + return getresp + end + end + + # + # Sends a command and expect a response beginning with '2'. + # + def voidcmd(cmd) + synchronize do + putline(cmd) + voidresp + end + end + + def sendport(host, port) + af = (@sock.peeraddr)[0] + if af == "AF_INET" + hbytes = host.split(".") + pbytes = [port / 256, port % 256] + bytes = hbytes + pbytes + cmd = "PORT " + bytes.join(",") + elsif af == "AF_INET6" + cmd = "EPRT |2|" + host + "|" + sprintf("%d", port) + "|" + else + raise FTPProtoError, host + end + voidcmd(cmd) + end + private :sendport + + def makeport + sock = TCPServer.open(@sock.addr[3], 0) + port = sock.addr[1] + host = sock.addr[3] + resp = sendport(host, port) + return sock + end + private :makeport + + def makepasv + if @sock.peeraddr[0] == "AF_INET" + host, port = parse227(sendcmd("PASV")) + else + host, port = parse229(sendcmd("EPSV")) + # host, port = parse228(sendcmd("LPSV")) + end + return host, port + end + private :makepasv + + def transfercmd(cmd, rest_offset = nil) + if @passive + host, port = makepasv + conn = open_socket(host, port) + if @resume and rest_offset + resp = sendcmd("REST " + rest_offset.to_s) + if resp[0] != ?3 + raise FTPReplyError, resp + end + end + resp = sendcmd(cmd) + if resp[0] != ?1 + raise FTPReplyError, resp + end + else + sock = makeport + if @resume and rest_offset + resp = sendcmd("REST " + rest_offset.to_s) + if resp[0] != ?3 + raise FTPReplyError, resp + end + end + resp = sendcmd(cmd) + if resp[0] != ?1 + raise FTPReplyError, resp + end + conn = sock.accept + sock.close + end + return conn + end + private :transfercmd + + def getaddress + thishost = Socket.gethostname + if not thishost.index(".") + thishost = Socket.gethostbyname(thishost)[0] + end + if ENV.has_key?("LOGNAME") + realuser = ENV["LOGNAME"] + elsif ENV.has_key?("USER") + realuser = ENV["USER"] + else + realuser = "anonymous" + end + return realuser + "@" + thishost + end + private :getaddress + + # + # Logs in to the remote host. The session must have been previously + # connected. If +user+ is the string "anonymous" and the +password+ is + # +nil+, a password of user@host is synthesized. If the +acct+ + # parameter is not +nil+, an FTP ACCT command is sent following the + # successful login. Raises an exception on error (typically + # Net::FTPPermError). + # + def login(user = "anonymous", passwd = nil, acct = nil) + if user == "anonymous" and passwd == nil + passwd = getaddress + end + + resp = "" + synchronize do + resp = sendcmd('USER ' + user) + if resp[0] == ?3 + resp = sendcmd('PASS ' + passwd) + end + if resp[0] == ?3 + resp = sendcmd('ACCT ' + acct) + end + end + if resp[0] != ?2 + raise FTPReplyError, resp + end + @welcome = resp + end + + # + # Puts the connection into binary (image) mode, issues the given command, + # and fetches the data returned, passing it to the associated block in + # chunks of +blocksize+ characters. Note that +cmd+ is a server command + # (such as "RETR myfile"). + # + def retrbinary(cmd, blocksize, rest_offset = nil) # :yield: data + synchronize do + voidcmd("TYPE I") + conn = transfercmd(cmd, rest_offset) + loop do + data = conn.read(blocksize) + break if data == nil + yield(data) + end + conn.close + voidresp + end + end + + # + # Puts the connection into ASCII (text) mode, issues the given command, and + # passes the resulting data, one line at a time, to the associated block. If + # no block is given, prints the lines. Note that +cmd+ is a server command + # (such as "RETR myfile"). + # + def retrlines(cmd) # :yield: line + synchronize do + voidcmd("TYPE A") + conn = transfercmd(cmd) + loop do + line = conn.gets + break if line == nil + if line[-2, 2] == CRLF + line = line[0 .. -3] + elsif line[-1] == ?\n + line = line[0 .. -2] + end + yield(line) + end + conn.close + voidresp + end + end + + # + # Puts the connection into binary (image) mode, issues the given server-side + # command (such as "STOR myfile"), and sends the contents of the file named + # +file+ to the server. If the optional block is given, it also passes it + # the data, in chunks of +blocksize+ characters. + # + def storbinary(cmd, file, blocksize, rest_offset = nil, &block) # :yield: data + if rest_offset + file.seek(rest_offset, IO::SEEK_SET) + end + synchronize do + voidcmd("TYPE I") + conn = transfercmd(cmd, rest_offset) + loop do + buf = file.read(blocksize) + break if buf == nil + conn.write(buf) + yield(buf) if block + end + conn.close + voidresp + end + end + + # + # Puts the connection into ASCII (text) mode, issues the given server-side + # command (such as "STOR myfile"), and sends the contents of the file + # named +file+ to the server, one line at a time. If the optional block is + # given, it also passes it the lines. + # + def storlines(cmd, file, &block) # :yield: line + synchronize do + voidcmd("TYPE A") + conn = transfercmd(cmd) + loop do + buf = file.gets + break if buf == nil + if buf[-2, 2] != CRLF + buf = buf.chomp + CRLF + end + conn.write(buf) + yield(buf) if block + end + conn.close + voidresp + end + end + + # + # Retrieves +remotefile+ in binary mode, storing the result in +localfile+. + # If a block is supplied, it is passed the retrieved data in +blocksize+ + # chunks. + # + def getbinaryfile(remotefile, localfile = File.basename(remotefile), + blocksize = DEFAULT_BLOCKSIZE, &block) # :yield: data + if @resume + rest_offset = File.size?(localfile) + f = open(localfile, "a") + else + rest_offset = nil + f = open(localfile, "w") + end + begin + f.binmode + retrbinary("RETR " + remotefile, blocksize, rest_offset) do |data| + f.write(data) + yield(data) if block + end + ensure + f.close + end + end + + # + # Retrieves +remotefile+ in ASCII (text) mode, storing the result in + # +localfile+. If a block is supplied, it is passed the retrieved data one + # line at a time. + # + def gettextfile(remotefile, localfile = File.basename(remotefile), &block) # :yield: line + f = open(localfile, "w") + begin + retrlines("RETR " + remotefile) do |line| + f.puts(line) + yield(line) if block + end + ensure + f.close + end + end + + # + # Retrieves +remotefile+ in whatever mode the session is set (text or + # binary). See #gettextfile and #getbinaryfile. + # + def get(remotefile, localfile = File.basename(remotefile), + blocksize = DEFAULT_BLOCKSIZE, &block) # :yield: data + unless @binary + gettextfile(remotefile, localfile, &block) + else + getbinaryfile(remotefile, localfile, blocksize, &block) + end + end + + # + # Transfers +localfile+ to the server in binary mode, storing the result in + # +remotefile+. If a block is supplied, calls it, passing in the transmitted + # data in +blocksize+ chunks. + # + def putbinaryfile(localfile, remotefile = File.basename(localfile), + blocksize = DEFAULT_BLOCKSIZE, &block) # :yield: data + if @resume + begin + rest_offset = size(remotefile) + rescue Net::FTPPermError + rest_offset = nil + end + else + rest_offset = nil + end + f = open(localfile) + begin + f.binmode + storbinary("STOR " + remotefile, f, blocksize, rest_offset, &block) + ensure + f.close + end + end + + # + # Transfers +localfile+ to the server in ASCII (text) mode, storing the result + # in +remotefile+. If callback or an associated block is supplied, calls it, + # passing in the transmitted data one line at a time. + # + def puttextfile(localfile, remotefile = File.basename(localfile), &block) # :yield: line + f = open(localfile) + begin + storlines("STOR " + remotefile, f, &block) + ensure + f.close + end + end + + # + # Transfers +localfile+ to the server in whatever mode the session is set + # (text or binary). See #puttextfile and #putbinaryfile. + # + def put(localfile, remotefile = File.basename(localfile), + blocksize = DEFAULT_BLOCKSIZE, &block) + unless @binary + puttextfile(localfile, remotefile, &block) + else + putbinaryfile(localfile, remotefile, blocksize, &block) + end + end + + # + # Sends the ACCT command. TODO: more info. + # + def acct(account) + cmd = "ACCT " + account + voidcmd(cmd) + end + + # + # Returns an array of filenames in the remote directory. + # + def nlst(dir = nil) + cmd = "NLST" + if dir + cmd = cmd + " " + dir + end + files = [] + retrlines(cmd) do |line| + files.push(line) + end + return files + end + + # + # Returns an array of file information in the directory (the output is like + # `ls -l`). If a block is given, it iterates through the listing. + # + def list(*args, &block) # :yield: line + cmd = "LIST" + args.each do |arg| + cmd = cmd + " " + arg + end + if block + retrlines(cmd, &block) + else + lines = [] + retrlines(cmd) do |line| + lines << line + end + return lines + end + end + alias ls list + alias dir list + + # + # Renames a file on the server. + # + def rename(fromname, toname) + resp = sendcmd("RNFR " + fromname) + if resp[0] != ?3 + raise FTPReplyError, resp + end + voidcmd("RNTO " + toname) + end + + # + # Deletes a file on the server. + # + def delete(filename) + resp = sendcmd("DELE " + filename) + if resp[0, 3] == "250" + return + elsif resp[0] == ?5 + raise FTPPermError, resp + else + raise FTPReplyError, resp + end + end + + # + # Changes the (remote) directory. + # + def chdir(dirname) + if dirname == ".." + begin + voidcmd("CDUP") + return + rescue FTPPermError + if $![0, 3] != "500" + raise FTPPermError, $! + end + end + end + cmd = "CWD " + dirname + voidcmd(cmd) + end + + # + # Returns the size of the given (remote) filename. + # + def size(filename) + voidcmd("TYPE I") + resp = sendcmd("SIZE " + filename) + if resp[0, 3] != "213" + raise FTPReplyError, resp + end + return resp[3..-1].strip.to_i + end + + MDTM_REGEXP = /^(\d\d\d\d)(\d\d)(\d\d)(\d\d)(\d\d)(\d\d)/ # :nodoc: + + # + # Returns the last modification time of the (remote) file. If +local+ is + # +true+, it is returned as a local time, otherwise it's a UTC time. + # + def mtime(filename, local = false) + str = mdtm(filename) + ary = str.scan(MDTM_REGEXP)[0].collect {|i| i.to_i} + return local ? Time.local(*ary) : Time.gm(*ary) + end + + # + # Creates a remote directory. + # + def mkdir(dirname) + resp = sendcmd("MKD " + dirname) + return parse257(resp) + end + + # + # Removes a remote directory. + # + def rmdir(dirname) + voidcmd("RMD " + dirname) + end + + # + # Returns the current remote directory. + # + def pwd + resp = sendcmd("PWD") + return parse257(resp) + end + alias getdir pwd + + # + # Returns system information. + # + def system + resp = sendcmd("SYST") + if resp[0, 3] != "215" + raise FTPReplyError, resp + end + return resp[4 .. -1] + end + + # + # Aborts the previous command (ABOR command). + # + def abort + line = "ABOR" + CRLF + print "put: ABOR\n" if @debug_mode + @sock.send(line, Socket::MSG_OOB) + resp = getmultiline + unless ["426", "226", "225"].include?(resp[0, 3]) + raise FTPProtoError, resp + end + return resp + end + + # + # Returns the status (STAT command). + # + def status + line = "STAT" + CRLF + print "put: STAT\n" if @debug_mode + @sock.send(line, Socket::MSG_OOB) + return getresp + end + + # + # Issues the MDTM command. TODO: more info. + # + def mdtm(filename) + resp = sendcmd("MDTM " + filename) + if resp[0, 3] == "213" + return resp[3 .. -1].strip + end + end + + # + # Issues the HELP command. + # + def help(arg = nil) + cmd = "HELP" + if arg + cmd = cmd + " " + arg + end + sendcmd(cmd) + end + + # + # Exits the FTP session. + # + def quit + voidcmd("QUIT") + end + + # + # Issues a NOOP command. + # + def noop + voidcmd("NOOP") + end + + # + # Issues a SITE command. + # + def site(arg) + cmd = "SITE " + arg + voidcmd(cmd) + end + + # + # Closes the connection. Further operations are impossible until you open + # a new connection with #connect. + # + def close + @sock.close if @sock and not @sock.closed? + end + + # + # Returns +true+ iff the connection is closed. + # + def closed? + @sock == nil or @sock.closed? + end + + def parse227(resp) + if resp[0, 3] != "227" + raise FTPReplyError, resp + end + left = resp.index("(") + right = resp.index(")") + if left == nil or right == nil + raise FTPProtoError, resp + end + numbers = resp[left + 1 .. right - 1].split(",") + if numbers.length != 6 + raise FTPProtoError, resp + end + host = numbers[0, 4].join(".") + port = (numbers[4].to_i << 8) + numbers[5].to_i + return host, port + end + private :parse227 + + def parse228(resp) + if resp[0, 3] != "228" + raise FTPReplyError, resp + end + left = resp.index("(") + right = resp.index(")") + if left == nil or right == nil + raise FTPProtoError, resp + end + numbers = resp[left + 1 .. right - 1].split(",") + if numbers[0] == "4" + if numbers.length != 9 || numbers[1] != "4" || numbers[2 + 4] != "2" + raise FTPProtoError, resp + end + host = numbers[2, 4].join(".") + port = (numbers[7].to_i << 8) + numbers[8].to_i + elsif numbers[0] == "6" + if numbers.length != 21 || numbers[1] != "16" || numbers[2 + 16] != "2" + raise FTPProtoError, resp + end + v6 = ["", "", "", "", "", "", "", ""] + for i in 0 .. 7 + v6[i] = sprintf("%02x%02x", numbers[(i * 2) + 2].to_i, + numbers[(i * 2) + 3].to_i) + end + host = v6[0, 8].join(":") + port = (numbers[19].to_i << 8) + numbers[20].to_i + end + return host, port + end + private :parse228 + + def parse229(resp) + if resp[0, 3] != "229" + raise FTPReplyError, resp + end + left = resp.index("(") + right = resp.index(")") + if left == nil or right == nil + raise FTPProtoError, resp + end + numbers = resp[left + 1 .. right - 1].split(resp[left + 1, 1]) + if numbers.length != 4 + raise FTPProtoError, resp + end + port = numbers[3].to_i + host = (@sock.peeraddr())[3] + return host, port + end + private :parse229 + + def parse257(resp) + if resp[0, 3] != "257" + raise FTPReplyError, resp + end + if resp[3, 2] != ' "' + return "" + end + dirname = "" + i = 5 + n = resp.length + while i < n + c = resp[i, 1] + i = i + 1 + if c == '"' + if i > n or resp[i, 1] != '"' + break + end + i = i + 1 + end + dirname = dirname + c + end + return dirname + end + private :parse257 + end + +end + + +# Documentation comments: +# - sourced from pickaxe and nutshell, with improvements (hopefully) +# - three methods should be private (search WRITEME) +# - two methods need more information (search TODO) -- cgit v1.2.3