# # = 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)