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
@@ -0,0 +1,176 @@
1
+ require 'net/ssh/loggable'
2
+ require 'net/sftp/operations/file'
3
+
4
+ module Net; module SFTP; module Operations
5
+
6
+ # A wrapper around an SFTP file handle, that exposes an IO-like interface
7
+ # for interacting with the remote file. All operations are synchronous
8
+ # (blocking), making this a very convenient way to deal with remote files.
9
+ #
10
+ # A wrapper is usually created via the Net::SFTP::Session#file factory:
11
+ #
12
+ # file = sftp.file.open("/path/to/remote")
13
+ # puts file.gets
14
+ # file.close
15
+ class File
16
+ # A reference to the Net::SFTP::Session instance that drives this wrapper
17
+ attr_reader :sftp
18
+
19
+ # The SFTP file handle object that this object wraps
20
+ attr_reader :handle
21
+
22
+ # The current position within the remote file
23
+ attr_reader :pos
24
+
25
+ # Creates a new wrapper that encapsulates the given +handle+ (such as
26
+ # would be returned by Net::SFTP::Session#open!). The +sftp+ parameter
27
+ # must be the same Net::SFTP::Session instance that opened the file.
28
+ def initialize(sftp, handle)
29
+ @sftp = sftp
30
+ @handle = handle
31
+ @pos = 0
32
+ @real_pos = 0
33
+ @real_eof = false
34
+ @buffer = ""
35
+ end
36
+
37
+ # Repositions the file pointer to the given offset (relative to the
38
+ # start of the file). This will also reset the EOF flag.
39
+ def pos=(offset)
40
+ @real_pos = @pos = offset
41
+ @buffer = ""
42
+ @real_eof = false
43
+ end
44
+
45
+ # Closes the underlying file and sets the handle to +nil+. Subsequent
46
+ # operations on this object will fail.
47
+ def close
48
+ sftp.close!(handle)
49
+ @handle = nil
50
+ end
51
+
52
+ # Returns true if the end of the file has been encountered by a previous
53
+ # read. Setting the current file position via #pos= will reset this
54
+ # flag (useful if the file's contents have changed since the EOF was
55
+ # encountered).
56
+ def eof?
57
+ @real_eof && @buffer.empty?
58
+ end
59
+
60
+ # Reads up to +n+ bytes of data from the stream. Fewer bytes will be
61
+ # returned if EOF is encountered before the requested number of bytes
62
+ # could be read. Without an argument (or with a nil argument) all data
63
+ # to the end of the file will be read and returned.
64
+ #
65
+ # This will advance the file pointer (#pos).
66
+ def read(n=nil)
67
+ loop do
68
+ break if n && @buffer.length >= n
69
+ break unless fill
70
+ end
71
+
72
+ if n
73
+ result, @buffer = @buffer[0,n], (@buffer[n..-1] || "")
74
+ else
75
+ result, @buffer = @buffer, ""
76
+ end
77
+
78
+ @pos += result.length
79
+ return result
80
+ end
81
+
82
+ # Reads up to the next instance of +sep_string+ in the stream, and
83
+ # returns the bytes read (including +sep_string+). If +sep_string+ is
84
+ # omitted, it defaults to +$/+. If EOF is encountered before any data
85
+ # could be read, #gets will return +nil+.
86
+ def gets(sep_string=$/)
87
+ delim = if sep_string.length == 0
88
+ "#{$/}#{$/}"
89
+ else
90
+ sep_string
91
+ end
92
+
93
+ loop do
94
+ at = @buffer.index(delim)
95
+ if at
96
+ offset = at + delim.length
97
+ @pos += offset
98
+ line, @buffer = @buffer[0,offset], @buffer[offset..-1]
99
+ return line
100
+ elsif !fill
101
+ return nil if @buffer.empty?
102
+ @pos += @buffer.length
103
+ line, @buffer = @buffer, ""
104
+ return line
105
+ end
106
+ end
107
+ end
108
+
109
+ # Same as #gets, but raises EOFError if EOF is encountered before any
110
+ # data could be read.
111
+ def readline(sep_string=$/)
112
+ line = gets(sep_string)
113
+ raise EOFError if line.nil?
114
+ return line
115
+ end
116
+
117
+ # Writes the given data to the stream, incrementing the file position and
118
+ # returning the number of bytes written.
119
+ def write(data)
120
+ data = data.to_s
121
+ sftp.write!(handle, @real_pos, data)
122
+ @real_pos += data.length
123
+ @pos = @real_pos
124
+ data.length
125
+ end
126
+
127
+ # Writes each argument to the stream. If +$\+ is set, it will be written
128
+ # after all arguments have been written.
129
+ def print(*items)
130
+ items.each { |item| write(item) }
131
+ write($\) if $\
132
+ nil
133
+ end
134
+
135
+ # Writes each argument to the stream, appending a newline to any item
136
+ # that does not already end in a newline. Array arguments are flattened.
137
+ def puts(*items)
138
+ items.each do |item|
139
+ if Array === item
140
+ puts(*item)
141
+ else
142
+ write(item)
143
+ write("\n") unless item[-1] == ?\n
144
+ end
145
+ end
146
+ nil
147
+ end
148
+
149
+ # Performs an fstat operation on the handle and returns the attribute
150
+ # object (Net::SFTP::Protocol::V01::Attributes, Net::SFTP::Protool::V04::Attributes,
151
+ # or Net::SFTP::Protocol::V06::Attributes, depending on the SFTP protocol
152
+ # version in use).
153
+ def stat
154
+ sftp.fstat!(handle)
155
+ end
156
+
157
+ private
158
+
159
+ # Fills the buffer. Returns +true+ if it succeeded, and +false+ if
160
+ # EOF was encountered before any data was read.
161
+ def fill
162
+ data = sftp.read!(handle, @real_pos, 8192)
163
+
164
+ if data.nil?
165
+ @real_eof = true
166
+ return false
167
+ else
168
+ @real_pos += data.length
169
+ @buffer << data
170
+ end
171
+
172
+ !@real_eof
173
+ end
174
+ end
175
+
176
+ end; end; end
@@ -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,387 @@
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", it's contents, it's subdirectories,
31
+ # and their contents, recursively, to "/path/to/remote" on the remote server.
32
+ #
33
+ # If you want to send data to a file on the remote server, but the data is
34
+ # in memory, you can pass an IO object and upload it's contents:
35
+ #
36
+ # require 'stringio'
37
+ # io = StringIO.new(data)
38
+ # sftp.upload!(io, "/path/to/remote")
39
+ #
40
+ # The following options are supported:
41
+ #
42
+ # * <tt>:progress</tt> - either a block or an object to act as a progress
43
+ # callback. See the discussion of "progress monitoring" below.
44
+ # * <tt>:requests</tt> - the number of pending SFTP requests to allow at
45
+ # any given time. When uploading an entire directory tree recursively,
46
+ # this will default to 16, otherwise it will default to 2. Setting this
47
+ # higher might improve throughput. Reducing it will reduce throughput.
48
+ # * <tt>:read_size</tt> - the maximum number of bytes to read at a time
49
+ # from the source. Increasing this value might improve throughput. It
50
+ # defaults to 32,000 bytes.
51
+ # * <tt>:name</tt> - the filename to report to the progress monitor when
52
+ # an IO object is given as +local+. This defaults to "<memory>".
53
+ #
54
+ # == Progress Monitoring
55
+ #
56
+ # Sometimes it is desirable to track the progress of an upload. There are
57
+ # two ways to do this: either using a callback block, or a special custom
58
+ # object.
59
+ #
60
+ # Using a block it's pretty straightforward:
61
+ #
62
+ # sftp.upload!("local", "remote") do |event, uploader, *args|
63
+ # case event
64
+ # when :open then
65
+ # # args[0] : file metadata
66
+ # puts "starting upload: #{args[0].local} -> #{args[0].remote} (#{args[0].size} bytes}"
67
+ # when :put then
68
+ # # args[0] : file metadata
69
+ # # args[1] : byte offset in remote file
70
+ # # args[2] : data being written (as string)
71
+ # puts "writing #{args[2].length} bytes to #{args[0].remote} starting at #{args[1]}"
72
+ # when :close then
73
+ # # args[0] : file metadata
74
+ # puts "finished with #{args[0].remote}"
75
+ # when :mkdir then
76
+ # # args[0] : remote path name
77
+ # puts "creating directory #{args[0]}"
78
+ # when :finish then
79
+ # puts "all done!"
80
+ # end
81
+ #
82
+ # However, for more complex implementations (e.g., GUI interfaces and such)
83
+ # a block can become cumbersome. In those cases, you can create custom
84
+ # handler objects that respond to certain methods, and then pass your handler
85
+ # to the uploader:
86
+ #
87
+ # class CustomHandler
88
+ # def on_open(uploader, file)
89
+ # puts "starting upload: #{file.local} -> #{file.remote} (#{file.size} bytes)"
90
+ # end
91
+ #
92
+ # def on_put(uploader, file, offset, data)
93
+ # puts "writing #{data.length} bytes to #{file.remote} starting at #{offset}"
94
+ # end
95
+ #
96
+ # def on_close(uploader, file)
97
+ # puts "finished with #{file.remote}"
98
+ # end
99
+ #
100
+ # def on_mkdir(uploader, path)
101
+ # puts "creating directory #{path}"
102
+ # end
103
+ #
104
+ # def on_finish(uploader)
105
+ # puts "all done!"
106
+ # end
107
+ # end
108
+ #
109
+ # sftp.upload!("local", "remote", :progress => CustomHandler.new)
110
+ #
111
+ # If you omit any of those methods, the progress updates for those missing
112
+ # events will be ignored. You can create a catchall method named "call" for
113
+ # those, instead.
114
+ class Upload
115
+ include Net::SSH::Loggable
116
+
117
+ # The source of the upload (on the local server)
118
+ attr_reader :local
119
+
120
+ # The destination of the upload (on the remote server)
121
+ attr_reader :remote
122
+
123
+ # The hash of options that were given when the object was instantiated
124
+ attr_reader :options
125
+
126
+ # The SFTP session object used by this upload instance
127
+ attr_reader :sftp
128
+
129
+ # The properties hash for this object
130
+ attr_reader :properties
131
+
132
+ # Instantiates a new uploader process on top of the given SFTP session.
133
+ # +local+ is either an IO object containing data to upload, or a string
134
+ # identifying a file or directory on the local host. +remote+ is a string
135
+ # identifying the location on the remote host that the upload should
136
+ # target.
137
+ #
138
+ # This will return immediately, and requires that the SSH event loop be
139
+ # run in order to effect the upload. (See #wait.)
140
+ def initialize(sftp, local, remote, options={}, &progress) #:nodoc:
141
+ @sftp = sftp
142
+ @local = local
143
+ @remote = remote
144
+ @progress = progress || options[:progress]
145
+ @options = options
146
+ @properties = options[:properties] || {}
147
+ @active = 0
148
+
149
+ self.logger = sftp.logger
150
+
151
+ @uploads = []
152
+ @recursive = local.respond_to?(:read) ? false : ::File.directory?(local)
153
+
154
+ if recursive?
155
+ @stack = [entries_for(local)]
156
+ @local_cwd = local
157
+ @remote_cwd = remote
158
+
159
+ @active += 1
160
+ sftp.mkdir(remote) do |response|
161
+ @active -= 1
162
+ raise StatusException.new(response, "mkdir `#{remote}'") unless response.ok?
163
+ (options[:requests] || RECURSIVE_READERS).to_i.times do
164
+ break unless process_next_entry
165
+ end
166
+ end
167
+ else
168
+ raise ArgumentError, "expected a file to upload" unless local.respond_to?(:read) || ::File.exists?(local)
169
+ @stack = [[local]]
170
+ process_next_entry
171
+ end
172
+ end
173
+
174
+ # Returns true if a directory tree is being uploaded, and false if only a
175
+ # single file is being uploaded.
176
+ def recursive?
177
+ @recursive
178
+ end
179
+
180
+ # Returns true if the uploader is currently running. When this is false,
181
+ # the uploader has finished processing.
182
+ def active?
183
+ @active > 0 || @stack.any?
184
+ end
185
+
186
+ # Forces the transfer to stop.
187
+ def abort!
188
+ @active = 0
189
+ @stack.clear
190
+ @uploads.clear
191
+ end
192
+
193
+ # Blocks until the upload has completed.
194
+ def wait
195
+ sftp.loop { active? }
196
+ self
197
+ end
198
+
199
+ # Returns the property with the given name. This allows Upload instances
200
+ # to store their own state when used as part of a state machine.
201
+ def [](name)
202
+ @properties[name.to_sym]
203
+ end
204
+
205
+ # Sets the given property to the given name. This allows Upload instances
206
+ # to store their own state when used as part of a state machine.
207
+ def []=(name, value)
208
+ @properties[name.to_sym] = value
209
+ end
210
+
211
+ private
212
+
213
+ #--
214
+ # "ruby -w" hates private attributes, so we have to do this longhand.
215
+ #++
216
+
217
+ # The progress handler for this instance. Possibly nil.
218
+ def progress; @progress; end
219
+
220
+ # A simple struct for recording metadata about the file currently being
221
+ # uploaded.
222
+ LiveFile = Struct.new(:local, :remote, :io, :size, :handle)
223
+
224
+ # The default # of bytes to read from disk at a time.
225
+ DEFAULT_READ_SIZE = 32_000
226
+
227
+ # The number of readers to use when uploading a single file.
228
+ SINGLE_FILE_READERS = 2
229
+
230
+ # The number of readers to use when uploading a directory.
231
+ RECURSIVE_READERS = 16
232
+
233
+ # Examines the stack and determines what action to take. This is the
234
+ # starting point of the state machine.
235
+ def process_next_entry
236
+ if @stack.empty?
237
+ if @uploads.any?
238
+ write_next_chunk(@uploads.first)
239
+ elsif !active?
240
+ update_progress(:finish)
241
+ end
242
+ return false
243
+ elsif @stack.last.empty?
244
+ @stack.pop
245
+ @local_cwd = ::File.dirname(@local_cwd)
246
+ @remote_cwd = ::File.dirname(@remote_cwd)
247
+ process_next_entry
248
+ elsif recursive?
249
+ entry = @stack.last.shift
250
+ lpath = ::File.join(@local_cwd, entry)
251
+ rpath = ::File.join(@remote_cwd, entry)
252
+
253
+ if ::File.directory?(lpath)
254
+ @stack.push(entries_for(lpath))
255
+ @local_cwd = lpath
256
+ @remote_cwd = rpath
257
+
258
+ @active += 1
259
+ update_progress(:mkdir, rpath)
260
+ request = sftp.mkdir(rpath, &method(:on_mkdir))
261
+ request[:dir] = rpath
262
+ else
263
+ open_file(lpath, rpath)
264
+ end
265
+ else
266
+ open_file(@stack.pop.first, remote)
267
+ end
268
+ return true
269
+ end
270
+
271
+ # Prepares to send +local+ to +remote+.
272
+ def open_file(local, remote)
273
+ @active += 1
274
+
275
+ if local.respond_to?(:read)
276
+ file = local
277
+ name = options[:name] || "<memory>"
278
+ else
279
+ file = ::File.open(local)
280
+ name = local
281
+ end
282
+
283
+ if file.respond_to?(:stat)
284
+ size = file.stat.size
285
+ else
286
+ size = file.size
287
+ end
288
+
289
+ metafile = LiveFile.new(name, remote, file, size)
290
+ update_progress(:open, metafile)
291
+
292
+ request = sftp.open(remote, "w", &method(:on_open))
293
+ request[:file] = metafile
294
+ end
295
+
296
+ # Called when a +mkdir+ request finishes, successfully or otherwise.
297
+ # If the request failed, this will raise a StatusException, otherwise
298
+ # it will call #process_next_entry to continue the state machine.
299
+ def on_mkdir(response)
300
+ @active -= 1
301
+ dir = response.request[:dir]
302
+ raise StatusException.new(response, "mkdir #{dir}") unless response.ok?
303
+
304
+ process_next_entry
305
+ end
306
+
307
+ # Called when an +open+ request finishes. Raises StatusException if the
308
+ # open failed, otherwise it calls #write_next_chunk to begin sending
309
+ # data to the remote server.
310
+ def on_open(response)
311
+ @active -= 1
312
+ file = response.request[:file]
313
+ raise StatusException.new(response, "open #{file.remote}") unless response.ok?
314
+
315
+ file.handle = response[:handle]
316
+
317
+ @uploads << file
318
+ write_next_chunk(file)
319
+
320
+ if !recursive?
321
+ (options[:requests] || SINGLE_FILE_READERS).to_i.times { write_next_chunk(file) }
322
+ end
323
+ end
324
+
325
+ # Called when a +write+ request finishes. Raises StatusException if the
326
+ # write failed, otherwise it calls #write_next_chunk to continue the
327
+ # write.
328
+ def on_write(response)
329
+ @active -= 1
330
+ file = response.request[:file]
331
+ raise StatusException.new(response, "write #{file.remote}") unless response.ok?
332
+ write_next_chunk(file)
333
+ end
334
+
335
+ # Called when a +close+ request finishes. Raises a StatusException if the
336
+ # close failed, otherwise it calls #process_next_entry to continue the
337
+ # state machine.
338
+ def on_close(response)
339
+ @active -= 1
340
+ file = response.request[:file]
341
+ raise StatusException.new(response, "close #{file.remote}") unless response.ok?
342
+ process_next_entry
343
+ end
344
+
345
+ # Attempts to send the next chunk from the given file (where +file+ is
346
+ # a LiveFile instance).
347
+ def write_next_chunk(file)
348
+ if file.io.nil?
349
+ process_next_entry
350
+ else
351
+ @active += 1
352
+ offset = file.io.pos
353
+ data = file.io.read(options[:read_size] || DEFAULT_READ_SIZE)
354
+ if data.nil?
355
+ update_progress(:close, file)
356
+ request = sftp.close(file.handle, &method(:on_close))
357
+ request[:file] = file
358
+ file.io.close
359
+ file.io = nil
360
+ @uploads.delete(file)
361
+ else
362
+ update_progress(:put, file, offset, data)
363
+ request = sftp.write(file.handle, offset, data, &method(:on_write))
364
+ request[:file] = file
365
+ end
366
+ end
367
+ end
368
+
369
+ # Returns all directory entries for the given path, removing the '.'
370
+ # and '..' relative paths.
371
+ def entries_for(local)
372
+ ::Dir.entries(local).reject { |v| %w(. ..).include?(v) }
373
+ end
374
+
375
+ # Attempts to notify the progress monitor (if one was given) about
376
+ # progress made for the given event.
377
+ def update_progress(event, *args)
378
+ on = "on_#{event}"
379
+ if progress.respond_to?(on)
380
+ progress.send(on, self, *args)
381
+ elsif progress.respond_to?(:call)
382
+ progress.call(event, self, *args)
383
+ end
384
+ end
385
+ end
386
+
387
+ end; end; end