sshkit 1.18.0 → 1.23.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (52) hide show
  1. checksums.yaml +4 -4
  2. data/.docker/Dockerfile +6 -0
  3. data/.docker/ubuntu_setup.sh +22 -0
  4. data/.github/dependabot.yml +16 -0
  5. data/.github/release-drafter.yml +25 -0
  6. data/.github/workflows/ci.yml +98 -0
  7. data/.github/workflows/push.yml +12 -0
  8. data/.gitignore +0 -1
  9. data/.rubocop.yml +63 -0
  10. data/.rubocop_todo.yml +630 -0
  11. data/CHANGELOG.md +1 -769
  12. data/CONTRIBUTING.md +2 -2
  13. data/Dangerfile +1 -1
  14. data/EXAMPLES.md +35 -13
  15. data/Gemfile +0 -12
  16. data/README.md +35 -17
  17. data/RELEASING.md +4 -5
  18. data/Rakefile +3 -10
  19. data/docker-compose.yml +8 -0
  20. data/examples/simple_connection.rb +9 -0
  21. data/lib/sshkit/backends/abstract.rb +9 -6
  22. data/lib/sshkit/backends/connection_pool/cache.rb +12 -5
  23. data/lib/sshkit/backends/connection_pool.rb +8 -4
  24. data/lib/sshkit/backends/netssh/known_hosts.rb +8 -8
  25. data/lib/sshkit/backends/netssh/scp_transfer.rb +26 -0
  26. data/lib/sshkit/backends/netssh/sftp_transfer.rb +46 -0
  27. data/lib/sshkit/backends/netssh.rb +38 -8
  28. data/lib/sshkit/command.rb +18 -13
  29. data/lib/sshkit/deprecation_logger.rb +2 -0
  30. data/lib/sshkit/host.rb +31 -4
  31. data/lib/sshkit/version.rb +1 -1
  32. data/sshkit.gemspec +6 -1
  33. data/test/functional/backends/netssh_transfer_tests.rb +83 -0
  34. data/test/functional/backends/test_local.rb +18 -0
  35. data/test/functional/backends/test_netssh.rb +24 -79
  36. data/test/functional/backends/test_netssh_scp.rb +23 -0
  37. data/test/functional/backends/test_netssh_sftp.rb +23 -0
  38. data/test/helper.rb +5 -43
  39. data/test/support/docker_wrapper.rb +71 -0
  40. data/test/unit/backends/test_abstract.rb +13 -1
  41. data/test/unit/backends/test_netssh.rb +55 -0
  42. data/test/unit/formatters/test_pretty.rb +1 -1
  43. data/test/unit/test_command.rb +32 -7
  44. data/test/unit/test_command_map.rb +8 -8
  45. data/test/unit/test_deprecation_logger.rb +1 -1
  46. data/test/unit/test_host.rb +44 -0
  47. metadata +58 -18
  48. data/.travis.yml +0 -14
  49. data/Vagrantfile +0 -15
  50. data/test/boxes.json +0 -17
  51. data/test/functional/test_ssh_server_comes_up_for_functional_tests.rb +0 -24
  52. data/test/support/vagrant_wrapper.rb +0 -55
data/CONTRIBUTING.md CHANGED
@@ -24,8 +24,8 @@ using unsupported features.
24
24
 
25
25
  ## Tests
26
26
 
27
- SSHKit has a unit test suite and a functional test suite. Some functional tests run against
28
- [Vagrant](https://www.vagrantup.com/) VMs. If possible, you should make sure that the
27
+ SSHKit has a unit test suite and a functional test suite. Some functional tests run using
28
+ [Docker](https://docs.docker.com/get-docker/). If possible, you should make sure that the
29
29
  tests pass for each commit by running `rake` in the sshkit directory. This is in case we
30
30
  need to cherry pick commits or rebase. You should ensure the tests pass, (preferably on
31
31
  the minimum and maximum ruby version), before creating a PR.
data/Dangerfile CHANGED
@@ -1 +1 @@
1
- danger.import_dangerfile(github: "capistrano/danger")
1
+ danger.import_dangerfile(github: "capistrano/danger", branch: "no-changelog")
data/EXAMPLES.md CHANGED
@@ -121,9 +121,6 @@ on hosts do |host|
121
121
  end
122
122
  ```
123
123
 
124
- **Note:** The `upload!()` method doesn't honor the values of `as()` etc, this
125
- will be improved as the library matures, but we're not there yet.
126
-
127
124
  ## Upload a file from a stream
128
125
 
129
126
  ```ruby
@@ -148,9 +145,6 @@ end
148
145
  This spares one from having to figure out the correct escaping sequences for
149
146
  something like "echo(:cat, '...?...', '> /etc/sudoers.d/yolo')".
150
147
 
151
- **Note:** The `upload!()` method doesn't honor the values of `within()`, `as()`
152
- etc, this will be improved as the library matures, but we're not there yet.
153
-
154
148
  ## Upload a directory of files
155
149
 
156
150
  ```ruby
@@ -160,7 +154,25 @@ end
160
154
  ```
161
155
 
162
156
  In this case the `recursive: true` option mirrors the same options which are
163
- available to [`Net::{SCP,SFTP}`](http://net-ssh.github.io/net-scp/).
157
+ available to [`Net::SCP`](https://github.com/net-ssh/net-scp) and
158
+ [`Net::SFTP`](https://github.com/net-ssh/net-sftp).
159
+
160
+ ## Set the upload/download method (SCP or SFTP).
161
+
162
+ SSHKit can use SCP or SFTP for file transfers. The default is SCP, but this can be changed to SFTP per host:
163
+
164
+ ```ruby
165
+ host = SSHKit::Host.new('user@example.com')
166
+ host.transfer_method = :sftp
167
+ ```
168
+
169
+ or globally:
170
+
171
+ ```ruby
172
+ SSHKit::Backend::Netssh.configure do |ssh|
173
+ ssh.transfer_method = :sftp
174
+ end
175
+ ```
164
176
 
165
177
  ## Setting global SSH options
166
178
 
@@ -171,6 +183,7 @@ individual hosts:
171
183
  SSHKit::Backend::Netssh.configure do |ssh|
172
184
  ssh.connection_timeout = 30
173
185
  ssh.ssh_options = {
186
+ user: 'adifferentuser',
174
187
  keys: %w(/home/user/.ssh/id_rsa),
175
188
  forward_agent: false,
176
189
  auth_methods: %w(publickey password)
@@ -234,16 +247,16 @@ end
234
247
 
235
248
  ```ruby
236
249
  # The default format is pretty, which outputs colored text
237
- SSHKit.config.format = :pretty
250
+ SSHKit.config.use_format :pretty
238
251
 
239
252
  # Text with no coloring
240
- SSHKit.config.format = :simpletext
253
+ SSHKit.config.use_format :simpletext
241
254
 
242
255
  # Red / Green dots for each completed step
243
- SSHKit.config.format = :dot
256
+ SSHKit.config.use_format :dot
244
257
 
245
258
  # No output
246
- SSHKit.config.format = :blackhole
259
+ SSHKit.config.use_format :blackhole
247
260
  ```
248
261
 
249
262
  ## Implement a dirt-simple formatter class
@@ -253,7 +266,7 @@ module SSHKit
253
266
  module Formatter
254
267
  class MyFormatter < SSHKit::Formatter::Abstract
255
268
  def write(obj)
256
- case obj.is_a? SSHKit::Command
269
+ if obj.is_a? SSHKit::Command
257
270
  # Do something here, see the SSHKit::Command documentation
258
271
  end
259
272
  end
@@ -262,7 +275,7 @@ module SSHKit
262
275
  end
263
276
 
264
277
  # If your formatter is defined in the SSHKit::Formatter module configure with the format option:
265
- SSHKit.config.format = :myformatter
278
+ SSHKit.config.use_format :myformatter
266
279
 
267
280
  # Or configure the output directly
268
281
  SSHKit.config.output = MyFormatter.new($stdout)
@@ -338,6 +351,15 @@ end
338
351
  This will resolve the `example.com` hostname into a `SSHKit::Host` object, and
339
352
  try to pull up the correct configuration for it.
340
353
 
354
+ ## Connect to a host on a port different than 22
355
+
356
+ If your ssh server is running on a port different than 22, you can change this is
357
+ shown:
358
+
359
+ ```ruby
360
+ on('example.com', {port: 1234}) do
361
+ end
362
+ ```
341
363
 
342
364
  ## Run a command without it being command-mapped
343
365
 
data/Gemfile CHANGED
@@ -2,18 +2,6 @@ source 'https://rubygems.org'
2
2
 
3
3
  gemspec
4
4
 
5
- platforms :rbx do
6
- gem 'rubysl', '~> 2.0'
7
- gem 'json'
8
- end
9
-
10
- # Chandler requires Ruby >= 2.1.0, but depending on the Travis environment,
11
- # we may not meet that requirement. Only include the chandler gem if the Ruby
12
- # requirement is met. (Chandler is used only for `rake release`; see Rakefile.)
13
- if Gem::Requirement.new('>= 2.1.0').satisfied_by?(Gem::Version.new(RUBY_VERSION))
14
- gem 'chandler', '>= 0.1.1'
15
- end
16
-
17
5
  # public_suffix 3+ requires ruby 2.1+
18
6
  if Gem::Requirement.new('< 2.1').satisfied_by?(Gem::Version.new(RUBY_VERSION))
19
7
  gem 'public_suffix', '< 3'
data/README.md CHANGED
@@ -4,31 +4,33 @@
4
4
  more servers.
5
5
 
6
6
  [![Gem Version](https://badge.fury.io/rb/sshkit.svg)](https://rubygems.org/gems/sshkit)
7
- [![Build Status](https://travis-ci.org/capistrano/sshkit.svg?branch=master)](https://travis-ci.org/capistrano/sshkit)
7
+ [![Build Status](https://github.com/capistrano/sshkit/actions/workflows/ci.yml/badge.svg)](https://github.com/capistrano/sshkit/actions/workflows/ci.yml)
8
8
 
9
- ## How might it work?
9
+ ## Example
10
10
 
11
- The typical use-case looks something like this:
11
+ - Connect to 2 servers
12
+ - Execute commands as `deploy` user with `RAILS_ENV=production`
13
+ - Execute commands in serial (default is `:parallel`)
12
14
 
13
15
  ```ruby
14
16
  require 'sshkit'
15
17
  require 'sshkit/dsl'
16
18
  include SSHKit::DSL
17
19
 
18
- on %w{1.example.com 2.example.com}, in: :sequence, wait: 5 do |host|
20
+ on ["1.example.com", "2.example.com"], in: :sequence do |host|
21
+ puts "Now executing on #{host}"
19
22
  within "/opt/sites/example.com" do
20
23
  as :deploy do
21
- with rails_env: :production do
22
- rake "assets:precompile"
23
- runner "S3::Sync.notify"
24
- execute :node, "socket_server.js"
24
+ with RAILS_ENV: 'production' do
25
+ execute :rake, "assets:precompile"
26
+ execute :rails, "runner", "S3::Sync.notify"
25
27
  end
26
28
  end
27
29
  end
28
30
  end
29
31
  ```
30
32
 
31
- You can find many other examples of how to use SSHKit over in [EXAMPLES.md](EXAMPLES.md).
33
+ Many other examples are in [EXAMPLES.md](EXAMPLES.md).
32
34
 
33
35
  ## Basic usage
34
36
 
@@ -37,7 +39,7 @@ You can pass one or more hosts as parameters; this runs commands via SSH. Altern
37
39
  pass `:local` to run commands locally. By default SSKit will run the commands on all hosts in
38
40
  parallel.
39
41
 
40
- #### Running commands
42
+ ### Running commands
41
43
 
42
44
  All backends support the `execute(*args)`, `test(*args)` & `capture(*args)` methods
43
45
  for executing a command. You can call any of these methods in the context of an `on()`
@@ -63,19 +65,20 @@ end
63
65
  By default the `capture` methods strips whitespace. If you need to preserve whitespace
64
66
  you can pass the `strip: false` option: `capture(:ls, '-l', strip: false)`
65
67
 
66
- #### Transferring files
68
+ ### Transferring files
67
69
 
68
70
  All backends also support the `upload!` and `download!` methods for transferring files.
69
- For the remote backend, the file is transferred with scp.
71
+ For the remote backend, the file is transferred with scp by default, but sftp is also
72
+ supported. See [EXAMPLES.md](EXAMPLES.md) for details.
70
73
 
71
74
  ```ruby
72
75
  on '1.example.com' do
73
76
  upload! 'some_local_file.txt', '/home/some_user/somewhere'
74
- download! '/home/some_user/some_remote_file.txt', 'somewhere_local', :log_percent 25
77
+ download! '/home/some_user/some_remote_file.txt', 'somewhere_local', log_percent: 25
75
78
  end
76
79
  ```
77
80
 
78
- #### Users, working directories, environment variables and umask
81
+ ### Users, working directories, environment variables and umask
79
82
 
80
83
  When running commands, you can tell SSHKit to set up the context for those
81
84
  commands using the following methods:
@@ -113,6 +116,11 @@ the raised error.
113
116
  Helpers such as `runner()` and `rake()` which expand to `execute(:rails, "runner", ...)` and
114
117
  `execute(:rake, ...)` are convenience helpers for Ruby, and Rails based apps.
115
118
 
119
+ ### Verbosity / Silence
120
+
121
+ - raise verbosity of a command: `execute "echo DEAD", verbosity: :ERROR`
122
+ - hide a command from output: `execute "echo HIDDEN", verbosity: :DEBUG`
123
+
116
124
  ## Parallel
117
125
 
118
126
  Notice on the `on()` call the `in: :sequence` option, the following will do
@@ -567,10 +575,20 @@ In order to do special gymnastics with SSH, tunneling, aliasing, complex options
567
575
 
568
576
  These system level files are the preferred way of setting up tunneling and proxies because the system implementations of these things are faster and better than the Ruby implementations you would get if you were to configure them through Net::SSH. In cases where it's not possible (Windows?), it should be possible to make use of the Net::SSH APIs to setup tunnels and proxy commands before deferring control to Capistrano/SSHKit..
569
577
 
570
- ## SSHKit Related Blog Posts
578
+ ## Proxying
571
579
 
572
- [SSHKit Gem Basics](http://www.rubyplus.com/articles/591)
580
+ To connect to the target host via a jump/bastion host, use a `Net::SSH::Proxy::Jump`
573
581
 
574
- [SSHKit Gem Part 2](http://www.rubyplus.com/articles/601)
582
+ ```ruby
583
+ host = SSHKit::Host.new(
584
+ hostname: 'target.host.com',
585
+ ssh_options: { proxy: Net::SSH::Proxy::Jump.new("proxy.bar.com") }
586
+ )
587
+ on [host] do
588
+ execute :echo, '1'
589
+ end
590
+ ```
591
+
592
+ ## SSHKit Related Blog Posts
575
593
 
576
594
  [Embedded Capistrano with SSHKit](http://ryandoyle.net/posts/embedded-capistrano-with-sshkit/)
data/RELEASING.md CHANGED
@@ -5,14 +5,13 @@
5
5
  * You must have commit rights to the SSHKit repository.
6
6
  * You must have push rights for the sshkit gem on rubygems.org.
7
7
  * You must be using Ruby >= 2.1.0.
8
- * Your `~/.netrc` must be configured with your GitHub credentials, [as explained here](https://github.com/mattbrictson/chandler#2-configure-netrc).
9
8
 
10
9
  ## How to release
11
10
 
12
11
  1. Run `bundle install` to make sure that you have all the gems necessary for testing and releasing.
13
- 2. **Ensure the tests are passing by running `rake test`.** If functional tests fail, ensure you have [Vagrant](https://www.vagrantup.com) installed and have started it with `vagrant up`.
12
+ 2. **Ensure the tests are passing by running `rake test`.** If functional tests fail, ensure you have [Docker installed](https://docs.docker.com/get-docker/) and running.
14
13
  3. Determine which would be the correct next version number according to [semver](http://semver.org/).
15
14
  4. Update the version in `./lib/sshkit/version.rb`.
16
- 5. Update the `CHANGELOG`.
17
- 6. Commit the changelog and version in a single commit, the message should be "Preparing vX.Y.Z"
18
- 7. Run `rake release`; this will tag, push to GitHub, publish to rubygems.org, and upload the latest changelog entry to the [GitHub releases page](https://github.com/capistrano/sshkit/releases).
15
+ 5. Commit the `version.rb` change with a message like "Preparing vX.Y.Z"
16
+ 6. Run `rake release`; this will tag, push to GitHub, and publish to rubygems.org
17
+ 7. Update the draft release on the [GitHub releases page](https://github.com/capistrano/sshkit/releases) to point to the new tag and publish the release
data/Rakefile CHANGED
@@ -21,19 +21,12 @@ namespace :test do
21
21
 
22
22
  end
23
23
 
24
- Rake::Task["test:functional"].enhance do
25
- warn "Remember there are still some VMs running, kill them with `vagrant halt` if you are finished using them."
26
- end
27
-
28
24
  desc 'Run RuboCop lint checks'
29
25
  RuboCop::RakeTask.new(:lint) do |task|
30
26
  task.options = ['--lint']
31
27
  end
32
28
 
33
- task "release:rubygem_push" do
34
- # Delay loading Chandler until this point, since it requires Ruby >= 2.1,
35
- # which may not be available in all environments (e.g. Travis).
36
- # We assume that the person doing `rake release` has Ruby >= 2.1.
37
- require "chandler/tasks"
38
- Rake.application.invoke_task("chandler:push")
29
+ Rake::Task["release"].enhance do
30
+ puts "Don't forget to publish the release on GitHub!"
31
+ system "open https://github.com/capistrano/sshkit/releases"
39
32
  end
@@ -0,0 +1,8 @@
1
+ name: sshkit
2
+
3
+ services:
4
+ ssh_server:
5
+ build:
6
+ context: .docker
7
+ ports:
8
+ - "2122:22"
@@ -0,0 +1,9 @@
1
+ # tiny example so you can play with the sshkit or make a failing example for an issue
2
+ require 'bundler/setup'
3
+ require 'sshkit'
4
+ require 'sshkit/dsl'
5
+ include SSHKit::DSL
6
+
7
+ on [ENV.fetch("HOST")] do
8
+ execute "echo hello"
9
+ end
@@ -1,3 +1,5 @@
1
+ require 'shellwords'
2
+
1
3
  module SSHKit
2
4
 
3
5
  module Backend
@@ -80,13 +82,14 @@ module SSHKit
80
82
 
81
83
  def within(directory, &_block)
82
84
  (@pwd ||= []).push directory.to_s
85
+ escaped = Command.shellescape_except_tilde(pwd_path)
83
86
  execute <<-EOTEST, verbosity: Logger::DEBUG
84
- if test ! -d #{File.join(@pwd)}
85
- then echo "Directory does not exist '#{File.join(@pwd)}'" 1>&2
87
+ if test ! -d #{escaped}
88
+ then echo "Directory does not exist '#{escaped}'" 1>&2
86
89
  false
87
90
  fi
88
- EOTEST
89
- yield
91
+ EOTEST
92
+ yield
90
93
  ensure
91
94
  @pwd.pop
92
95
  end
@@ -108,8 +111,8 @@ module SSHKit
108
111
  @group = nil
109
112
  end
110
113
  execute <<-EOTEST, verbosity: Logger::DEBUG
111
- if ! sudo -u #{@user} whoami > /dev/null
112
- then echo "You cannot switch to user '#{@user}' using sudo, please check the sudoers file" 1>&2
114
+ if ! sudo -u #{@user.to_s.shellescape} whoami > /dev/null
115
+ then echo "You cannot switch to user '#{@user.to_s.shellescape}' using sudo, please check the sudoers file" 1>&2
113
116
  false
114
117
  fi
115
118
  EOTEST
@@ -36,12 +36,12 @@ class SSHKit::Backend::ConnectionPool::Cache
36
36
  def evict
37
37
  # Peek at the first connection to see if it is still fresh. If so, we can
38
38
  # return right away without needing to use `synchronize`.
39
- first_expires_at, _connection = connections.first
40
- return if first_expires_at.nil? || fresh?(first_expires_at)
39
+ first_expires_at, _first_conn = connections.first
40
+ return if (first_expires_at.nil? || fresh?(first_expires_at))
41
41
 
42
42
  connections.synchronize do
43
- fresh, stale = connections.partition do |expires_at, _|
44
- fresh?(expires_at)
43
+ fresh, stale = connections.partition do |expires_at, conn|
44
+ fresh?(expires_at) && !closed?(conn)
45
45
  end
46
46
  connections.replace(fresh)
47
47
  stale.each { |_, conn| closer.call(conn) }
@@ -71,6 +71,13 @@ class SSHKit::Backend::ConnectionPool::Cache
71
71
  end
72
72
 
73
73
  def closed?(conn)
74
- conn.respond_to?(:closed?) && conn.closed?
74
+ return true if conn.respond_to?(:closed?) && conn.closed?
75
+ # test if connection is alive
76
+ conn.process(0) if conn.respond_to?(:process)
77
+ return false
78
+ rescue IOError => e
79
+ # connection is closed by server
80
+ return true if e.message == 'closed stream'
81
+ raise
75
82
  end
76
83
  end
@@ -21,7 +21,7 @@ end
21
21
 
22
22
  # The ConnectionPool caches connections and allows them to be reused, so long as
23
23
  # the reuse happens within the `idle_timeout` period. Timed out connections are
24
- # closed, forcing a new connection to be used in that case.
24
+ # eventually closed, forcing a new connection to be used in that case.
25
25
  #
26
26
  # Additionally, a background thread is started to check for abandoned
27
27
  # connections that have timed out without any attempt at being reused. These
@@ -46,7 +46,11 @@ class SSHKit::Backend::ConnectionPool
46
46
  @caches = {}
47
47
  @caches.extend(MonitorMixin)
48
48
  @timed_out_connections = Queue.new
49
- Thread.new { run_eviction_loop }
49
+
50
+ # Spin up eviction loop only if caching is enabled
51
+ if cache_enabled?
52
+ Thread.new { run_eviction_loop }
53
+ end
50
54
  end
51
55
 
52
56
  # Creates a new connection or reuses a cached connection (if possible) and
@@ -118,9 +122,9 @@ class SSHKit::Backend::ConnectionPool
118
122
  # Update cache key with changed args to prevent cache miss
119
123
  def update_key_if_args_changed(cache, args)
120
124
  new_key = cache_key_for_connection_args(args)
121
- return if cache.same_key?(new_key)
122
125
 
123
126
  caches.synchronize do
127
+ return if cache.same_key?(new_key)
124
128
  caches[new_key] = caches.delete(cache.key)
125
129
  cache.key = new_key
126
130
  end
@@ -133,7 +137,7 @@ class SSHKit::Backend::ConnectionPool
133
137
  process_deferred_close
134
138
 
135
139
  # Periodically sweep all Caches to evict stale connections
136
- sleep([idle_timeout, 5].min)
140
+ sleep(5)
137
141
  caches.values.each(&:evict)
138
142
  end
139
143
  end
@@ -1,3 +1,5 @@
1
+ require "base64"
2
+
1
3
  module SSHKit
2
4
 
3
5
  module Backend
@@ -5,12 +7,11 @@ module SSHKit
5
7
  class Netssh < Abstract
6
8
 
7
9
  class KnownHostsKeys
8
- include Mutex_m
9
-
10
10
  def initialize(path)
11
11
  super()
12
12
  @path = File.expand_path(path)
13
13
  @hosts_keys = nil
14
+ @mutex = Mutex.new
14
15
  end
15
16
 
16
17
  def keys_for(hostlist)
@@ -44,7 +45,7 @@ module SSHKit
44
45
  end
45
46
 
46
47
  def parse_file
47
- synchronize do
48
+ @mutex.synchronize do
48
49
  return if hosts_keys && hosts_hashes
49
50
 
50
51
  unless File.readable?(path)
@@ -110,11 +111,10 @@ module SSHKit
110
111
  end
111
112
 
112
113
  class KnownHosts
113
- include Mutex_m
114
-
115
114
  def initialize
116
115
  super()
117
116
  @files = {}
117
+ @mutex = Mutex.new
118
118
  end
119
119
 
120
120
  def search_for(host, options = {})
@@ -126,13 +126,13 @@ module SSHKit
126
126
 
127
127
  def add(*args)
128
128
  ::Net::SSH::KnownHosts.add(*args)
129
- synchronize { @files = {} }
129
+ @mutex.synchronize { @files = {} }
130
130
  end
131
131
 
132
132
  private
133
133
 
134
134
  def known_hosts_file(path)
135
- @files[path] || synchronize { @files[path] ||= KnownHostsKeys.new(path) }
135
+ @files[path] || @mutex.synchronize { @files[path] ||= KnownHostsKeys.new(path) }
136
136
  end
137
137
  end
138
138
 
@@ -140,4 +140,4 @@ module SSHKit
140
140
 
141
141
  end
142
142
 
143
- end
143
+ end
@@ -0,0 +1,26 @@
1
+ require "net/scp"
2
+
3
+ module SSHKit
4
+ module Backend
5
+ class Netssh < Abstract
6
+ class ScpTransfer
7
+ def initialize(ssh, summarizer)
8
+ @ssh = ssh
9
+ @summarizer = summarizer
10
+ end
11
+
12
+ def upload!(local, remote, options)
13
+ ssh.scp.upload!(local, remote, options, &summarizer)
14
+ end
15
+
16
+ def download!(remote, local, options)
17
+ ssh.scp.download!(remote, local, options, &summarizer)
18
+ end
19
+
20
+ private
21
+
22
+ attr_reader :ssh, :summarizer
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,46 @@
1
+ require "net/sftp"
2
+
3
+ module SSHKit
4
+ module Backend
5
+ class Netssh < Abstract
6
+ class SftpTransfer
7
+ def initialize(ssh, summarizer)
8
+ @ssh = ssh
9
+ @summarizer = summarizer
10
+ end
11
+
12
+ def upload!(local, remote, options)
13
+ options = { progress: self }.merge(options || {})
14
+ ssh.sftp.connect!
15
+ ssh.sftp.upload!(local, remote, options)
16
+ ensure
17
+ ssh.sftp.close_channel
18
+ end
19
+
20
+ def download!(remote, local, options)
21
+ options = { progress: self }.merge(options || {})
22
+ destination = local ? local : StringIO.new.tap { |io| io.set_encoding('BINARY') }
23
+
24
+ ssh.sftp.connect!
25
+ ssh.sftp.download!(remote, destination, options)
26
+ local ? true : destination.string
27
+ ensure
28
+ ssh.sftp.close_channel
29
+ end
30
+
31
+ def on_get(download, entry, offset, data)
32
+ entry.size ||= download.sftp.file.open(entry.remote) { |file| file.stat.size }
33
+ summarizer.call(nil, entry.remote, offset + data.bytesize, entry.size)
34
+ end
35
+
36
+ def on_put(_upload, file, offset, data)
37
+ summarizer.call(nil, file.local, offset + data.bytesize, file.size)
38
+ end
39
+
40
+ private
41
+
42
+ attr_reader :ssh, :summarizer
43
+ end
44
+ end
45
+ end
46
+ end
@@ -1,8 +1,6 @@
1
1
  require 'English'
2
2
  require 'strscan'
3
- require 'mutex_m'
4
3
  require 'net/ssh'
5
- require 'net/scp'
6
4
 
7
5
  module Net
8
6
  module SSH
@@ -23,10 +21,27 @@ module SSHKit
23
21
  module Backend
24
22
 
25
23
  class Netssh < Abstract
24
+ def self.assert_valid_transfer_method!(method)
25
+ return if [:scp, :sftp].include?(method)
26
+
27
+ raise ArgumentError, "#{method.inspect} is not a valid transfer method. Supported methods are :scp, :sftp."
28
+ end
29
+
26
30
  class Configuration
27
31
  attr_accessor :connection_timeout, :pty
32
+ attr_reader :transfer_method
28
33
  attr_writer :ssh_options
29
34
 
35
+ def initialize
36
+ self.transfer_method = :scp
37
+ end
38
+
39
+ def transfer_method=(method)
40
+ Netssh.assert_valid_transfer_method!(method)
41
+
42
+ @transfer_method = method
43
+ end
44
+
30
45
  def ssh_options
31
46
  default_options.merge(@ssh_options ||= {})
32
47
  end
@@ -64,16 +79,16 @@ module SSHKit
64
79
  def upload!(local, remote, options = {})
65
80
  summarizer = transfer_summarizer('Uploading', options)
66
81
  remote = File.join(pwd_path, remote) unless remote.to_s.start_with?("/") || pwd_path.nil?
67
- with_ssh do |ssh|
68
- ssh.scp.upload!(local, remote, options, &summarizer)
82
+ with_transfer(summarizer) do |transfer|
83
+ transfer.upload!(local, remote, options)
69
84
  end
70
85
  end
71
86
 
72
87
  def download!(remote, local=nil, options = {})
73
88
  summarizer = transfer_summarizer('Downloading', options)
74
89
  remote = File.join(pwd_path, remote) unless remote.to_s.start_with?("/") || pwd_path.nil?
75
- with_ssh do |ssh|
76
- ssh.scp.download!(remote, local, options, &summarizer)
90
+ with_transfer(summarizer) do |transfer|
91
+ transfer.download!(remote, local, options)
77
92
  end
78
93
  end
79
94
 
@@ -105,11 +120,12 @@ module SSHKit
105
120
  last_percentage = nil
106
121
  proc do |_ch, name, transferred, total|
107
122
  percentage = (transferred.to_f * 100 / total.to_f)
108
- unless percentage.nan?
123
+ unless percentage.nan? || percentage.infinite?
109
124
  message = "#{action} #{name} #{percentage.round(2)}%"
110
125
  percentage_r = (percentage / log_percent).truncate * log_percent
111
126
  if percentage_r > 0 && (last_name != name || last_percentage != percentage_r)
112
- info message
127
+ verbosity = (options[:verbosity] || :INFO).downcase # TODO: ideally reuse command.rb logic
128
+ public_send verbosity, message
113
129
  last_name = name
114
130
  last_percentage = percentage_r
115
131
  else
@@ -182,6 +198,20 @@ module SSHKit
182
198
  )
183
199
  end
184
200
 
201
+ def with_transfer(summarizer)
202
+ transfer_method = host.transfer_method || self.class.config.transfer_method
203
+ transfer_class = if transfer_method == :sftp
204
+ require_relative "netssh/sftp_transfer"
205
+ SftpTransfer
206
+ else
207
+ require_relative "netssh/scp_transfer"
208
+ ScpTransfer
209
+ end
210
+
211
+ with_ssh do |ssh|
212
+ yield(transfer_class.new(ssh, summarizer))
213
+ end
214
+ end
185
215
  end
186
216
  end
187
217