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