net-scp 1.0.0

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/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