net-sftp 1.1.1 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (138) hide show
  1. data/CHANGELOG.rdoc +23 -0
  2. data/Manifest +55 -0
  3. data/README.rdoc +96 -0
  4. data/Rakefile +30 -0
  5. data/lib/net/sftp.rb +53 -38
  6. data/lib/net/sftp/constants.rb +187 -0
  7. data/lib/net/sftp/errors.rb +34 -20
  8. data/lib/net/sftp/operations/dir.rb +93 -0
  9. data/lib/net/sftp/operations/download.rb +364 -0
  10. data/lib/net/sftp/operations/file.rb +176 -0
  11. data/lib/net/sftp/operations/file_factory.rb +60 -0
  12. data/lib/net/sftp/operations/upload.rb +387 -0
  13. data/lib/net/sftp/packet.rb +21 -0
  14. data/lib/net/sftp/protocol.rb +32 -0
  15. data/lib/net/sftp/protocol/01/attributes.rb +265 -96
  16. data/lib/net/sftp/protocol/01/base.rb +268 -0
  17. data/lib/net/sftp/protocol/01/name.rb +43 -0
  18. data/lib/net/sftp/protocol/02/base.rb +31 -0
  19. data/lib/net/sftp/protocol/03/base.rb +35 -0
  20. data/lib/net/sftp/protocol/04/attributes.rb +120 -195
  21. data/lib/net/sftp/protocol/04/base.rb +94 -0
  22. data/lib/net/sftp/protocol/04/name.rb +67 -0
  23. data/lib/net/sftp/protocol/05/base.rb +66 -0
  24. data/lib/net/sftp/protocol/06/attributes.rb +107 -0
  25. data/lib/net/sftp/protocol/06/base.rb +63 -0
  26. data/lib/net/sftp/protocol/base.rb +50 -0
  27. data/lib/net/sftp/request.rb +91 -0
  28. data/lib/net/sftp/response.rb +76 -0
  29. data/lib/net/sftp/session.rb +914 -238
  30. data/lib/net/sftp/version.rb +14 -21
  31. data/net-sftp.gemspec +60 -0
  32. data/setup.rb +1331 -0
  33. data/test/common.rb +173 -0
  34. data/test/protocol/01/test_attributes.rb +97 -0
  35. data/test/protocol/01/test_base.rb +210 -0
  36. data/test/protocol/01/test_name.rb +27 -0
  37. data/test/protocol/02/test_base.rb +26 -0
  38. data/test/protocol/03/test_base.rb +27 -0
  39. data/test/protocol/04/test_attributes.rb +148 -0
  40. data/test/protocol/04/test_base.rb +74 -0
  41. data/test/protocol/04/test_name.rb +49 -0
  42. data/test/protocol/05/test_base.rb +62 -0
  43. data/test/protocol/06/test_attributes.rb +124 -0
  44. data/test/protocol/06/test_base.rb +51 -0
  45. data/test/protocol/test_base.rb +42 -0
  46. data/test/test_all.rb +3 -0
  47. data/test/test_dir.rb +47 -0
  48. data/test/test_download.rb +252 -0
  49. data/test/test_file.rb +159 -0
  50. data/test/test_file_factory.rb +48 -0
  51. data/test/test_packet.rb +9 -0
  52. data/test/test_protocol.rb +17 -0
  53. data/test/test_request.rb +71 -0
  54. data/test/test_response.rb +53 -0
  55. data/test/test_session.rb +741 -0
  56. data/test/test_upload.rb +219 -0
  57. metadata +59 -111
  58. data/doc/LICENSE-BSD +0 -27
  59. data/doc/LICENSE-GPL +0 -280
  60. data/doc/LICENSE-RUBY +0 -56
  61. data/doc/faq/faq.html +0 -298
  62. data/doc/faq/faq.rb +0 -154
  63. data/doc/faq/faq.yml +0 -183
  64. data/examples/asynchronous.rb +0 -57
  65. data/examples/get-put.rb +0 -45
  66. data/examples/sftp-open-uri.rb +0 -30
  67. data/examples/ssh-service.rb +0 -30
  68. data/examples/synchronous.rb +0 -131
  69. data/lib/net/sftp/operations/abstract.rb +0 -108
  70. data/lib/net/sftp/operations/close.rb +0 -31
  71. data/lib/net/sftp/operations/errors.rb +0 -76
  72. data/lib/net/sftp/operations/fsetstat.rb +0 -36
  73. data/lib/net/sftp/operations/fstat.rb +0 -32
  74. data/lib/net/sftp/operations/lstat.rb +0 -31
  75. data/lib/net/sftp/operations/mkdir.rb +0 -33
  76. data/lib/net/sftp/operations/open.rb +0 -32
  77. data/lib/net/sftp/operations/opendir.rb +0 -32
  78. data/lib/net/sftp/operations/read.rb +0 -88
  79. data/lib/net/sftp/operations/readdir.rb +0 -55
  80. data/lib/net/sftp/operations/realpath.rb +0 -37
  81. data/lib/net/sftp/operations/remove.rb +0 -31
  82. data/lib/net/sftp/operations/rename.rb +0 -32
  83. data/lib/net/sftp/operations/rmdir.rb +0 -31
  84. data/lib/net/sftp/operations/services.rb +0 -42
  85. data/lib/net/sftp/operations/setstat.rb +0 -33
  86. data/lib/net/sftp/operations/stat.rb +0 -31
  87. data/lib/net/sftp/operations/write.rb +0 -63
  88. data/lib/net/sftp/protocol/01/impl.rb +0 -251
  89. data/lib/net/sftp/protocol/01/packet-assistant.rb +0 -82
  90. data/lib/net/sftp/protocol/01/services.rb +0 -47
  91. data/lib/net/sftp/protocol/02/impl.rb +0 -39
  92. data/lib/net/sftp/protocol/02/packet-assistant.rb +0 -32
  93. data/lib/net/sftp/protocol/02/services.rb +0 -44
  94. data/lib/net/sftp/protocol/03/impl.rb +0 -42
  95. data/lib/net/sftp/protocol/03/packet-assistant.rb +0 -35
  96. data/lib/net/sftp/protocol/03/services.rb +0 -44
  97. data/lib/net/sftp/protocol/04/impl.rb +0 -86
  98. data/lib/net/sftp/protocol/04/packet-assistant.rb +0 -45
  99. data/lib/net/sftp/protocol/04/services.rb +0 -44
  100. data/lib/net/sftp/protocol/05/impl.rb +0 -90
  101. data/lib/net/sftp/protocol/05/packet-assistant.rb +0 -34
  102. data/lib/net/sftp/protocol/05/services.rb +0 -44
  103. data/lib/net/sftp/protocol/constants.rb +0 -60
  104. data/lib/net/sftp/protocol/driver.rb +0 -235
  105. data/lib/net/sftp/protocol/packet-assistant.rb +0 -84
  106. data/lib/net/sftp/protocol/services.rb +0 -55
  107. data/lib/uri/open-sftp.rb +0 -54
  108. data/lib/uri/sftp.rb +0 -42
  109. data/test/ALL-TESTS.rb +0 -23
  110. data/test/operations/tc_abstract.rb +0 -124
  111. data/test/operations/tc_close.rb +0 -40
  112. data/test/operations/tc_fsetstat.rb +0 -48
  113. data/test/operations/tc_fstat.rb +0 -40
  114. data/test/operations/tc_lstat.rb +0 -40
  115. data/test/operations/tc_mkdir.rb +0 -48
  116. data/test/operations/tc_open.rb +0 -42
  117. data/test/operations/tc_opendir.rb +0 -40
  118. data/test/operations/tc_read.rb +0 -103
  119. data/test/operations/tc_readdir.rb +0 -88
  120. data/test/operations/tc_realpath.rb +0 -54
  121. data/test/operations/tc_remove.rb +0 -40
  122. data/test/operations/tc_rmdir.rb +0 -40
  123. data/test/operations/tc_setstat.rb +0 -48
  124. data/test/operations/tc_stat.rb +0 -40
  125. data/test/operations/tc_write.rb +0 -91
  126. data/test/protocol/01/tc_attributes.rb +0 -138
  127. data/test/protocol/01/tc_impl.rb +0 -294
  128. data/test/protocol/01/tc_packet_assistant.rb +0 -81
  129. data/test/protocol/02/tc_impl.rb +0 -41
  130. data/test/protocol/02/tc_packet_assistant.rb +0 -31
  131. data/test/protocol/03/tc_impl.rb +0 -48
  132. data/test/protocol/03/tc_packet_assistant.rb +0 -34
  133. data/test/protocol/04/tc_attributes.rb +0 -174
  134. data/test/protocol/04/tc_impl.rb +0 -91
  135. data/test/protocol/04/tc_packet_assistant.rb +0 -38
  136. data/test/protocol/05/tc_impl.rb +0 -61
  137. data/test/protocol/05/tc_packet_assistant.rb +0 -32
  138. data/test/protocol/tc_driver.rb +0 -219
@@ -1,25 +1,39 @@
1
- #--
2
- # =============================================================================
3
- # Copyright (c) 2004, Jamis Buck (jamis@37signals.com)
4
- # All rights reserved.
5
- #
6
- # This source file is distributed as part of the Net::SFTP Secure FTP Client
7
- # library for Ruby. This file (and the library as a whole) may be used only as
8
- # allowed by either the BSD license, or the Ruby license (or, by association
9
- # with the Ruby license, the GPL). See the "doc" subdirectory of the Net::SFTP
10
- # distribution for the texts of these licenses.
11
- # -----------------------------------------------------------------------------
12
- # net-sftp website: http://net-ssh.rubyforge.org/sftp
13
- # project website : http://rubyforge.org/projects/net-ssh
14
- # =============================================================================
15
- #++
16
-
17
- module Net ; module SFTP
1
+ module Net; module SFTP
18
2
 
19
3
  # The base exception class for the SFTP system.
20
4
  class Exception < RuntimeError; end
21
5
 
22
- # An exception class representing a bug condition.
23
- class Bug < Exception; end
6
+ # A exception class for reporting a non-success result of an operation.
7
+ class StatusException < Net::SFTP::Exception
24
8
 
25
- end ; end
9
+ # The response object that caused the exception.
10
+ attr_reader :response
11
+
12
+ # The error code (numeric)
13
+ attr_reader :code
14
+
15
+ # The description of the error
16
+ attr_reader :description
17
+
18
+ # Any incident-specific text given when the exception was raised
19
+ attr_reader :text
20
+
21
+ # Create a new status exception that reports the given code and
22
+ # description.
23
+ def initialize(response, text=nil)
24
+ @response, @text = response, text
25
+ @code = response.code
26
+ @description = response.message
27
+ @description = Response::MAP[@code] if @description.nil? || @description.empty?
28
+ end
29
+
30
+ # Override the default message format, to include the code and
31
+ # description.
32
+ def message
33
+ m = super
34
+ m << " #{text}" if text
35
+ m << " (#{code}, #{description.inspect})"
36
+ end
37
+
38
+ end
39
+ end; end
@@ -0,0 +1,93 @@
1
+ require 'net/ssh/loggable'
2
+
3
+ module Net; module SFTP; module Operations
4
+
5
+ # A convenience class for working with remote directories. It provides methods
6
+ # for searching and enumerating directory entries, similarly to the standard
7
+ # ::Dir class.
8
+ #
9
+ # sftp.dir.foreach("/remote/path") do |entry|
10
+ # puts entry.name
11
+ # end
12
+ #
13
+ # p sftp.dir.entries("/remote/path").map { |e| e.name }
14
+ #
15
+ # sftp.dir.glob("/remote/path", "**/*.rb") do |entry|
16
+ # puts entry.name
17
+ # end
18
+ class Dir
19
+ # The SFTP session object that drives this directory factory.
20
+ attr_reader :sftp
21
+
22
+ # Create a new instance on top of the given SFTP session instance.
23
+ def initialize(sftp)
24
+ @sftp = sftp
25
+ end
26
+
27
+ # Calls the block once for each entry in the named directory on the
28
+ # remote server. Yields a Name object to the block, rather than merely
29
+ # the name of the entry.
30
+ def foreach(path)
31
+ handle = sftp.opendir!(path)
32
+ while entries = sftp.readdir!(handle)
33
+ entries.each { |entry| yield entry }
34
+ end
35
+ return nil
36
+ ensure
37
+ sftp.close!(handle) if handle
38
+ end
39
+
40
+ # Returns an array of Name objects representing the items in the given
41
+ # remote directory, +path+.
42
+ def entries(path)
43
+ results = []
44
+ foreach(path) { |entry| results << entry }
45
+ return results
46
+ end
47
+
48
+ # Works as ::Dir.glob, matching (possibly recursively) all directory
49
+ # entries under +path+ against +pattern+. If a block is given, matches
50
+ # will be yielded to the block as they are found; otherwise, they will
51
+ # be returned in an array when the method finishes.
52
+ #
53
+ # Because working over an SFTP connection is always going to be slower than
54
+ # working purely locally, don't expect this method to perform with the
55
+ # same level of alacrity that ::Dir.glob does; it will work best for
56
+ # shallow directory hierarchies with relatively few directories, though
57
+ # it should be able to handle modest numbers of files in each directory.
58
+ def glob(path, pattern, flags=0)
59
+ flags |= ::File::FNM_PATHNAME
60
+ path = path.chop if path[-1,1] == "/"
61
+
62
+ results = [] unless block_given?
63
+ queue = entries(path).reject { |e| e.name == "." || e.name == ".." }
64
+ while queue.any?
65
+ entry = queue.shift
66
+
67
+ if entry.directory? && !%w(. ..).include?(::File.basename(entry.name))
68
+ queue += entries("#{path}/#{entry.name}").map do |e|
69
+ e.name.replace("#{entry.name}/#{e.name}")
70
+ e
71
+ end
72
+ end
73
+
74
+ if ::File.fnmatch(pattern, entry.name, flags)
75
+ if block_given?
76
+ yield entry
77
+ else
78
+ results << entry
79
+ end
80
+ end
81
+ end
82
+
83
+ return results unless block_given?
84
+ end
85
+
86
+ # Identical to calling #glob with a +flags+ parameter of 0 and no block.
87
+ # Simply returns the matched entries as an array.
88
+ def [](path, pattern)
89
+ glob(path, pattern, 0)
90
+ end
91
+ end
92
+
93
+ end; end; end
@@ -0,0 +1,364 @@
1
+ require 'net/ssh/loggable'
2
+
3
+ module Net; module SFTP; module Operations
4
+
5
+ # A general purpose downloader module for Net::SFTP. It can download files
6
+ # into IO objects, or directly to files on the local file system. It can
7
+ # even download entire directory trees via SFTP, and provides a flexible
8
+ # progress reporting mechanism.
9
+ #
10
+ # To download a single file from the remote server, simply specify both the
11
+ # remote and local paths:
12
+ #
13
+ # downloader = sftp.download("/path/to/remote.txt", "/path/to/local.txt")
14
+ #
15
+ # By default, this operates asynchronously, so if you want to block until
16
+ # the download finishes, you can use the 'bang' variant:
17
+ #
18
+ # sftp.download!("/path/to/remote.txt", "/path/to/local.txt")
19
+ #
20
+ # Or, if you have multiple downloads that you want to run in parallel, you can
21
+ # employ the #wait method of the returned object:
22
+ #
23
+ # dls = %w(file1 file2 file3).map { |f| sftp.download("remote/#{f}", f) }
24
+ # dls.each { |d| d.wait }
25
+ #
26
+ # To download an entire directory tree, recursively, simply specify :recursive => true:
27
+ #
28
+ # sftp.download!("/path/to/remotedir", "/path/to/local", :recursive => true)
29
+ #
30
+ # This will download "/path/to/remotedir", it's contents, it's subdirectories,
31
+ # and their contents, recursively, to "/path/to/local" on the local host.
32
+ # (If you specify :recursive => true and the source is not a directory,
33
+ # you'll get an error!)
34
+ #
35
+ # If you want to pull the contents of a file on the remote server, and store
36
+ # the data in memory rather than immediately to disk, you can pass an IO
37
+ # object as the destination:
38
+ #
39
+ # require 'stringio'
40
+ # io = StringIO.new
41
+ # sftp.download!("/path/to/remote", io)
42
+ #
43
+ # This will only work for single-file downloads. Trying to do so with
44
+ # :recursive => true will cause an error.
45
+ #
46
+ # The following options are supported:
47
+ #
48
+ # * <tt>:progress</tt> - either a block or an object to act as a progress
49
+ # callback. See the discussion of "progress monitoring" below.
50
+ # * <tt>:requests</tt> - the number of pending SFTP requests to allow at
51
+ # any given time. When downloading an entire directory tree recursively,
52
+ # this will default to 16. Setting this higher might improve throughput.
53
+ # Reducing it will reduce throughput.
54
+ # * <tt>:read_size</tt> - the maximum number of bytes to read at a time
55
+ # from the source. Increasing this value might improve throughput. It
56
+ # defaults to 32,000 bytes.
57
+ #
58
+ # == Progress Monitoring
59
+ #
60
+ # Sometimes it is desirable to track the progress of a download. There are
61
+ # two ways to do this: either using a callback block, or a special custom
62
+ # object.
63
+ #
64
+ # Using a block it's pretty straightforward:
65
+ #
66
+ # sftp.download!("remote", "local") do |event, downloader, *args|
67
+ # case event
68
+ # when :open then
69
+ # # args[0] : file metadata
70
+ # puts "starting download: #{args[0].remote} -> #{args[0].local} (#{args[0].size} bytes}"
71
+ # when :get then
72
+ # # args[0] : file metadata
73
+ # # args[1] : byte offset in remote file
74
+ # # args[2] : data that was received
75
+ # puts "writing #{args[2].length} bytes to #{args[0].local} starting at #{args[1]}"
76
+ # when :close then
77
+ # # args[0] : file metadata
78
+ # puts "finished with #{args[0].remote}"
79
+ # when :mkdir then
80
+ # # args[0] : local path name
81
+ # puts "creating directory #{args[0]}"
82
+ # when :finish then
83
+ # puts "all done!"
84
+ # end
85
+ #
86
+ # However, for more complex implementations (e.g., GUI interfaces and such)
87
+ # a block can become cumbersome. In those cases, you can create custom
88
+ # handler objects that respond to certain methods, and then pass your handler
89
+ # to the downloader:
90
+ #
91
+ # class CustomHandler
92
+ # def on_open(downloader, file)
93
+ # puts "starting download: #{file.remote} -> #{file.local} (#{file.size} bytes)"
94
+ # end
95
+ #
96
+ # def on_get(downloader, file, offset, data)
97
+ # puts "writing #{data.length} bytes to #{file.local} starting at #{offset}"
98
+ # end
99
+ #
100
+ # def on_close(downloader, file)
101
+ # puts "finished with #{file.remote}"
102
+ # end
103
+ #
104
+ # def on_mkdir(downloader, path)
105
+ # puts "creating directory #{path}"
106
+ # end
107
+ #
108
+ # def on_finish(downloader)
109
+ # puts "all done!"
110
+ # end
111
+ # end
112
+ #
113
+ # sftp.download!("remote", "local", :progress => CustomHandler.new)
114
+ #
115
+ # If you omit any of those methods, the progress updates for those missing
116
+ # events will be ignored. You can create a catchall method named "call" for
117
+ # those, instead.
118
+ class Download
119
+ include Net::SSH::Loggable
120
+
121
+ # The destination of the download (the name of a file or directory on
122
+ # the local server, or an IO object)
123
+ attr_reader :local
124
+
125
+ # The source of the download (the name of a file or directory on the
126
+ # remote server)
127
+ attr_reader :remote
128
+
129
+ # The hash of options that was given to this Download instance.
130
+ attr_reader :options
131
+
132
+ # The SFTP session instance that drives this download.
133
+ attr_reader :sftp
134
+
135
+ # The properties hash for this object
136
+ attr_reader :properties
137
+
138
+ # Instantiates a new downloader process on top of the given SFTP session.
139
+ # +local+ is either an IO object that should receive the data, or a string
140
+ # identifying the target file or directory on the local host. +remote+ is
141
+ # a string identifying the location on the remote host that the download
142
+ # should source.
143
+ #
144
+ # This will return immediately, and requires that the SSH event loop be
145
+ # run in order to effect the download. (See #wait.)
146
+ def initialize(sftp, local, remote, options={}, &progress)
147
+ @sftp = sftp
148
+ @local = local
149
+ @remote = remote
150
+ @progress = progress || options[:progress]
151
+ @options = options
152
+ @active = 0
153
+ @properties = options[:properties] || {}
154
+
155
+ self.logger = sftp.logger
156
+
157
+ if recursive? && local.respond_to?(:write)
158
+ raise ArgumentError, "cannot download a directory tree in-memory"
159
+ end
160
+
161
+ @stack = [Entry.new(remote, local, recursive?)]
162
+ process_next_entry
163
+ end
164
+
165
+ # Returns the value of the :recursive key in the options hash that was
166
+ # given when the object was instantiated.
167
+ def recursive?
168
+ options[:recursive]
169
+ end
170
+
171
+ # Returns true if there are any active requests or pending files or
172
+ # directories.
173
+ def active?
174
+ @active > 0 || stack.any?
175
+ end
176
+
177
+ # Forces the transfer to stop.
178
+ def abort!
179
+ @active = 0
180
+ @stack.clear
181
+ end
182
+
183
+ # Runs the SSH event loop for as long as the downloader is active (see
184
+ # #active?). This can be used to block until the download completes.
185
+ def wait
186
+ sftp.loop { active? }
187
+ self
188
+ end
189
+
190
+ # Returns the property with the given name. This allows Download instances
191
+ # to store their own state when used as part of a state machine.
192
+ def [](name)
193
+ @properties[name.to_sym]
194
+ end
195
+
196
+ # Sets the given property to the given name. This allows Download instances
197
+ # to store their own state when used as part of a state machine.
198
+ def []=(name, value)
199
+ @properties[name.to_sym] = value
200
+ end
201
+
202
+ private
203
+
204
+ # A simple struct for encapsulating information about a single remote
205
+ # file or directory that needs to be downloaded.
206
+ Entry = Struct.new(:remote, :local, :directory, :size, :handle, :offset, :sink)
207
+
208
+ #--
209
+ # "ruby -w" hates private attributes, so we have to do these longhand
210
+ #++
211
+
212
+ # The stack of Entry instances, indicating which files and directories
213
+ # on the remote host remain to be downloaded.
214
+ def stack; @stack; end
215
+
216
+ # The progress handler for this instance. Possibly nil.
217
+ def progress; @progress; end
218
+
219
+ # The default read size.
220
+ DEFAULT_READ_SIZE = 32_000
221
+
222
+ # The number of bytes to read at a time from remote files.
223
+ def read_size
224
+ options[:read_size] || DEFAULT_READ_SIZE
225
+ end
226
+
227
+ # The number of simultaneou SFTP requests to use to effect the download.
228
+ # Defaults to 16 for recursive downloads.
229
+ def requests
230
+ options[:requests] || (recursive? ? 16 : 2)
231
+ end
232
+
233
+ # Enqueues as many files and directories from the stack as possible
234
+ # (see #requests).
235
+ def process_next_entry
236
+ while stack.any? && requests > @active
237
+ entry = stack.shift
238
+ @active += 1
239
+
240
+ if entry.directory
241
+ update_progress(:mkdir, entry.local)
242
+ ::Dir.mkdir(entry.local) unless ::File.directory?(entry.local)
243
+ request = sftp.opendir(entry.remote, &method(:on_opendir))
244
+ request[:entry] = entry
245
+ else
246
+ open_file(entry)
247
+ end
248
+ end
249
+
250
+ update_progress(:finish) if !active?
251
+ end
252
+
253
+ # Called when a remote directory is "opened" for reading, e.g. to
254
+ # enumerate its contents. Starts an readdir operation if the opendir
255
+ # operation was successful.
256
+ def on_opendir(response)
257
+ entry = response.request[:entry]
258
+ raise "opendir #{entry.remote}: #{response}" unless response.ok?
259
+ entry.handle = response[:handle]
260
+ request = sftp.readdir(response[:handle], &method(:on_readdir))
261
+ request[:parent] = entry
262
+ end
263
+
264
+ # Called when the next batch of items is read from a directory on the
265
+ # remote server. If any items were read, they are added to the queue
266
+ # and #process_next_entry is called.
267
+ def on_readdir(response)
268
+ entry = response.request[:parent]
269
+ if response.eof?
270
+ request = sftp.close(entry.handle, &method(:on_closedir))
271
+ request[:parent] = entry
272
+ elsif !response.ok?
273
+ raise "readdir #{entry.remote}: #{response}"
274
+ else
275
+ response[:names].each do |item|
276
+ next if item.name == "." || item.name == ".."
277
+ stack << Entry.new(::File.join(entry.remote, item.name), ::File.join(entry.local, item.name), item.directory?, item.attributes.size)
278
+ end
279
+
280
+ # take this opportunity to enqueue more requests
281
+ process_next_entry
282
+
283
+ request = sftp.readdir(entry.handle, &method(:on_readdir))
284
+ request[:parent] = entry
285
+ end
286
+ end
287
+
288
+ # Called when a file is to be opened for reading from the remote server.
289
+ def open_file(entry)
290
+ update_progress(:open, entry)
291
+ request = sftp.open(entry.remote, &method(:on_open))
292
+ request[:entry] = entry
293
+ end
294
+
295
+ # Called when a directory handle is closed.
296
+ def on_closedir(response)
297
+ @active -= 1
298
+ entry = response.request[:parent]
299
+ raise "close #{entry.remote}: #{response}" unless response.ok?
300
+ process_next_entry
301
+ end
302
+
303
+ # Called when a file has been opened. This will call #download_next_chunk
304
+ # to initiate the data transfer.
305
+ def on_open(response)
306
+ entry = response.request[:entry]
307
+ raise "open #{entry.remote}: #{response}" unless response.ok?
308
+
309
+ entry.handle = response[:handle]
310
+ entry.sink = entry.local.respond_to?(:write) ? entry.local : ::File.open(entry.local, "w")
311
+ entry.offset = 0
312
+
313
+ download_next_chunk(entry)
314
+ end
315
+
316
+ # Initiates a read of the next #read_size bytes from the file.
317
+ def download_next_chunk(entry)
318
+ request = sftp.read(entry.handle, entry.offset, read_size, &method(:on_read))
319
+ request[:entry] = entry
320
+ request[:offset] = entry.offset
321
+ entry.offset += read_size
322
+ end
323
+
324
+ # Called when a read from a file finishes. If the read was successful
325
+ # and returned data, this will call #download_next_chunk to read the
326
+ # next bit from the file. Otherwise the file will be closed.
327
+ def on_read(response)
328
+ entry = response.request[:entry]
329
+
330
+ if response.eof?
331
+ update_progress(:close, entry)
332
+ entry.sink.close
333
+ request = sftp.close(entry.handle, &method(:on_close))
334
+ request[:entry] = entry
335
+ elsif !response.ok?
336
+ raise "read #{entry.remote}: #{response}"
337
+ else
338
+ update_progress(:get, entry, response.request[:offset], response[:data])
339
+ entry.sink.write(response[:data])
340
+ download_next_chunk(entry)
341
+ end
342
+ end
343
+
344
+ # Called when a file handle is closed.
345
+ def on_close(response)
346
+ @active -= 1
347
+ entry = response.request[:entry]
348
+ raise "close #{entry.remote}: #{response}" unless response.ok?
349
+ process_next_entry
350
+ end
351
+
352
+ # If a progress callback or object has been set, this will report
353
+ # the progress to that callback or object.
354
+ def update_progress(hook, *args)
355
+ on = "on_#{hook}"
356
+ if progress.respond_to?(on)
357
+ progress.send(on, self, *args)
358
+ elsif progress.respond_to?(:call)
359
+ progress.call(hook, self, *args)
360
+ end
361
+ end
362
+ end
363
+
364
+ end; end; end