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 +8 -0
- data/Manifest +17 -0
- data/README.rdoc +98 -0
- data/Rakefile +30 -0
- data/lib/net/scp.rb +410 -0
- data/lib/net/scp/download.rb +150 -0
- data/lib/net/scp/errors.rb +5 -0
- data/lib/net/scp/upload.rb +142 -0
- data/lib/net/scp/version.rb +18 -0
- data/lib/uri/open-scp.rb +18 -0
- data/lib/uri/scp.rb +35 -0
- data/net-scp.gemspec +60 -0
- data/setup.rb +1331 -0
- data/test/common.rb +138 -0
- data/test/test_all.rb +3 -0
- data/test/test_download.rb +142 -0
- data/test/test_scp.rb +60 -0
- data/test/test_upload.rb +241 -0
- metadata +78 -0
data/CHANGELOG.rdoc
ADDED
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
|