capissh 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/LICENSE +18 -0
- data/README.md +37 -0
- data/lib/capissh.rb +43 -0
- data/lib/capissh/command.rb +197 -0
- data/lib/capissh/command/tree.rb +138 -0
- data/lib/capissh/configuration.rb +65 -0
- data/lib/capissh/connection_manager.rb +250 -0
- data/lib/capissh/errors.rb +19 -0
- data/lib/capissh/file_transfers.rb +54 -0
- data/lib/capissh/invocation.rb +278 -0
- data/lib/capissh/logger.rb +157 -0
- data/lib/capissh/processable.rb +54 -0
- data/lib/capissh/server_definition.rb +111 -0
- data/lib/capissh/ssh.rb +110 -0
- data/lib/capissh/transfer.rb +218 -0
- data/lib/capissh/version.rb +3 -0
- metadata +204 -0
@@ -0,0 +1,157 @@
|
|
1
|
+
module Capissh
|
2
|
+
class Logger
|
3
|
+
attr_accessor :level, :device, :disable_formatters
|
4
|
+
|
5
|
+
IMPORTANT = 0
|
6
|
+
INFO = 1
|
7
|
+
DEBUG = 2
|
8
|
+
TRACE = 3
|
9
|
+
|
10
|
+
MAX_LEVEL = 3
|
11
|
+
|
12
|
+
COLORS = {
|
13
|
+
:none => "0",
|
14
|
+
:black => "30",
|
15
|
+
:red => "31",
|
16
|
+
:green => "32",
|
17
|
+
:yellow => "33",
|
18
|
+
:blue => "34",
|
19
|
+
:magenta => "35",
|
20
|
+
:cyan => "36",
|
21
|
+
:white => "37"
|
22
|
+
}
|
23
|
+
|
24
|
+
STYLES = {
|
25
|
+
:bright => 1,
|
26
|
+
:dim => 2,
|
27
|
+
:underscore => 4,
|
28
|
+
:blink => 5,
|
29
|
+
:reverse => 7,
|
30
|
+
:hidden => 8
|
31
|
+
}
|
32
|
+
|
33
|
+
# Set up default formatters
|
34
|
+
@formatters = [
|
35
|
+
# TRACE
|
36
|
+
{ :match => /command finished/, :color => :white, :style => :dim, :level => 3, :priority => -10 },
|
37
|
+
{ :match => /executing locally/, :color => :yellow, :level => 3, :priority => -20 },
|
38
|
+
|
39
|
+
# DEBUG
|
40
|
+
{ :match => /executing `.*/, :color => :green, :level => 2, :priority => -10, :timestamp => true },
|
41
|
+
{ :match => /.*/, :color => :yellow, :level => 2, :priority => -30 },
|
42
|
+
|
43
|
+
# INFO
|
44
|
+
{ :match => /.*out\] (fatal:|ERROR:).*/, :color => :red, :level => 1, :priority => -10 },
|
45
|
+
{ :match => /Permission denied/, :color => :red, :level => 1, :priority => -20 },
|
46
|
+
{ :match => /sh: .+: command not found/, :color => :magenta, :level => 1, :priority => -30 },
|
47
|
+
|
48
|
+
# IMPORTANT
|
49
|
+
{ :match => /^err ::/, :color => :red, :level => 0, :priority => -10 },
|
50
|
+
{ :match => /.*/, :color => :blue, :level => 0, :priority => -20 }
|
51
|
+
]
|
52
|
+
|
53
|
+
class << self
|
54
|
+
def add_formatter(options)
|
55
|
+
@formatters.push(options)
|
56
|
+
@sorted_formatters = nil
|
57
|
+
end
|
58
|
+
|
59
|
+
def sorted_formatters
|
60
|
+
# Sort matchers in reverse order so we can break if we found a match.
|
61
|
+
@sorted_formatters ||= @formatters.sort_by { |i| -(i[:priority] || i[:prio] || 0) }
|
62
|
+
end
|
63
|
+
|
64
|
+
def default
|
65
|
+
@default ||= new(:level => 3)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def initialize(options={})
|
70
|
+
output = options[:output] || $stderr
|
71
|
+
if output.respond_to?(:puts)
|
72
|
+
@device = output
|
73
|
+
else
|
74
|
+
@device = File.open(output.to_str, "a")
|
75
|
+
@needs_close = true
|
76
|
+
end
|
77
|
+
|
78
|
+
@options = options
|
79
|
+
@level = options[:level] || 0
|
80
|
+
@disable_formatters = options[:disable_formatters]
|
81
|
+
end
|
82
|
+
|
83
|
+
def close
|
84
|
+
device.close if @needs_close
|
85
|
+
end
|
86
|
+
|
87
|
+
def log(level, message, line_prefix=nil)
|
88
|
+
if level <= self.level
|
89
|
+
# Only format output if device is a TTY and formatters are not disabled
|
90
|
+
if device.tty? && !@disable_formatters
|
91
|
+
color = :none
|
92
|
+
style = nil
|
93
|
+
|
94
|
+
Logger.sorted_formatters.each do |formatter|
|
95
|
+
if (formatter[:level] == level || formatter[:level].nil?)
|
96
|
+
if message =~ formatter[:match] || line_prefix =~ formatter[:match]
|
97
|
+
color = formatter[:color] if formatter[:color]
|
98
|
+
style = formatter[:style] || formatter[:attribute] # (support original cap colors)
|
99
|
+
message.gsub!(formatter[:match], formatter[:replace]) if formatter[:replace]
|
100
|
+
message = formatter[:prepend] + message unless formatter[:prepend].nil?
|
101
|
+
message = message + formatter[:append] unless formatter[:append].nil?
|
102
|
+
message = Time.now.strftime('%Y-%m-%d %T') + ' ' + message if formatter[:timestamp]
|
103
|
+
break unless formatter[:replace]
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
if color == :hide
|
109
|
+
# Don't do anything if color is set to :hide
|
110
|
+
return false
|
111
|
+
end
|
112
|
+
|
113
|
+
term_color = COLORS[color]
|
114
|
+
term_style = STYLES[style]
|
115
|
+
|
116
|
+
# Don't format message if no color or style
|
117
|
+
unless color == :none and style.nil?
|
118
|
+
unless line_prefix.nil?
|
119
|
+
line_prefix = format(line_prefix, term_color, term_style, nil)
|
120
|
+
end
|
121
|
+
message = format(message, term_color, term_style)
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
indent = "%*s" % [MAX_LEVEL, "*" * (MAX_LEVEL - level)]
|
126
|
+
(RUBY_VERSION >= "1.9" ? message.lines : message).each do |line|
|
127
|
+
if line_prefix
|
128
|
+
device.puts "#{indent} [#{line_prefix}] #{line.strip}\n"
|
129
|
+
else
|
130
|
+
device.puts "#{indent} #{line.strip}\n"
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
def important(message, line_prefix=nil)
|
137
|
+
log(IMPORTANT, message, line_prefix)
|
138
|
+
end
|
139
|
+
|
140
|
+
def info(message, line_prefix=nil)
|
141
|
+
log(INFO, message, line_prefix)
|
142
|
+
end
|
143
|
+
|
144
|
+
def debug(message, line_prefix=nil)
|
145
|
+
log(DEBUG, message, line_prefix)
|
146
|
+
end
|
147
|
+
|
148
|
+
def trace(message, line_prefix=nil)
|
149
|
+
log(TRACE, message, line_prefix)
|
150
|
+
end
|
151
|
+
|
152
|
+
def format(message, color, style, nl = "\n")
|
153
|
+
style = "#{style};" if style
|
154
|
+
"\e[#{style}#{color}m" + message.to_s.strip + "\e[0m#{nl}"
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
module Capissh
|
2
|
+
module Processable
|
3
|
+
module SessionAssociation
|
4
|
+
def self.on(exception, session)
|
5
|
+
unless exception.respond_to?(:session)
|
6
|
+
exception.extend(self)
|
7
|
+
exception.session = session
|
8
|
+
end
|
9
|
+
|
10
|
+
return exception
|
11
|
+
end
|
12
|
+
|
13
|
+
attr_accessor :session
|
14
|
+
end
|
15
|
+
|
16
|
+
# perform ssh session looping with reading/writing until the block returns false
|
17
|
+
def process_iteration(wait=nil, &block)
|
18
|
+
ensure_each_session { |session| session.preprocess }
|
19
|
+
|
20
|
+
return false if block && !block.call(self)
|
21
|
+
|
22
|
+
readers = sessions.map { |session| session.listeners.keys }.flatten.reject { |io| io.closed? }
|
23
|
+
writers = readers.select { |io| io.respond_to?(:pending_write?) && io.pending_write? }
|
24
|
+
|
25
|
+
if readers.any? || writers.any?
|
26
|
+
readers, writers, = IO.select(readers, writers, nil, wait)
|
27
|
+
end
|
28
|
+
|
29
|
+
if readers
|
30
|
+
ensure_each_session do |session|
|
31
|
+
ios = session.listeners.keys
|
32
|
+
session.postprocess(ios & readers, ios & writers)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
true
|
37
|
+
end
|
38
|
+
|
39
|
+
def ensure_each_session
|
40
|
+
errors = []
|
41
|
+
|
42
|
+
sessions.each do |session|
|
43
|
+
begin
|
44
|
+
yield session
|
45
|
+
rescue Exception => error
|
46
|
+
errors << SessionAssociation.on(error, session)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
raise errors.first if errors.any?
|
51
|
+
sessions
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,111 @@
|
|
1
|
+
module Capissh
|
2
|
+
class ServerDefinition
|
3
|
+
include Comparable
|
4
|
+
|
5
|
+
# Wraps a string in a ServerDefinition, if it isn't already.
|
6
|
+
def self.wrap_server(item, options)
|
7
|
+
item.is_a?(ServerDefinition) ? item : ServerDefinition.new(item, options)
|
8
|
+
end
|
9
|
+
|
10
|
+
# Turns a list, or something resembling a list, into a properly-formatted
|
11
|
+
# ServerDefinition list. Keep an eye on this one -- it's entirely too
|
12
|
+
# magical for its own good. In particular, if ServerDefinition ever inherits
|
13
|
+
# from Array, this will break.
|
14
|
+
def self.wrap_list(*list)
|
15
|
+
options = list.last.is_a?(Hash) ? list.pop : {}
|
16
|
+
if list.length == 1
|
17
|
+
if list.first.nil?
|
18
|
+
return []
|
19
|
+
elsif list.first.is_a?(Array)
|
20
|
+
list = list.first
|
21
|
+
end
|
22
|
+
end
|
23
|
+
options.merge! list.pop if list.last.is_a?(Hash)
|
24
|
+
list.map do |item|
|
25
|
+
self.wrap_server item, options
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
# The default user name to use when a user name is not explicitly provided
|
30
|
+
def self.default_user
|
31
|
+
ENV['USER'] || ENV['USERNAME'] || "not-specified"
|
32
|
+
end
|
33
|
+
|
34
|
+
attr_reader :user, :host, :port, :options, :server
|
35
|
+
|
36
|
+
# Initialize a ServerDefinition with a string or object that describes the
|
37
|
+
# authority URI part, "user@host:port", for connecting with SSH.
|
38
|
+
#
|
39
|
+
# Any object that responds to the following methods, in order of priority,
|
40
|
+
# can be used in a ServerDefinition:
|
41
|
+
#
|
42
|
+
# 1. #host, #user (optional), and #port (optional, default: 22)
|
43
|
+
# 2. #authority - responding with something like "[user@]host[:port]"
|
44
|
+
# 3. #to_s - responding with something like "[user@]host[:port]"
|
45
|
+
#
|
46
|
+
# If options are passed for the second argument, certain keys will be used:
|
47
|
+
#
|
48
|
+
# * :user - sets the user if one was not given in the authority
|
49
|
+
# * :port - sets the port if one was not given in the authority
|
50
|
+
# * :ssh_options - used for connecting with Net::SSH
|
51
|
+
def initialize(server, options={})
|
52
|
+
@server = server
|
53
|
+
|
54
|
+
if @server.respond_to?(:host)
|
55
|
+
@host = @server.host
|
56
|
+
@port = @server.port if @server.respond_to?(:port)
|
57
|
+
@user = @server.user if @server.respond_to?(:user)
|
58
|
+
else
|
59
|
+
if @server.respond_to?(:authority)
|
60
|
+
string = @server.authority
|
61
|
+
elsif @server.respond_to?(:to_s)
|
62
|
+
string = @server.to_s
|
63
|
+
else
|
64
|
+
raise ArgumentError, "Invalid server for ServerDefinition: #{@server.inspect}. Must respond to #host, #authority, or #to_s"
|
65
|
+
end
|
66
|
+
@user, @host, @port = string.match(/^(?:([^;,:=]+)@|)(.*?)(?::(\d+)|)$/)[1,3]
|
67
|
+
end
|
68
|
+
|
69
|
+
@options = options.dup
|
70
|
+
user_opt, port_opt = @options.delete(:user), @options.delete(:port)
|
71
|
+
|
72
|
+
@user ||= user_opt
|
73
|
+
@port ||= port_opt
|
74
|
+
|
75
|
+
@port = @port.to_i if @port
|
76
|
+
end
|
77
|
+
|
78
|
+
def <=>(server)
|
79
|
+
[host, port, user] <=> [server.host, server.port, server.user]
|
80
|
+
end
|
81
|
+
|
82
|
+
# Redefined, so that Array#uniq will work to remove duplicate server
|
83
|
+
# definitions, based solely on their host names.
|
84
|
+
def eql?(server)
|
85
|
+
host == server.host &&
|
86
|
+
user == server.user &&
|
87
|
+
port == server.port
|
88
|
+
end
|
89
|
+
|
90
|
+
alias :== :eql?
|
91
|
+
|
92
|
+
# Redefined, so that Array#uniq will work to remove duplicate server
|
93
|
+
# definitions, based on their connection information.
|
94
|
+
def hash
|
95
|
+
@hash ||= [host, user, port].hash
|
96
|
+
end
|
97
|
+
|
98
|
+
def connect_to_port
|
99
|
+
port || 22
|
100
|
+
end
|
101
|
+
|
102
|
+
def to_s
|
103
|
+
@to_s ||= begin
|
104
|
+
s = host
|
105
|
+
s = "#{user}@#{s}" if user
|
106
|
+
s = "#{s}:#{port}" if port && port != 22
|
107
|
+
s
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
data/lib/capissh/ssh.rb
ADDED
@@ -0,0 +1,110 @@
|
|
1
|
+
require 'net/ssh'
|
2
|
+
|
3
|
+
module Capissh
|
4
|
+
# A helper class for dealing with SSH connections.
|
5
|
+
class SSH
|
6
|
+
# Patch an accessor onto an SSH connection so that we can record the server
|
7
|
+
# definition object that defines the connection. This is useful because
|
8
|
+
# the gateway returns connections whose "host" is 127.0.0.1, instead of
|
9
|
+
# the host on the other side of the tunnel.
|
10
|
+
module Server
|
11
|
+
def self.apply_to(connection, server)
|
12
|
+
connection.extend(Server)
|
13
|
+
connection.xserver = server
|
14
|
+
connection
|
15
|
+
end
|
16
|
+
|
17
|
+
attr_accessor :xserver
|
18
|
+
end
|
19
|
+
|
20
|
+
# An abstraction to make it possible to connect to the server via public key
|
21
|
+
# without prompting for the password. If the public key authentication fails
|
22
|
+
# this will fall back to password authentication.
|
23
|
+
#
|
24
|
+
# +server+ must be an instance of ServerDefinition.
|
25
|
+
#
|
26
|
+
# If a block is given, the new session is yielded to it, otherwise the new
|
27
|
+
# session is returned.
|
28
|
+
#
|
29
|
+
# If an :ssh_options key exists in +options+, it is passed to the Net::SSH
|
30
|
+
# constructor. Values in +options+ are then merged into it, and any
|
31
|
+
# connection information in +server+ is added last, so that +server+ info
|
32
|
+
# takes precedence over +options+, which takes precendence over ssh_options.
|
33
|
+
def self.connect(server, options={})
|
34
|
+
connection_strategy(server, options) do |host, user, connection_options|
|
35
|
+
connection = Net::SSH.start(host, user, connection_options)
|
36
|
+
Server.apply_to(connection, server)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def self.gateway(gateway, options={})
|
41
|
+
connection_strategy(gateway, options) do |host, user, connection_options|
|
42
|
+
connection = Net::SSH::Gateway.new(host, user, connection_options)
|
43
|
+
Server.apply_to(connection, gateway)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
# Abstracts the logic for establishing an SSH connection (which includes
|
48
|
+
# testing for connection failures and retrying with a password, and so forth,
|
49
|
+
# mostly made complicated because of the fact that some of these variables
|
50
|
+
# might be lazily evaluated and try to do something like prompt the user,
|
51
|
+
# which should only happen when absolutely necessary.
|
52
|
+
#
|
53
|
+
# This will yield the hostname, username, and a hash of connection options
|
54
|
+
# to the given block, which should return a new connection.
|
55
|
+
def self.connection_strategy(server, options={}, &block)
|
56
|
+
auth_methods = [ %w(publickey hostbased), %w(password keyboard-interactive) ]
|
57
|
+
password_value = nil
|
58
|
+
|
59
|
+
# construct the hash of ssh options that should be passed more-or-less
|
60
|
+
# directly to Net::SSH. This will be the general ssh options, merged with
|
61
|
+
# the server-specific ssh-options.
|
62
|
+
input_ssh_options = (options[:ssh_options] || {}).merge(server.options[:ssh_options] || {})
|
63
|
+
|
64
|
+
# load any SSH configuration files that were specified in the SSH options. This
|
65
|
+
# will load from ~/.ssh/config and /etc/ssh_config by default (see Net::SSH
|
66
|
+
# for details). Merge the explicitly given ssh_options over the top of the info
|
67
|
+
# from the config file.
|
68
|
+
ssh_options = Net::SSH.configuration_for(server.host, input_ssh_options.fetch(:config, true)).merge(input_ssh_options)
|
69
|
+
|
70
|
+
# Once we've loaded the config, we don't need Net::SSH to do it again.
|
71
|
+
ssh_options[:config] = false
|
72
|
+
|
73
|
+
ssh_options[:verbose] = :debug if options[:verbose] && options[:verbose] > 0
|
74
|
+
|
75
|
+
user =
|
76
|
+
server.user ||
|
77
|
+
options[:user] ||
|
78
|
+
ssh_options[:username] ||
|
79
|
+
ssh_options[:user] ||
|
80
|
+
ServerDefinition.default_user
|
81
|
+
|
82
|
+
port =
|
83
|
+
server.port ||
|
84
|
+
options[:port] ||
|
85
|
+
ssh_options[:port]
|
86
|
+
|
87
|
+
# the .ssh/config file might have changed the host-name on us
|
88
|
+
host = ssh_options.fetch(:host_name, server.host)
|
89
|
+
|
90
|
+
ssh_options[:port] = port if port
|
91
|
+
|
92
|
+
# delete these, since we've determined which username to use by this point
|
93
|
+
ssh_options.delete(:username)
|
94
|
+
ssh_options.delete(:user)
|
95
|
+
|
96
|
+
begin
|
97
|
+
connection_options = ssh_options.merge(
|
98
|
+
:password => password_value,
|
99
|
+
:auth_methods => ssh_options[:auth_methods] || auth_methods.shift
|
100
|
+
)
|
101
|
+
|
102
|
+
yield host, user, connection_options
|
103
|
+
rescue Net::SSH::AuthenticationFailed
|
104
|
+
raise if auth_methods.empty? || ssh_options[:auth_methods]
|
105
|
+
password_value = options[:password]
|
106
|
+
retry
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
@@ -0,0 +1,218 @@
|
|
1
|
+
require 'net/scp'
|
2
|
+
require 'net/sftp'
|
3
|
+
|
4
|
+
require 'capissh/processable'
|
5
|
+
|
6
|
+
module Capissh
|
7
|
+
class Transfer
|
8
|
+
include Processable
|
9
|
+
|
10
|
+
def self.process(direction, from, to, sessions, options={}, &block)
|
11
|
+
new(direction, from, to, sessions, options, &block).process!
|
12
|
+
end
|
13
|
+
|
14
|
+
attr_reader :sessions
|
15
|
+
attr_reader :options
|
16
|
+
attr_reader :callback
|
17
|
+
|
18
|
+
attr_reader :transport
|
19
|
+
attr_reader :direction
|
20
|
+
attr_reader :from
|
21
|
+
attr_reader :to
|
22
|
+
|
23
|
+
attr_reader :logger
|
24
|
+
attr_reader :transfers
|
25
|
+
|
26
|
+
def initialize(direction, from, to, sessions, options={}, &block)
|
27
|
+
@direction = direction
|
28
|
+
@from = from
|
29
|
+
@to = to
|
30
|
+
@sessions = sessions
|
31
|
+
@options = options
|
32
|
+
@callback = block
|
33
|
+
|
34
|
+
@transport = options.fetch(:via, :sftp)
|
35
|
+
@logger = options.delete(:logger)
|
36
|
+
|
37
|
+
@session_map = {}
|
38
|
+
|
39
|
+
prepare_transfers
|
40
|
+
end
|
41
|
+
|
42
|
+
def process!
|
43
|
+
loop do
|
44
|
+
begin
|
45
|
+
break unless process_iteration { active? }
|
46
|
+
rescue Exception => error
|
47
|
+
if error.respond_to?(:session)
|
48
|
+
handle_error(error)
|
49
|
+
else
|
50
|
+
raise
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
failed = transfers.select { |txfr| txfr[:failed] }
|
56
|
+
if failed.any?
|
57
|
+
hosts = failed.map { |txfr| txfr[:server] }
|
58
|
+
errors = failed.map { |txfr| "#{txfr[:error]} (#{txfr[:error].message})" }.uniq.join(", ")
|
59
|
+
error = TransferError.new("#{operation} via #{transport} failed on #{hosts.join(',')}: #{errors}")
|
60
|
+
error.hosts = hosts
|
61
|
+
|
62
|
+
logger.important(error.message) if logger
|
63
|
+
raise error
|
64
|
+
end
|
65
|
+
|
66
|
+
logger.debug "#{transport} #{operation} complete" if logger
|
67
|
+
self
|
68
|
+
end
|
69
|
+
|
70
|
+
def active?
|
71
|
+
transfers.any? { |transfer| transfer.active? }
|
72
|
+
end
|
73
|
+
|
74
|
+
def operation
|
75
|
+
"#{direction}load"
|
76
|
+
end
|
77
|
+
|
78
|
+
def sanitized_from
|
79
|
+
if from.responds_to?(:read)
|
80
|
+
"#<#{from.class}>"
|
81
|
+
else
|
82
|
+
from
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def sanitized_to
|
87
|
+
if to.responds_to?(:read)
|
88
|
+
"#<#{to.class}>"
|
89
|
+
else
|
90
|
+
to
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
private
|
95
|
+
|
96
|
+
def session_map
|
97
|
+
@session_map
|
98
|
+
end
|
99
|
+
|
100
|
+
def prepare_transfers
|
101
|
+
logger.info "#{transport} #{operation} #{from} -> #{to}" if logger
|
102
|
+
|
103
|
+
@transfers = sessions.map do |session|
|
104
|
+
session_from = normalize(from, session)
|
105
|
+
session_to = normalize(to, session)
|
106
|
+
|
107
|
+
session_map[session] = case transport
|
108
|
+
when :sftp
|
109
|
+
prepare_sftp_transfer(session_from, session_to, session)
|
110
|
+
when :scp
|
111
|
+
prepare_scp_transfer(session_from, session_to, session)
|
112
|
+
else
|
113
|
+
raise ArgumentError, "unsupported transport type: #{transport.inspect}"
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
def prepare_scp_transfer(from, to, session)
|
119
|
+
real_callback = callback || Proc.new do |channel, name, sent, total|
|
120
|
+
logger.trace "[#{channel[:host]}] #{name}" if logger && sent == 0
|
121
|
+
end
|
122
|
+
|
123
|
+
channel = case direction
|
124
|
+
when :up
|
125
|
+
session.scp.upload(from, to, options, &real_callback)
|
126
|
+
when :down
|
127
|
+
session.scp.download(from, to, options, &real_callback)
|
128
|
+
else
|
129
|
+
raise ArgumentError, "unsupported transfer direction: #{direction.inspect}"
|
130
|
+
end
|
131
|
+
|
132
|
+
channel[:server] = session.xserver
|
133
|
+
channel[:host] = session.xserver.host
|
134
|
+
|
135
|
+
return channel
|
136
|
+
end
|
137
|
+
|
138
|
+
class SFTPTransferWrapper
|
139
|
+
attr_reader :operation
|
140
|
+
|
141
|
+
def initialize(session, &callback)
|
142
|
+
session.sftp(false).connect do |sftp|
|
143
|
+
@operation = callback.call(sftp)
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
def active?
|
148
|
+
@operation.nil? || @operation.active?
|
149
|
+
end
|
150
|
+
|
151
|
+
def [](key)
|
152
|
+
@operation[key]
|
153
|
+
end
|
154
|
+
|
155
|
+
def []=(key, value)
|
156
|
+
@operation[key] = value
|
157
|
+
end
|
158
|
+
|
159
|
+
def abort!
|
160
|
+
@operation.abort!
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
def prepare_sftp_transfer(from, to, session)
|
165
|
+
SFTPTransferWrapper.new(session) do |sftp|
|
166
|
+
real_callback = Proc.new do |event, op, *args|
|
167
|
+
if callback
|
168
|
+
callback.call(event, op, *args)
|
169
|
+
elsif event == :open
|
170
|
+
logger.trace "[#{op[:host]}] #{args[0].remote}"
|
171
|
+
elsif event == :finish
|
172
|
+
logger.trace "[#{op[:host]}] done"
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
opts = options.dup
|
177
|
+
opts[:properties] = (opts[:properties] || {}).merge(
|
178
|
+
:server => session.xserver,
|
179
|
+
:host => session.xserver.host)
|
180
|
+
|
181
|
+
case direction
|
182
|
+
when :up
|
183
|
+
sftp.upload(from, to, opts, &real_callback)
|
184
|
+
when :down
|
185
|
+
sftp.download(from, to, opts, &real_callback)
|
186
|
+
else
|
187
|
+
raise ArgumentError, "unsupported transfer direction: #{direction.inspect}"
|
188
|
+
end
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
192
|
+
def normalize(argument, session)
|
193
|
+
if argument.is_a?(String)
|
194
|
+
argument.gsub(/\$CAPISSH:HOST\$/, session.xserver.host)
|
195
|
+
elsif argument.respond_to?(:read)
|
196
|
+
pos = argument.pos
|
197
|
+
clone = StringIO.new(argument.read)
|
198
|
+
clone.pos = argument.pos = pos
|
199
|
+
clone
|
200
|
+
else
|
201
|
+
argument
|
202
|
+
end
|
203
|
+
end
|
204
|
+
|
205
|
+
def handle_error(error)
|
206
|
+
raise error if error.message.include?('expected a file to upload')
|
207
|
+
|
208
|
+
transfer = session_map[error.session]
|
209
|
+
transfer[:error] = error
|
210
|
+
transfer[:failed] = true
|
211
|
+
|
212
|
+
case transport
|
213
|
+
when :sftp then transfer.abort!
|
214
|
+
when :scp then transfer.close
|
215
|
+
end
|
216
|
+
end
|
217
|
+
end
|
218
|
+
end
|