diff options
Diffstat (limited to 'vendor/rsync_command/lib')
-rw-r--r-- | vendor/rsync_command/lib/rsync_command.rb | 96 | ||||
-rw-r--r-- | vendor/rsync_command/lib/rsync_command/ssh_options.rb | 159 | ||||
-rw-r--r-- | vendor/rsync_command/lib/rsync_command/thread_pool.rb | 36 | ||||
-rw-r--r-- | vendor/rsync_command/lib/rsync_command/version.rb | 3 |
4 files changed, 294 insertions, 0 deletions
diff --git a/vendor/rsync_command/lib/rsync_command.rb b/vendor/rsync_command/lib/rsync_command.rb new file mode 100644 index 0000000..39e5945 --- /dev/null +++ b/vendor/rsync_command/lib/rsync_command.rb @@ -0,0 +1,96 @@ +require "rsync_command/version" +require "rsync_command/ssh_options" +require "rsync_command/thread_pool" + +require 'monitor' + +class RsyncCommand + attr_accessor :failures, :logger + + def initialize(options={}) + @options = options.dup + @logger = @options.delete(:logger) + @flags = @options.delete(:flags) + @failures = [] + @failures.extend(MonitorMixin) + end + + # + # takes an Enumerable and iterates each item in the list in parallel. + # + def asynchronously(array, &block) + pool = ThreadPool.new + array.each do |item| + pool.schedule(item, &block) + end + pool.shutdown + end + + # + # runs rsync, recording failures + # + def exec(src, dest, options={}) + @failures.synchronize do + @failures.clear + end + rsync_cmd = command(src, dest, options) + if options[:chdir] + rsync_cmd = "cd '#{options[:chdir]}'; #{rsync_cmd}" + end + @logger.debug rsync_cmd if @logger + ok = system(rsync_cmd) + unless ok + @failures.synchronize do + @failures << {:source => src, :dest => dest, :options => options.dup} + end + end + end + + # + # returns true if last exec returned a failure + # + def failed? + @failures && @failures.any? + end + + # + # build rsync command + # + def command(src, dest, options={}) + src = remote_address(src) + dest = remote_address(dest) + options = @options.merge(options) + flags = [] + flags << @flags if @flags + flags << options[:flags] if options.has_key?(:flags) + flags << '--delete' if options[:delete] + flags << includes(options[:includes]) if options.has_key?(:includes) + flags << excludes(options[:excludes]) if options.has_key?(:excludes) + flags << SshOptions.new(options[:ssh]).to_flags if options.has_key?(:ssh) + "rsync #{flags.compact.join(' ')} #{src} #{dest}" + end + + private + + # + # Creates an rsync location if the +address+ is a hash with keys :user, :host, and :path + # (each component is optional). If +address+ is a string, we just pass it through. + # + def remote_address(address) + if address.is_a? String + address # assume it is already formatted. + elsif address.is_a? Hash + [[address[:user], address[:host]].compact.join('@'), address[:path]].compact.join(':') + end + end + + def excludes(patterns) + [patterns].flatten.compact.map { |p| "--exclude='#{p}'" } + end + + def includes(patterns) + [patterns].flatten.compact.map { |p| "--include='#{p}'" } + end + +end + diff --git a/vendor/rsync_command/lib/rsync_command/ssh_options.rb b/vendor/rsync_command/lib/rsync_command/ssh_options.rb new file mode 100644 index 0000000..494ec9d --- /dev/null +++ b/vendor/rsync_command/lib/rsync_command/ssh_options.rb @@ -0,0 +1,159 @@ +# +# Converts capistrano-style ssh configuration (which uses Net::SSH) into a OpenSSH command line flags suitable for rsync. +# +# For a list of the options normally support by Net::SSH (and thus Capistrano), see +# http://net-ssh.github.com/net-ssh/classes/Net/SSH.html#method-c-start +# +# Also, to see how Net::SSH does the opposite of the conversion we are doing here, check out: +# https://github.com/net-ssh/net-ssh/blob/master/lib/net/ssh/config.rb +# +# API mismatch: +# +# * many OpenSSH options not supported +# * some options only make sense for Net::SSH +# * compression: for Net::SSH, this option is supposed to accept true, false, or algorithm. OpenSSH accepts 'yes' or 'no' +# +class RsyncCommand + class SshOptions + + def initialize(options={}) + @options = parse_options(options) + end + + def to_flags + if @options.empty? + nil + else + %[-e "ssh #{@options.join(' ')}"] + end + end + + private + + def parse_options(options) + options.map do |key, value| + next unless value + # Convert Net::SSH options into OpenSSH options. + case key + when :auth_methods then opt_auth_methods(value) + when :bind_address then opt('BindAddress', value) + when :compression then opt('Compression', value ? 'yes' : 'no') + when :compression_level then opt('CompressionLevel', value.to_i) + when :config then "-F '#{value}'" + when :encryption then opt('Ciphers', [value].flatten.join(',')) + when :forward_agent then opt('ForwardAgent', value) + when :global_known_hosts_file then opt('GlobalKnownHostsFile', value) + when :hmac then opt('MACs', [value].flatten.join(',')) + when :host_key then opt('HostKeyAlgorithms', [value].flatten.join(',')) + when :host_key_alias then opt('HostKeyAlias', value) + when :host_name then opt('HostName', value) + when :kex then opt('KexAlgorithms', [value].flatten.join(',')) + when :key_data then nil # not supported + when :keys then [value].flatten.select { |k| File.exist?(k) }.map { |k| "-i '#{k}'" } + when :keys_only then opt('IdentitiesOnly', value ? 'yes' : 'no') + when :languages then nil # not applicable + when :logger then nil # not applicable + when :paranoid then opt('StrictHostKeyChecking', value ? 'yes' : 'no') + when :passphrase then nil # not supported + when :password then nil # not supported + when :port then "-p #{value.to_i}" + when :properties then nil # not applicable + when :proxy then nil # not applicable + when :rekey_blocks_limit then nil # not supported + when :rekey_limit then opt('RekeyLimit', reverse_interpret_size(value)) + when :rekey_packet_limit then nil # not supported + when :timeout then opt('ConnectTimeout', value.to_i) + when :user then "-l #{value}" + when :user_known_hosts_file then multi_opt('UserKnownHostsFile', value) + when :verbose then opt('LogLevel', interpret_log_level(value)) + end + end.compact + end + + private + + def opt(option_name, option_value) + "-o #{option_name}='#{option_value}'" + end + + def multi_opt(option_name, option_values) + [option_values].flatten.map do |value| + opt(option_name, value) + end.join(' ') + end + + # + # In OpenSSH, password and pubkey default to 'yes', hostbased defaults to 'no'. + # Regardless, if :auth_method is configured, then we explicitly set the auth method. + # + def opt_auth_methods(value) + value = [value].flatten + opts = [] + if value.any? + if value.include? 'password' + opts << opt('PasswordAuthentication', 'yes') + else + opts << opt('PasswordAuthentication', 'no') + end + if value.include? 'publickey' + opts << opt('PubkeyAuthentication', 'yes') + else + opts << opt('PubkeyAuthentication', 'no') + end + if value.include? 'hostbased' + opts << opt('HostbasedAuthentication', 'yes') + else + opts << opt('HostbasedAuthentication', 'no') + end + end + if opts.any? + return opts.join(' ') + else + nil + end + end + + # + # Converts the given integer size in bytes into a string with 'K', 'M', 'G' suffix, as appropriate. + # + # reverse of interpret_size in https://github.com/net-ssh/net-ssh/blob/master/lib/net/ssh/config.rb + # + def reverse_interpret_size(size) + size = size.to_i + if size < 1024 + "#{size}" + elsif size < 1024 * 1024 + "#{size/1024}K" + elsif size < 1024 * 1024 * 1024 + "#{size/(1024*1024)}M" + else + "#{size/(1024*1024*1024)}G" + end + end + + def interpret_log_level(level) + if level.is_a? Symbol + case level + when :debug then "DEBUG" + when :info then "INFO" + when :warn then "ERROR" + when :error then "ERROR" + when :fatal then "FATAL" + else "INFO" + end + elsif level.is_a?(Integer) && defined?(Logger) + case level + when Logger::DEBUG then "DEBUG" + when Logger::INFO then "INFO" + when Logger::WARN then "ERROR" + when Logger::ERROR then "ERROR" + when Logger::FATAL then "FATAL" + else "INFO" + end + else + "INFO" + end + end + + end +end diff --git a/vendor/rsync_command/lib/rsync_command/thread_pool.rb b/vendor/rsync_command/lib/rsync_command/thread_pool.rb new file mode 100644 index 0000000..c788ee2 --- /dev/null +++ b/vendor/rsync_command/lib/rsync_command/thread_pool.rb @@ -0,0 +1,36 @@ +require 'thread' + +class RsyncCommand + class ThreadPool + class << self + attr_accessor :default_size + end + + def initialize(size=nil) + @size = size || ThreadPool.default_size || 10 + @jobs = Queue.new + @retvals = [] + @pool = Array.new(@size) do |i| + Thread.new do + Thread.current[:id] = i + catch(:exit) do + loop do + job, args = @jobs.pop + @retvals << job.call(*args) + end + end + end + end + end + def schedule(*args, &block) + @jobs << [block, args] + end + def shutdown + @size.times do + schedule { throw :exit } + end + @pool.map(&:join) + @retvals + end + end +end diff --git a/vendor/rsync_command/lib/rsync_command/version.rb b/vendor/rsync_command/lib/rsync_command/version.rb new file mode 100644 index 0000000..654a308 --- /dev/null +++ b/vendor/rsync_command/lib/rsync_command/version.rb @@ -0,0 +1,3 @@ +class RsyncCommand + VERSION = "0.0.1" +end |