net-scp 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGELOG.rdoc ADDED
@@ -0,0 +1,8 @@
1
+ === 1.0.0 / 1 May 2008
2
+
3
+ * Pass the channel object as the first argument to the progress callback [Jamis Buck]
4
+
5
+
6
+ === 1.0 Preview Release 1 (0.99.0) / 22 Mar 2008
7
+
8
+ * Birthday!
data/Manifest ADDED
@@ -0,0 +1,17 @@
1
+ CHANGELOG.rdoc
2
+ lib/net/scp/download.rb
3
+ lib/net/scp/errors.rb
4
+ lib/net/scp/upload.rb
5
+ lib/net/scp/version.rb
6
+ lib/net/scp.rb
7
+ lib/uri/open-scp.rb
8
+ lib/uri/scp.rb
9
+ Rakefile
10
+ README.rdoc
11
+ setup.rb
12
+ test/common.rb
13
+ test/test_all.rb
14
+ test/test_download.rb
15
+ test/test_scp.rb
16
+ test/test_upload.rb
17
+ Manifest
data/README.rdoc ADDED
@@ -0,0 +1,98 @@
1
+ = Net::SCP
2
+
3
+ * http://net-ssh.rubyforge.org/scp
4
+
5
+ == DESCRIPTION:
6
+
7
+ Net::SCP is a pure-Ruby implementation of the SCP protocol. This operates over SSH (and requires the Net::SSH library), and allows files and directory trees to copied to and from a remote server.
8
+
9
+ == FEATURES/PROBLEMS:
10
+
11
+ * Transfer files or entire directory trees to or from a remote host via SCP
12
+ * Can preserve file attributes across transfers
13
+ * Can download files in-memory, or direct-to-disk
14
+ * Support for SCP URI's, and OpenURI
15
+
16
+ == SYNOPSIS:
17
+
18
+ In a nutshell:
19
+
20
+ require 'net/scp'
21
+
22
+ # upload a file to a remote server
23
+ Net::SCP.upload!("remote.host.com", "username",
24
+ "/local/path", "/remote/path",
25
+ :password => "password")
26
+
27
+ # download a file from a remote server
28
+ Net::SCP.download!("remote.host.com", "username",
29
+ "/remote/path", "/local/path",
30
+ :password => password)
31
+
32
+ # download a file to an in-memory buffer
33
+ data = Net::SCP::download!("remote.host.com", "username", "/remote/path")
34
+
35
+ # use a persistent connection to transfer files
36
+ Net::SCP.start("remote.host.com", "username", :password => "password") do |scp|
37
+ # upload a file to a remote server
38
+ scp.upload! "/local/path", "/remote/path"
39
+
40
+ # upload from an in-memory buffer
41
+ scp.upload! StringIO.new("some data to upload"), "/remote/path"
42
+
43
+ # run multiple downloads in parallel
44
+ d1 = scp.download("/remote/path", "/local/path")
45
+ d2 = scp.download("/remote/path2", "/local/path2")
46
+ [d1, d2].each { |d| d.wait }
47
+ end
48
+
49
+ # You can also use open-uri to grab data via scp:
50
+ require 'uri/open-scp'
51
+ data = open("scp://user@host/path/to/file.txt").read
52
+
53
+ For more information, see Net::SCP.
54
+
55
+ == REQUIREMENTS:
56
+
57
+ * Net::SSH 2
58
+
59
+ If you wish to run the tests, you'll also need:
60
+
61
+ * Echoe (for Rakefile use)
62
+ * Mocha (for tests)
63
+
64
+ == INSTALL:
65
+
66
+ * gem install net-scp (might need sudo privileges)
67
+
68
+ Or, you can do it the hard way (without Rubygems):
69
+
70
+ * tar xzf net-scp-*.tgz
71
+ * cd net-scp-*
72
+ * ruby setup.rb config
73
+ * ruby setup.rb install (might need sudo privileges)
74
+
75
+ == LICENSE:
76
+
77
+ (The MIT License)
78
+
79
+ Copyright (c) 2008 Jamis Buck <jamis@37signals.com>
80
+
81
+ Permission is hereby granted, free of charge, to any person obtaining
82
+ a copy of this software and associated documentation files (the
83
+ 'Software'), to deal in the Software without restriction, including
84
+ without limitation the rights to use, copy, modify, merge, publish,
85
+ distribute, sublicense, and/or sell copies of the Software, and to
86
+ permit persons to whom the Software is furnished to do so, subject to
87
+ the following conditions:
88
+
89
+ The above copyright notice and this permission notice shall be
90
+ included in all copies or substantial portions of the Software.
91
+
92
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
93
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
94
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
95
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
96
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
97
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
98
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/Rakefile ADDED
@@ -0,0 +1,30 @@
1
+ $LOAD_PATH.unshift "../net-ssh/lib"
2
+ require './lib/net/scp/version'
3
+
4
+ begin
5
+ require 'echoe'
6
+ rescue LoadError
7
+ abort "You'll need to have `echoe' installed to use Net::SCP's Rakefile"
8
+ end
9
+
10
+ version = Net::SCP::Version::STRING.dup
11
+ if ENV['SNAPSHOT'].to_i == 1
12
+ version << "." << Time.now.utc.strftime("%Y%m%d%H%M%S")
13
+ end
14
+
15
+ Echoe.new('net-scp', version) do |p|
16
+ p.project = "net-ssh"
17
+ p.changelog = "CHANGELOG.rdoc"
18
+
19
+ p.author = "Jamis Buck"
20
+ p.email = "jamis@jamisbuck.org"
21
+ p.summary = "A pure Ruby implementation of the SCP client protocol"
22
+ p.url = "http://net-ssh.rubyforge.org/scp"
23
+
24
+ p.dependencies = ["net-ssh >=1.99.1"]
25
+
26
+ p.need_zip = true
27
+ p.include_rakefile = true
28
+
29
+ p.rdoc_pattern = /^(lib|README.rdoc|CHANGELOG.rdoc)/
30
+ end
data/lib/net/scp.rb ADDED
@@ -0,0 +1,410 @@
1
+ require 'stringio'
2
+
3
+ require 'net/ssh'
4
+ require 'net/scp/errors'
5
+ require 'net/scp/upload'
6
+ require 'net/scp/download'
7
+
8
+ module Net
9
+
10
+ # Net::SCP implements the SCP (Secure CoPy) client protocol, allowing Ruby
11
+ # programs to securely and programmatically transfer individual files or
12
+ # entire directory trees to and from remote servers. It provides support for
13
+ # multiple simultaneous SCP copies working in parallel over the same
14
+ # connection, as well as for synchronous, serial copies.
15
+ #
16
+ # Basic usage:
17
+ #
18
+ # require 'net/scp'
19
+ #
20
+ # Net::SCP.start("remote.host", "username", :password => "passwd") do |scp|
21
+ # # synchronous (blocking) upload; call blocks until upload completes
22
+ # scp.upload! "/local/path", "/remote/path"
23
+ #
24
+ # # asynchronous upload; call returns immediately and requires SSH
25
+ # # event loop to run
26
+ # channel = scp.upload("/local/path", "/remote/path")
27
+ # channel.wait
28
+ # end
29
+ #
30
+ # Net::SCP also provides an open-uri tie-in, so you can use the Kernel#open
31
+ # method to open and read a remote file:
32
+ #
33
+ # # if you just want to parse SCP URL's:
34
+ # require 'uri/scp'
35
+ # url = URI.parse("scp://user@remote.host/path/to/file")
36
+ #
37
+ # # if you want to read from a URL voa SCP:
38
+ # require 'uri/open-scp'
39
+ # puts open("scp://user@remote.host/path/to/file").read
40
+ #
41
+ # Lastly, Net::SCP adds a method to the Net::SSH::Connection::Session class,
42
+ # allowing you to easily grab a Net::SCP reference from an existing Net::SSH
43
+ # session:
44
+ #
45
+ # require 'net/ssh'
46
+ # require 'net/scp'
47
+ #
48
+ # Net::SSH.start("remote.host", "username", :password => "passwd") do |ssh|
49
+ # ssh.scp.download! "/remote/path", "/local/path"
50
+ # end
51
+ #
52
+ # == Progress Reporting
53
+ #
54
+ # By default, uploading and downloading proceed silently, without any
55
+ # outword indication of their progress. For long running uploads or downloads
56
+ # (and especially in interactive environments) it is desirable to report
57
+ # to the user the progress of the current operation.
58
+ #
59
+ # To receive progress reports for the current operation, just pass a block
60
+ # to #upload or #download (or one of their variants):
61
+ #
62
+ # scp.upload!("/path/to/local", "/path/to/remote") do |ch, name, sent, total|
63
+ # puts "#{name}: #{sent}/#{total}"
64
+ # end
65
+ #
66
+ # Whenever a new chunk of data is recieved for or sent to a file, the callback
67
+ # will be invoked, indicating the name of the file (local for downloads,
68
+ # remote for uploads), the number of bytes that have been sent or received
69
+ # so far for the file, and the size of the file.
70
+ #
71
+ #--
72
+ # = Protocol Description
73
+ #
74
+ # Although this information has zero relevance to consumers of the Net::SCP
75
+ # library, I'm documenting it here so that anyone else looking for documentation
76
+ # of the SCP protocol won't be left high-and-dry like I was. The following is
77
+ # reversed engineered from the OpenSSH SCP implementation, and so may
78
+ # contain errors. You have been warned!
79
+ #
80
+ # The first step is to invoke the "scp" command on the server. It accepts
81
+ # the following parameters, which must be set correctly to avoid errors:
82
+ #
83
+ # * "-t" -- tells the remote scp process that data will be sent "to" it,
84
+ # e.g., that data will be uploaded and it should initialize itself
85
+ # accordingly.
86
+ # * "-f" -- tells the remote scp process that data should come "from" it,
87
+ # e.g., that data will be downloaded and it should initialize itself
88
+ # accordingly.
89
+ # * "-v" -- verbose mode; the remote scp process should chatter about what
90
+ # it is doing via stderr.
91
+ # * "-p" -- preserve timestamps. 'T' directives (see below) should be/will
92
+ # be sent to indicate the modification and access times of each file.
93
+ # * "-r" -- recursive transfers should be allowed. Without this, it is an
94
+ # error to upload or download a directory.
95
+ #
96
+ # After those flags, the name of the remote file/directory should be passed
97
+ # as the sole non-switch argument to scp.
98
+ #
99
+ # Then the fun begins. If you're doing a download, enter the download_start_state.
100
+ # Otherwise, look for upload_start_state.
101
+ #
102
+ # == Net::SCP::Download#download_start_state
103
+ #
104
+ # This is the start state for downloads. It simply sends a 0-byte to the
105
+ # server. The next state is Net::SCP::Download#read_directive_state.
106
+ #
107
+ # == Net::SCP::Upload#upload_start_state
108
+ #
109
+ # Sets up the initial upload scaffolding and waits for a 0-byte from the
110
+ # server, and then switches to Net::SCP::Upload#upload_current_state.
111
+ #
112
+ # == Net::SCP::Download#read_directive_state
113
+ #
114
+ # Reads a directive line from the input. The following directives are
115
+ # recognized:
116
+ #
117
+ # * T%d %d %d %d -- a "times" packet. Indicates that the next file to be
118
+ # downloaded must have mtime/usec/atime/usec attributes preserved.
119
+ # * D%o %d %s -- a directory change. The process is changing to a directory
120
+ # with the given permissions/size/name, and the recipient should create
121
+ # a directory with the same name and permissions. Subsequent files and
122
+ # directories will be children of this directory, until a matching 'E'
123
+ # directive.
124
+ # * C%o %d %s -- a file is being sent next. The file will have the given
125
+ # permissions/size/name. Immediately following this line, +size+ bytes
126
+ # will be sent, raw.
127
+ # * E -- terminator directive. Indicates the end of a directory, and subsequent
128
+ # files and directories should be received by the parent of the current
129
+ # directory.
130
+ #
131
+ # If a 'C' directive is received, we switch over to
132
+ # Net::SCP::Download#read_data_state. If an 'E' directive is received, and
133
+ # there is no parent directory, we switch over to Net::SCP#finish_state.
134
+ #
135
+ # Regardless of what the next state is, we send a 0-byte to the server
136
+ # before moving to the next state.
137
+ #
138
+ # == Net::SCP::Download#read_data_state
139
+ #
140
+ # Bytes are read to satisfy the size of the incoming file. When all pending
141
+ # data has been read, we wait for the server to send a 0-byte, and then we
142
+ # switch to the Net::SCP::Download#finish_read_state.
143
+ #
144
+ # == Net::SCP::Download#finish_read_state
145
+ #
146
+ # We sent a 0-byte to the server to indicate that the file was successfully
147
+ # received. If there is no parent directory, then we're downloading a single
148
+ # file and we switch to Net::SCP#finish_state. Otherwise we jump back to the
149
+ # Net::SCP::Download#read_directive state to see what we get to download next.
150
+ #
151
+ # == Net::SCP::Upload#upload_current_state
152
+ #
153
+ # If the current item is a file, send a file. Sending a file starts with a
154
+ # 'T' directive (if :preserve is true), then a wait for the server to respond,
155
+ # and then a 'C' directive, and then a wait for the server to respond, and
156
+ # then a jump to Net::SCP::Upload#send_data_state.
157
+ #
158
+ # If current item is a directory, send a 'D' directive, and wait for the
159
+ # server to respond with a 0-byte. Then jump to Net::SCP::Upload#next_item_state.
160
+ #
161
+ # == Net::SCP::Upload#send_data_state
162
+ #
163
+ # Reads and sends the next chunk of data to the server. The state machine
164
+ # remains in this state until all data has been sent, at which point we
165
+ # send a 0-byte to the server, and wait for the server to respond with a
166
+ # 0-byte of its own. Then we jump back to Net::SCP::Upload#next_item_state.
167
+ #
168
+ # == Net::SCP::Upload#next_item_state
169
+ #
170
+ # If there is nothing left to upload, and there is no parent directory,
171
+ # jump to Net::SCP#finish_state.
172
+ #
173
+ # If there is nothing left to upload from the current directory, send an
174
+ # 'E' directive and wait for the server to respond with a 0-byte. Then go
175
+ # to Net::SCP::Upload#next_item_state.
176
+ #
177
+ # Otherwise, set the current upload source and go to
178
+ # Net::SCP::Upload#upload_current_state.
179
+ #
180
+ # == Net::SCP#finish_state
181
+ #
182
+ # Tells the server that no more data is forthcoming from this end of the
183
+ # pipe (via Net::SSH::Connection::Channel#eof!) and leaves the pipe to drain.
184
+ # It will be terminated when the remote process closes with an exit status
185
+ # of zero.
186
+ #++
187
+ class SCP
188
+ include Net::SSH::Loggable
189
+ include Upload, Download
190
+
191
+ # Starts up a new SSH connection and instantiates a new SCP session on
192
+ # top of it. If a block is given, the SCP session is yielded, and the
193
+ # SSH session is closed automatically when the block terminates. If no
194
+ # block is given, the SCP session is returned.
195
+ def self.start(host, username, options={})
196
+ session = Net::SSH.start(host, username, options)
197
+ scp = new(session)
198
+
199
+ if block_given?
200
+ begin
201
+ yield scp
202
+ session.loop
203
+ ensure
204
+ session.close
205
+ end
206
+ else
207
+ return scp
208
+ end
209
+ end
210
+
211
+ # Starts up a new SSH connection using the +host+ and +username+ parameters,
212
+ # instantiates a new SCP session on top of it, and then begins an
213
+ # upload from +local+ to +remote+. If the +options+ hash includes an
214
+ # :ssh key, the value for that will be passed to the SSH connection as
215
+ # options (e.g., to set the password, etc.). All other options are passed
216
+ # to the #upload! method. If a block is given, it will be used to report
217
+ # progress (see "Progress Reporting", under Net::SCP).
218
+ def self.upload!(host, username, local, remote, options={}, &progress)
219
+ options = options.dup
220
+ start(host, username, options.delete(:ssh) || {}) do |scp|
221
+ scp.upload!(local, remote, options, &progress)
222
+ end
223
+ end
224
+
225
+ # Starts up a new SSH connection using the +host+ and +username+ parameters,
226
+ # instantiates a new SCP session on top of it, and then begins a
227
+ # download from +remote+ to +local+. If the +options+ hash includes an
228
+ # :ssh key, the value for that will be passed to the SSH connection as
229
+ # options (e.g., to set the password, etc.). All other options are passed
230
+ # to the #download! method. If a block is given, it will be used to report
231
+ # progress (see "Progress Reporting", under Net::SCP).
232
+ def self.download!(host, username, remote, local=nil, options={}, &progress)
233
+ options = options.dup
234
+ start(host, username, options.delete(:ssh) || {}) do |scp|
235
+ return scp.download!(remote, local, options, &progress)
236
+ end
237
+ end
238
+
239
+ # The underlying Net::SSH session that acts as transport for the SCP
240
+ # packets.
241
+ attr_reader :session
242
+
243
+ # Creates a new Net::SCP session on top of the given Net::SSH +session+
244
+ # object.
245
+ def initialize(session)
246
+ @session = session
247
+ self.logger = session.logger
248
+ end
249
+
250
+ # Inititiate a synchronous (non-blocking) upload from +local+ to +remote+.
251
+ # The following options are recognized:
252
+ #
253
+ # * :recursive - the +local+ parameter refers to a local directory, which
254
+ # should be uploaded to a new directory named +remote+ on the remote
255
+ # server.
256
+ # * :preserve - the atime and mtime of the file should be preserved.
257
+ # * :verbose - the process should result in verbose output on the server
258
+ # end (useful for debugging).
259
+ # * :chunk_size - the size of each "chunk" that should be sent. Defaults
260
+ # to 2048. Changing this value may improve throughput at the expense
261
+ # of decreasing interactivity.
262
+ #
263
+ # This method will return immediately, returning the Net::SSH::Connection::Channel
264
+ # object that will support the upload. To wait for the upload to finish,
265
+ # you can either call the #wait method on the channel, or otherwise run
266
+ # the Net::SSH event loop until the channel's #active? method returns false.
267
+ #
268
+ # channel = scp.upload("/local/path", "/remote/path")
269
+ # channel.wait
270
+ def upload(local, remote, options={}, &progress)
271
+ start_command(:upload, local, remote, options, &progress)
272
+ end
273
+
274
+ # Same as #upload, but blocks until the upload finishes. Identical to
275
+ # calling #upload and then calling the #wait method on the channel object
276
+ # that is returned. The return value is not defined.
277
+ def upload!(local, remote, options={}, &progress)
278
+ upload(local, remote, options, &progress).wait
279
+ end
280
+
281
+ # Inititiate a synchronous (non-blocking) download from +remote+ to +local+.
282
+ # The following options are recognized:
283
+ #
284
+ # * :recursive - the +remote+ parameter refers to a remote directory, which
285
+ # should be downloaded to a new directory named +local+ on the local
286
+ # machine.
287
+ # * :preserve - the atime and mtime of the file should be preserved.
288
+ # * :verbose - the process should result in verbose output on the server
289
+ # end (useful for debugging).
290
+ #
291
+ # This method will return immediately, returning the Net::SSH::Connection::Channel
292
+ # object that will support the download. To wait for the download to finish,
293
+ # you can either call the #wait method on the channel, or otherwise run
294
+ # the Net::SSH event loop until the channel's #active? method returns false.
295
+ #
296
+ # channel = scp.download("/remote/path", "/local/path")
297
+ # channel.wait
298
+ def download(remote, local, options={}, &progress)
299
+ start_command(:download, local, remote, options, &progress)
300
+ end
301
+
302
+ # Same as #download, but blocks until the download finishes. Identical to
303
+ # calling #download and then calling the #wait method on the channel
304
+ # object that is returned.
305
+ #
306
+ # scp.download!("/remote/path", "/local/path")
307
+ #
308
+ # If +local+ is nil, and the download is not recursive (e.g., it is downloading
309
+ # only a single file), the file will be downloaded to an in-memory buffer
310
+ # and the resulting string returned.
311
+ #
312
+ # data = download!("/remote/path")
313
+ def download!(remote, local=nil, options={}, &progress)
314
+ destination = local ? local : StringIO.new
315
+ download(remote, destination, options, &progress).wait
316
+ local ? true : destination.string
317
+ end
318
+
319
+ private
320
+
321
+ # Constructs the scp command line needed to initiate and SCP session
322
+ # for the given +mode+ (:upload or :download) and with the given options
323
+ # (:verbose, :recursive, :preserve). Returns the command-line as a
324
+ # string, ready to execute.
325
+ def scp_command(mode, options)
326
+ command = "scp "
327
+ command << (mode == :upload ? "-t" : "-f")
328
+ command << " -v" if options[:verbose]
329
+ command << " -r" if options[:recursive]
330
+ command << " -p" if options[:preserve]
331
+ command
332
+ end
333
+
334
+ # Opens a new SSH channel and executes the necessary SCP command over
335
+ # it (see #scp_command). It then sets up the necessary callbacks, and
336
+ # sets up a state machine to use to process the upload or download.
337
+ # (See Net::SCP::Upload and Net::SCP::Download).
338
+ def start_command(mode, local, remote, options={}, &callback)
339
+ session.open_channel do |channel|
340
+ command = "#{scp_command(mode, options)} #{remote}"
341
+ channel.exec(command) do |ch, success|
342
+ if success
343
+ channel[:local ] = local
344
+ channel[:remote ] = remote
345
+ channel[:options ] = options.dup
346
+ channel[:callback] = callback
347
+ channel[:buffer ] = Net::SSH::Buffer.new
348
+ channel[:state ] = "#{mode}_start"
349
+ channel[:stack ] = []
350
+
351
+ channel.on_close { |ch| raise Net::SCP::Error, "SCP did not finish successfully (#{ch[:exit]})" if ch[:exit] != 0 }
352
+ channel.on_data { |ch, data| channel[:buffer].append(data) }
353
+ channel.on_extended_data { |ch, type, data| debug { data.chomp } }
354
+ channel.on_request("exit-status") { |ch, data| channel[:exit] = data.read_long }
355
+ channel.on_process { send("#{channel[:state]}_state", channel) }
356
+ else
357
+ channel.close
358
+ raise Net::SCP::Error, "could not exec scp on the remote host"
359
+ end
360
+ end
361
+ end
362
+ end
363
+
364
+ # Causes the state machine to enter the "await response" state, where
365
+ # things just pause until the server replies with a 0 (see
366
+ # #await_response_state), at which point the state machine will pick up
367
+ # at +next_state+ and continue processing.
368
+ def await_response(channel, next_state)
369
+ channel[:state] = :await_response
370
+ channel[:next ] = next_state.to_sym
371
+ # check right away, to see if the response is immediately available
372
+ await_response_state(channel)
373
+ end
374
+
375
+ # The action invoked while the state machine remains in the "await
376
+ # response" state. As long as there is no data ready to process, the
377
+ # machine will remain in this state. As soon as the server replies with
378
+ # an integer 0 as the only byte, the state machine is kicked into the
379
+ # next state (see +await_response+). If the response is not a 0, an
380
+ # exception is raised.
381
+ def await_response_state(channel)
382
+ return if channel[:buffer].available == 0
383
+ c = channel[:buffer].read_byte
384
+ raise "#{c.chr}#{channel[:buffer].read}" if c != 0
385
+ channel[:next], channel[:state] = nil, channel[:next]
386
+ send("#{channel[:state]}_state", channel)
387
+ end
388
+
389
+ # The action invoked when the state machine is in the "finish" state.
390
+ # It just tells the server not to expect any more data from this end
391
+ # of the pipe, and allows the pipe to drain until the server closes it.
392
+ def finish_state(channel)
393
+ channel.eof!
394
+ end
395
+
396
+ # Invoked to report progress back to the client. If a callback was not
397
+ # set, this does nothing.
398
+ def progress_callback(channel, name, sent, total)
399
+ channel[:callback].call(channel, name, sent, total) if channel[:callback]
400
+ end
401
+ end
402
+ end
403
+
404
+ class Net::SSH::Connection::Session
405
+ # Provides a convenient way to initialize a SCP session given a Net::SSH
406
+ # session. Returns the Net::SCP instance, ready to use.
407
+ def scp
408
+ @scp ||= Net::SCP.new(self)
409
+ end
410
+ end