C0 code coverage information
Generated on Wed Oct 07 08:33:59 -0700 2009 with rcov 0.8.2.1
Code reported as executed by Ruby looks like this...
and this: this line is also marked as covered.
Lines considered as run by rcov, but not reported by Ruby, look like this,
and this: these lines were inferred by rcov (using simple heuristics).
Finally, here's a line marked as not executed.
1 # Licensed to the Apache Software Foundation (ASF) under one or more
2 # contributor license agreements. See the NOTICE file distributed with this
3 # work for additional information regarding copyright ownership. The ASF
4 # licenses this file to you under the Apache License, Version 2.0 (the
5 # "License"); you may not use this file except in compliance with the License.
6 # You may obtain a copy of the License at
7 #
8 # http://www.apache.org/licenses/LICENSE-2.0
9 #
10 # Unless required by applicable law or agreed to in writing, software
11 # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12 # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13 # License for the specific language governing permissions and limitations under
14 # the License.
15
16
17 require 'uri'
18 require 'net/http'
19 # PATCH: On Windows, Net::SSH 2.0.2 attempts to load the Pageant DLLs which break on JRuby.
20 $LOADED_FEATURES << 'net/ssh/authentication/pageant.rb' if RUBY_PLATFORM =~ /java/
21 gem 'net-ssh' ; Net.autoload :SSH, 'net/ssh'
22 gem 'net-sftp' ; Net.autoload :SFTP, 'net/sftp'
23 autoload :CGI, 'cgi'
24 require 'digest/md5'
25 require 'digest/sha1'
26 require 'stringio'
27 autoload :ProgressBar, 'buildr/core/progressbar'
28
29
30 # Not quite open-uri, but similar. Provides read and write methods for the resource represented by the URI.
31 # Currently supports reads for URI::HTTP and writes for URI::SFTP. Also provides convenience methods for
32 # downloads and uploads.
33 module URI
34
35 # Raised when trying to read/download a resource that doesn't exist.
36 class NotFoundError < RuntimeError
37 end
38
39 # How many bytes to read/write at once. Do not change without checking BUILDR-214 first.
40 RW_CHUNK_SIZE = 128 * 1024 #:nodoc:
41
42 class << self
43
44 # :call-seq:
45 # read(uri, options?) => content
46 # read(uri, options?) { |chunk| ... }
47 #
48 # Reads from the resource behind this URI. The first form returns the content of the resource,
49 # the second form yields to the block with each chunk of content (usually more than one).
50 #
51 # For example:
52 # File.open 'image.jpg', 'w' do |file|
53 # URI.read('http://example.com/image.jpg') { |chunk| file.write chunk }
54 # end
55 # Shorter version:
56 # File.open('image.jpg', 'w') { |file| file.write URI.read('http://example.com/image.jpg') }
57 #
58 # Supported options:
59 # * :modified -- Only download if file modified since this timestamp. Returns nil if not modified.
60 # * :progress -- Show the progress bar while reading.
61 def read(uri, options = nil, &block)
62 uri = URI.parse(uri.to_s) unless URI === uri
63 uri.read options, &block
64 end
65
66 # :call-seq:
67 # download(uri, target, options?)
68 #
69 # Downloads the resource to the target.
70 #
71 # The target may be a file name (string or task), in which case the file is created from the resource.
72 # The target may also be any object that responds to +write+, e.g. File, StringIO, Pipe.
73 #
74 # Use the progress bar when running in verbose mode.
75 def download(uri, target, options = nil)
76 uri = URI.parse(uri.to_s) unless URI === uri
77 uri.download target, options
78 end
79
80 # :call-seq:
81 # write(uri, content, options?)
82 # write(uri, options?) { |bytes| .. }
83 #
84 # Writes to the resource behind the URI. The first form writes the content from a string or an object
85 # that responds to +read+ and optionally +size+. The second form writes the content by yielding to the
86 # block. Each yield should return up to the specified number of bytes, the last yield returns nil.
87 #
88 # For example:
89 # File.open 'killer-app.jar', 'rb' do |file|
90 # write('sftp://localhost/jars/killer-app.jar') { |chunk| file.read(chunk) }
91 # end
92 # Or:
93 # write 'sftp://localhost/jars/killer-app.jar', File.read('killer-app.jar')
94 #
95 # Supported options:
96 # * :progress -- Show the progress bar while reading.
97 def write(uri, *args, &block)
98 uri = URI.parse(uri.to_s) unless URI === uri
99 uri.write *args, &block
100 end
101
102 # :call-seq:
103 # upload(uri, source, options?)
104 #
105 # Uploads from source to the resource.
106 #
107 # The source may be a file name (string or task), in which case the file is uploaded to the resource.
108 # The source may also be any object that responds to +read+ (and optionally +size+), e.g. File, StringIO, Pipe.
109 #
110 # Use the progress bar when running in verbose mode.
111 def upload(uri, source, options = nil)
112 uri = URI.parse(uri.to_s) unless URI === uri
113 uri.upload source, options
114 end
115
116 end
117
118 class Generic
119
120 # :call-seq:
121 # read(options?) => content
122 # read(options?) { |chunk| ... }
123 #
124 # Reads from the resource behind this URI. The first form returns the content of the resource,
125 # the second form yields to the block with each chunk of content (usually more than one).
126 #
127 # For options, see URI::read.
128 def read(options = nil, &block)
129 fail 'This protocol doesn\'t support reading (yet, how about helping by implementing it?)'
130 end
131
132 # :call-seq:
133 # download(target, options?)
134 #
135 # Downloads the resource to the target.
136 #
137 # The target may be a file name (string or task), in which case the file is created from the resource.
138 # The target may also be any object that responds to +write+, e.g. File, StringIO, Pipe.
139 #
140 # Use the progress bar when running in verbose mode.
141 def download(target, options = nil)
142 case target
143 when Rake::Task
144 download target.name, options
145 when String
146 # If download breaks we end up with a partial file which is
147 # worse than not having a file at all, so download to temporary
148 # file and then move over.
149 modified = File.stat(target).mtime if File.exist?(target)
150 temp = Tempfile.new(File.basename(target))
151 temp.binmode
152 read({:progress=>verbose}.merge(options || {}).merge(:modified=>modified)) { |chunk| temp.write chunk }
153 temp.close
154 mkpath File.dirname(target)
155 mv temp.path, target
156 when File
157 read({:progress=>verbose}.merge(options || {}).merge(:modified=>target.mtime)) { |chunk| target.write chunk }
158 target.flush
159 else
160 raise ArgumentError, 'Expecting a target that is either a file name (string, task) or object that responds to write (file, pipe).' unless target.respond_to?(:write)
161 read({:progress=>verbose}.merge(options || {})) { |chunk| target.write chunk }
162 target.flush
163 end
164 end
165
166 # :call-seq:
167 # write(content, options?)
168 # write(options?) { |bytes| .. }
169 #
170 # Writes to the resource behind the URI. The first form writes the content from a string or an object
171 # that responds to +read+ and optionally +size+. The second form writes the content by yielding to the
172 # block. Each yield should return up to the specified number of bytes, the last yield returns nil.
173 #
174 # For options, see URI::write.
175 def write(*args, &block)
176 options = args.pop if Hash === args.last
177 options ||= {}
178 if String === args.first
179 ios = StringIO.new(args.first, 'r')
180 write(options.merge(:size=>args.first.size)) { |bytes| ios.read(bytes) }
181 elsif args.first.respond_to?(:read)
182 size = args.first.size rescue nil
183 write({:size=>size}.merge(options)) { |bytes| args.first.read(bytes) }
184 elsif args.empty? && block
185 write_internal options, &block
186 else
187 raise ArgumentError, 'Either give me the content, or pass me a block, otherwise what would I upload?'
188 end
189 end
190
191 # :call-seq:
192 # upload(source, options?)
193 #
194 # Uploads from source to the resource.
195 #
196 # The source may be a file name (string or task), in which case the file is uploaded to the resource.
197 # If the source is a directory, uploads all files inside the directory (including nested directories).
198 # The source may also be any object that responds to +read+ (and optionally +size+), e.g. File, StringIO, Pipe.
199 #
200 # Use the progress bar when running in verbose mode.
201 def upload(source, options = nil)
202 source = source.name if Rake::Task === source
203 options ||= {}
204 if String === source
205 raise NotFoundError, 'No source file/directory to upload.' unless File.exist?(source)
206 if File.directory?(source)
207 Dir.glob("#{source}/**/*").reject { |file| File.directory?(file) }.each do |file|
208 uri = self + (File.join(self.path, file.sub(source, '')))
209 uri.upload file, {:digests=>[]}.merge(options)
210 end
211 else
212 File.open(source, 'rb') { |input| upload input, options }
213 end
214 elsif source.respond_to?(:read)
215 digests = (options[:digests] || [:md5, :sha1]).
216 inject({}) { |hash, name| hash[name] = Digest.const_get(name.to_s.upcase).new ; hash }
217 size = source.size rescue nil
218 write (options).merge(:progress=>verbose && size, :size=>size) do |bytes|
219 source.read(bytes).tap do |chunk|
220 digests.values.each { |digest| digest << chunk } if chunk
221 end
222 end
223 digests.each do |key, digest|
224 self.merge("#{self.path}.#{key}").write "#{digest.hexdigest} #{File.basename(path)}",
225 (options).merge(:progress=>false)
226 end
227 else
228 raise ArgumentError, 'Expecting source to be a file name (string, task) or any object that responds to read (file, pipe).'
229 end
230 end
231
232 protected
233
234 # :call-seq:
235 # with_progress_bar(show, file_name, size) { |progress| ... }
236 #
237 # Displays a progress bar while executing the block. The first argument must be true for the
238 # progress bar to show (TTY output also required), as a convenient for selectively using the
239 # progress bar from a single block.
240 #
241 # The second argument provides a filename to display, the third its size in bytes.
242 #
243 # The block is yielded with a progress object that implements a single method.
244 # Call << for each block of bytes down/uploaded.
245 def with_progress_bar(show, file_name, size, &block) #:nodoc:
246 options = { :total=>size || 0, :title=>file_name }
247 options[:hidden] = true unless show
248 ProgressBar.start options, &block
249 end
250
251 # :call-seq:
252 # proxy_uri => URI?
253 #
254 # Returns the proxy server to use. Obtains the proxy from the relevant environment variable (e.g. HTTP_PROXY).
255 # Supports exclusions based on host name and port number from environment variable NO_PROXY.
256 def proxy_uri
257 proxy = ENV["#{scheme.upcase}_PROXY"]
258 proxy = URI.parse(proxy) if String === proxy
259 excludes = ENV['NO_PROXY'].to_s.split(/\s*,\s*/).compact
260 excludes = excludes.map { |exclude| exclude =~ /:\d+$/ ? exclude : "#{exclude}:*" }
261 return proxy unless excludes.any? { |exclude| File.fnmatch(exclude, "#{host}:#{port}") }
262 end
263
264 def write_internal(options, &block) #:nodoc:
265 fail 'This protocol doesn\'t support writing (yet, how about helping by implementing it?)'
266 end
267
268 end
269
270
271 class HTTP #:nodoc:
272
273 # See URI::Generic#read
274 def read(options = nil, &block)
275 options ||= {}
276 connect do |http|
277 trace "Requesting #{self}"
278 headers = { 'If-Modified-Since' => CGI.rfc1123_date(options[:modified].utc) } if options[:modified]
279 request = Net::HTTP::Get.new(request_uri.empty? ? '/' : request_uri, headers)
280 request.basic_auth self.user, self.password if self.user
281 http.request request do |response|
282 case response
283 when Net::HTTPNotModified
284 # No modification, nothing to do.
285 trace 'Not modified since last download'
286 return nil
287 when Net::HTTPRedirection
288 # Try to download from the new URI, handle relative redirects.
289 trace "Redirected to #{response['Location']}"
290 rself = self + URI.parse(response['Location'])
291 rself.user, rself.password = self.user, self.password
292 return rself.read(options, &block)
293 when Net::HTTPOK
294 info "Downloading #{self}"
295 result = nil
296 with_progress_bar options[:progress], path.split('/').last, response.content_length do |progress|
297 if block
298 response.read_body do |chunk|
299 block.call chunk
300 progress << chunk
301 end
302 else
303 result = ''
304 response.read_body do |chunk|
305 result << chunk
306 progress << chunk
307 end
308 end
309 end
310 return result
311 when Net::HTTPNotFound
312 raise NotFoundError, "Looking for #{self} and all I got was a 404!"
313 else
314 raise RuntimeError, "Failed to download #{self}: #{response.message}"
315 end
316 end
317 end
318 end
319
320 private
321
322 def write_internal(options, &block) #:nodoc:
323 options ||= {}
324 connect do |http|
325 trace "Uploading to #{path}"
326 content = StringIO.new
327 while chunk = yield(RW_CHUNK_SIZE)
328 content << chunk
329 end
330 headers = { 'Content-MD5'=>Digest::MD5.hexdigest(content.string) }
331 request = Net::HTTP::Put.new(request_uri.empty? ? '/' : request_uri, headers)
332 request.basic_auth self.user, self.password if self.user
333 response = nil
334 with_progress_bar options[:progress], path.split('/').last, content.size do |progress|
335 request.content_length = content.size
336 content.rewind
337 stream = Object.new
338 class << stream ; self ;end.send :define_method, :read do |count|
339 bytes = content.read(count)
340 progress << bytes if bytes
341 bytes
342 end
343 request.body_stream = stream
344 response = http.request(request)
345 end
346
347 case response
348 when Net::HTTPRedirection
349 # Try to download from the new URI, handle relative redirects.
350 trace "Redirected to #{response['Location']}"
351 content.rewind
352 return (self + URI.parse(response['location'])).write_internal(options) { |bytes| content.read(bytes) }
353 when Net::HTTPSuccess
354 else
355 raise RuntimeError, "Failed to upload #{self}: #{response.message}"
356 end
357 end
358 end
359
360 def connect
361 if proxy = proxy_uri
362 proxy = URI.parse(proxy) if String === proxy
363 http = Net::HTTP.new(host, port, proxy.host, proxy.port, proxy.user, proxy.password)
364 else
365 http = Net::HTTP.new(host, port)
366 end
367 if self.instance_of? URI::HTTPS
368 require 'net/https'
369 http.use_ssl = true
370 end
371 yield http
372 end
373
374 end
375
376
377 class SFTP < Generic #:nodoc:
378
379 DEFAULT_PORT = 22
380 COMPONENT = [ :scheme, :userinfo, :host, :port, :path ].freeze
381
382 class << self
383 # Caching of passwords, so we only need to ask once.
384 def passwords
385 @passwords ||= {}
386 end
387 end
388
389 def initialize(*arg)
390 super
391 end
392
393 def read(options = {}, &block)
394 # SSH options are based on the username/password from the URI.
395 ssh_options = { :port=>port, :password=>password }.merge(options[:ssh_options] || {})
396 ssh_options[:password] ||= SFTP.passwords[host]
397 begin
398 trace "Connecting to #{host}"
399 result = nil
400 Net::SFTP.start(host, user, ssh_options) do |sftp|
401 SFTP.passwords[host] = ssh_options[:password]
402 trace 'connected'
403
404 with_progress_bar options[:progress] && options[:size], path.split('/'), options[:size] || 0 do |progress|
405 trace "Downloading to #{path}"
406 sftp.file.open(path, 'r') do |file|
407 if block
408 while chunk = file.read(RW_CHUNK_SIZE)
409 block.call chunk
410 progress << chunk
411 end
412 else
413 result = ''
414 while chunk = file.read(RW_CHUNK_SIZE)
415 result << chunk
416 progress << chunk
417 end
418 end
419 end
420 end
421 end
422 return result
423 rescue Net::SSH::AuthenticationFailed=>ex
424 # Only if running with console, prompt for password.
425 if !ssh_options[:password] && $stdout.isatty
426 password = ask("Password for #{host}:") { |q| q.echo = '*' }
427 ssh_options[:password] = password
428 retry
429 end
430 raise
431 end
432 end
433
434 protected
435
436 def write_internal(options, &block) #:nodoc:
437 # SSH options are based on the username/password from the URI.
438 ssh_options = { :port=>port, :password=>password }.merge(options[:ssh_options] || {})
439 ssh_options[:password] ||= SFTP.passwords[host]
440 begin
441 trace "Connecting to #{host}"
442 Net::SFTP.start(host, user, ssh_options) do |sftp|
443 SFTP.passwords[host] = ssh_options[:password]
444 trace 'Connected'
445
446 # To create a path, we need to create all its parent. We use realpath to determine if
447 # the path already exists, otherwise mkdir fails.
448 trace "Creating path #{path}"
449 File.dirname(path).split('/').reject(&:empty?).inject('/') do |base, part|
450 combined = base + part
451 sftp.close(sftp.opendir!(combined)) rescue sftp.mkdir! combined, {}
452 "#{combined}/"
453 end
454
455 with_progress_bar options[:progress] && options[:size], path.split('/'), options[:size] || 0 do |progress|
456 trace "Uploading to #{path}"
457 sftp.file.open(path, 'w') do |file|
458 while chunk = yield(RW_CHUNK_SIZE)
459 file.write chunk
460 progress << chunk
461 end
462 sftp.setstat(path, :permissions => options[:permissions]) if options[:permissions]
463 end
464 end
465 end
466 rescue Net::SSH::AuthenticationFailed=>ex
467 # Only if running with console, prompt for password.
468 if !ssh_options[:password] && $stdout.isatty
469 password = ask("Password for #{host}:") { |q| q.echo = '*' }
470 ssh_options[:password] = password
471 retry
472 end
473 raise
474 end
475 end
476
477 end
478
479 @@schemes['SFTP'] = SFTP
480
481
482 # File URL. Keep in mind that file URLs take the form of <code>file://host/path</code>, although the host
483 # is not used, so typically all you will see are three backslashes. This methods accept common variants,
484 # like <code>file:/path</code> but always returns a valid URL.
485 class FILE < Generic
486
487 COMPONENT = [ :host, :path ].freeze
488
489 def initialize(*args)
490 super
491 # file:something (opaque) becomes file:///something
492 if path.nil?
493 set_path "/#{opaque}"
494 unless opaque.nil?
495 set_opaque nil
496 warn "#{caller[2]}: We'll accept this URL, but just so you know, it needs three slashes, as in: #{to_s}"
497 end
498 end
499 # Sadly, file://something really means file://something/ (something being server)
500 set_path '/' if path.empty?
501
502 # On windows, file://c:/something is not a valid URL, but people do it anyway, so if we see a drive-as-host,
503 # we'll just be nice enough to fix it. (URI actually strips the colon here)
504 if host =~ /^[a-zA-Z]$/
505 set_path "/#{host}:#{path}"
506 set_host nil
507 end
508 end
509
510 # See URI::Generic#read
511 def read(options = nil, &block)
512 options ||= {}
513 raise ArgumentError, 'Either you\'re attempting to read a file from another host (which we don\'t support), or you used two slashes by mistake, where you should have file:///<path>.' if host
514
515 path = real_path
516 # TODO: complain about clunky URLs
517 raise NotFoundError, "Looking for #{self} and can't find it." unless File.exists?(path)
518 raise NotFoundError, "Looking for the file #{self}, and it happens to be a directory." if File.directory?(path)
519 File.open path, 'rb' do |input|
520 with_progress_bar options[:progress], path.split('/').last, input.stat.size do |progress|
521 block ? block.call(input.read) : input.read
522 end
523 end
524 end
525
526 def to_s
527 "file://#{host}#{path}"
528 end
529
530 # The URL path always starts with a backslash. On most operating systems (Linux, Darwin, BSD) it points
531 # to the absolute path on the file system. But on Windows, it comes before the drive letter, creating an
532 # unusable path, so real_path fixes that. Ugly but necessary hack.
533 def real_path #:nodoc:
534 RUBY_PLATFORM =~ /win32/ && path =~ /^\/[a-zA-Z]:\// ? path[1..-1] : path
535 end
536
537 protected
538
539 def write_internal(options, &block) #:nodoc:
540 raise ArgumentError, 'Either you\'re attempting to write a file to another host (which we don\'t support), or you used two slashes by mistake, where you should have file:///<path>.' if host
541 temp = Tempfile.new(File.basename(path))
542 temp.binmode
543 with_progress_bar options[:progress] && options[:size], path.split('/'), options[:size] || 0 do |progress|
544 while chunk = yield(RW_CHUNK_SIZE)
545 temp.write chunk
546 progress << chunk
547 end
548 end
549 temp.close
550 mkpath File.dirname(real_path)
551 mv temp.path, real_path
552 real_path
553 end
554
555 @@schemes['FILE'] = FILE
556
557 end
558
559 end
Generated using the rcov code coverage analysis tool for Ruby
version 0.8.2.1.