net-sftp-backports 4.0.0.backports

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.
@@ -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