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,954 @@
1
+ require 'net/ssh'
2
+ require 'net/sftp/constants'
3
+ require 'net/sftp/errors'
4
+ require 'net/sftp/protocol'
5
+ require 'net/sftp/request'
6
+ require 'net/sftp/operations/dir'
7
+ require 'net/sftp/operations/upload'
8
+ require 'net/sftp/operations/download'
9
+ require 'net/sftp/operations/file_factory'
10
+
11
+ module Net; module SFTP
12
+
13
+ # The Session class encapsulates a single SFTP channel on a Net::SSH
14
+ # connection. Instances of this class are what most applications will
15
+ # interact with most, as it provides access to both low-level (mkdir,
16
+ # rename, remove, symlink, etc.) and high-level (upload, download, etc.)
17
+ # SFTP operations.
18
+ #
19
+ # Although Session makes it easy to do SFTP operations serially, you can
20
+ # also set up multiple operations to be done in parallel, too, without
21
+ # needing to resort to threading. You merely need to fire off the requests,
22
+ # and then run the event loop until all of the requests have completed:
23
+ #
24
+ # handle1 = sftp.open!("/path/to/file1")
25
+ # handle2 = sftp.open!("/path/to/file2")
26
+ #
27
+ # r1 = sftp.read(handle1, 0, 1024)
28
+ # r2 = sftp.read(handle2, 0, 1024)
29
+ # sftp.loop { [r1, r2].any? { |r| r.pending? } }
30
+ #
31
+ # puts "chunk #1: #{r1.response[:data]}"
32
+ # puts "chunk #2: #{r2.response[:data]}"
33
+ #
34
+ # By passing blocks to the operations, you can set up powerful state
35
+ # machines, to fire off subsequent operations. In fact, the Net::SFTP::Operations::Upload
36
+ # and Net::SFTP::Operations::Download classes set up such state machines, so that
37
+ # multiple uploads and/or downloads can be running simultaneously.
38
+ #
39
+ # The convention with the names of the operations is as follows: if the method
40
+ # name ends with an exclamation mark, like #read!, it will be synchronous
41
+ # (e.g., it will block until the server responds). Methods without an
42
+ # exclamation mark (e.g. #read) are asynchronous, and return before the
43
+ # server has responded. You will need to make sure the SSH event loop is
44
+ # run in order to process these requests. (See #loop.)
45
+ class Session
46
+ include Net::SSH::Loggable
47
+ include Net::SFTP::Constants::PacketTypes
48
+
49
+ # The highest protocol version supported by the Net::SFTP library.
50
+ HIGHEST_PROTOCOL_VERSION_SUPPORTED = 6
51
+
52
+ # A reference to the Net::SSH session object that powers this SFTP session.
53
+ attr_reader :session
54
+
55
+ # The Net::SSH::Connection::Channel object that the SFTP session is being
56
+ # processed by.
57
+ attr_reader :channel
58
+
59
+ # The state of the SFTP connection. It will be :opening, :subsystem, :init,
60
+ # :open, or :closed.
61
+ attr_reader :state
62
+
63
+ # The protocol instance being used by this SFTP session. Useful for
64
+ # querying the protocol version in effect.
65
+ attr_reader :protocol
66
+
67
+ # The hash of pending requests. Any requests that have been sent and which
68
+ # the server has not yet responded to will be represented here.
69
+ attr_reader :pending_requests
70
+
71
+ # Creates a new Net::SFTP instance atop the given Net::SSH connection.
72
+ # This will return immediately, before the SFTP connection has been properly
73
+ # initialized. Once the connection is ready, the given block will be called.
74
+ # If you want to block until the connection has been initialized, try this:
75
+ #
76
+ # sftp = Net::SFTP::Session.new(ssh)
77
+ # sftp.loop { sftp.opening? }
78
+ def initialize(session, version = nil, &block)
79
+ @session = session
80
+ @version = version
81
+ @input = Net::SSH::Buffer.new
82
+ self.logger = session.logger
83
+ @state = :closed
84
+ @pending_requests = {}
85
+
86
+ connect(&block)
87
+ end
88
+
89
+ public # high-level SFTP operations
90
+
91
+ # Initiates an upload from +local+ to +remote+, asynchronously. This
92
+ # method will return a new Net::SFTP::Operations::Upload instance, and requires
93
+ # the event loop to be run in order for the upload to progress. See
94
+ # Net::SFTP::Operations::Upload for a full discussion of how this method can be
95
+ # used.
96
+ #
97
+ # uploader = sftp.upload("/local/path", "/remote/path")
98
+ # uploader.wait
99
+ def upload(local, remote = File.basename(local), options={}, &block)
100
+ Operations::Upload.new(self, local, remote, options, &block)
101
+ end
102
+
103
+ # Identical to #upload, but blocks until the upload is complete.
104
+ def upload!(local, remote = File.basename(local), options={}, &block)
105
+ upload(local, remote, options, &block).wait
106
+ end
107
+
108
+ # Initiates a download from +remote+ to +local+, asynchronously. This
109
+ # method will return a new Net::SFTP::Operations::Download instance, and requires
110
+ # that the event loop be run in order for the download to progress. See
111
+ # Net::SFTP::Operations::Download for a full discussion of how this method can be
112
+ # used.
113
+ #
114
+ # download = sftp.download("/remote/path", "/local/path")
115
+ # download.wait
116
+ def download(remote, local, options={}, &block)
117
+ Operations::Download.new(self, local, remote, options, &block)
118
+ end
119
+
120
+ # Identical to #download, but blocks until the download is complete.
121
+ # If +local+ is omitted, downloads the file to an in-memory buffer
122
+ # and returns the result as a string; otherwise, returns the
123
+ # Net::SFTP::Operations::Download instance.
124
+ def download!(remote, local=nil, options={}, &block)
125
+ require 'stringio' unless defined?(StringIO)
126
+ destination = local || StringIO.new
127
+ result = download(remote, destination, options, &block).wait
128
+ local ? result : destination.string
129
+ end
130
+
131
+ # Returns an Net::SFTP::Operations::FileFactory instance, which can be used to
132
+ # mimic synchronous, IO-like file operations on a remote file via
133
+ # SFTP.
134
+ #
135
+ # sftp.file.open("/path/to/file") do |file|
136
+ # while line = file.gets
137
+ # puts line
138
+ # end
139
+ # end
140
+ #
141
+ # See Net::SFTP::Operations::FileFactory and Net::SFTP::Operations::File for more details.
142
+ def file
143
+ @file ||= Operations::FileFactory.new(self)
144
+ end
145
+
146
+ # Returns a Net::SFTP::Operations::Dir instance, which can be used to
147
+ # conveniently iterate over and search directories on the remote server.
148
+ #
149
+ # sftp.dir.glob("/base/path", "*/**/*.rb") do |entry|
150
+ # p entry.name
151
+ # end
152
+ #
153
+ # See Net::SFTP::Operations::Dir for a more detailed discussion of how
154
+ # to use this.
155
+ def dir
156
+ @dir ||= Operations::Dir.new(self)
157
+ end
158
+
159
+ public # low-level SFTP operations
160
+
161
+ # :call-seq:
162
+ # open(path, flags="r", options={}) -> request
163
+ # open(path, flags="r", options={}) { |response| ... } -> request
164
+ #
165
+ # Opens a file on the remote server. The +flags+ parameter determines
166
+ # how the flag is open, and accepts the same format as IO#open (e.g.,
167
+ # either a string like "r" or "w", or a combination of the IO constants).
168
+ # The +options+ parameter is a hash of attributes to be associated
169
+ # with the file, and varies greatly depending on the SFTP protocol
170
+ # version in use, but some (like :permissions) are always available.
171
+ #
172
+ # Returns immediately with a Request object. If a block is given, it will
173
+ # be invoked when the server responds, with a Response object as the only
174
+ # parameter. The :handle property of the response is the handle of the
175
+ # opened file, and may be passed to other methods (like #close, #read,
176
+ # #write, and so forth).
177
+ #
178
+ # sftp.open("/path/to/file") do |response|
179
+ # raise "fail!" unless response.ok?
180
+ # sftp.close(response[:handle])
181
+ # end
182
+ # sftp.loop
183
+ def open(path, flags="r", options={}, &callback)
184
+ request :open, path, flags, options, &callback
185
+ end
186
+
187
+ # Identical to #open, but blocks until the server responds. It will raise
188
+ # a StatusException if the request was unsuccessful. Otherwise, it will
189
+ # return the handle of the newly opened file.
190
+ #
191
+ # handle = sftp.open!("/path/to/file")
192
+ def open!(path, flags="r", options={}, &callback)
193
+ wait_for(open(path, flags, options, &callback), :handle)
194
+ end
195
+
196
+ # :call-seq:
197
+ # close(handle) -> request
198
+ # close(handle) { |response| ... } -> request
199
+ #
200
+ # Closes an open handle, whether obtained via #open, or #opendir. Returns
201
+ # immediately with a Request object. If a block is given, it will be
202
+ # invoked when the server responds.
203
+ #
204
+ # sftp.open("/path/to/file") do |response|
205
+ # raise "fail!" unless response.ok?
206
+ # sftp.close(response[:handle])
207
+ # end
208
+ # sftp.loop
209
+ def close(handle, &callback)
210
+ request :close, handle, &callback
211
+ end
212
+
213
+ # Identical to #close, but blocks until the server responds. It will
214
+ # raise a StatusException if the request was unsuccessful. Otherwise,
215
+ # it returns the Response object for this request.
216
+ #
217
+ # sftp.close!(handle)
218
+ def close!(handle, &callback)
219
+ wait_for(close(handle, &callback))
220
+ end
221
+
222
+ # :call-seq:
223
+ # read(handle, offset, length) -> request
224
+ # read(handle, offset, length) { |response| ... } -> request
225
+ #
226
+ # Requests that +length+ bytes, starting at +offset+ bytes from the
227
+ # beginning of the file, be read from the file identified by
228
+ # +handle+. (The +handle+ should be a value obtained via the #open
229
+ # method.) Returns immediately with a Request object. If a block is
230
+ # given, it will be invoked when the server responds.
231
+ #
232
+ # The :data property of the response will contain the requested data,
233
+ # assuming the call was successful.
234
+ #
235
+ # request = sftp.read(handle, 0, 1024) do |response|
236
+ # if response.eof?
237
+ # puts "end of file reached before reading any data"
238
+ # elsif !response.ok?
239
+ # puts "error (#{response})"
240
+ # else
241
+ # print(response[:data])
242
+ # end
243
+ # end
244
+ # request.wait
245
+ #
246
+ # To read an entire file will usually require multiple calls to #read,
247
+ # unless you know in advance how large the file is.
248
+ def read(handle, offset, length, &callback)
249
+ request :read, handle, offset, length, &callback
250
+ end
251
+
252
+ # Identical to #read, but blocks until the server responds. It will raise
253
+ # a StatusException if the request was unsuccessful. If the end of the file
254
+ # was reached, +nil+ will be returned. Otherwise, it returns the data that
255
+ # was read, as a String.
256
+ #
257
+ # data = sftp.read!(handle, 0, 1024)
258
+ def read!(handle, offset, length, &callback)
259
+ wait_for(read(handle, offset, length, &callback), :data)
260
+ end
261
+
262
+ # :call-seq:
263
+ # write(handle, offset, data) -> request
264
+ # write(handle, offset, data) { |response| ... } -> request
265
+ #
266
+ # Requests that +data+ be written to the file identified by +handle+,
267
+ # starting at +offset+ bytes from the start of the file. The file must
268
+ # have been opened for writing via #open. Returns immediately with a
269
+ # Request object. If a block is given, it will be invoked when the
270
+ # server responds.
271
+ #
272
+ # request = sftp.write(handle, 0, "hello, world!\n")
273
+ # request.wait
274
+ def write(handle, offset, data, &callback)
275
+ request :write, handle, offset, data, &callback
276
+ end
277
+
278
+ # Identical to #write, but blocks until the server responds. It will raise
279
+ # a StatusException if the request was unsuccessful, or the end of the file
280
+ # was reached. Otherwise, it returns the Response object for this request.
281
+ #
282
+ # sftp.write!(handle, 0, "hello, world!\n")
283
+ def write!(handle, offset, data, &callback)
284
+ wait_for(write(handle, offset, data, &callback))
285
+ end
286
+
287
+ # :call-seq:
288
+ # lstat(path, flags=nil) -> request
289
+ # lstat(path, flags=nil) { |response| ... } -> request
290
+ #
291
+ # This method is identical to the #stat method, with the exception that
292
+ # it will not follow symbolic links (thus allowing you to stat the
293
+ # link itself, rather than what it refers to). The +flags+ parameter
294
+ # is not used in SFTP protocol versions prior to 4, and will be ignored
295
+ # in those versions of the protocol that do not use it. For those that
296
+ # do, however, you may provide hints as to which file proprties you wish
297
+ # to query (e.g., if all you want is permissions, you could pass the
298
+ # Net::SFTP::Protocol::V04::Attributes::F_PERMISSIONS flag as the value
299
+ # for the +flags+ parameter).
300
+ #
301
+ # The method returns immediately with a Request object. If a block is given,
302
+ # it will be invoked when the server responds. The :attrs property of
303
+ # the response will contain an Attributes instance appropriate for the
304
+ # the protocol version (see Protocol::V01::Attributes, Protocol::V04::Attributes,
305
+ # and Protocol::V06::Attributes).
306
+ #
307
+ # request = sftp.lstat("/path/to/file") do |response|
308
+ # raise "fail!" unless response.ok?
309
+ # puts "permissions: %04o" % response[:attrs].permissions
310
+ # end
311
+ # request.wait
312
+ def lstat(path, flags=nil, &callback)
313
+ request :lstat, path, flags, &callback
314
+ end
315
+
316
+ # Identical to the #lstat method, but blocks until the server responds.
317
+ # It will raise a StatusException if the request was unsuccessful.
318
+ # Otherwise, it will return the attribute object describing the path.
319
+ #
320
+ # puts sftp.lstat!("/path/to/file").permissions
321
+ def lstat!(path, flags=nil, &callback)
322
+ wait_for(lstat(path, flags, &callback), :attrs)
323
+ end
324
+
325
+ # The fstat method is identical to the #stat and #lstat methods, with
326
+ # the exception that it takes a +handle+ as the first parameter, such
327
+ # as would be obtained via the #open or #opendir methods. (See the #lstat
328
+ # method for full documentation).
329
+ def fstat(handle, flags=nil, &callback)
330
+ request :fstat, handle, flags, &callback
331
+ end
332
+
333
+ # Identical to the #fstat method, but blocks until the server responds.
334
+ # It will raise a StatusException if the request was unsuccessful.
335
+ # Otherwise, it will return the attribute object describing the path.
336
+ #
337
+ # puts sftp.fstat!(handle).permissions
338
+ def fstat!(handle, flags=nil, &callback)
339
+ wait_for(fstat(handle, flags, &callback), :attrs)
340
+ end
341
+
342
+ # :call-seq:
343
+ # setstat(path, attrs) -> request
344
+ # setstat(path, attrs) { |response| ... } -> request
345
+ #
346
+ # This method may be used to set file metadata (such as permissions, or
347
+ # user/group information) on a remote file. The exact metadata that may
348
+ # be tweaked is dependent on the SFTP protocol version in use, but in
349
+ # general you may set at least the permissions, user, and group. (See
350
+ # Protocol::V01::Attributes, Protocol::V04::Attributes, and Protocol::V06::Attributes
351
+ # for the full lists of attributes that may be set for the different
352
+ # protocols.)
353
+ #
354
+ # The +attrs+ parameter is a hash, where the keys are symbols identifying
355
+ # the attributes to set.
356
+ #
357
+ # The method returns immediately with a Request object. If a block is given,
358
+ # it will be invoked when the server responds.
359
+ #
360
+ # request = sftp.setstat("/path/to/file", :permissions => 0644)
361
+ # request.wait
362
+ # puts "success: #{request.response.ok?}"
363
+ def setstat(path, attrs, &callback)
364
+ request :setstat, path, attrs, &callback
365
+ end
366
+
367
+ # Identical to the #setstat method, but blocks until the server responds.
368
+ # It will raise a StatusException if the request was unsuccessful.
369
+ # Otherwise, it will return the Response object for the request.
370
+ #
371
+ # sftp.setstat!("/path/to/file", :permissions => 0644)
372
+ def setstat!(path, attrs, &callback)
373
+ wait_for(setstat(path, attrs, &callback))
374
+ end
375
+
376
+ # The fsetstat method is identical to the #setstat method, with the
377
+ # exception that it takes a +handle+ as the first parameter, such as
378
+ # would be obtained via the #open or #opendir methods. (See the
379
+ # #setstat method for full documentation.)
380
+ def fsetstat(handle, attrs, &callback)
381
+ request :fsetstat, handle, attrs, &callback
382
+ end
383
+
384
+ # Identical to the #fsetstat method, but blocks until the server responds.
385
+ # It will raise a StatusException if the request was unsuccessful.
386
+ # Otherwise, it will return the Response object for the request.
387
+ #
388
+ # sftp.fsetstat!(handle, :permissions => 0644)
389
+ def fsetstat!(handle, attrs, &callback)
390
+ wait_for(fsetstat(handle, attrs, &callback))
391
+ end
392
+
393
+ # :call-seq:
394
+ # opendir(path) -> request
395
+ # opendir(path) { |response| ... } -> request
396
+ #
397
+ # Attempts to open a directory on the remote host for reading. Once the
398
+ # handle is obtained, directory entries may be retrieved using the
399
+ # #readdir method. The method returns immediately with a Request object.
400
+ # If a block is given, it will be invoked when the server responds.
401
+ #
402
+ # sftp.opendir("/path/to/directory") do |response|
403
+ # raise "fail!" unless response.ok?
404
+ # sftp.close(response[:handle])
405
+ # end
406
+ # sftp.loop
407
+ def opendir(path, &callback)
408
+ request :opendir, path, &callback
409
+ end
410
+
411
+ # Identical to #opendir, but blocks until the server responds. It will raise
412
+ # a StatusException if the request was unsuccessful. Otherwise, it will
413
+ # return a handle to the given path.
414
+ #
415
+ # handle = sftp.opendir!("/path/to/directory")
416
+ def opendir!(path, &callback)
417
+ wait_for(opendir(path, &callback), :handle)
418
+ end
419
+
420
+ # :call-seq:
421
+ # readdir(handle) -> request
422
+ # readdir(handle) { |response| ... } -> request
423
+ #
424
+ # Reads a set of entries from the given directory handle (which must
425
+ # have been obtained via #opendir). If the response is EOF, then there
426
+ # are no more entries in the directory. Otherwise, the entries will be
427
+ # in the :names property of the response:
428
+ #
429
+ # loop do
430
+ # request = sftp.readdir(handle).wait
431
+ # break if request.response.eof?
432
+ # raise "fail!" unless request.response.ok?
433
+ # request.response[:names].each do |entry|
434
+ # puts entry.name
435
+ # end
436
+ # end
437
+ #
438
+ # See also Protocol::V01::Name and Protocol::V04::Name for the specific
439
+ # properties of each individual entry (which vary based on the SFTP
440
+ # protocol version in use).
441
+ def readdir(handle, &callback)
442
+ request :readdir, handle, &callback
443
+ end
444
+
445
+ # Identical to #readdir, but blocks until the server responds. It will raise
446
+ # a StatusException if the request was unsuccessful. Otherwise, it will
447
+ # return nil if there were no more names to read, or an array of name
448
+ # entries.
449
+ #
450
+ # while (entries = sftp.readdir!(handle)) do
451
+ # entries.each { |entry| puts(entry.name) }
452
+ # end
453
+ def readdir!(handle, &callback)
454
+ wait_for(readdir(handle, &callback), :names)
455
+ end
456
+
457
+ # :call-seq:
458
+ # remove(filename) -> request
459
+ # remove(filename) { |response| ... } -> request
460
+ #
461
+ # Attempts to remove the given file from the remote file system. Returns
462
+ # immediately with a Request object. If a block is given, the block will
463
+ # be invoked when the server responds, and will be passed a Response
464
+ # object.
465
+ #
466
+ # sftp.remove("/path/to/file").wait
467
+ def remove(filename, &callback)
468
+ request :remove, filename, &callback
469
+ end
470
+
471
+ # Identical to #remove, but blocks until the server responds. It will raise
472
+ # a StatusException if the request was unsuccessful. Otherwise, it will
473
+ # return the Response object for the request.
474
+ #
475
+ # sftp.remove!("/path/to/file")
476
+ def remove!(filename, &callback)
477
+ wait_for(remove(filename, &callback))
478
+ end
479
+
480
+ # :call-seq:
481
+ # mkdir(path, attrs={}) -> request
482
+ # mkdir(path, attrs={}) { |response| ... } -> request
483
+ #
484
+ # Creates the named directory on the remote server. If an attribute hash
485
+ # is given, it must map to the set of attributes supported by the version
486
+ # of the SFTP protocol in use. (See Protocol::V01::Attributes,
487
+ # Protocol::V04::Attributes, and Protocol::V06::Attributes.)
488
+ #
489
+ # sftp.mkdir("/path/to/directory", :permissions => 0550).wait
490
+ def mkdir(path, attrs={}, &callback)
491
+ request :mkdir, path, attrs, &callback
492
+ end
493
+
494
+ # Identical to #mkdir, but blocks until the server responds. It will raise
495
+ # a StatusException if the request was unsuccessful. Otherwise, it will
496
+ # return the Response object for the request.
497
+ #
498
+ # sftp.mkdir!("/path/to/directory", :permissions => 0550)
499
+ def mkdir!(path, attrs={}, &callback)
500
+ wait_for(mkdir(path, attrs, &callback))
501
+ end
502
+
503
+ # :call-seq:
504
+ # rmdir(path) -> request
505
+ # rmdir(path) { |response| ... } -> request
506
+ #
507
+ # Removes the named directory on the remote server. The directory must
508
+ # be empty before it can be removed.
509
+ #
510
+ # sftp.rmdir("/path/to/directory").wait
511
+ def rmdir(path, &callback)
512
+ request :rmdir, path, &callback
513
+ end
514
+
515
+ # Identical to #rmdir, but blocks until the server responds. It will raise
516
+ # a StatusException if the request was unsuccessful. Otherwise, it will
517
+ # return the Response object for the request.
518
+ #
519
+ # sftp.rmdir!("/path/to/directory")
520
+ def rmdir!(path, &callback)
521
+ wait_for(rmdir(path, &callback))
522
+ end
523
+
524
+ # :call-seq:
525
+ # realpath(path) -> request
526
+ # realpath(path) { |response| ... } -> request
527
+ #
528
+ # Tries to canonicalize the given path, turning any given path into an
529
+ # absolute path. This is primarily useful for converting a path with
530
+ # ".." or "." segments into an identical path without those segments.
531
+ # The answer will be in the response's :names attribute, as a
532
+ # one-element array.
533
+ #
534
+ # request = sftp.realpath("/path/../to/../directory").wait
535
+ # puts request[:names].first.name
536
+ def realpath(path, &callback)
537
+ request :realpath, path, &callback
538
+ end
539
+
540
+ # Identical to #realpath, but blocks until the server responds. It will raise
541
+ # a StatusException if the request was unsuccessful. Otherwise, it will
542
+ # return a name object identifying the path.
543
+ #
544
+ # puts(sftp.realpath!("/path/../to/../directory"))
545
+ def realpath!(path, &callback)
546
+ wait_for(realpath(path, &callback), :names).first
547
+ end
548
+
549
+ # Identical to the #lstat method, except that it follows symlinks
550
+ # (e.g., if you give it the path to a symlink, it will stat the target
551
+ # of the symlink rather than the symlink itself). See the #lstat method
552
+ # for full documentation.
553
+ def stat(path, flags=nil, &callback)
554
+ request :stat, path, flags, &callback
555
+ end
556
+
557
+ # Identical to #stat, but blocks until the server responds. It will raise
558
+ # a StatusException if the request was unsuccessful. Otherwise, it will
559
+ # return an attribute object for the named path.
560
+ #
561
+ # attrs = sftp.stat!("/path/to/file")
562
+ def stat!(path, flags=nil, &callback)
563
+ wait_for(stat(path, flags, &callback), :attrs)
564
+ end
565
+
566
+ # :call-seq:
567
+ # rename(name, new_name, flags=nil) -> request
568
+ # rename(name, new_name, flags=nil) { |response| ... } -> request
569
+ #
570
+ # Renames the given file. This operation is only available in SFTP
571
+ # protocol versions two and higher. The +flags+ parameter is ignored
572
+ # in versions prior to 5. In versions 5 and higher, the +flags+
573
+ # parameter can be used to specify how the rename should be performed
574
+ # (atomically, etc.).
575
+ #
576
+ # The following flags are defined in protocol version 5:
577
+ #
578
+ # * 0x0001 - overwrite an existing file if the new name specifies a file
579
+ # that already exists.
580
+ # * 0x0002 - perform the rewrite atomically.
581
+ # * 0x0004 - allow the server to perform the rename as it prefers.
582
+ def rename(name, new_name, flags=nil, &callback)
583
+ request :rename, name, new_name, flags, &callback
584
+ end
585
+
586
+ # Identical to #rename, but blocks until the server responds. It will raise
587
+ # a StatusException if the request was unsuccessful. Otherwise, it will
588
+ # return the Response object for the request.
589
+ #
590
+ # sftp.rename!("/path/to/old", "/path/to/new")
591
+ def rename!(name, new_name, flags=nil, &callback)
592
+ wait_for(rename(name, new_name, flags, &callback))
593
+ end
594
+
595
+ # :call-seq:
596
+ # readlink(path) -> request
597
+ # readlink(path) { |response| ... } -> request
598
+ #
599
+ # Queries the server for the target of the specified symbolic link.
600
+ # This operation is only available in protocol versions 3 and higher.
601
+ # The response to this request will include a names property, a one-element
602
+ # array naming the target of the symlink.
603
+ #
604
+ # request = sftp.readlink("/path/to/symlink").wait
605
+ # puts request.response[:names].first.name
606
+ def readlink(path, &callback)
607
+ request :readlink, path, &callback
608
+ end
609
+
610
+ # Identical to #readlink, but blocks until the server responds. It will raise
611
+ # a StatusException if the request was unsuccessful. Otherwise, it will
612
+ # return the Name object for the path that the symlink targets.
613
+ #
614
+ # item = sftp.readlink!("/path/to/symlink")
615
+ def readlink!(path, &callback)
616
+ wait_for(readlink(path, &callback), :names).first
617
+ end
618
+
619
+ # :call-seq:
620
+ # symlink(path, target) -> request
621
+ # symlink(path, target) { |response| ... } -> request
622
+ #
623
+ # Attempts to create a symlink to +path+ at +target+. This operation
624
+ # is only available in protocol versions 3, 4, and 5, but the Net::SFTP
625
+ # library mimics the symlink behavior in protocol version 6 using the
626
+ # #link method, so it is safe to use this method in protocol version 6.
627
+ #
628
+ # sftp.symlink("/path/to/file", "/path/to/symlink").wait
629
+ def symlink(path, target, &callback)
630
+ request :symlink, path, target, &callback
631
+ end
632
+
633
+ # Identical to #symlink, but blocks until the server responds. It will raise
634
+ # a StatusException if the request was unsuccessful. Otherwise, it will
635
+ # return the Response object for the request.
636
+ #
637
+ # sftp.symlink!("/path/to/file", "/path/to/symlink")
638
+ def symlink!(path, target, &callback)
639
+ wait_for(symlink(path, target, &callback))
640
+ end
641
+
642
+ # :call-seq:
643
+ # link(new_link_path, existing_path, symlink=true) -> request
644
+ # link(new_link_path, existing_path, symlink=true) { |response| ... } -> request
645
+ #
646
+ # Attempts to create a link, either hard or symbolic. This operation is
647
+ # only available in SFTP protocol versions 6 and higher. If the +symlink+
648
+ # paramter is true, a symbolic link will be created, otherwise a hard
649
+ # link will be created. The link will be named +new_link_path+, and will
650
+ # point to the path +existing_path+.
651
+ #
652
+ # sftp.link("/path/to/symlink", "/path/to/file", true).wait
653
+ #
654
+ # Note that #link is only available for SFTP protocol 6 and higher. You
655
+ # can use #symlink for protocols 3 and higher.
656
+ def link(new_link_path, existing_path, symlink=true, &callback)
657
+ request :link, new_link_path, existing_path, symlink, &callback
658
+ end
659
+
660
+ # Identical to #link, but blocks until the server responds. It will raise
661
+ # a StatusException if the request was unsuccessful. Otherwise, it will
662
+ # return the Response object for the request.
663
+ #
664
+ # sftp.link!("/path/to/symlink", "/path/to/file", true)
665
+ def link!(new_link_path, existing_path, symlink=true, &callback)
666
+ wait_for(link(new_link_path, existing_path, symlink, &callback))
667
+ end
668
+
669
+ # :call-seq:
670
+ # block(handle, offset, length, mask) -> request
671
+ # block(handle, offset, length, mask) { |response| ... } -> request
672
+ #
673
+ # Creates a byte-range lock on the file specified by the given +handle+.
674
+ # This operation is only available in SFTP protocol versions 6 and
675
+ # higher. The lock may be either mandatory or advisory.
676
+ #
677
+ # The +handle+ parameter is a file handle, as obtained by the #open method.
678
+ #
679
+ # The +offset+ and +length+ parameters describe the location and size of
680
+ # the byte range.
681
+ #
682
+ # The +mask+ describes how the lock should be defined, and consists of
683
+ # some combination of the following bit masks:
684
+ #
685
+ # * 0x0040 - Read lock. The byte range may not be accessed for reading
686
+ # by via any other handle, though it may be written to.
687
+ # * 0x0080 - Write lock. The byte range may not be written to via any
688
+ # other handle, though it may be read from.
689
+ # * 0x0100 - Delete lock. No other handle may delete this file.
690
+ # * 0x0200 - Advisory lock. The server need not honor the lock instruction.
691
+ #
692
+ # Once created, the lock may be removed via the #unblock method.
693
+ def block(handle, offset, length, mask, &callback)
694
+ request :block, handle, offset, length, mask, &callback
695
+ end
696
+
697
+ # Identical to #block, but blocks until the server responds. It will raise
698
+ # a StatusException if the request was unsuccessful. Otherwise, it will
699
+ # return the Response object for the request.
700
+ def block!(handle, offset, length, mask, &callback)
701
+ wait_for(block(handle, offset, length, mask, &callback))
702
+ end
703
+
704
+ # :call-seq:
705
+ # unblock(handle, offset, length) -> request
706
+ # unblock(handle, offset, length) { |response| ... } -> request
707
+ #
708
+ # Removes a previously created byte-range lock. This operation is only
709
+ # available in protocol versions 6 and higher. The +offset+ and +length+
710
+ # parameters must exactly match those that were given to #block when the
711
+ # lock was acquired.
712
+ def unblock(handle, offset, length, &callback)
713
+ request :unblock, handle, offset, length, &callback
714
+ end
715
+
716
+ # Identical to #unblock, but blocks until the server responds. It will raise
717
+ # a StatusException if the request was unsuccessful. Otherwise, it will
718
+ # return the Response object for the request.
719
+ def unblock!(handle, offset, length, &callback)
720
+ wait_for(unblock(handle, offset, length, &callback))
721
+ end
722
+
723
+ public # miscellaneous methods
724
+
725
+ # Closes the SFTP connection, but not the SSH connection. Blocks until the
726
+ # session has terminated. Once the session has terminated, further operations
727
+ # on this object will result in errors. You can reopen the SFTP session
728
+ # via the #connect method.
729
+ def close_channel
730
+ return unless open?
731
+ channel.close
732
+ loop { !closed? }
733
+ end
734
+
735
+ # Returns true if the connection has been initialized.
736
+ def open?
737
+ state == :open
738
+ end
739
+
740
+ # Returns true if the connection has been closed.
741
+ def closed?
742
+ state == :closed
743
+ end
744
+
745
+ # Returns true if the connection is in the process of being initialized
746
+ # (e.g., it is not closed, but is not yet fully open).
747
+ def opening?
748
+ !(open? || closed?)
749
+ end
750
+
751
+ # Attempts to establish an SFTP connection over the SSH session given when
752
+ # this object was instantiated. If the object is already open, this will
753
+ # simply execute the given block (if any), passing the SFTP session itself
754
+ # as argument. If the session is currently being opened, this will add
755
+ # the given block to the list of callbacks, to be executed when the session
756
+ # is fully open.
757
+ #
758
+ # This method does not block, and will return immediately. If you pass a
759
+ # block to it, that block will be invoked when the connection has been
760
+ # fully established. Thus, you can do something like this:
761
+ #
762
+ # sftp.connect do
763
+ # puts "open!"
764
+ # end
765
+ #
766
+ # If you just want to block until the connection is ready, see the #connect!
767
+ # method.
768
+ def connect(&block)
769
+ case state
770
+ when :open
771
+ block.call(self) if block
772
+ when :closed
773
+ @state = :opening
774
+ @channel = session.open_channel(&method(:when_channel_confirmed))
775
+ @packet_length = nil
776
+ @protocol = nil
777
+ @on_ready = Array(block)
778
+ else # opening
779
+ @on_ready << block if block
780
+ end
781
+
782
+ self
783
+ end
784
+
785
+ # Same as the #connect method, but blocks until the SFTP connection has
786
+ # been fully initialized.
787
+ def connect!(&block)
788
+ connect(&block)
789
+ loop { opening? }
790
+ self
791
+ end
792
+
793
+ alias :loop_forever :loop
794
+
795
+ # Runs the SSH event loop while the given block returns true. This lets
796
+ # you set up a state machine and then "fire it off". If you do not specify
797
+ # a block, the event loop will run for as long as there are any pending
798
+ # SFTP requests. This makes it easy to do thing like this:
799
+ #
800
+ # sftp.remove("/path/to/file")
801
+ # sftp.loop
802
+ def loop(&block)
803
+ block ||= Proc.new { pending_requests.any? }
804
+ session.loop(&block)
805
+ end
806
+
807
+ # Formats, constructs, and sends an SFTP packet of the given type and with
808
+ # the given data. This does not block, but merely enqueues the packet for
809
+ # sending and returns.
810
+ #
811
+ # You should probably use the operation methods, rather than building and
812
+ # sending the packet directly. (See #open, #close, etc.)
813
+ def send_packet(type, *args)
814
+ data = Net::SSH::Buffer.from(*args)
815
+ msg = Net::SSH::Buffer.from(:long, data.length+1, :byte, type, :raw, data)
816
+ channel.send_data(msg.to_s)
817
+ end
818
+
819
+ private
820
+
821
+ #--
822
+ # "ruby -w" hates private attributes, so we have to do this longhand
823
+ #++
824
+
825
+ # The input buffer used to accumulate packet data
826
+ def input; @input; end
827
+
828
+ # Create and enqueue a new SFTP request of the given type, with the
829
+ # given arguments. Returns a new Request instance that encapsulates the
830
+ # request.
831
+ def request(type, *args, &callback)
832
+ request = Request.new(self, type, protocol.send(type, *args), &callback)
833
+ info { "sending #{type} packet (#{request.id})" }
834
+ pending_requests[request.id] = request
835
+ end
836
+
837
+ # Waits for the given request to complete. If the response is
838
+ # EOF, nil is returned. If the response was not successful
839
+ # (e.g., !response.ok?), a StatusException will be raised.
840
+ # If +property+ is given, the corresponding property from the response
841
+ # will be returned; otherwise, the response object itself will be
842
+ # returned.
843
+ def wait_for(request, property=nil)
844
+ request.wait
845
+ if request.response.eof?
846
+ nil
847
+ elsif !request.response.ok?
848
+ raise StatusException.new(request.response)
849
+ elsif property
850
+ request.response[property.to_sym]
851
+ else
852
+ request.response
853
+ end
854
+ end
855
+
856
+ # Called when the SSH channel is confirmed as "open" by the server.
857
+ # This is one of the states of the SFTP state machine, and is followed
858
+ # by the #when_subsystem_started state.
859
+ def when_channel_confirmed(channel)
860
+ debug { "requesting sftp subsystem" }
861
+ @state = :subsystem
862
+ channel.subsystem("sftp", &method(:when_subsystem_started))
863
+ end
864
+
865
+ # Called when the SSH server confirms that the SFTP subsystem was
866
+ # successfully started. This sets up the appropriate callbacks on the
867
+ # SSH channel and then starts the SFTP protocol version negotiation
868
+ # process.
869
+ def when_subsystem_started(channel, success)
870
+ raise Net::SFTP::Exception, "could not start SFTP subsystem" unless success
871
+
872
+ debug { "sftp subsystem successfully started" }
873
+ @state = :init
874
+
875
+ channel.on_data { |c,data| input.append(data) }
876
+ channel.on_extended_data { |c,t,data| debug { data } }
877
+
878
+ channel.on_close(&method(:when_channel_closed))
879
+ channel.on_process(&method(:when_channel_polled))
880
+
881
+ send_packet(FXP_INIT, :long, @version || HIGHEST_PROTOCOL_VERSION_SUPPORTED)
882
+ end
883
+
884
+ # Called when the SSH server closes the underlying channel.
885
+ def when_channel_closed(channel)
886
+ debug { "sftp channel closed" }
887
+ @channel = nil
888
+ @state = :closed
889
+ end
890
+
891
+ # Called whenever Net::SSH polls the SFTP channel for pending activity.
892
+ # This basically checks the input buffer to see if enough input has been
893
+ # accumulated to handle. If there has, the packet is parsed and
894
+ # dispatched, according to its type (see #do_version and #dispatch_request).
895
+ def when_channel_polled(channel)
896
+ while input.length > 0
897
+ if @packet_length.nil?
898
+ # make sure we've read enough data to tell how long the packet is
899
+ return unless input.length >= 4
900
+ @packet_length = input.read_long
901
+ end
902
+
903
+ return unless input.length >= @packet_length + 4
904
+ packet = Net::SFTP::Packet.new(input.read(@packet_length))
905
+ input.consume!
906
+ @packet_length = nil
907
+
908
+ debug { "received sftp packet #{packet.type} len #{packet.length}" }
909
+
910
+ if packet.type == FXP_VERSION
911
+ do_version(packet)
912
+ else
913
+ dispatch_request(packet)
914
+ end
915
+ end
916
+ end
917
+
918
+ # Called to handle FXP_VERSION packets. This performs the SFTP protocol
919
+ # version negotiation, instantiating the appropriate Protocol instance
920
+ # and invoking the callback given to #connect, if any.
921
+ def do_version(packet)
922
+ debug { "negotiating sftp protocol version, mine is #{HIGHEST_PROTOCOL_VERSION_SUPPORTED}" }
923
+
924
+ server_version = packet.read_long
925
+ debug { "server reports sftp version #{server_version}" }
926
+
927
+ negotiated_version = [server_version, HIGHEST_PROTOCOL_VERSION_SUPPORTED].min
928
+ info { "negotiated version is #{negotiated_version}" }
929
+
930
+ extensions = {}
931
+ until packet.eof?
932
+ name = packet.read_string
933
+ data = packet.read_string
934
+ extensions[name] = data
935
+ end
936
+
937
+ @protocol = Protocol.load(self, negotiated_version)
938
+ @pending_requests = {}
939
+
940
+ @state = :open
941
+ @on_ready.each { |callback| callback.call(self) }
942
+ @on_ready = nil
943
+ end
944
+
945
+ # Parses the packet, finds the associated Request instance, and tells
946
+ # the Request instance to respond to the packet (see Request#respond_to).
947
+ def dispatch_request(packet)
948
+ id = packet.read_long
949
+ request = pending_requests.delete(id) or raise Net::SFTP::Exception, "no such request `#{id}'"
950
+ request.respond_to(packet)
951
+ end
952
+ end
953
+
954
+ end; end