#
# = net/ftp.rb - FTP Client Library
#
# Written by Shugo Maeda <shugo@ruby-lang.org>.
#
# 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
class FTPConnectionError < 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_reader :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 <tt>FTP.new</tt>, 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
@sock = NullSocket.new
@logged_in = false
if host
connect(host)
if user
login(user, passwd, acct)
end
end
end
# A setter to toggle transfers in binary mode.
# +newmode+ is either +true+ or +false+
def binary=(newmode)
if newmode != @binary
@binary = newmode
send_type_command if @logged_in
end
end
# Sends a command to destination host, with the current binary sendmode
# type.
#
# If binary mode is +true+, then "TYPE I" (image) is sent, otherwise "TYPE
# A" (ascii) is sent.
def send_type_command # :nodoc:
if @binary
voidcmd("TYPE I")
else
voidcmd("TYPE A")
end
end
private :send_type_command
# Toggles transfers in binary mode and yields to a block.
# This preserves your current binary send mode, but allows a temporary
# transaction with binary sendmode of +newmode+.
#
# +newmode+ is either +true+ or +false+
def with_binary(newmode) # :nodoc:
oldmode = binary
self.binary = newmode
begin
yield
ensure
self.binary = oldmode
end
end
private :with_binary
# Obsolete
def return_code # :nodoc:
$stderr.puts("warning: Net::FTP#return_code is obsolete and do nothing")
return "\n"
end
# Obsolete
def return_code=(s) # :nodoc:
$stderr.puts("warning: Net::FTP#return_code= is obsolete and do nothing")
end
# Contructs a socket with +host+ and +port+.
#
# If SOCKSSocket is defined and the environment (ENV) defines
# 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
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
# <tt>Errno::ECONNREFUSED</tt>) 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
# If string +s+ includes the PASS command (password), then the contents of
# the password are cleaned from the string using "*"
def sanitize(s) # :nodoc:
if s =~ /^PASS /i
return s[0, 5] + "*" * (s.length - 5)
else
return s
end
end
private :sanitize
# Ensures that +line+ has a control return / line feed (CRLF) and writes
# it to the socket.
def putline(line) # :nodoc:
if @debug_mode
print "put: ", sanitize(line), "\n"
end
line = line + CRLF
@sock.write(line)
end
private :putline
# Reads a line from the sock. If EOF, then it will raise EOFError
def getline # :nodoc:
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
# Receive a section of lines until the response code's match.
def getmultiline # :nodoc:
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
# Recieves a response from the destination host.
#
# Returns the response code or raises FTPTempError, FTPPermError, or
# FTPProtoError
def getresp # :nodoc:
@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
# Recieves a response.
#
# Raises FTPReplyError if the first position of the response code is not
# equal 2.
def voidresp # :nodoc:
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
# Constructs and send the appropriate PORT (or EPRT) command
def sendport(host, port) # :nodoc:
af = (@sock.peeraddr)[0]
if af == "AF_INET"
cmd = "PORT " + (host.split(".") + port.divmod(256)).join(",")
elsif af == "AF_INET6"
cmd = sprintf("EPRT |2|%s|%d|", host, port)
Loading ...