lib/io_streams/paths/sftp.rb in iostreams-1.0.0.beta3 vs lib/io_streams/paths/sftp.rb in iostreams-1.0.0.beta4
- old
+ new
@@ -11,11 +11,11 @@
@sftp_bin = 'sftp'
@sshpass_bin = 'sshpass'
@sshpass_wait_seconds = 5
- attr_reader :hostname, :username, :ssh_options, :url
+ attr_reader :hostname, :username, :ssh_options, :url, :port
# Stream to a remote file over sftp.
#
# url: [String]
# "sftp://<host_name>/<file_name>"
@@ -56,18 +56,22 @@
# end
def initialize(url, username: nil, password: nil, ruby: true, ssh_options: {})
uri = URI.parse(url)
raise(ArgumentError, "Invalid URL. Required Format: 'sftp://<host_name>/<file_name>'") unless uri.scheme == 'sftp'
- @hostname = uri.hostname
- @mkdir = false
- @username = username || uri.user
- @url = url
- @password = password || uri.password
- @port = uri.port || 22
- @ssh_options = ssh_options
+ @hostname = uri.hostname
+ @mkdir = false
+ @username = username || uri.user
+ @url = url
+ @password = password || uri.password
+ @port = uri.port || 22
+ # Not Ruby 2.5 yet: transform_keys(&:to_s)
+ @ssh_options = {}
+ ssh_options.each_pair { |key, value| @ssh_options[key.to_s] = value }
+ URI.decode_www_form(uri.query).each { |key, value| @ssh_options[key] = value } if uri.query
+
super(uri.path)
end
def to_s
url
@@ -150,54 +154,108 @@
attr_reader :password
# Use sftp and sshpass executables to download to a local file
def sftp_download(remote_file_name, local_file_name)
- Open3.popen2e(*sftp_args) do |writer, reader, waith_thr|
- writer.puts password
- # Give time for password to be processed and stdin to be passed to sftp process.
- sleep self.class.sshpass_wait_seconds
- writer.puts "get #{remote_file_name} #{local_file_name}"
- writer.puts 'bye'
- writer.close
- out = reader.read.chomp
- raise(Errors::CommunicationsFailure, "Failed calling #{self.class.sftp_bin} via #{self.class.sshpass_bin}: #{out}") unless waith_thr.value.success?
- out
+ with_sftp_args do |args|
+ Open3.popen2e(*args) do |writer, reader, waith_thr|
+ begin
+ writer.puts password
+ # Give time for password to be processed and stdin to be passed to sftp process.
+ sleep self.class.sshpass_wait_seconds
+ writer.puts "get #{remote_file_name} #{local_file_name}"
+ writer.puts 'bye'
+ writer.close
+ out = reader.read.chomp
+ raise(Errors::CommunicationsFailure, "Download failed calling #{self.class.sftp_bin} via #{self.class.sshpass_bin}: #{out}") unless waith_thr.value.success?
+ out
+ rescue Errno::EPIPE
+ out = reader.read.chomp rescue nil
+ raise(Errors::CommunicationsFailure, "Download failed calling #{self.class.sftp_bin} via #{self.class.sshpass_bin}: #{out}")
+ end
+ end
end
end
def sftp_upload(local_file_name, remote_file_name)
- Open3.popen2e(*sftp_args) do |writer, reader, waith_thr|
- writer.puts(password) if password
- # Give time for password to be processed and stdin to be passed to sftp process.
- sleep self.class.sshpass_wait_seconds
- writer.puts "put #{local_file_name.inspect} #{remote_file_name.inspect}"
- writer.puts 'bye'
- writer.close
- out = reader.read.chomp
- raise(Errors::CommunicationsFailure, "Failed calling #{self.class.sftp_bin} via #{self.class.sshpass_bin}: #{out}") unless waith_thr.value.success?
- out
+ with_sftp_args do |args|
+ Open3.popen2e(*args) do |writer, reader, waith_thr|
+ begin
+ writer.puts(password) if password
+ # Give time for password to be processed and stdin to be passed to sftp process.
+ sleep self.class.sshpass_wait_seconds
+ writer.puts "put #{local_file_name.inspect} #{remote_file_name.inspect}"
+ writer.puts 'bye'
+ writer.close
+ out = reader.read.chomp
+ raise(Errors::CommunicationsFailure, "Upload failed calling #{self.class.sftp_bin} via #{self.class.sshpass_bin}: #{out}") unless waith_thr.value.success?
+ out
+ rescue Errno::EPIPE
+ out = reader.read.chomp rescue nil
+ raise(Errors::CommunicationsFailure, "Upload failed calling #{self.class.sftp_bin} via #{self.class.sshpass_bin}: #{out}")
+ end
+ end
end
end
- def sftp_args
- args = [self.class.sshpass_bin, self.class.sftp_bin, '-oBatchMode=no']
- # Force it to use the password when supplied.
- args << "-oPubkeyAuthentication=no" if password
+ def with_sftp_args
+ return yield sftp_args(ssh_options) unless ssh_options.key?('IdentityKey')
+
+ Utils.temp_file_name('iostreams-sftp-args', 'key') do |file_name|
+ options = ssh_options.dup
+ key = options.delete('IdentityKey')
+ # sftp requires that private key is only readable by the current user
+ File.open(file_name, 'wb', 0600) { |io| io.write(key) }
+
+ options['IdentityFile'] = file_name
+ yield sftp_args(ssh_options)
+ end
+ end
+
+ def sftp_args(ssh_options)
+ args = [self.class.sshpass_bin, self.class.sftp_bin]
+ # Force sftp to use the password when supplied,
+ # and stop sftp from prompting for a password when none was supplied.
+ if password
+ args << "-oBatchMode=no"
+ args << "-oNumberOfPasswordPrompts=1"
+ args << "-oPubkeyAuthentication=no"
+ else
+ args << "-oBatchMode=yes"
+ args << "-oPasswordAuthentication=no"
+ end
+ args << "-oIdentitiesOnly=yes" if ssh_options.key?('IdentityFile')
+ # Default is ask, but this is non-interactive so make the default fail without asking.
+ args << "-oStrictHostKeyChecking=yes" unless ssh_options.key?('StrictHostKeyChecking')
+ args << "-oLogLevel=#{map_log_level}" unless ssh_options.key?('LogLevel')
+ args << "-oPort=#{port}" unless port == 22
ssh_options.each_pair { |key, value| args << "-o#{key}=#{value}" }
args << '-b'
args << '-'
args << "#{username}@#{hostname}"
args
end
def build_ssh_options
options = ssh_options.dup
options[:logger] ||= self.logger if defined?(SemanticLogger)
- options[:port] ||= @port
+ options[:port] ||= port
options[:max_pkt_size] ||= 65_536
options[:password] ||= @password
options
+ end
+
+ def map_log_level
+ return "INFO" unless defined?(SemanticLogger)
+
+ case logger.level
+ when :trace
+ "DEBUG3"
+ when :warn
+ "ERROR"
+ else
+ logger.level.to_s
+ end
end
end
end
end