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