net-sftp 1.1.1 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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