lib/core/transports.rb in buildr-1.1.3 vs lib/core/transports.rb in buildr-1.2.0

- old
+ new

@@ -6,10 +6,12 @@ require "uri/sftp" require "digest/md5" require "digest/sha1" require "facet/progressbar" require "highline" +require "tempfile" +require "uri/sftp" # Monkeypatching: SFTP never defines the mkdir method on its session or the underlying # driver, it just redirect calls through method_missing. Rake, on the other hand, decides # to define mkdir on Object, and so routes our calls to FileUtils. @@ -25,453 +27,476 @@ method_missing :mkdir, first, path, attrs end end end -# Monkeypatching Net::HTTP to solve keep_alive bug, see http://blade.nagaokaut.ac.jp/cgi-bin/scat.rb/ruby/ruby-core/10818 -module Net - class HTTP - def keep_alive?(req, res) - return false if /close/i =~ req['connection'].to_s - return false if @seems_1_0_server - return false if /close/i =~ res['connection'].to_s - return true if /keep-alive/i =~ res['connection'].to_s - return false if /close/i =~ res['proxy-connection'].to_s - return true if /keep-alive/i =~ res['proxy-connection'].to_s - (@curr_http_version == '1.1') - end - end -end +# Not quite open-uri, but similar. Provides read and write methods for the resource represented by the URI. +# Currently supports reads for URI::HTTP and writes for URI::SFTP. Also provides convenience methods for +# downloads and uploads. +module URI -module Buildr + # Raised when trying to read/download a resource that doesn't exist. + class NotFoundError < RuntimeError + end - # Transports are used for downloading artifacts from remote repositories, uploading - # artifacts to deployment repositories, and anything else you need to move around. - # - # The HTTP transport is used for all URLs with the scheme http or https. You can only - # use the HTTP transport to download artifacts. - # - # The HTTP transport supports the following options: - # * :proxy -- Proxy server to use. A hash with the values host, port, user and password. - # You can also pass a URL (string or URI object). - # - # The SFTP transport is used for all URLs with the schema sftp. You can only use the - # SFTP transport to upload artifacts. - # - # The SFTP transport supports the following options: - # * :username -- The username. - # * :password -- A password. If unspecified, you will be prompted to enter a password. - # * :permissions -- Permissions to set on the uploaded file. - # You can also pass the username/password in the URL. - # - # The SFTP transport will automatically create MD5 and SHA1 digest files for each file - # it uploads. - module Transports + class << self - # Indicates the requested resource was not found. - class NotFound < Exception + # :call-seq: + # read(uri, options?) => content + # read(uri, options?) { |chunk| ... } + # + # Reads from the resource behind this URI. The first form returns the content of the resource, + # the second form yields to the block with each chunk of content (usually more than one). + # + # For example: + # File.open "image.jpg", "w" do |file| + # URI.read("http://example.com/image.jpg") { |chunk| file.write chunk } + # end + # Shorter version: + # File.open("image.jpg", "w") { |file| file.write URI.read("http://example.com/image.jpg") } + # + # Supported options: + # * :proxy -- Collection of proxy settings, accessed by scheme. + # * :modified -- Only download if file modified since this timestamp. Returns nil if not modified. + # * :progress -- Show the progress bar while reading. + def read(uri, options = nil, &block) + uri = URI.parse(uri.to_s) unless URI === uri + uri.read options, &block end - class << self + # :call-seq: + # download(uri, target, options?) + # + # Downloads the resource to the target. + # + # The target may be a file name (string or task), in which case the file is created from the resource. + # The target may also be any object that responds to +write+, e.g. File, StringIO, Pipe. + # + # Use the progress bar when running in verbose mode. + def download(uri, target, options = nil) + uri = URI.parse(uri.to_s) unless URI === uri + uri.download target, options + end + + # :call-seq: + # write(uri, content, options?) + # write(uri, options?) { |bytes| .. } + # + # Writes to the resource behind the URI. The first form writes the content from a string or an object + # that responds to +read+ and optionally +size+. The second form writes the content by yielding to the + # block. Each yield should return up to the specified number of bytes, the last yield returns nil. + # + # For example: + # File.open "killer-app.jar", "rb" do |file| + # write("sftp://localhost/jars/killer-app.jar") { |chunk| file.read(chunk) } + # end + # Or: + # write "sftp://localhost/jars/killer-app.jar", File.read("killer-app.jar") + # + # Supported options: + # * :proxy -- Collection of proxy settings, accessed by scheme. + # * :progress -- Show the progress bar while reading. + def write(uri, *args, &block) + uri = URI.parse(uri.to_s) unless URI === uri + uri.write *args, &block + end + + # :call-seq: + # upload(uri, source, options?) + # + # Uploads from source to the resource. + # + # The source may be a file name (string or task), in which case the file is uploaded to the resource. + # The source may also be any object that responds to +read+ (and optionally +size+), e.g. File, StringIO, Pipe. + # + # Use the progress bar when running in verbose mode. + def upload(uri, source, options = nil) + uri = URI.parse(uri.to_s) unless URI === uri + uri.upload source, options + end - # :call-seq: - # perform(url, options?) { |transport| ... } - # - # Perform one or more operations using an open connection to the - # specified URL. For examples, see Transport#download and Transport#upload. - def perform(url, options = nil, &block) - uri = URI.parse(url.to_s) - const_get(uri.scheme.upcase).perform(uri, options, &block) - end + end - # :call-seq: - # download(url, target, options?) - # - # Convenience method for downloading a single file from the specified - # URL to the target file. - def download(url, target, options = nil, &block) - uri = URI.parse(url.to_s) - path, uri.path = uri.path, "" - const_get(uri.scheme.upcase).perform(uri, options) do |transport| - transport.download(path, target, &block) - end - end + class Generic + # :call-seq: + # read(options?) => content + # read(options?) { |chunk| ... } + # + # Reads from the resource behind this URI. The first form returns the content of the resource, + # the second form yields to the block with each chunk of content (usually more than one). + # + # For options, see URI::read. + def read(options = nil, &block) + fail "This protocol doesn't support reading (yet, how about helping by implementing it?)" end - # Extend this class if you are implementing a new transport. - class Transport - - class << self - - # :call-seq: - # perform(url, options?) { |transport| ... } - # - # Perform one or more operations using an open connection to the - # specified URL. For examples, see #download and #upload. - def perform(url, options = nil) - instance = new(url, options) - begin - yield instance - ensure - instance.close - end + # :call-seq: + # download(target, options?) + # + # Downloads the resource to the target. + # + # The target may be a file name (string or task), in which case the file is created from the resource. + # The target may also be any object that responds to +write+, e.g. File, StringIO, Pipe. + # + # Use the progress bar when running in verbose mode. + def download(target, options = nil) + case target + when Rake::Task + download target.name, options + when String + # If download breaks we end up with a partial file which is + # worse than not having a file at all, so download to temporary + # file and then move over. + modified = File.stat(target).mtime if File.exist?(target) + temp = nil + Tempfile.open File.basename(target) do |temp| + temp.binmode + read({:progress=>verbose}.merge(options || {}).merge(:modified=>modified)) { |chunk| temp.write chunk } end + mkpath File.dirname(target) + File.move temp.path, target + when File + read({:progress=>verbose}.merge(options || {}).merge(:modified=>target.mtime)) { |chunk| target.write chunk } + target.flush + else + 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) + read({:progress=>verbose}.merge(options || {})) { |chunk| target.write chunk } + target.flush end + end + + # :call-seq: + # write(content, options?) + # write(options?) { |bytes| .. } + # + # Writes to the resource behind the URI. The first form writes the content from a string or an object + # that responds to +read+ and optionally +size+. The second form writes the content by yielding to the + # block. Each yield should return up to the specified number of bytes, the last yield returns nil. + # + # For options, see URI::write. + def write(*args, &block) + fail "This protocol doesn't support writing (yet, how about helping by implementing it?)" + end - # The server URI. - attr_reader :uri - # The base path on the server, always ending with a slash. - attr_reader :base_path - # Options passed during construction. - attr_reader :options - - # Initialize the transport with the specified URL and options. - def initialize(url, options) - @uri = URI.parse(url.to_s) - @base_path = @uri.path || "/" - @base_path += "/" unless @base_path[-1] == ?/ - @options = options || {} - end - - # Downloads a file from the specified path, relative to the server URI. - # Downloads to either the target file, or by calling the block with each - # chunk of the file. Returns the file's modified timestamp, or now. - # - # For example: - # Transports.perform("http://server/libs") do |http| - # http.download("my_project/test.jar", "test.jar") - # http.download("my_project/readme") { |text| $stdout.write text } - # end - def download(path, target, &block) - fail "Upload not implemented for this transport" - end - - # Uploads a file (source) to the server at the specified path, - # relative to the server URI. - # - # For example: - # Transports.perform("sftp://server/libs") do |sftp| - # sftp.mkpath "my_project" - # sftp.upload("target/test.jar", "my_project/test.jar") - # end - def upload(source, path) - fail "Upload not implemented for this transport" - end - - # Creates a path on the server relative to the server URI. - # See #upload for example. - def mkpath(path) - fail "Upload not implemented for this transport" - end - - protected - - # :call-seq: - # with_progress_bar(file_name, length) { |progress| ... } - # - # Displays a progress bar while executing the block. The first - # argument provides a filename to display, the second argument - # its size in bytes. - # - # The block is yielded with a progress object that implements - # a single method. Call << for each block of bytes down/uploaded. - def with_progress_bar(file_name, length) - if verbose && $stdout.isatty - progress_bar = Console::ProgressBar.new(file_name, length) - # Extend the progress bar so we can display count/total. - class << progress_bar - def total() - convert_bytes(@total) - end + # :call-seq: + # upload(source, options?) + # + # Uploads from source to the resource. + # + # The source may be a file name (string or task), in which case the file is uploaded to the resource. + # If the source is a directory, uploads all files inside the directory (including nested directories). + # The source may also be any object that responds to +read+ (and optionally +size+), e.g. File, StringIO, Pipe. + # + # Use the progress bar when running in verbose mode. + def upload(source, options = nil) + source = source.name if Rake::Task === source + options ||= {} + if String === source + raise NotFoundError, "No source file/directory to upload." unless File.exist?(source) + if File.directory?(source) + Dir.glob("#{source}/**/*").reject { |file| File.directory?(file) }.each do |file| + path = self.path + file.sub(source, "") + (self + path).upload file, {:digests=>[]}.merge(options) end - # Squeeze the filename into 30 characters. - if file_name.size > 30 - base, ext = file_name.split(".") - ext ||= "" - truncated = "#{base[0..26-ext.size]}...#{ext}" - else - truncated = file_name + else + File.open(source, "rb") { |input| upload input, options } + end + elsif source.respond_to?(:read) + digests = (options[:digests] || [:md5, :sha1]). + inject({}) { |hash, name| hash[name] = Digest.const_get(name.to_s.upcase).new ; hash } + size = source.size rescue nil + write (options).merge(:progress=>verbose && size, :size=>size) do |bytes| + source.read(bytes).tap do |chunk| + digests.values.each { |digest| digest << chunk } if chunk end - progress_bar.format = "#{truncated}: %3d%% %s %s/%s %s" - progress_bar.format = "%3d%% %s %s/%s %s" - progress_bar.format_arguments = [:percentage, :bar, :bytes, :total, :stat] - progress_bar.bar_mark = "." + end + digests.each do |key, digest| + self.merge("#{self.path}.#{key}").write "#{digest.hexdigest} #{File.basename(path)}", + (options).merge(:progress=>false) + end + else + raise ArgumentError, "Expecting source to be a file name (string, task) or any object that responds to read (file, pipe)." + end + end + protected - begin - class << progress_bar - def <<(bytes) - inc bytes.respond_to?(:size) ? bytes.size : bytes - end - end - yield progress_bar - ensure - progress_bar.finish + # :call-seq: + # with_progress_bar(enable, file_name, size) { |progress| ... } + # + # Displays a progress bar while executing the block. The first argument must be true for the + # progress bar to show (TTY output also required), as a convenient for selectively using the + # progress bar from a single block. + # + # The second argument provides a filename to display, the third its size in bytes. + # + # The block is yielded with a progress object that implements a single method. + # Call << for each block of bytes down/uploaded. + def with_progress_bar(enable, file_name, size) #:nodoc: + if enable && $stdout.isatty + progress_bar = Console::ProgressBar.new(file_name, size) + # Extend the progress bar so we can display count/total. + class << progress_bar + def total() + convert_bytes(@total) end + end + # Squeeze the filename into 30 characters. + if file_name.size > 30 + base, ext = file_name.split(".") + truncated = "#{base[0..26-ext.to_s.size]}...#{ext}" else - progress_bar = Object.new + truncated = file_name + end + progress_bar.format = "#{truncated}: %3d%% %s %s/%s %s" + progress_bar.format = "%3d%% %s %s/%s %s" + progress_bar.format_arguments = [:percentage, :bar, :bytes, :total, :stat] + progress_bar.bar_mark = "." + + begin class << progress_bar def <<(bytes) + inc bytes.respond_to?(:size) ? bytes.size : bytes end end yield progress_bar + ensure + progress_bar.finish end - end - - # :call-seq: - # with_digests(types?) { |digester| ... } => hash - # - # Use the Digester to create digests for files you are downloading or - # uploading, and either verify their signatures (download) or create - # signatures (upload). - # - # The method takes one argument with the list of digest algorithms to - # support. Leave if empty and it will default to MD5 and SHA1. - # - # The method then yields the block passing it a Digester. The Digester - # supports two methods. Use << to pass data that you are down/uploading. - # Once all data is transmitted, use each to iterate over the digests. - # The each method calls the block with the digest type (e.g. "md5") - # and the hexadecimal digest value. - # - # For example: - # with_digests do |digester| - # download url do |block| - # digester << block - # end - # digester.each do |type, hexdigest| - # signature = download "#{url}.#{type}" - # fail "Mismatch" unless signature == hexdigest - # end - # end - def with_digests(types = nil) - digester = Digester.new(types) - yield digester - digester.to_hash - end - - class Digester #:nodoc: - - def initialize(types) - # Digests disabled for now, we have a keep-alive problem with Net::HTTP. - #types ||= [ "md5", "sha1" ] - types ||= [] - @digests = types.inject({}) do |hash, type| - hash[type.to_s.downcase] = Digest.const_get(type.to_s.upcase).new - hash + else + progress_bar = Object.new + class << progress_bar + def <<(bytes) end end - - # Add bytes for digestion. - def <<(bytes) - @digests.each { |type, digest| digest << bytes } - end - - # Iterate over all the digests calling the block with two arguments: - # the digest type (e.g. "md5") and the hexadecimal digest value. - def each() - @digests.each { |type, digest| yield type, digest.hexdigest } - end - - # Returns a hash that maps each digest type to its hexadecimal digest value. - def to_hash() - @digests.keys.inject({}) do |hash, type| - hash[type] = @digests[type].hexdigest - hash - end - end - + yield progress_bar end - end + end - class HTTP < Transport #:nodoc: + class HTTP #:nodoc: - def initialize(url, options) - super - if options - rake_check_options options, :proxy, :digests - proxy = options[:proxy] - end + # See URI::Generic#read + def read(options = nil, &block) + options ||= {} - case proxy - when Hash - @http = Net::HTTP.start(@uri.host, @uri.port, proxy[:host], proxy[:port], proxy[:user], proxy[:password]) - when URI, String - proxy = URI.parse(proxy.to_s) - @http = Net::HTTP.start(@uri.host, @uri.port, proxy.host, proxy.port, proxy.user, proxy.password) - else - @http = Net::HTTP.start(@uri.host, @uri.port) - end - end - - def download(path, target = nil, &block) - puts "Requesting #{@uri}/#{path} " if Rake.application.options.trace - if target && File.exist?(target) - last_modified = File.stat(target).mtime.utc - headers = { "If-Modified-Since" => CGI.rfc1123_date(last_modified) } - end - path = path[1..-1] if path[0..0] == '/' - @http.request_get(@base_path + path, headers) do |response| + headers = { "If-Modified-Since" => CGI.rfc1123_date(options[:modified].utc) } if options[:modified] + result = nil + request = lambda do |http| + puts "Requesting #{self}" if Rake.application.options.trace + http.request_get(path, headers) do |response| case response when Net::HTTPNotModified # No modification, nothing to do. puts "Not modified since last download" if Rake.application.options.trace when Net::HTTPRedirection # Try to download from the new URI, handle relative redirects. puts "Redirected to #{response['Location']}" if Rake.application.options.trace - last_modified = Transports.download(@uri + URI.parse(response["location"]), target, @options, &block) + result = (self + URI.parse(response["location"])).read(options, &block) when Net::HTTPOK - puts "Downloading #{@uri}/#{path}" if verbose - last_modified = Time.parse(response["Last-Modified"] || "") - with_progress_bar path.split("/").last, response.content_length do |progress| - with_digests(@options[:digests]) do |digester| - - download = proc do |write| - # Read the body of the page and write it out. - response.read_body do |chunk| - write[chunk] - digester << chunk - progress << chunk - end - # Check server digests before approving the download. - digester.each do |type, hexdigest| - @http.request_get("#{@base_path}#{path}.#{type.to_s.downcase}") do |response| - if Net::HTTPOK === response - puts "Checking signature from #{@uri}/#{path}" if Rake.application.options.trace - fail "Checksum failure for #{@uri}/#{path}: #{type.to_s.upcase} digest on server did not match downloaded file" unless - response.read_body.split.first == hexdigest - end - end - end + puts "Downloading #{self}" if verbose + with_progress_bar options[:progress], path.split("/").last, response.content_length do |progress| + if block + response.read_body do |chunk| + block.call chunk + progress << chunk end - - if target - # If download breaks we end up with a partial file which is - # worse than not having a file at all, so download to temporary - # file and then move over. - temp = Tempfile.open(File.basename(target)) - temp.binmode - download[ proc { |chunk| temp.write chunk } ] - temp.close - File.move temp.path, target - else - download[ block ] + else + result = "" + response.read_body do |chunk| + result << chunk + progress << chunk end - end end + when Net::HTTPNotFound - raise NotFound, "Looking for #{@uri}/#{path} and all I got was a 404!" + raise NotFoundError, "Looking for #{self} and all I got was a 404!" else - fail "Failed to download #{@uri}/#{path}: #{response.message}" + raise RuntimeError, "Failed to download #{self}: #{response.message}" end end - last_modified end - def close() - @http.finish - @http = nil + proxy = options[:proxy] && options[:proxy].http + if proxy + proxy = URI.parse(proxy) if String === proxy + Net::HTTP.start(host, port, proxy.host, proxy.port, proxy.user, proxy.password) { |http| request[http] } + else + Net::HTTP.start(host, port) { |http| request[http] } end - + result end - # Use the HTTP transport for HTTPS connections. - HTTPS = HTTP #:nodoc: + end + class SFTP #:nodoc: - class SFTP < Transport #:nodoc: - - class << self - def passwords() - @passwords ||= {} - end + class << self + # Caching of passwords, so we only need to ask once. + def passwords() + @passwords ||= {} end + end - attr_reader :sftp + # See URI::Generic#write + def write(*args, &block) + options = args.pop if Hash === args.last + options ||= {} + if String === args.first + ios = StringIO.new(args.first, "r") + write(options.merge(:size=>args.first.size)) { |bytes| ios.read(bytes) } + elsif args.first.respond_to?(:read) + size = args.first.size rescue nil + write({:size=>size}.merge(options)) { |bytes| args.first.read(bytes) } + elsif args.empty? && block - def initialize(url, options) - super - rake_check_options options, :digests, :permissions, :port, :uri, :username, :password - @permissions = options.delete :permissions # SSH options are based on the username/password from the URI. - ssh_options = { :port=>@uri.port, :username=>@uri.user }.merge(options || {}) - ssh_options[:password] ||= SFTP.passwords[@uri.host] + ssh_options = { :port=>port, :username=>user }.merge(options[:ssh_options] || {}) + ssh_options[:password] ||= SFTP.passwords[host] begin - puts "Connecting to #{@uri.host}" if Rake.application.options.trace - session = Net::SSH.start(@uri.host, ssh_options) - SFTP.passwords[@uri.host] = ssh_options[:password] + puts "Connecting to #{host}" if Rake.application.options.trace + session = Net::SSH.start(host, ssh_options) + SFTP.passwords[host] = ssh_options[:password] rescue Net::SSH::AuthenticationFailed=>ex # Only if running with console, prompt for password. if !ssh_options[:password] && $stdout.isatty - password = HighLine.new.ask("Password for #{@uri.host}:") { |q| q.echo = "*" } + password = HighLine.new.ask("Password for #{host}:") { |q| q.echo = "*" } ssh_options[:password] = password retry end raise end - @sftp = session.sftp.connect - puts "connected" if Rake.application.options.trace - end - def upload(source, path) - File.open(source) do |file| - with_progress_bar path.split("/").last, File.size(source) do |progress| - with_digests(@options[:digests]) do |digester| - target_path = "#{@base_path}#{path}" - puts "Uploading to #{target_path}" if Rake.application.options.trace - @sftp.open_handle(target_path, "w") do |handle| - # Writing in chunks gives us the benefit of a progress bar, - # but also require that we maintain a position in the file, - # since write() with two arguments always writes at position 0. - pos = 0 - while chunk = file.read(32 * 4096) - @sftp.write(handle, chunk, pos) - pos += chunk.size - digester << chunk - progress << chunk - end - end - @sftp.setstat(target_path, :permissions => @permissions) if @permissions + session.sftp.connect do |sftp| + puts "connected" if Rake.application.options.trace - # Upload all the digests. - digester.each do |type, hexdigest| - digest_file = "#{@base_path}#{path}.#{type}" - puts "Uploading signature to #{digest_file}" if Rake.application.options.trace - @sftp.open_handle(digest_file, "w") do |handle| - @sftp.write(handle, "#{hexdigest} #{path}") - end - @sftp.setstat(digest_file, :permissions => @permissions) if @permissions + # To create a path, we need to create all its parent. We use realpath to determine if + # the path already exists, otherwise mkdir fails. + puts "Creating path #{@base_path}" if Rake.application.options.trace + path.split("/").inject("") do |base, part| + combined = base + part + sftp.realpath combined rescue sftp.mkdir combined, {} + "#{combined}/" + end + + with_progress_bar options[:progress] && options[:size], path.split("/"), options[:size] || 0 do |progress| + puts "Uploading to #{path}" if Rake.application.options.trace + sftp.open_handle(path, "w") do |handle| + # Writing in chunks gives us the benefit of a progress bar, + # but also require that we maintain a position in the file, + # since write() with two arguments always writes at position 0. + pos = 0 + while chunk = yield(32 * 4096) + sftp.write(handle, chunk, pos) + pos += chunk.size + progress << chunk end + sftp.setstat(target_path, :permissions => options[:permissions]) if options[:permissions] end - end end + else + raise ArgumentError, "Either give me the content, or pass me a block, otherwise what would I upload?" end + end - def mkpath(path) - # To create a path, we need to create all its parent. - # We use realpath to determine if the path already exists, - # otherwise mkdir fails. - puts "Creating path #{@base_path}" if Rake.application.options.trace - path.split("/").inject(@base_path) do |base, part| - combined = base + part - @sftp.realpath combined rescue @sftp.mkdir combined, {} - "#{combined}/" + end + + + # File URL. Keep in mind that file URLs take the form of <code>file://host/path</code>, although the host + # is not used, so typically all you will see are three backslashes. This methods accept common variants, + # like <code>file:/path</code> but always returns a valid URL. + class FILE < Generic + + COMPONENT = [ :host, :path ].freeze + + def initialize(*args) + super + # file:something (opaque) becomes file:///something + if path.nil? + set_path "/#{opaque}" + unless opaque.nil? + set_opaque nil + warn "#{caller[2]}: We'll accept this URL, but just so you know, it needs three slashes, as in: #{to_s}" end end + # Sadly, file://something really means file://something/ (something being server) + set_path "/" if path.empty? - def close() - @sftp.close - @sftp = nil + # On windows, file://c:/something is not a valid URL, but people do it anyway, so if we see a drive-as-host, + # we'll just be nice enough to fix it. (URI actually strips the colon here) + if host =~ /^[a-zA-Z]$/ + set_path "/#{host}:#{path}" + set_host nil end + end + # See URI::Generic#read + def read(options = nil, &block) + options ||= {} + 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>." unless host.blank? + + path = real_path + # TODO: complain about clunky URLs + raise NotFoundError, "Looking for #{self} and can't find it." unless File.exists?(path) + raise NotFoundError, "Looking for the file #{self}, and it happens to be a directory." if File.directory?(path) + File.open path, "rb" do |input| + with_progress_bar options[:progress], path.split("/").last, input.stat.size do |progress| + block ? block.call(input.read) : input.read + end + end end + # See URI::Generic#write + def write(*args, &block) + options = args.pop if Hash === args.last + options ||= {} + 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>." unless host.blank? + + if String === args.first + ios = StringIO.new(args.first, "r") + write(options.merge(:size=>args.first.size)) { |bytes| ios.read(bytes) } + elsif args.first.respond_to?(:read) + size = args.first.size rescue nil + write({:size=>size}.merge(options)) { |bytes| args.first.read(bytes) } + elsif args.empty? && block + temp = nil + Tempfile.open File.basename(path) do |temp| + temp.binmode + with_progress_bar options[:progress] && options[:size], path.split("/"), options[:size] || 0 do |progress| + while chunk = yield(32 * 4096) + temp.write chunk + progress << chunk + end + end + end + real_path.tap do |path| + mkpath File.dirname(path) + File.move temp.path, path + end + else + raise ArgumentError, "Either give me the content, or pass me a block, otherwise what would I upload?" + end + end + + def to_s() + "file://#{host}#{path}" + end + + # The URL path always starts with a backslash. On most operating systems (Linux, Darwin, BSD) it points + # to the absolute path on the file system. But on Windows, it comes before the drive letter, creating an + # unusable path, so real_path fixes that. Ugly but necessary hack. + def real_path() #:nodoc: + RUBY_PLATFORM =~ /win32/ && path =~ /^\/[a-zA-Z]:\// ? path[1..-1] : path + end + + @@schemes["FILE"] = FILE + end + end