Skip to content

Commit

Permalink
Allow SFTP to be used for upload!/download! instead of SCP
Browse files Browse the repository at this point in the history
  • Loading branch information
mattbrictson committed Dec 22, 2023
1 parent 8981ad8 commit 5d98e4a
Show file tree
Hide file tree
Showing 7 changed files with 135 additions and 7 deletions.
17 changes: 17 additions & 0 deletions EXAMPLES.md
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,23 @@ end
In this case the `recursive: true` option mirrors the same options which are
available to [`Net::{SCP,SFTP}`](http://net-ssh.github.io/net-scp/).

## Set the upload/download method (SCP or SFTP).

SSHKit can use SCP or SFTP for file transfers. The default is SCP, but this can be changed to SFTP per host:

```ruby
host = SSHKit::Host.new('[email protected]')
host.transfer_method = :sftp
```

or globally:

```ruby
SSHKit::Backend::Netssh.configure do |ssh|
ssh.transfer_method = :sftp
end
```

## Setting global SSH options

Setting global SSH options, these will be overwritten by options set on the
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,8 @@ you can pass the `strip: false` option: `capture(:ls, '-l', strip: false)`
### Transferring files

All backends also support the `upload!` and `download!` methods for transferring files.
For the remote backend, the file is transferred with scp.
For the remote backend, the file is transferred with scp by default, but sftp is also
supported. See [EXAMPLES.md](EXAMPLES.md) for details.

```ruby
on '1.example.com' do
Expand Down
42 changes: 36 additions & 6 deletions lib/sshkit/backends/netssh.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
require 'strscan'
require 'mutex_m'
require 'net/ssh'
require 'net/scp'

module Net
module SSH
Expand All @@ -23,10 +22,27 @@ module SSHKit
module Backend

class Netssh < Abstract
def self.assert_valid_transfer_method!(method)
return if [nil, :scp, :sftp].include?(method)

raise ArgumentError, "#{method.inspect} is not a valid transfer method. Supported methods are :scp, :sftp."
end

class Configuration
attr_accessor :connection_timeout, :pty
attr_reader :transfer_method
attr_writer :ssh_options

def initialize
self.transfer_method = :scp
end

def transfer_method=(method)
Netssh.assert_valid_transfer_method!(method)

@transfer_method = method
end

def ssh_options
default_options.merge(@ssh_options ||= {})
end
Expand Down Expand Up @@ -64,16 +80,16 @@ def assign_defaults
def upload!(local, remote, options = {})
summarizer = transfer_summarizer('Uploading', options)
remote = File.join(pwd_path, remote) unless remote.to_s.start_with?("/") || pwd_path.nil?
with_ssh do |ssh|
ssh.scp.upload!(local, remote, options, &summarizer)
with_transfer(summarizer) do |transfer|
transfer.upload!(local, remote, options)
end
end

def download!(remote, local=nil, options = {})
summarizer = transfer_summarizer('Downloading', options)
remote = File.join(pwd_path, remote) unless remote.to_s.start_with?("/") || pwd_path.nil?
with_ssh do |ssh|
ssh.scp.download!(remote, local, options, &summarizer)
with_transfer(summarizer) do |transfer|
transfer.download!(remote, local, options)
end
end

Expand Down Expand Up @@ -105,7 +121,7 @@ def transfer_summarizer(action, options = {})
last_percentage = nil
proc do |_ch, name, transferred, total|
percentage = (transferred.to_f * 100 / total.to_f)
unless percentage.nan?
unless percentage.nan? || percentage.infinite?
message = "#{action} #{name} #{percentage.round(2)}%"
percentage_r = (percentage / log_percent).truncate * log_percent
if percentage_r > 0 && (last_name != name || last_percentage != percentage_r)
Expand Down Expand Up @@ -183,6 +199,20 @@ def with_ssh(&block)
)
end

def with_transfer(summarizer)
transfer_method = host.transfer_method || self.class.config.transfer_method
transfer_class = if transfer_method == :sftp
require_relative "netssh/sftp_transfer"
SftpTransfer
else
require_relative "netssh/scp_transfer"
ScpTransfer
end

with_ssh do |ssh|
yield(transfer_class.new(ssh, summarizer))
end
end
end
end

Expand Down
26 changes: 26 additions & 0 deletions lib/sshkit/backends/netssh/scp_transfer.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
require "net/scp"

module SSHKit
module Backend
class Netssh < Abstract
class ScpTransfer
def initialize(ssh, summarizer)
@ssh = ssh
@summarizer = summarizer
end

def upload!(local, remote, options)
ssh.scp.upload!(local, remote, options, &summarizer)
end

def download!(remote, local, options)
ssh.scp.download!(remote, local, options, &summarizer)
end

private

attr_reader :ssh, :summarizer
end
end
end
end
46 changes: 46 additions & 0 deletions lib/sshkit/backends/netssh/sftp_transfer.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
require "net/sftp"

module SSHKit
module Backend
class Netssh < Abstract
class SftpTransfer
def initialize(ssh, summarizer)
@ssh = ssh
@summarizer = summarizer
end

def upload!(local, remote, options)
options = {progress: self}.merge(options || {})
ssh.sftp.connect!
ssh.sftp.upload!(local, remote, options)
ensure
ssh.sftp.close_channel
end

def download!(remote, local, options)
options = {progress: self}.merge(options || {})
destination = local ? local : StringIO.new.tap { |io| io.set_encoding('BINARY') }

ssh.sftp.connect!
ssh.sftp.download!(remote, destination, options)
local ? true : destination.string
ensure
ssh.sftp.close_channel
end

def on_get(download, entry, offset, data)
entry.size ||= download.sftp.file.open(entry.remote, &:size)
summarizer.call(nil, entry.remote, offset + data.bytesize, entry.size)
end

def on_put(_upload, file, offset, data)
summarizer.call(nil, file.local, offset + data.bytesize, file.size)
end

private

attr_reader :ssh, :summarizer
end
end
end
end
7 changes: 7 additions & 0 deletions lib/sshkit/host.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ module SSHKit
class Host

attr_accessor :password, :hostname, :port, :user, :ssh_options
attr_reader :transfer_method

def key=(new_key)
@keys = [new_key]
Expand Down Expand Up @@ -41,6 +42,12 @@ def initialize(host_string_or_options_hash)
end
end

def transfer_method=(method)
Backend::Netssh.assert_valid_transfer_method!(method)

@transfer_method = method
end

def local?
@local
end
Expand Down
1 change: 1 addition & 0 deletions sshkit.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ Gem::Specification.new do |gem|

gem.add_runtime_dependency('net-ssh', '>= 2.8.0')
gem.add_runtime_dependency('net-scp', '>= 1.1.2')
gem.add_runtime_dependency('net-sftp', '>= 2.1.2')

gem.add_development_dependency('danger')
gem.add_development_dependency('minitest', '>= 5.0.0')
Expand Down

0 comments on commit 5d98e4a

Please sign in to comment.