net-sftp-backports 4.0.0.backports

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,60 @@
1
+ require 'net/ssh/loggable'
2
+ require 'net/sftp/operations/file'
3
+
4
+ module Net; module SFTP; module Operations
5
+
6
+ # A factory class for opening files and returning Operations::File instances
7
+ # that wrap the SFTP handles that represent them. This is a convenience
8
+ # class for use when working with files synchronously. Rather than relying
9
+ # on the programmer to provide callbacks that define a state machine that
10
+ # describes the behavior of the program, this class (and Operations::File)
11
+ # provide an interface where calls will block until they return, mimicking
12
+ # the IO class' interface.
13
+ class FileFactory
14
+ # The SFTP session object that drives this file factory.
15
+ attr_reader :sftp
16
+
17
+ # Create a new instance on top of the given SFTP session instance.
18
+ def initialize(sftp)
19
+ @sftp = sftp
20
+ end
21
+
22
+ # :call-seq:
23
+ # open(name, flags="r", mode=nil) -> file
24
+ # open(name, flags="r", mode=nil) { |file| ... }
25
+ #
26
+ # Attempt to open a file on the remote server. The +flags+ parameter
27
+ # accepts the same values as the standard Ruby ::File#open method. The
28
+ # +mode+ parameter must be an integer describing the permissions to use
29
+ # if a new file is being created.
30
+ #
31
+ # If a block is given, the new Operations::File instance will be yielded
32
+ # to it, and closed automatically when the block terminates. Otherwise
33
+ # the object will be returned, and it is the caller's responsibility to
34
+ # close the file.
35
+ #
36
+ # sftp.file.open("/tmp/names.txt", "w") do |f|
37
+ # # ...
38
+ # end
39
+ def open(name, flags="r", mode=nil, &block)
40
+ handle = sftp.open!(name, flags, :permissions => mode)
41
+ file = Operations::File.new(sftp, handle)
42
+
43
+ if block_given?
44
+ begin
45
+ yield file
46
+ ensure
47
+ file.close
48
+ end
49
+ else
50
+ return file
51
+ end
52
+ end
53
+
54
+ # Returns +true+ if the argument refers to a directory on the remote host.
55
+ def directory?(path)
56
+ sftp.lstat!(path).directory?
57
+ end
58
+ end
59
+
60
+ end; end; end
@@ -0,0 +1,395 @@
1
+ require 'net/ssh/loggable'
2
+
3
+ module Net; module SFTP; module Operations
4
+
5
+ # A general purpose uploader module for Net::SFTP. It can upload IO objects,
6
+ # files, and even entire directory trees via SFTP, and provides a flexible
7
+ # progress reporting mechanism.
8
+ #
9
+ # To upload a single file to the remote server, simply specify both the
10
+ # local and remote paths:
11
+ #
12
+ # uploader = sftp.upload("/path/to/local.txt", "/path/to/remote.txt")
13
+ #
14
+ # By default, this operates asynchronously, so if you want to block until
15
+ # the upload finishes, you can use the 'bang' variant:
16
+ #
17
+ # sftp.upload!("/path/to/local.txt", "/path/to/remote.txt")
18
+ #
19
+ # Or, if you have multiple uploads that you want to run in parallel, you can
20
+ # employ the #wait method of the returned object:
21
+ #
22
+ # uploads = %w(file1 file2 file3).map { |f| sftp.upload(f, "remote/#{f}") }
23
+ # uploads.each { |u| u.wait }
24
+ #
25
+ # To upload an entire directory tree, recursively, simply pass the directory
26
+ # path as the first parameter:
27
+ #
28
+ # sftp.upload!("/path/to/directory", "/path/to/remote")
29
+ #
30
+ # This will upload "/path/to/directory", its contents, its subdirectories,
31
+ # and their contents, recursively, to "/path/to/remote" on the remote server.
32
+ #
33
+ # For uploading a directory without creating it, do
34
+ # sftp.upload!("/path/to/directory", "/path/to/remote", :mkdir => false)
35
+ #
36
+ # If you want to send data to a file on the remote server, but the data is
37
+ # in memory, you can pass an IO object and upload its contents:
38
+ #
39
+ # require 'stringio'
40
+ # io = StringIO.new(data)
41
+ # sftp.upload!(io, "/path/to/remote")
42
+ #
43
+ # The following options are supported:
44
+ #
45
+ # * <tt>:progress</tt> - either a block or an object to act as a progress
46
+ # callback. See the discussion of "progress monitoring" below.
47
+ # * <tt>:requests</tt> - the number of pending SFTP requests to allow at
48
+ # any given time. When uploading an entire directory tree recursively,
49
+ # this will default to 16, otherwise it will default to 2. Setting this
50
+ # higher might improve throughput. Reducing it will reduce throughput.
51
+ # * <tt>:read_size</tt> - the maximum number of bytes to read at a time
52
+ # from the source. Increasing this value might improve throughput. It
53
+ # defaults to 32,000 bytes.
54
+ # * <tt>:name</tt> - the filename to report to the progress monitor when
55
+ # an IO object is given as +local+. This defaults to "<memory>".
56
+ #
57
+ # == Progress Monitoring
58
+ #
59
+ # Sometimes it is desirable to track the progress of an upload. There are
60
+ # two ways to do this: either using a callback block, or a special custom
61
+ # object.
62
+ #
63
+ # Using a block it's pretty straightforward:
64
+ #
65
+ # sftp.upload!("local", "remote") do |event, uploader, *args|
66
+ # case event
67
+ # when :open then
68
+ # # args[0] : file metadata
69
+ # puts "starting upload: #{args[0].local} -> #{args[0].remote} (#{args[0].size} bytes}"
70
+ # when :put then
71
+ # # args[0] : file metadata
72
+ # # args[1] : byte offset in remote file
73
+ # # args[2] : data being written (as string)
74
+ # puts "writing #{args[2].length} bytes to #{args[0].remote} starting at #{args[1]}"
75
+ # when :close then
76
+ # # args[0] : file metadata
77
+ # puts "finished with #{args[0].remote}"
78
+ # when :mkdir then
79
+ # # args[0] : remote path name
80
+ # puts "creating directory #{args[0]}"
81
+ # when :finish then
82
+ # puts "all done!"
83
+ # end
84
+ #
85
+ # However, for more complex implementations (e.g., GUI interfaces and such)
86
+ # a block can become cumbersome. In those cases, you can create custom
87
+ # handler objects that respond to certain methods, and then pass your handler
88
+ # to the uploader:
89
+ #
90
+ # class CustomHandler
91
+ # def on_open(uploader, file)
92
+ # puts "starting upload: #{file.local} -> #{file.remote} (#{file.size} bytes)"
93
+ # end
94
+ #
95
+ # def on_put(uploader, file, offset, data)
96
+ # puts "writing #{data.length} bytes to #{file.remote} starting at #{offset}"
97
+ # end
98
+ #
99
+ # def on_close(uploader, file)
100
+ # puts "finished with #{file.remote}"
101
+ # end
102
+ #
103
+ # def on_mkdir(uploader, path)
104
+ # puts "creating directory #{path}"
105
+ # end
106
+ #
107
+ # def on_finish(uploader)
108
+ # puts "all done!"
109
+ # end
110
+ # end
111
+ #
112
+ # sftp.upload!("local", "remote", :progress => CustomHandler.new)
113
+ #
114
+ # If you omit any of those methods, the progress updates for those missing
115
+ # events will be ignored. You can create a catchall method named "call" for
116
+ # those, instead.
117
+ class Upload
118
+ include Net::SSH::Loggable
119
+
120
+ # The source of the upload (on the local server)
121
+ attr_reader :local
122
+
123
+ # The destination of the upload (on the remote server)
124
+ attr_reader :remote
125
+
126
+ # The hash of options that were given when the object was instantiated
127
+ attr_reader :options
128
+
129
+ # The SFTP session object used by this upload instance
130
+ attr_reader :sftp
131
+
132
+ # The properties hash for this object
133
+ attr_reader :properties
134
+
135
+ # Instantiates a new uploader process on top of the given SFTP session.
136
+ # +local+ is either an IO object containing data to upload, or a string
137
+ # identifying a file or directory on the local host. +remote+ is a string
138
+ # identifying the location on the remote host that the upload should
139
+ # target.
140
+ #
141
+ # This will return immediately, and requires that the SSH event loop be
142
+ # run in order to effect the upload. (See #wait.)
143
+ def initialize(sftp, local, remote, options={}, &progress) #:nodoc:
144
+ @sftp = sftp
145
+ @local = local
146
+ @remote = remote
147
+ @progress = progress || options[:progress]
148
+ @options = options
149
+ @properties = options[:properties] || {}
150
+ @active = 0
151
+
152
+ self.logger = sftp.logger
153
+
154
+ @uploads = []
155
+ @recursive = local.respond_to?(:read) ? false : ::File.directory?(local)
156
+
157
+ if recursive?
158
+ @stack = [entries_for(local)]
159
+ @local_cwd = local
160
+ @remote_cwd = remote
161
+
162
+ @active += 1
163
+ if @options[:mkdir]
164
+ sftp.mkdir(remote) do |response|
165
+ @active -= 1
166
+ raise StatusException.new(response, "mkdir `#{remote}'") unless response.ok?
167
+ (options[:requests] || RECURSIVE_READERS).to_i.times do
168
+ break unless process_next_entry
169
+ end
170
+ end
171
+ else
172
+ @active -= 1
173
+ process_next_entry
174
+ end
175
+ else
176
+ raise ArgumentError, "expected a file to upload" unless local.respond_to?(:read) || ::File.exist?(local)
177
+ @stack = [[local]]
178
+ process_next_entry
179
+ end
180
+ end
181
+
182
+ # Returns true if a directory tree is being uploaded, and false if only a
183
+ # single file is being uploaded.
184
+ def recursive?
185
+ @recursive
186
+ end
187
+
188
+ # Returns true if the uploader is currently running. When this is false,
189
+ # the uploader has finished processing.
190
+ def active?
191
+ @active > 0 || @stack.any?
192
+ end
193
+
194
+ # Forces the transfer to stop.
195
+ def abort!
196
+ @active = 0
197
+ @stack.clear
198
+ @uploads.clear
199
+ end
200
+
201
+ # Blocks until the upload has completed.
202
+ def wait
203
+ sftp.loop { active? }
204
+ self
205
+ end
206
+
207
+ # Returns the property with the given name. This allows Upload instances
208
+ # to store their own state when used as part of a state machine.
209
+ def [](name)
210
+ @properties[name.to_sym]
211
+ end
212
+
213
+ # Sets the given property to the given name. This allows Upload instances
214
+ # to store their own state when used as part of a state machine.
215
+ def []=(name, value)
216
+ @properties[name.to_sym] = value
217
+ end
218
+
219
+ private
220
+
221
+ #--
222
+ # "ruby -w" hates private attributes, so we have to do this longhand.
223
+ #++
224
+
225
+ # The progress handler for this instance. Possibly nil.
226
+ def progress; @progress; end
227
+
228
+ # A simple struct for recording metadata about the file currently being
229
+ # uploaded.
230
+ LiveFile = Struct.new(:local, :remote, :io, :size, :handle)
231
+
232
+ # The default # of bytes to read from disk at a time.
233
+ DEFAULT_READ_SIZE = 32_000
234
+
235
+ # The number of readers to use when uploading a single file.
236
+ SINGLE_FILE_READERS = 2
237
+
238
+ # The number of readers to use when uploading a directory.
239
+ RECURSIVE_READERS = 16
240
+
241
+ # Examines the stack and determines what action to take. This is the
242
+ # starting point of the state machine.
243
+ def process_next_entry
244
+ if @stack.empty?
245
+ if @uploads.any?
246
+ write_next_chunk(@uploads.first)
247
+ elsif !active?
248
+ update_progress(:finish)
249
+ end
250
+ return false
251
+ elsif @stack.last.empty?
252
+ @stack.pop
253
+ @local_cwd = ::File.dirname(@local_cwd)
254
+ @remote_cwd = ::File.dirname(@remote_cwd)
255
+ process_next_entry
256
+ elsif recursive?
257
+ entry = @stack.last.shift
258
+ lpath = ::File.join(@local_cwd, entry)
259
+ rpath = ::File.join(@remote_cwd, entry)
260
+
261
+ if ::File.directory?(lpath)
262
+ @stack.push(entries_for(lpath))
263
+ @local_cwd = lpath
264
+ @remote_cwd = rpath
265
+
266
+ @active += 1
267
+ update_progress(:mkdir, rpath)
268
+ request = sftp.mkdir(rpath, &method(:on_mkdir))
269
+ request[:dir] = rpath
270
+ else
271
+ open_file(lpath, rpath)
272
+ end
273
+ else
274
+ open_file(@stack.pop.first, remote)
275
+ end
276
+ return true
277
+ end
278
+
279
+ # Prepares to send +local+ to +remote+.
280
+ def open_file(local, remote)
281
+ @active += 1
282
+
283
+ if local.respond_to?(:read)
284
+ file = local
285
+ name = options[:name] || "<memory>"
286
+ else
287
+ file = ::File.open(local, "rb")
288
+ name = local
289
+ end
290
+
291
+ if file.respond_to?(:stat)
292
+ size = file.stat.size
293
+ else
294
+ size = file.size
295
+ end
296
+
297
+ metafile = LiveFile.new(name, remote, file, size)
298
+ update_progress(:open, metafile)
299
+
300
+ request = sftp.open(remote, "w", &method(:on_open))
301
+ request[:file] = metafile
302
+ end
303
+
304
+ # Called when a +mkdir+ request finishes, successfully or otherwise.
305
+ # If the request failed, this will raise a StatusException, otherwise
306
+ # it will call #process_next_entry to continue the state machine.
307
+ def on_mkdir(response)
308
+ @active -= 1
309
+ dir = response.request[:dir]
310
+ raise StatusException.new(response, "mkdir #{dir}") unless response.ok?
311
+
312
+ process_next_entry
313
+ end
314
+
315
+ # Called when an +open+ request finishes. Raises StatusException if the
316
+ # open failed, otherwise it calls #write_next_chunk to begin sending
317
+ # data to the remote server.
318
+ def on_open(response)
319
+ @active -= 1
320
+ file = response.request[:file]
321
+ raise StatusException.new(response, "open #{file.remote}") unless response.ok?
322
+
323
+ file.handle = response[:handle]
324
+
325
+ @uploads << file
326
+ write_next_chunk(file)
327
+
328
+ if !recursive?
329
+ (options[:requests] || SINGLE_FILE_READERS).to_i.times { write_next_chunk(file) }
330
+ end
331
+ end
332
+
333
+ # Called when a +write+ request finishes. Raises StatusException if the
334
+ # write failed, otherwise it calls #write_next_chunk to continue the
335
+ # write.
336
+ def on_write(response)
337
+ @active -= 1
338
+ file = response.request[:file]
339
+ raise StatusException.new(response, "write #{file.remote}") unless response.ok?
340
+ write_next_chunk(file)
341
+ end
342
+
343
+ # Called when a +close+ request finishes. Raises a StatusException if the
344
+ # close failed, otherwise it calls #process_next_entry to continue the
345
+ # state machine.
346
+ def on_close(response)
347
+ @active -= 1
348
+ file = response.request[:file]
349
+ raise StatusException.new(response, "close #{file.remote}") unless response.ok?
350
+ process_next_entry
351
+ end
352
+
353
+ # Attempts to send the next chunk from the given file (where +file+ is
354
+ # a LiveFile instance).
355
+ def write_next_chunk(file)
356
+ if file.io.nil?
357
+ process_next_entry
358
+ else
359
+ @active += 1
360
+ offset = file.io.pos
361
+ data = file.io.read(options[:read_size] || DEFAULT_READ_SIZE)
362
+ if data.nil?
363
+ update_progress(:close, file)
364
+ request = sftp.close(file.handle, &method(:on_close))
365
+ request[:file] = file
366
+ file.io.close
367
+ file.io = nil
368
+ @uploads.delete(file)
369
+ else
370
+ update_progress(:put, file, offset, data)
371
+ request = sftp.write(file.handle, offset, data, &method(:on_write))
372
+ request[:file] = file
373
+ end
374
+ end
375
+ end
376
+
377
+ # Returns all directory entries for the given path, removing the '.'
378
+ # and '..' relative paths.
379
+ def entries_for(local)
380
+ ::Dir.entries(local).reject { |v| %w(. ..).include?(v) }
381
+ end
382
+
383
+ # Attempts to notify the progress monitor (if one was given) about
384
+ # progress made for the given event.
385
+ def update_progress(event, *args)
386
+ on = "on_#{event}"
387
+ if progress.respond_to?(on)
388
+ progress.send(on, self, *args)
389
+ elsif progress.respond_to?(:call)
390
+ progress.call(event, self, *args)
391
+ end
392
+ end
393
+ end
394
+
395
+ end; end; end
@@ -0,0 +1,21 @@
1
+ require 'net/ssh/buffer'
2
+
3
+ module Net; module SFTP
4
+
5
+ # A specialization of the Net::SSH::Buffer class, which simply auto-reads
6
+ # the type byte from the front of every packet it represents.
7
+ class Packet < Net::SSH::Buffer
8
+ # The (intger) type of this packet. See Net::SFTP::Constants for all
9
+ # possible packet types.
10
+ attr_reader :type
11
+
12
+ # Create a new Packet object that wraps the given +data+ (which should be
13
+ # a String). The first byte of the data will be consumed automatically and
14
+ # interpreted as the #type of this packet.
15
+ def initialize(data)
16
+ super
17
+ @type = read_byte
18
+ end
19
+ end
20
+
21
+ end; end