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