net-sftp-3.0.0/0000755000004100000410000000000013672727541013320 5ustar www-datawww-datanet-sftp-3.0.0/.travis.yml0000644000004100000410000000040413672727541015427 0ustar www-datawww-datalanguage: ruby sudo: false rvm: - 2.3.7 - 2.4.5 - 2.5.3 - 2.6.5 - 2.7.1 - ruby-head - jruby-head - rbx-3.107 install: gem install net-ssh test-unit mocha script: rake test matrix: allow_failures: - rvm: jruby-head - rvm: rbx-3.107 net-sftp-3.0.0/net-sftp.gemspec0000644000004100000410000000351713672727541016433 0ustar www-datawww-datarequire_relative 'lib/net/sftp/version' Gem::Specification.new do |spec| spec.name = "net-sftp" spec.version = Net::SFTP::Version::STRING spec.authors = ["Jamis Buck", "Delano Mandelbaum", "Mikl\u{f3}s Fazekas"] spec.email = ["net-ssh@solutious.com"] if ENV['NET_SSH_BUILDGEM_SIGNED'] spec.cert_chain = ["net-sftp-public_cert.pem"] spec.signing_key = "/mnt/gem/net-ssh-private_key.pem" end spec.summary = %q{A pure Ruby implementation of the SFTP client protocol.} spec.description = %q{A pure Ruby implementation of the SFTP client protocol} spec.homepage = "https://github.com/net-ssh/net-sftp" spec.license = "MIT" spec.required_rubygems_version = Gem::Requirement.new(">= 0") if spec.respond_to? :required_rubygems_version= spec.extra_rdoc_files = [ "LICENSE.txt", "README.rdoc" ] spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) } spec.bindir = "exe" spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } spec.require_paths = ["lib"] if spec.respond_to? :specification_version then spec.specification_version = 3 if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then spec.add_runtime_dependency(%q, [">= 5.0.0", "< 7.0.0"]) spec.add_development_dependency(%q, [">= 5"]) spec.add_development_dependency(%q, [">= 0"]) else spec.add_dependency(%q, [">= 5.0.0", "< 7.0.0"]) spec.add_dependency(%q, [">= 5"]) spec.add_dependency(%q, [">= 0"]) end else spec.add_dependency(%q, [">= 5.0.0", "< 7.0.0"]) spec.add_dependency(%q, [">= 5"]) spec.add_dependency(%q, [">= 0"]) spec.add_dependency(%q, [">= 0"]) end end net-sftp-3.0.0/data.tar.gz.sig0000444000004100000410000000040013672727541016131 0ustar www-datawww-dataAsngs;9 )oH ɒM"؉3þrG0QK^Dl˕+tDŒ{wD>þ,m-tVݼн*]69 .}ֳO:26y9Ŀ_OIf3VFa_SD\%vQk6K$VJl:`~Ak\78cP1%gƵIR~o&Jc{Fʾe?&sQ` ;{net-sftp-3.0.0/metadata.gz.sig0000444000004100000410000000040013672727541016213 0ustar www-datawww-data,'}"!z't( )P]l4'ϳ?aެ[W(CBzuYtur ןt`SC]#XR۝ rF䜒bF JL0 J332϶θ32O;ͦ@P& w~~;k:*+!.rY ["build"] CLEAN.include [ 'pkg', 'rdoc' ] name = "net-sftp" require_relative "lib/net/sftp/version" version = Net::SFTP::Version::CURRENT namespace :cert do desc "Update public cert from private - only run if public is expired" task :update_public_when_expired do require 'openssl' require 'time' raw = File.read "net-sftp-public_cert.pem" certificate = OpenSSL::X509::Certificate.new raw raise Exception, "Not yet expired: #{certificate.not_after}" unless certificate.not_after < Time.now sh "gem cert --build netssh@solutious.com --days 365*5 --private-key /mnt/gem/net-ssh-private_key.pem" sh "mv gem-public_cert.pem net-sftp-public_cert.pem" sh "gem cert --add net-sftp-public_cert.pem" end end require 'rake/testtask' Rake::TestTask.new do |t| t.libs = ["lib", "test"] end extra_files = %w[LICENSE.txt THANKS.txt CHANGES.txt ] RDoc::Task.new do |rdoc| rdoc.rdoc_dir = "rdoc" rdoc.title = "#{name} #{version}" rdoc.generator = 'hanna' # gem install hanna-nouveau rdoc.main = 'README.rdoc' rdoc.rdoc_files.include("README*") rdoc.rdoc_files.include("bin/*.rb") rdoc.rdoc_files.include("lib/**/*.rb") extra_files.each { |file| rdoc.rdoc_files.include(file) if File.exist?(file) } end net-sftp-3.0.0/lib/0000755000004100000410000000000013672727541014066 5ustar www-datawww-datanet-sftp-3.0.0/lib/net/0000755000004100000410000000000013672727541014654 5ustar www-datawww-datanet-sftp-3.0.0/lib/net/sftp.rb0000644000004100000410000000541713672727541016164 0ustar www-datawww-datarequire 'net/ssh' require 'net/sftp/session' module Net # Net::SFTP is a pure-Ruby module for programmatically interacting with a # remote host via the SFTP protocol (that's SFTP as in "Secure File Transfer # Protocol" produced by the Secure Shell Working Group, not "Secure FTP" # and certainly not "Simple FTP"). # # See Net::SFTP#start for an introduction to the library. Also, see # Net::SFTP::Session for further documentation. module SFTP # A convenience method for starting a standalone SFTP session. It will # start up an SSH session using the given arguments (see the documentation # for Net::SSH::Session for details), and will then start a new SFTP session # with the SSH session. This will block until the new SFTP is fully open # and initialized before returning it. # # sftp = Net::SFTP.start("localhost", "user") # sftp.upload! "/local/file.tgz", "/remote/file.tgz" # # If a block is given, it will be passed to the SFTP session and will be # called once the SFTP session is fully open and initialized. When the # block terminates, the new SSH session will automatically be closed. # # Net::SFTP.start("localhost", "user") do |sftp| # sftp.upload! "/local/file.tgz", "/remote/file.tgz" # end # # Extra parameters can be passed: # - The Net::SSH connection options (see Net::SSH for more information) # - The Net::SFTP connection options (only :version is supported, to let you # set the SFTP protocol version to be used) def self.start(host, user, ssh_options={}, sftp_options={}, &block) session = Net::SSH.start(host, user, ssh_options) # We only use a single option here, but this leaves room for more later # without breaking the external API. version = sftp_options.fetch(:version, nil) sftp = Net::SFTP::Session.new(session, version, &block).connect! if block_given? sftp.loop session.close return nil end sftp rescue Object => anything begin session.shutdown! rescue ::Exception # swallow exceptions that occur while trying to shutdown end raise anything end end end class Net::SSH::Connection::Session # A convenience method for starting up a new SFTP connection on the current # SSH session. Blocks until the SFTP session is fully open, and then # returns the SFTP session. # # Net::SSH.start("localhost", "user", :password => "password") do |ssh| # ssh.sftp.upload!("/local/file.tgz", "/remote/file.tgz") # ssh.exec! "cd /some/path && tar xf /remote/file.tgz && rm /remote/file.tgz" # end def sftp(wait=true) @sftp ||= begin sftp = Net::SFTP::Session.new(self) sftp.connect! if wait sftp end end end net-sftp-3.0.0/lib/net/sftp/0000755000004100000410000000000013672727541015630 5ustar www-datawww-datanet-sftp-3.0.0/lib/net/sftp/session.rb0000644000004100000410000011475013672727541017650 0ustar www-datawww-datarequire 'net/ssh' require 'net/sftp/constants' require 'net/sftp/errors' require 'net/sftp/protocol' require 'net/sftp/request' require 'net/sftp/operations/dir' require 'net/sftp/operations/upload' require 'net/sftp/operations/download' require 'net/sftp/operations/file_factory' module Net; module SFTP # The Session class encapsulates a single SFTP channel on a Net::SSH # connection. Instances of this class are what most applications will # interact with most, as it provides access to both low-level (mkdir, # rename, remove, symlink, etc.) and high-level (upload, download, etc.) # SFTP operations. # # Although Session makes it easy to do SFTP operations serially, you can # also set up multiple operations to be done in parallel, too, without # needing to resort to threading. You merely need to fire off the requests, # and then run the event loop until all of the requests have completed: # # handle1 = sftp.open!("/path/to/file1") # handle2 = sftp.open!("/path/to/file2") # # r1 = sftp.read(handle1, 0, 1024) # r2 = sftp.read(handle2, 0, 1024) # sftp.loop { [r1, r2].any? { |r| r.pending? } } # # puts "chunk #1: #{r1.response[:data]}" # puts "chunk #2: #{r2.response[:data]}" # # By passing blocks to the operations, you can set up powerful state # machines, to fire off subsequent operations. In fact, the Net::SFTP::Operations::Upload # and Net::SFTP::Operations::Download classes set up such state machines, so that # multiple uploads and/or downloads can be running simultaneously. # # The convention with the names of the operations is as follows: if the method # name ends with an exclamation mark, like #read!, it will be synchronous # (e.g., it will block until the server responds). Methods without an # exclamation mark (e.g. #read) are asynchronous, and return before the # server has responded. You will need to make sure the SSH event loop is # run in order to process these requests. (See #loop.) class Session include Net::SSH::Loggable include Net::SFTP::Constants::PacketTypes # The highest protocol version supported by the Net::SFTP library. HIGHEST_PROTOCOL_VERSION_SUPPORTED = 6 # A reference to the Net::SSH session object that powers this SFTP session. attr_reader :session # The Net::SSH::Connection::Channel object that the SFTP session is being # processed by. attr_reader :channel # The state of the SFTP connection. It will be :opening, :subsystem, :init, # :open, or :closed. attr_reader :state # The protocol instance being used by this SFTP session. Useful for # querying the protocol version in effect. attr_reader :protocol # The hash of pending requests. Any requests that have been sent and which # the server has not yet responded to will be represented here. attr_reader :pending_requests # Creates a new Net::SFTP instance atop the given Net::SSH connection. # This will return immediately, before the SFTP connection has been properly # initialized. Once the connection is ready, the given block will be called. # If you want to block until the connection has been initialized, try this: # # sftp = Net::SFTP::Session.new(ssh) # sftp.loop { sftp.opening? } def initialize(session, version = nil, &block) @session = session @version = version @input = Net::SSH::Buffer.new self.logger = session.logger @state = :closed @pending_requests = {} connect(&block) end public # high-level SFTP operations # Initiates an upload from +local+ to +remote+, asynchronously. This # method will return a new Net::SFTP::Operations::Upload instance, and requires # the event loop to be run in order for the upload to progress. See # Net::SFTP::Operations::Upload for a full discussion of how this method can be # used. # # uploader = sftp.upload("/local/path", "/remote/path") # uploader.wait def upload(local, remote = File.basename(local), options={}, &block) Operations::Upload.new(self, local, remote, options, &block) end # Identical to #upload, but blocks until the upload is complete. def upload!(local, remote = File.basename(local), options={}, &block) upload(local, remote, options, &block).wait end # Initiates a download from +remote+ to +local+, asynchronously. This # method will return a new Net::SFTP::Operations::Download instance, and requires # that the event loop be run in order for the download to progress. See # Net::SFTP::Operations::Download for a full discussion of how this method can be # used. # # download = sftp.download("/remote/path", "/local/path") # download.wait def download(remote, local, options={}, &block) Operations::Download.new(self, local, remote, options, &block) end # Identical to #download, but blocks until the download is complete. # If +local+ is omitted, downloads the file to an in-memory buffer # and returns the result as a string; otherwise, returns the # Net::SFTP::Operations::Download instance. def download!(remote, local=nil, options={}, &block) require 'stringio' unless defined?(StringIO) destination = local || StringIO.new result = download(remote, destination, options, &block).wait local ? result : destination.string end # Returns an Net::SFTP::Operations::FileFactory instance, which can be used to # mimic synchronous, IO-like file operations on a remote file via # SFTP. # # sftp.file.open("/path/to/file") do |file| # while line = file.gets # puts line # end # end # # See Net::SFTP::Operations::FileFactory and Net::SFTP::Operations::File for more details. def file @file ||= Operations::FileFactory.new(self) end # Returns a Net::SFTP::Operations::Dir instance, which can be used to # conveniently iterate over and search directories on the remote server. # # sftp.dir.glob("/base/path", "*/**/*.rb") do |entry| # p entry.name # end # # See Net::SFTP::Operations::Dir for a more detailed discussion of how # to use this. def dir @dir ||= Operations::Dir.new(self) end public # low-level SFTP operations # :call-seq: # open(path, flags="r", options={}) -> request # open(path, flags="r", options={}) { |response| ... } -> request # # Opens a file on the remote server. The +flags+ parameter determines # how the flag is open, and accepts the same format as IO#open (e.g., # either a string like "r" or "w", or a combination of the IO constants). # The +options+ parameter is a hash of attributes to be associated # with the file, and varies greatly depending on the SFTP protocol # version in use, but some (like :permissions) are always available. # # Returns immediately with a Request object. If a block is given, it will # be invoked when the server responds, with a Response object as the only # parameter. The :handle property of the response is the handle of the # opened file, and may be passed to other methods (like #close, #read, # #write, and so forth). # # sftp.open("/path/to/file") do |response| # raise "fail!" unless response.ok? # sftp.close(response[:handle]) # end # sftp.loop def open(path, flags="r", options={}, &callback) request :open, path, flags, options, &callback end # Identical to #open, but blocks until the server responds. It will raise # a StatusException if the request was unsuccessful. Otherwise, it will # return the handle of the newly opened file. # # handle = sftp.open!("/path/to/file") def open!(path, flags="r", options={}, &callback) wait_for(open(path, flags, options, &callback), :handle) end # :call-seq: # close(handle) -> request # close(handle) { |response| ... } -> request # # Closes an open handle, whether obtained via #open, or #opendir. Returns # immediately with a Request object. If a block is given, it will be # invoked when the server responds. # # sftp.open("/path/to/file") do |response| # raise "fail!" unless response.ok? # sftp.close(response[:handle]) # end # sftp.loop def close(handle, &callback) request :close, handle, &callback end # Identical to #close, but blocks until the server responds. It will # raise a StatusException if the request was unsuccessful. Otherwise, # it returns the Response object for this request. # # sftp.close!(handle) def close!(handle, &callback) wait_for(close(handle, &callback)) end # :call-seq: # read(handle, offset, length) -> request # read(handle, offset, length) { |response| ... } -> request # # Requests that +length+ bytes, starting at +offset+ bytes from the # beginning of the file, be read from the file identified by # +handle+. (The +handle+ should be a value obtained via the #open # method.) Returns immediately with a Request object. If a block is # given, it will be invoked when the server responds. # # The :data property of the response will contain the requested data, # assuming the call was successful. # # request = sftp.read(handle, 0, 1024) do |response| # if response.eof? # puts "end of file reached before reading any data" # elsif !response.ok? # puts "error (#{response})" # else # print(response[:data]) # end # end # request.wait # # To read an entire file will usually require multiple calls to #read, # unless you know in advance how large the file is. def read(handle, offset, length, &callback) request :read, handle, offset, length, &callback end # Identical to #read, but blocks until the server responds. It will raise # a StatusException if the request was unsuccessful. If the end of the file # was reached, +nil+ will be returned. Otherwise, it returns the data that # was read, as a String. # # data = sftp.read!(handle, 0, 1024) def read!(handle, offset, length, &callback) wait_for(read(handle, offset, length, &callback), :data) end # :call-seq: # write(handle, offset, data) -> request # write(handle, offset, data) { |response| ... } -> request # # Requests that +data+ be written to the file identified by +handle+, # starting at +offset+ bytes from the start of the file. The file must # have been opened for writing via #open. Returns immediately with a # Request object. If a block is given, it will be invoked when the # server responds. # # request = sftp.write(handle, 0, "hello, world!\n") # request.wait def write(handle, offset, data, &callback) request :write, handle, offset, data, &callback end # Identical to #write, but blocks until the server responds. It will raise # a StatusException if the request was unsuccessful, or the end of the file # was reached. Otherwise, it returns the Response object for this request. # # sftp.write!(handle, 0, "hello, world!\n") def write!(handle, offset, data, &callback) wait_for(write(handle, offset, data, &callback)) end # :call-seq: # lstat(path, flags=nil) -> request # lstat(path, flags=nil) { |response| ... } -> request # # This method is identical to the #stat method, with the exception that # it will not follow symbolic links (thus allowing you to stat the # link itself, rather than what it refers to). The +flags+ parameter # is not used in SFTP protocol versions prior to 4, and will be ignored # in those versions of the protocol that do not use it. For those that # do, however, you may provide hints as to which file proprties you wish # to query (e.g., if all you want is permissions, you could pass the # Net::SFTP::Protocol::V04::Attributes::F_PERMISSIONS flag as the value # for the +flags+ parameter). # # The method returns immediately with a Request object. If a block is given, # it will be invoked when the server responds. The :attrs property of # the response will contain an Attributes instance appropriate for the # the protocol version (see Protocol::V01::Attributes, Protocol::V04::Attributes, # and Protocol::V06::Attributes). # # request = sftp.lstat("/path/to/file") do |response| # raise "fail!" unless response.ok? # puts "permissions: %04o" % response[:attrs].permissions # end # request.wait def lstat(path, flags=nil, &callback) request :lstat, path, flags, &callback end # Identical to the #lstat method, but blocks until the server responds. # It will raise a StatusException if the request was unsuccessful. # Otherwise, it will return the attribute object describing the path. # # puts sftp.lstat!("/path/to/file").permissions def lstat!(path, flags=nil, &callback) wait_for(lstat(path, flags, &callback), :attrs) end # The fstat method is identical to the #stat and #lstat methods, with # the exception that it takes a +handle+ as the first parameter, such # as would be obtained via the #open or #opendir methods. (See the #lstat # method for full documentation). def fstat(handle, flags=nil, &callback) request :fstat, handle, flags, &callback end # Identical to the #fstat method, but blocks until the server responds. # It will raise a StatusException if the request was unsuccessful. # Otherwise, it will return the attribute object describing the path. # # puts sftp.fstat!(handle).permissions def fstat!(handle, flags=nil, &callback) wait_for(fstat(handle, flags, &callback), :attrs) end # :call-seq: # setstat(path, attrs) -> request # setstat(path, attrs) { |response| ... } -> request # # This method may be used to set file metadata (such as permissions, or # user/group information) on a remote file. The exact metadata that may # be tweaked is dependent on the SFTP protocol version in use, but in # general you may set at least the permissions, user, and group. (See # Protocol::V01::Attributes, Protocol::V04::Attributes, and Protocol::V06::Attributes # for the full lists of attributes that may be set for the different # protocols.) # # The +attrs+ parameter is a hash, where the keys are symbols identifying # the attributes to set. # # The method returns immediately with a Request object. If a block is given, # it will be invoked when the server responds. # # request = sftp.setstat("/path/to/file", :permissions => 0644) # request.wait # puts "success: #{request.response.ok?}" def setstat(path, attrs, &callback) request :setstat, path, attrs, &callback end # Identical to the #setstat method, but blocks until the server responds. # It will raise a StatusException if the request was unsuccessful. # Otherwise, it will return the Response object for the request. # # sftp.setstat!("/path/to/file", :permissions => 0644) def setstat!(path, attrs, &callback) wait_for(setstat(path, attrs, &callback)) end # The fsetstat method is identical to the #setstat method, with the # exception that it takes a +handle+ as the first parameter, such as # would be obtained via the #open or #opendir methods. (See the # #setstat method for full documentation.) def fsetstat(handle, attrs, &callback) request :fsetstat, handle, attrs, &callback end # Identical to the #fsetstat method, but blocks until the server responds. # It will raise a StatusException if the request was unsuccessful. # Otherwise, it will return the Response object for the request. # # sftp.fsetstat!(handle, :permissions => 0644) def fsetstat!(handle, attrs, &callback) wait_for(fsetstat(handle, attrs, &callback)) end # :call-seq: # opendir(path) -> request # opendir(path) { |response| ... } -> request # # Attempts to open a directory on the remote host for reading. Once the # handle is obtained, directory entries may be retrieved using the # #readdir method. The method returns immediately with a Request object. # If a block is given, it will be invoked when the server responds. # # sftp.opendir("/path/to/directory") do |response| # raise "fail!" unless response.ok? # sftp.close(response[:handle]) # end # sftp.loop def opendir(path, &callback) request :opendir, path, &callback end # Identical to #opendir, but blocks until the server responds. It will raise # a StatusException if the request was unsuccessful. Otherwise, it will # return a handle to the given path. # # handle = sftp.opendir!("/path/to/directory") def opendir!(path, &callback) wait_for(opendir(path, &callback), :handle) end # :call-seq: # readdir(handle) -> request # raeddir(handle) { |response| ... } -> request # # Reads a set of entries from the given directory handle (which must # have been obtained via #opendir). If the response is EOF, then there # are no more entries in the directory. Otherwise, the entries will be # in the :names property of the response: # # loop do # request = sftp.readdir(handle).wait # break if request.response.eof? # raise "fail!" unless request.response.ok? # request.response[:names].each do |entry| # puts entry.name # end # end # # See also Protocol::V01::Name and Protocol::V04::Name for the specific # properties of each individual entry (which vary based on the SFTP # protocol version in use). def readdir(handle, &callback) request :readdir, handle, &callback end # Identical to #readdir, but blocks until the server responds. It will raise # a StatusException if the request was unsuccessful. Otherwise, it will # return nil if there were no more names to read, or an array of name # entries. # # while (entries = sftp.readdir!(handle)) do # entries.each { |entry| puts(entry.name) } # end def readdir!(handle, &callback) wait_for(readdir(handle, &callback), :names) end # :call-seq: # remove(filename) -> request # remove(filename) { |response| ... } -> request # # Attempts to remove the given file from the remote file system. Returns # immediately with a Request object. If a block is given, the block will # be invoked when the server responds, and will be passed a Response # object. # # sftp.remove("/path/to/file").wait def remove(filename, &callback) request :remove, filename, &callback end # Identical to #remove, but blocks until the server responds. It will raise # a StatusException if the request was unsuccessful. Otherwise, it will # return the Response object for the request. # # sftp.remove!("/path/to/file") def remove!(filename, &callback) wait_for(remove(filename, &callback)) end # :call-seq: # mkdir(path, attrs={}) -> request # mkdir(path, attrs={}) { |response| ... } -> request # # Creates the named directory on the remote server. If an attribute hash # is given, it must map to the set of attributes supported by the version # of the SFTP protocol in use. (See Protocol::V01::Attributes, # Protocol::V04::Attributes, and Protocol::V06::Attributes.) # # sftp.mkdir("/path/to/directory", :permissions => 0550).wait def mkdir(path, attrs={}, &callback) request :mkdir, path, attrs, &callback end # Identical to #mkdir, but blocks until the server responds. It will raise # a StatusException if the request was unsuccessful. Otherwise, it will # return the Response object for the request. # # sftp.mkdir!("/path/to/directory", :permissions => 0550) def mkdir!(path, attrs={}, &callback) wait_for(mkdir(path, attrs, &callback)) end # :call-seq: # rmdir(path) -> request # rmdir(path) { |response| ... } -> request # # Removes the named directory on the remote server. The directory must # be empty before it can be removed. # # sftp.rmdir("/path/to/directory").wait def rmdir(path, &callback) request :rmdir, path, &callback end # Identical to #rmdir, but blocks until the server responds. It will raise # a StatusException if the request was unsuccessful. Otherwise, it will # return the Response object for the request. # # sftp.rmdir!("/path/to/directory") def rmdir!(path, &callback) wait_for(rmdir(path, &callback)) end # :call-seq: # realpath(path) -> request # realpath(path) { |response| ... } -> request # # Tries to canonicalize the given path, turning any given path into an # absolute path. This is primarily useful for converting a path with # ".." or "." segments into an identical path without those segments. # The answer will be in the response's :names attribute, as a # one-element array. # # request = sftp.realpath("/path/../to/../directory").wait # puts request[:names].first.name def realpath(path, &callback) request :realpath, path, &callback end # Identical to #realpath, but blocks until the server responds. It will raise # a StatusException if the request was unsuccessful. Otherwise, it will # return a name object identifying the path. # # puts(sftp.realpath!("/path/../to/../directory")) def realpath!(path, &callback) wait_for(realpath(path, &callback), :names).first end # Identical to the #lstat method, except that it follows symlinks # (e.g., if you give it the path to a symlink, it will stat the target # of the symlink rather than the symlink itself). See the #lstat method # for full documentation. def stat(path, flags=nil, &callback) request :stat, path, flags, &callback end # Identical to #stat, but blocks until the server responds. It will raise # a StatusException if the request was unsuccessful. Otherwise, it will # return an attribute object for the named path. # # attrs = sftp.stat!("/path/to/file") def stat!(path, flags=nil, &callback) wait_for(stat(path, flags, &callback), :attrs) end # :call-seq: # rename(name, new_name, flags=nil) -> request # rename(name, new_name, flags=nil) { |response| ... } -> request # # Renames the given file. This operation is only available in SFTP # protocol versions two and higher. The +flags+ parameter is ignored # in versions prior to 5. In versions 5 and higher, the +flags+ # parameter can be used to specify how the rename should be performed # (atomically, etc.). # # The following flags are defined in protocol version 5: # # * 0x0001 - overwrite an existing file if the new name specifies a file # that already exists. # * 0x0002 - perform the rewrite atomically. # * 0x0004 - allow the server to perform the rename as it prefers. def rename(name, new_name, flags=nil, &callback) request :rename, name, new_name, flags, &callback end # Identical to #rename, but blocks until the server responds. It will raise # a StatusException if the request was unsuccessful. Otherwise, it will # return the Response object for the request. # # sftp.rename!("/path/to/old", "/path/to/new") def rename!(name, new_name, flags=nil, &callback) wait_for(rename(name, new_name, flags, &callback)) end # :call-seq: # readlink(path) -> request # readlink(path) { |response| ... } -> request # # Queries the server for the target of the specified symbolic link. # This operation is only available in protocol versions 3 and higher. # The response to this request will include a names property, a one-element # array naming the target of the symlink. # # request = sftp.readlink("/path/to/symlink").wait # puts request.response[:names].first.name def readlink(path, &callback) request :readlink, path, &callback end # Identical to #readlink, but blocks until the server responds. It will raise # a StatusException if the request was unsuccessful. Otherwise, it will # return the Name object for the path that the symlink targets. # # item = sftp.readlink!("/path/to/symlink") def readlink!(path, &callback) wait_for(readlink(path, &callback), :names).first end # :call-seq: # symlink(path, target) -> request # symlink(path, target) { |response| ... } -> request # # Attempts to create a symlink to +path+ at +target+. This operation # is only available in protocol versions 3, 4, and 5, but the Net::SFTP # library mimics the symlink behavior in protocol version 6 using the # #link method, so it is safe to use this method in protocol version 6. # # sftp.symlink("/path/to/file", "/path/to/symlink").wait def symlink(path, target, &callback) request :symlink, path, target, &callback end # Identical to #symlink, but blocks until the server responds. It will raise # a StatusException if the request was unsuccessful. Otherwise, it will # return the Response object for the request. # # sftp.symlink!("/path/to/file", "/path/to/symlink") def symlink!(path, target, &callback) wait_for(symlink(path, target, &callback)) end # :call-seq: # link(new_link_path, existing_path, symlink=true) -> request # link(new_link_path, existing_path, symlink=true) { |response| ... } -> request # # Attempts to create a link, either hard or symbolic. This operation is # only available in SFTP protocol versions 6 and higher. If the +symlink+ # paramter is true, a symbolic link will be created, otherwise a hard # link will be created. The link will be named +new_link_path+, and will # point to the path +existing_path+. # # sftp.link("/path/to/symlink", "/path/to/file", true).wait # # Note that #link is only available for SFTP protocol 6 and higher. You # can use #symlink for protocols 3 and higher. def link(new_link_path, existing_path, symlink=true, &callback) request :link, new_link_path, existing_path, symlink, &callback end # Identical to #link, but blocks until the server responds. It will raise # a StatusException if the request was unsuccessful. Otherwise, it will # return the Response object for the request. # # sftp.link!("/path/to/symlink", "/path/to/file", true) def link!(new_link_path, existing_path, symlink=true, &callback) wait_for(link(new_link_path, existing_path, symlink, &callback)) end # :call-seq: # block(handle, offset, length, mask) -> request # block(handle, offset, length, mask) { |response| ... } -> request # # Creates a byte-range lock on the file specified by the given +handle+. # This operation is only available in SFTP protocol versions 6 and # higher. The lock may be either mandatory or advisory. # # The +handle+ parameter is a file handle, as obtained by the #open method. # # The +offset+ and +length+ parameters describe the location and size of # the byte range. # # The +mask+ describes how the lock should be defined, and consists of # some combination of the following bit masks: # # * 0x0040 - Read lock. The byte range may not be accessed for reading # by via any other handle, though it may be written to. # * 0x0080 - Write lock. The byte range may not be written to via any # other handle, though it may be read from. # * 0x0100 - Delete lock. No other handle may delete this file. # * 0x0200 - Advisory lock. The server need not honor the lock instruction. # # Once created, the lock may be removed via the #unblock method. def block(handle, offset, length, mask, &callback) request :block, handle, offset, length, mask, &callback end # Identical to #block, but blocks until the server responds. It will raise # a StatusException if the request was unsuccessful. Otherwise, it will # return the Response object for the request. def block!(handle, offset, length, mask, &callback) wait_for(block(handle, offset, length, mask, &callback)) end # :call-seq: # unblock(handle, offset, length) -> request # unblock(handle, offset, length) { |response| ... } -> request # # Removes a previously created byte-range lock. This operation is only # available in protocol versions 6 and higher. The +offset+ and +length+ # parameters must exactly match those that were given to #block when the # lock was acquired. def unblock(handle, offset, length, &callback) request :unblock, handle, offset, length, &callback end # Identical to #unblock, but blocks until the server responds. It will raise # a StatusException if the request was unsuccessful. Otherwise, it will # return the Response object for the request. def unblock!(handle, offset, length, &callback) wait_for(unblock(handle, offset, length, &callback)) end public # miscellaneous methods # Closes the SFTP connection, but not the SSH connection. Blocks until the # session has terminated. Once the session has terminated, further operations # on this object will result in errors. You can reopen the SFTP session # via the #connect method. def close_channel return unless open? channel.close loop { !closed? } end # Returns true if the connection has been initialized. def open? state == :open end # Returns true if the connection has been closed. def closed? state == :closed end # Returns true if the connection is in the process of being initialized # (e.g., it is not closed, but is not yet fully open). def opening? !(open? || closed?) end # Attempts to establish an SFTP connection over the SSH session given when # this object was instantiated. If the object is already open, this will # simply execute the given block (if any), passing the SFTP session itself # as argument. If the session is currently being opened, this will add # the given block to the list of callbacks, to be executed when the session # is fully open. # # This method does not block, and will return immediately. If you pass a # block to it, that block will be invoked when the connection has been # fully established. Thus, you can do something like this: # # sftp.connect do # puts "open!" # end # # If you just want to block until the connection is ready, see the #connect! # method. def connect(&block) case state when :open block.call(self) if block when :closed @state = :opening @channel = session.open_channel(&method(:when_channel_confirmed)) @packet_length = nil @protocol = nil @on_ready = Array(block) else # opening @on_ready << block if block end self end # Same as the #connect method, but blocks until the SFTP connection has # been fully initialized. def connect!(&block) connect(&block) loop { opening? } self end alias :loop_forever :loop # Runs the SSH event loop while the given block returns true. This lets # you set up a state machine and then "fire it off". If you do not specify # a block, the event loop will run for as long as there are any pending # SFTP requests. This makes it easy to do thing like this: # # sftp.remove("/path/to/file") # sftp.loop def loop(&block) block ||= Proc.new { pending_requests.any? } session.loop(&block) end # Formats, constructs, and sends an SFTP packet of the given type and with # the given data. This does not block, but merely enqueues the packet for # sending and returns. # # You should probably use the operation methods, rather than building and # sending the packet directly. (See #open, #close, etc.) def send_packet(type, *args) data = Net::SSH::Buffer.from(*args) msg = Net::SSH::Buffer.from(:long, data.length+1, :byte, type, :raw, data) channel.send_data(msg.to_s) end private #-- # "ruby -w" hates private attributes, so we have to do this longhand #++ # The input buffer used to accumulate packet data def input; @input; end # Create and enqueue a new SFTP request of the given type, with the # given arguments. Returns a new Request instance that encapsulates the # request. def request(type, *args, &callback) request = Request.new(self, type, protocol.send(type, *args), &callback) info { "sending #{type} packet (#{request.id})" } pending_requests[request.id] = request end # Waits for the given request to complete. If the response is # EOF, nil is returned. If the response was not successful # (e.g., !response.ok?), a StatusException will be raised. # If +property+ is given, the corresponding property from the response # will be returned; otherwise, the response object itself will be # returned. def wait_for(request, property=nil) request.wait if request.response.eof? nil elsif !request.response.ok? raise StatusException.new(request.response) elsif property request.response[property.to_sym] else request.response end end # Called when the SSH channel is confirmed as "open" by the server. # This is one of the states of the SFTP state machine, and is followed # by the #when_subsystem_started state. def when_channel_confirmed(channel) debug { "requesting sftp subsystem" } @state = :subsystem channel.subsystem("sftp", &method(:when_subsystem_started)) end # Called when the SSH server confirms that the SFTP subsystem was # successfully started. This sets up the appropriate callbacks on the # SSH channel and then starts the SFTP protocol version negotiation # process. def when_subsystem_started(channel, success) raise Net::SFTP::Exception, "could not start SFTP subsystem" unless success debug { "sftp subsystem successfully started" } @state = :init channel.on_data { |c,data| input.append(data) } channel.on_extended_data { |c,t,data| debug { data } } channel.on_close(&method(:when_channel_closed)) channel.on_process(&method(:when_channel_polled)) send_packet(FXP_INIT, :long, @version || HIGHEST_PROTOCOL_VERSION_SUPPORTED) end # Called when the SSH server closes the underlying channel. def when_channel_closed(channel) debug { "sftp channel closed" } @channel = nil @state = :closed end # Called whenever Net::SSH polls the SFTP channel for pending activity. # This basically checks the input buffer to see if enough input has been # accumulated to handle. If there has, the packet is parsed and # dispatched, according to its type (see #do_version and #dispatch_request). def when_channel_polled(channel) while input.length > 0 if @packet_length.nil? # make sure we've read enough data to tell how long the packet is return unless input.length >= 4 @packet_length = input.read_long end return unless input.length >= @packet_length + 4 packet = Net::SFTP::Packet.new(input.read(@packet_length)) input.consume! @packet_length = nil debug { "received sftp packet #{packet.type} len #{packet.length}" } if packet.type == FXP_VERSION do_version(packet) else dispatch_request(packet) end end end # Called to handle FXP_VERSION packets. This performs the SFTP protocol # version negotiation, instantiating the appropriate Protocol instance # and invoking the callback given to #connect, if any. def do_version(packet) debug { "negotiating sftp protocol version, mine is #{HIGHEST_PROTOCOL_VERSION_SUPPORTED}" } server_version = packet.read_long debug { "server reports sftp version #{server_version}" } negotiated_version = [server_version, HIGHEST_PROTOCOL_VERSION_SUPPORTED].min info { "negotiated version is #{negotiated_version}" } extensions = {} until packet.eof? name = packet.read_string data = packet.read_string extensions[name] = data end @protocol = Protocol.load(self, negotiated_version) @pending_requests = {} @state = :open @on_ready.each { |callback| callback.call(self) } @on_ready = nil end # Parses the packet, finds the associated Request instance, and tells # the Request instance to respond to the packet (see Request#respond_to). def dispatch_request(packet) id = packet.read_long request = pending_requests.delete(id) or raise Net::SFTP::Exception, "no such request `#{id}'" request.respond_to(packet) end end end; end net-sftp-3.0.0/lib/net/sftp/version.rb0000644000004100000410000000421713672727541017646 0ustar www-datawww-datamodule Net module SFTP # A class for describing the current version of a library. The version # consists of three parts: the +major+ number, the +minor+ number, and the # +tiny+ (or +patch+) number. # # Two Version instances may be compared, so that you can test that a version # of a library is what you require: # # require 'net/sftp/version' # # if Net::SFTP::Version::CURRENT < Net::SFTP::Version[2,1,0] # abort "your software is too old!" # end class Version include Comparable # A convenience method for instantiating a new Version instance with the # given +major+, +minor+, and +tiny+ components. def self.[](major, minor, tiny, pre = nil) new(major, minor, tiny, pre) end attr_reader :major, :minor, :tiny # Create a new Version object with the given components. def initialize(major, minor, tiny, pre = nil) @major, @minor, @tiny, @pre = major, minor, tiny, pre end # Compare this version to the given +version+ object. def <=>(version) to_i <=> version.to_i end # Converts this version object to a string, where each of the three # version components are joined by the '.' character. E.g., 2.0.0. def to_s @to_s ||= [@major, @minor, @tiny, @pre].compact.join(".") end # Converts this version to a canonical integer that may be compared # against other version objects. def to_i @to_i ||= @major * 1_000_000 + @minor * 1_000 + @tiny end # The major component of this version of the Net::SFTP library MAJOR = 3 # The minor component of this version of the Net::SFTP library MINOR = 0 # The tiny component of this version of the Net::SFTP library TINY = 0 # The prerelease component of this version of the Net::SFTP library # nil allowed PRE = nil # The current version of the Net::SFTP library as a Version instance CURRENT = new(*[MAJOR, MINOR, TINY, PRE].compact) # The current version of the Net::SFTP library as a String STRING = CURRENT.to_s end end end net-sftp-3.0.0/lib/net/sftp/protocol.rb0000644000004100000410000000201113672727541020010 0ustar www-datawww-datarequire 'net/sftp/protocol/01/base' require 'net/sftp/protocol/02/base' require 'net/sftp/protocol/03/base' require 'net/sftp/protocol/04/base' require 'net/sftp/protocol/05/base' require 'net/sftp/protocol/06/base' module Net; module SFTP # The Protocol module contains the definitions for all supported SFTP # protocol versions. module Protocol # Instantiates and returns a new protocol driver instance for the given # protocol version. +session+ must be a valid SFTP session object, and # +version+ must be an integer. If an unsupported version is given, # an exception will be raised. def self.load(session, version) case version when 1 then V01::Base.new(session) when 2 then V02::Base.new(session) when 3 then V03::Base.new(session) when 4 then V04::Base.new(session) when 5 then V05::Base.new(session) when 6 then V06::Base.new(session) else raise NotImplementedError, "unsupported SFTP version #{version.inspect}" end end end end; endnet-sftp-3.0.0/lib/net/sftp/errors.rb0000644000004100000410000000206613672727541017475 0ustar www-datawww-datamodule Net; module SFTP # The base exception class for the SFTP system. class Exception < RuntimeError; end # A exception class for reporting a non-success result of an operation. class StatusException < Net::SFTP::Exception # The response object that caused the exception. attr_reader :response # The error code (numeric) attr_reader :code # The description of the error attr_reader :description # Any incident-specific text given when the exception was raised attr_reader :text # Create a new status exception that reports the given code and # description. def initialize(response, text=nil) @response, @text = response, text @code = response.code @description = response.message @description = Response::MAP[@code] if @description.nil? || @description.empty? end # Override the default message format, to include the code and # description. def message m = super.dup m << " #{text}" if text m << " (#{code}, #{description.inspect})" end end end; end net-sftp-3.0.0/lib/net/sftp/request.rb0000644000004100000410000000610313672727541017645 0ustar www-datawww-datarequire 'net/sftp/constants' require 'net/sftp/response' module Net; module SFTP # Encapsulates a single active SFTP request. This is instantiated # automatically by the Net::SFTP::Session class when an operation is # executed. # # request = sftp.open("/path/to/file") # puts request.pending? #-> true # request.wait # puts request.pending? #-> false # result = request.response class Request include Constants::PacketTypes # The Net::SFTP session object that is servicing this request attr_reader :session # The SFTP packet identifier for this request attr_reader :id # The type of this request (e.g., :open, :symlink, etc.) attr_reader :type # The callback (if any) associated with this request. When the response # is recieved for this request, the callback will be invoked. attr_reader :callback # The hash of properties associated with this request. Properties allow # programmers to associate arbitrary data with a request, making state # machines richer. attr_reader :properties # The response that was received for this request (see Net::SFTP::Response) attr_reader :response # Instantiate a new Request object, serviced by the given +session+, and # being of the given +type+. The +id+ is the packet identifier for this # request. def initialize(session, type, id, &callback) #:nodoc: @session, @id, @type, @callback = session, id, type, callback @response = nil @properties = {} end # Returns the value of property with the given +key+. If +key+ is not a # symbol, it will be converted to a symbol before lookup. def [](key) properties[key.to_sym] end # Sets the value of the property with name +key+ to +value+. If +key+ is # not a symbol, it will be converted to a symbol before lookup. def []=(key, value) properties[key.to_sym] = value end # Returns +true+ if the request is still waiting for a response from the # server, and +false+ otherwise. The SSH event loop must be run in order # for a request to be processed; see #wait. def pending? session.pending_requests.key?(id) end # Waits (blocks) until the server responds to this packet. If prior # SFTP packets were also pending, they will be processed as well (since # SFTP packets are processed in the order in which they are received by # the server). Returns the request object itself. def wait session.loop { pending? } self end public # but not "published". Internal use only # When the server responds to this request, the packet is passed to # this method, which parses the packet and builds a Net::SFTP::Response # object to encapsulate it. If a #callback has been provided for this # request, the callback is invoked with the new response object. def respond_to(packet) #:nodoc: data = session.protocol.parse(packet) data[:type] = packet.type @response = Response.new(self, data) callback.call(@response) if callback end end end; endnet-sftp-3.0.0/lib/net/sftp/constants.rb0000644000004100000410000001456313672727541020202 0ustar www-datawww-datamodule Net module SFTP # The packet types and other general constants used by the SFTP protocol. # See the specification for the SFTP protocol for a full discussion of their # meaning and usage. module Constants # The various packet types supported by SFTP protocol versions 1 through 6. # The FXP_EXTENDED and FXP_EXTENDED_REPLY packet types are not currently # understood by Net::SFTP. module PacketTypes FXP_INIT = 1 FXP_VERSION = 2 FXP_OPEN = 3 FXP_CLOSE = 4 FXP_READ = 5 FXP_WRITE = 6 FXP_LSTAT = 7 FXP_FSTAT = 8 FXP_SETSTAT = 9 FXP_FSETSTAT = 10 FXP_OPENDIR = 11 FXP_READDIR = 12 FXP_REMOVE = 13 FXP_MKDIR = 14 FXP_RMDIR = 15 FXP_REALPATH = 16 FXP_STAT = 17 FXP_RENAME = 18 FXP_READLINK = 19 FXP_SYMLINK = 20 FXP_LINK = 21 FXP_BLOCK = 22 FXP_UNBLOCK = 23 FXP_STATUS = 101 FXP_HANDLE = 102 FXP_DATA = 103 FXP_NAME = 104 FXP_ATTRS = 105 FXP_EXTENDED = 200 FXP_EXTENDED_REPLY = 201 end # Beginning in version 5 of the protocol, Net::SFTP::Session#rename accepts # an optional +flags+ argument that must be either 0 or a combination of # these constants. module RenameFlags OVERWRITE = 0x00000001 ATOMIC = 0x00000002 NATIVE = 0x00000004 end # When an FXP_STATUS packet is received from the server, the +code+ will # be one of the following constants. module StatusCodes FX_OK = 0 FX_EOF = 1 FX_NO_SUCH_FILE = 2 FX_PERMISSION_DENIED = 3 FX_FAILURE = 4 FX_BAD_MESSAGE = 5 FX_NO_CONNECTION = 6 FX_CONNECTION_LOST = 7 FX_OP_UNSUPPORTED = 8 FX_INVALID_HANDLE = 9 FX_NO_SUCH_PATH = 10 FX_FILE_ALREADY_EXISTS = 11 FX_WRITE_PROTECT = 12 FX_NO_MEDIA = 13 FX_NO_SPACE_ON_FILESYSTEM = 14 FX_QUOTA_EXCEEDED = 15 FX_UNKNOWN_PRINCIPLE = 16 FX_LOCK_CONFlICT = 17 FX_DIR_NOT_EMPTY = 18 FX_NOT_A_DIRECTORY = 19 FX_INVALID_FILENAME = 20 FX_LINK_LOOP = 21 end # The Net::SFTP::Session#open operation is one of the worst casualties of # the revisions between SFTP protocol versions. The flags change considerably # between version 1 and version 6. Net::SFTP tries to shield programmers # from the differences, so you'll almost never need to use these flags # directly, but if you ever need to specify some flag that isn't exposed # by the higher-level API, these are the ones that are available to you. module OpenFlags # These are the flags that are understood by versions 1-4 of the the # open operation. module FV1 READ = 0x00000001 WRITE = 0x00000002 APPEND = 0x00000004 CREAT = 0x00000008 TRUNC = 0x00000010 EXCL = 0x00000020 end # Version 5 of the open operation totally discarded the flags understood # by versions 1-4, and replaced them with these. module FV5 CREATE_NEW = 0x00000000 CREATE_TRUNCATE = 0x00000001 OPEN_EXISTING = 0x00000002 OPEN_OR_CREATE = 0x00000003 TRUNCATE_EXISTING = 0x00000004 APPEND_DATA = 0x00000008 APPEND_DATA_ATOMIC = 0x00000010 TEXT_MODE = 0x00000020 READ_LOCK = 0x00000040 WRITE_LOCK = 0x00000080 DELETE_LOCK = 0x00000100 end # Version 6 of the open operation added these flags, in addition to the # flags understood by version 5. module FV6 ADVISORY_LOCK = 0x00000200 NOFOLLOW = 0x00000400 DELETE_ON_CLOSE = 0x00000800 ACCESS_AUDIT_ALARM_INFO = 0x00001000 ACCESS_BACKUP = 0x00002000 BACKUP_STREAM = 0x00004000 OVERRIDE_OWNER = 0x00008000 end end # The Net::SFTP::Session#block operation, implemented in version 6 of # the protocol, understands these constants for the +mask+ parameter. module LockTypes READ = OpenFlags::FV5::READ_LOCK WRITE = OpenFlags::FV5::WRITE_LOCK DELETE = OpenFlags::FV5::DELETE_LOCK ADVISORY = OpenFlags::FV6::ADVISORY_LOCK end module ACE # Access control entry types, used from version 4 of the protocol, # onward. See Net::SFTP::Protocol::V04::Attributes::ACL. module Type ACCESS_ALLOWED = 0x00000000 ACCESS_DENIED = 0x00000001 SYSTEM_AUDIT = 0x00000002 SYSTEM_ALARM = 0x00000003 end # Access control entry flags, used from version 4 of the protocol, # onward. See Net::SFTP::Protocol::V04::Attributes::ACL. module Flag FILE_INHERIT = 0x00000001 DIRECTORY_INHERIT = 0x00000002 NO_PROPAGATE_INHERIT = 0x00000004 INHERIT_ONLY = 0x00000008 SUCCESSFUL_ACCESS = 0x00000010 FAILED_ACCESS = 0x00000020 IDENTIFIER_GROUP = 0x00000040 end # Access control entry masks, used from version 4 of the protocol, # onward. See Net::SFTP::Protocol::V04::Attributes::ACL. module Mask READ_DATA = 0x00000001 LIST_DIRECTORY = 0x00000001 WRITE_DATA = 0x00000002 ADD_FILE = 0x00000002 APPEND_DATA = 0x00000004 ADD_SUBDIRECTORY = 0x00000004 READ_NAMED_ATTRS = 0x00000008 WRITE_NAMED_ATTRS = 0x00000010 EXECUTE = 0x00000020 DELETE_CHILD = 0x00000040 READ_ATTRIBUTES = 0x00000080 WRITE_ATTRIBUTES = 0x00000100 DELETE = 0x00010000 READ_ACL = 0x00020000 WRITE_ACL = 0x00040000 WRITE_OWNER = 0x00080000 SYNCHRONIZE = 0x00100000 end end end end end net-sftp-3.0.0/lib/net/sftp/operations/0000755000004100000410000000000013672727541020013 5ustar www-datawww-datanet-sftp-3.0.0/lib/net/sftp/operations/dir.rb0000644000004100000410000000567213672727541021130 0ustar www-datawww-datarequire 'net/ssh/loggable' module Net; module SFTP; module Operations # A convenience class for working with remote directories. It provides methods # for searching and enumerating directory entries, similarly to the standard # ::Dir class. # # sftp.dir.foreach("/remote/path") do |entry| # puts entry.name # end # # p sftp.dir.entries("/remote/path").map { |e| e.name } # # sftp.dir.glob("/remote/path", "**/*.rb") do |entry| # puts entry.name # end class Dir # The SFTP session object that drives this directory factory. attr_reader :sftp # Create a new instance on top of the given SFTP session instance. def initialize(sftp) @sftp = sftp end # Calls the block once for each entry in the named directory on the # remote server. Yields a Name object to the block, rather than merely # the name of the entry. def foreach(path) handle = sftp.opendir!(path) while entries = sftp.readdir!(handle) entries.each { |entry| yield entry } end return nil ensure sftp.close!(handle) if handle end # Returns an array of Name objects representing the items in the given # remote directory, +path+. def entries(path) results = [] foreach(path) { |entry| results << entry } return results end # Works as ::Dir.glob, matching (possibly recursively) all directory # entries under +path+ against +pattern+. If a block is given, matches # will be yielded to the block as they are found; otherwise, they will # be returned in an array when the method finishes. # # Because working over an SFTP connection is always going to be slower than # working purely locally, don't expect this method to perform with the # same level of alacrity that ::Dir.glob does; it will work best for # shallow directory hierarchies with relatively few directories, though # it should be able to handle modest numbers of files in each directory. def glob(path, pattern, flags=0) flags |= ::File::FNM_PATHNAME path = path.chop if path.end_with?('/') && path != '/' results = [] unless block_given? queue = entries(path).reject { |e| %w(. ..).include?(e.name) } while queue.any? entry = queue.shift if entry.directory? && !%w(. ..).include?(::File.basename(entry.name)) queue += entries("#{path}/#{entry.name}").map do |e| e.name.replace("#{entry.name}/#{e.name}") e end end if ::File.fnmatch(pattern, entry.name, flags) if block_given? yield entry else results << entry end end end return results unless block_given? end # Identical to calling #glob with a +flags+ parameter of 0 and no block. # Simply returns the matched entries as an array. def [](path, pattern) glob(path, pattern, 0) end end end; end; end net-sftp-3.0.0/lib/net/sftp/operations/file_factory.rb0000644000004100000410000000376413672727541023020 0ustar www-datawww-datarequire 'net/ssh/loggable' require 'net/sftp/operations/file' module Net; module SFTP; module Operations # A factory class for opening files and returning Operations::File instances # that wrap the SFTP handles that represent them. This is a convenience # class for use when working with files synchronously. Rather than relying # on the programmer to provide callbacks that define a state machine that # describes the behavior of the program, this class (and Operations::File) # provide an interface where calls will block until they return, mimicking # the IO class' interface. class FileFactory # The SFTP session object that drives this file factory. attr_reader :sftp # Create a new instance on top of the given SFTP session instance. def initialize(sftp) @sftp = sftp end # :call-seq: # open(name, flags="r", mode=nil) -> file # open(name, flags="r", mode=nil) { |file| ... } # # Attempt to open a file on the remote server. The +flags+ parameter # accepts the same values as the standard Ruby ::File#open method. The # +mode+ parameter must be an integer describing the permissions to use # if a new file is being created. # # If a block is given, the new Operations::File instance will be yielded # to it, and closed automatically when the block terminates. Otherwise # the object will be returned, and it is the caller's responsibility to # close the file. # # sftp.file.open("/tmp/names.txt", "w") do |f| # # ... # end def open(name, flags="r", mode=nil, &block) handle = sftp.open!(name, flags, :permissions => mode) file = Operations::File.new(sftp, handle) if block_given? begin yield file ensure file.close end else return file end end # Returns +true+ if the argument refers to a directory on the remote host. def directory?(path) sftp.lstat!(path).directory? end end end; end; end net-sftp-3.0.0/lib/net/sftp/operations/upload.rb0000644000004100000410000003176413672727541021637 0ustar www-datawww-datarequire 'net/ssh/loggable' module Net; module SFTP; module Operations # A general purpose uploader module for Net::SFTP. It can upload IO objects, # files, and even entire directory trees via SFTP, and provides a flexible # progress reporting mechanism. # # To upload a single file to the remote server, simply specify both the # local and remote paths: # # uploader = sftp.upload("/path/to/local.txt", "/path/to/remote.txt") # # By default, this operates asynchronously, so if you want to block until # the upload finishes, you can use the 'bang' variant: # # sftp.upload!("/path/to/local.txt", "/path/to/remote.txt") # # Or, if you have multiple uploads that you want to run in parallel, you can # employ the #wait method of the returned object: # # uploads = %w(file1 file2 file3).map { |f| sftp.upload(f, "remote/#{f}") } # uploads.each { |u| u.wait } # # To upload an entire directory tree, recursively, simply pass the directory # path as the first parameter: # # sftp.upload!("/path/to/directory", "/path/to/remote") # # This will upload "/path/to/directory", it's contents, it's subdirectories, # and their contents, recursively, to "/path/to/remote" on the remote server. # # For uploading a directory without creating it, do # sftp.upload!("/path/to/directory", "/path/to/remote", :mkdir => false) # # If you want to send data to a file on the remote server, but the data is # in memory, you can pass an IO object and upload it's contents: # # require 'stringio' # io = StringIO.new(data) # sftp.upload!(io, "/path/to/remote") # # The following options are supported: # # * :progress - either a block or an object to act as a progress # callback. See the discussion of "progress monitoring" below. # * :requests - the number of pending SFTP requests to allow at # any given time. When uploading an entire directory tree recursively, # this will default to 16, otherwise it will default to 2. Setting this # higher might improve throughput. Reducing it will reduce throughput. # * :read_size - the maximum number of bytes to read at a time # from the source. Increasing this value might improve throughput. It # defaults to 32,000 bytes. # * :name - the filename to report to the progress monitor when # an IO object is given as +local+. This defaults to "". # # == Progress Monitoring # # Sometimes it is desirable to track the progress of an upload. There are # two ways to do this: either using a callback block, or a special custom # object. # # Using a block it's pretty straightforward: # # sftp.upload!("local", "remote") do |event, uploader, *args| # case event # when :open then # # args[0] : file metadata # puts "starting upload: #{args[0].local} -> #{args[0].remote} (#{args[0].size} bytes}" # when :put then # # args[0] : file metadata # # args[1] : byte offset in remote file # # args[2] : data being written (as string) # puts "writing #{args[2].length} bytes to #{args[0].remote} starting at #{args[1]}" # when :close then # # args[0] : file metadata # puts "finished with #{args[0].remote}" # when :mkdir then # # args[0] : remote path name # puts "creating directory #{args[0]}" # when :finish then # puts "all done!" # end # # However, for more complex implementations (e.g., GUI interfaces and such) # a block can become cumbersome. In those cases, you can create custom # handler objects that respond to certain methods, and then pass your handler # to the uploader: # # class CustomHandler # def on_open(uploader, file) # puts "starting upload: #{file.local} -> #{file.remote} (#{file.size} bytes)" # end # # def on_put(uploader, file, offset, data) # puts "writing #{data.length} bytes to #{file.remote} starting at #{offset}" # end # # def on_close(uploader, file) # puts "finished with #{file.remote}" # end # # def on_mkdir(uploader, path) # puts "creating directory #{path}" # end # # def on_finish(uploader) # puts "all done!" # end # end # # sftp.upload!("local", "remote", :progress => CustomHandler.new) # # If you omit any of those methods, the progress updates for those missing # events will be ignored. You can create a catchall method named "call" for # those, instead. class Upload include Net::SSH::Loggable # The source of the upload (on the local server) attr_reader :local # The destination of the upload (on the remote server) attr_reader :remote # The hash of options that were given when the object was instantiated attr_reader :options # The SFTP session object used by this upload instance attr_reader :sftp # The properties hash for this object attr_reader :properties # Instantiates a new uploader process on top of the given SFTP session. # +local+ is either an IO object containing data to upload, or a string # identifying a file or directory on the local host. +remote+ is a string # identifying the location on the remote host that the upload should # target. # # This will return immediately, and requires that the SSH event loop be # run in order to effect the upload. (See #wait.) def initialize(sftp, local, remote, options={}, &progress) #:nodoc: @sftp = sftp @local = local @remote = remote @progress = progress || options[:progress] @options = options @properties = options[:properties] || {} @active = 0 self.logger = sftp.logger @uploads = [] @recursive = local.respond_to?(:read) ? false : ::File.directory?(local) if recursive? @stack = [entries_for(local)] @local_cwd = local @remote_cwd = remote @active += 1 if @options[:mkdir] sftp.mkdir(remote) do |response| @active -= 1 raise StatusException.new(response, "mkdir `#{remote}'") unless response.ok? (options[:requests] || RECURSIVE_READERS).to_i.times do break unless process_next_entry end end else @active -= 1 process_next_entry end else raise ArgumentError, "expected a file to upload" unless local.respond_to?(:read) || ::File.exist?(local) @stack = [[local]] process_next_entry end end # Returns true if a directory tree is being uploaded, and false if only a # single file is being uploaded. def recursive? @recursive end # Returns true if the uploader is currently running. When this is false, # the uploader has finished processing. def active? @active > 0 || @stack.any? end # Forces the transfer to stop. def abort! @active = 0 @stack.clear @uploads.clear end # Blocks until the upload has completed. def wait sftp.loop { active? } self end # Returns the property with the given name. This allows Upload instances # to store their own state when used as part of a state machine. def [](name) @properties[name.to_sym] end # Sets the given property to the given name. This allows Upload instances # to store their own state when used as part of a state machine. def []=(name, value) @properties[name.to_sym] = value end private #-- # "ruby -w" hates private attributes, so we have to do this longhand. #++ # The progress handler for this instance. Possibly nil. def progress; @progress; end # A simple struct for recording metadata about the file currently being # uploaded. LiveFile = Struct.new(:local, :remote, :io, :size, :handle) # The default # of bytes to read from disk at a time. DEFAULT_READ_SIZE = 32_000 # The number of readers to use when uploading a single file. SINGLE_FILE_READERS = 2 # The number of readers to use when uploading a directory. RECURSIVE_READERS = 16 # Examines the stack and determines what action to take. This is the # starting point of the state machine. def process_next_entry if @stack.empty? if @uploads.any? write_next_chunk(@uploads.first) elsif !active? update_progress(:finish) end return false elsif @stack.last.empty? @stack.pop @local_cwd = ::File.dirname(@local_cwd) @remote_cwd = ::File.dirname(@remote_cwd) process_next_entry elsif recursive? entry = @stack.last.shift lpath = ::File.join(@local_cwd, entry) rpath = ::File.join(@remote_cwd, entry) if ::File.directory?(lpath) @stack.push(entries_for(lpath)) @local_cwd = lpath @remote_cwd = rpath @active += 1 update_progress(:mkdir, rpath) request = sftp.mkdir(rpath, &method(:on_mkdir)) request[:dir] = rpath else open_file(lpath, rpath) end else open_file(@stack.pop.first, remote) end return true end # Prepares to send +local+ to +remote+. def open_file(local, remote) @active += 1 if local.respond_to?(:read) file = local name = options[:name] || "" else file = ::File.open(local, "rb") name = local end if file.respond_to?(:stat) size = file.stat.size else size = file.size end metafile = LiveFile.new(name, remote, file, size) update_progress(:open, metafile) request = sftp.open(remote, "w", &method(:on_open)) request[:file] = metafile end # Called when a +mkdir+ request finishes, successfully or otherwise. # If the request failed, this will raise a StatusException, otherwise # it will call #process_next_entry to continue the state machine. def on_mkdir(response) @active -= 1 dir = response.request[:dir] raise StatusException.new(response, "mkdir #{dir}") unless response.ok? process_next_entry end # Called when an +open+ request finishes. Raises StatusException if the # open failed, otherwise it calls #write_next_chunk to begin sending # data to the remote server. def on_open(response) @active -= 1 file = response.request[:file] raise StatusException.new(response, "open #{file.remote}") unless response.ok? file.handle = response[:handle] @uploads << file write_next_chunk(file) if !recursive? (options[:requests] || SINGLE_FILE_READERS).to_i.times { write_next_chunk(file) } end end # Called when a +write+ request finishes. Raises StatusException if the # write failed, otherwise it calls #write_next_chunk to continue the # write. def on_write(response) @active -= 1 file = response.request[:file] raise StatusException.new(response, "write #{file.remote}") unless response.ok? write_next_chunk(file) end # Called when a +close+ request finishes. Raises a StatusException if the # close failed, otherwise it calls #process_next_entry to continue the # state machine. def on_close(response) @active -= 1 file = response.request[:file] raise StatusException.new(response, "close #{file.remote}") unless response.ok? process_next_entry end # Attempts to send the next chunk from the given file (where +file+ is # a LiveFile instance). def write_next_chunk(file) if file.io.nil? process_next_entry else @active += 1 offset = file.io.pos data = file.io.read(options[:read_size] || DEFAULT_READ_SIZE) if data.nil? update_progress(:close, file) request = sftp.close(file.handle, &method(:on_close)) request[:file] = file file.io.close file.io = nil @uploads.delete(file) else update_progress(:put, file, offset, data) request = sftp.write(file.handle, offset, data, &method(:on_write)) request[:file] = file end end end # Returns all directory entries for the given path, removing the '.' # and '..' relative paths. def entries_for(local) ::Dir.entries(local).reject { |v| %w(. ..).include?(v) } end # Attempts to notify the progress monitor (if one was given) about # progress made for the given event. def update_progress(event, *args) on = "on_#{event}" if progress.respond_to?(on) progress.send(on, self, *args) elsif progress.respond_to?(:call) progress.call(event, self, *args) end end end end; end; end net-sftp-3.0.0/lib/net/sftp/operations/file.rb0000644000004100000410000001323413672727541021262 0ustar www-datawww-datarequire 'net/ssh/loggable' module Net; module SFTP; module Operations # A wrapper around an SFTP file handle, that exposes an IO-like interface # for interacting with the remote file. All operations are synchronous # (blocking), making this a very convenient way to deal with remote files. # # A wrapper is usually created via the Net::SFTP::Session#file factory: # # file = sftp.file.open("/path/to/remote") # puts file.gets # file.close class File # A reference to the Net::SFTP::Session instance that drives this wrapper attr_reader :sftp # The SFTP file handle object that this object wraps attr_reader :handle # The current position within the remote file attr_reader :pos # Creates a new wrapper that encapsulates the given +handle+ (such as # would be returned by Net::SFTP::Session#open!). The +sftp+ parameter # must be the same Net::SFTP::Session instance that opened the file. def initialize(sftp, handle) @sftp = sftp @handle = handle @pos = 0 @real_pos = 0 @real_eof = false @buffer = "" end # Repositions the file pointer to the given offset (relative to the # start of the file). This will also reset the EOF flag. def pos=(offset) @real_pos = @pos = offset @buffer = "" @real_eof = false end # Closes the underlying file and sets the handle to +nil+. Subsequent # operations on this object will fail. def close sftp.close!(handle) @handle = nil end # Returns true if the end of the file has been encountered by a previous # read. Setting the current file position via #pos= will reset this # flag (useful if the file's contents have changed since the EOF was # encountered). def eof? @real_eof && @buffer.empty? end # Reads up to +n+ bytes of data from the stream. Fewer bytes will be # returned if EOF is encountered before the requested number of bytes # could be read. Without an argument (or with a nil argument) all data # to the end of the file will be read and returned. # # This will advance the file pointer (#pos). def read(n=nil) loop do break if n && @buffer.length >= n break unless fill end if n result, @buffer = @buffer[0,n], (@buffer[n..-1] || "") else result, @buffer = @buffer, "" end @pos += result.length return result end # Reads up to the next instance of +sep_string+ in the stream, and # returns the bytes read (including +sep_string+). If +sep_string+ is # omitted, it defaults to +$/+. If EOF is encountered before any data # could be read, #gets will return +nil+. If the first argument is an # integer, or optional second argument is given, the returning string # would not be longer than the given value in bytes. def gets(sep_or_limit=$/, limit=Float::INFINITY) if sep_or_limit.is_a? Integer sep_string = $/ lim = sep_or_limit else sep_string = sep_or_limit lim = limit end delim = if sep_string && sep_string.length == 0 "#{$/}#{$/}" else sep_string end loop do at = @buffer.index(delim) if delim if at offset = [at + delim.length, lim].min @pos += offset line, @buffer = @buffer[0,offset], @buffer[offset..-1] return line elsif lim < @buffer.length @pos += lim line, @buffer = @buffer[0,lim], @buffer[lim..-1] return line elsif !fill return nil if @buffer.empty? @pos += @buffer.length line, @buffer = @buffer, "" return line end end end # Same as #gets, but raises EOFError if EOF is encountered before any # data could be read. def readline(sep_or_limit=$/, limit=Float::INFINITY) line = gets(sep_or_limit, limit) raise EOFError if line.nil? return line end # Writes the given data to the stream, incrementing the file position and # returning the number of bytes written. def write(data) data = data.to_s sftp.write!(handle, @real_pos, data) @real_pos += data.length @pos = @real_pos data.length end # Writes each argument to the stream. If +$\+ is set, it will be written # after all arguments have been written. def print(*items) items.each { |item| write(item) } write($\) if $\ nil end def size stat.size end # Resets position to beginning of file def rewind self.pos = 0 end # Writes each argument to the stream, appending a newline to any item # that does not already end in a newline. Array arguments are flattened. def puts(*items) items.each do |item| if Array === item puts(*item) else write(item) write("\n") unless item[-1] == ?\n end end nil end # Performs an fstat operation on the handle and returns the attribute # object (Net::SFTP::Protocol::V01::Attributes, Net::SFTP::Protool::V04::Attributes, # or Net::SFTP::Protocol::V06::Attributes, depending on the SFTP protocol # version in use). def stat sftp.fstat!(handle) end private # Fills the buffer. Returns +true+ if it succeeded, and +false+ if # EOF was encountered before any data was read. def fill data = sftp.read!(handle, @real_pos, 8192) if data.nil? @real_eof = true return false else @real_pos += data.length @buffer << data end !@real_eof end end end; end; end net-sftp-3.0.0/lib/net/sftp/operations/download.rb0000644000004100000410000003146713672727541022162 0ustar www-datawww-datarequire 'net/ssh/loggable' module Net; module SFTP; module Operations # A general purpose downloader module for Net::SFTP. It can download files # into IO objects, or directly to files on the local file system. It can # even download entire directory trees via SFTP, and provides a flexible # progress reporting mechanism. # # To download a single file from the remote server, simply specify both the # remote and local paths: # # downloader = sftp.download("/path/to/remote.txt", "/path/to/local.txt") # # By default, this operates asynchronously, so if you want to block until # the download finishes, you can use the 'bang' variant: # # sftp.download!("/path/to/remote.txt", "/path/to/local.txt") # # Or, if you have multiple downloads that you want to run in parallel, you can # employ the #wait method of the returned object: # # dls = %w(file1 file2 file3).map { |f| sftp.download("remote/#{f}", f) } # dls.each { |d| d.wait } # # To download an entire directory tree, recursively, simply specify :recursive => true: # # sftp.download!("/path/to/remotedir", "/path/to/local", :recursive => true) # # This will download "/path/to/remotedir", it's contents, it's subdirectories, # and their contents, recursively, to "/path/to/local" on the local host. # (If you specify :recursive => true and the source is not a directory, # you'll get an error!) # # If you want to pull the contents of a file on the remote server, and store # the data in memory rather than immediately to disk, you can pass an IO # object as the destination: # # require 'stringio' # io = StringIO.new # sftp.download!("/path/to/remote", io) # # This will only work for single-file downloads. Trying to do so with # :recursive => true will cause an error. # # The following options are supported: # # * :progress - either a block or an object to act as a progress # callback. See the discussion of "progress monitoring" below. # * :requests - the number of pending SFTP requests to allow at # any given time. When downloading an entire directory tree recursively, # this will default to 16. Setting this higher might improve throughput. # Reducing it will reduce throughput. # * :read_size - the maximum number of bytes to read at a time # from the source. Increasing this value might improve throughput. It # defaults to 32,000 bytes. # # == Progress Monitoring # # Sometimes it is desirable to track the progress of a download. There are # two ways to do this: either using a callback block, or a special custom # object. # # Using a block it's pretty straightforward: # # sftp.download!("remote", "local") do |event, downloader, *args| # case event # when :open then # # args[0] : file metadata # puts "starting download: #{args[0].remote} -> #{args[0].local} (#{args[0].size} bytes}" # when :get then # # args[0] : file metadata # # args[1] : byte offset in remote file # # args[2] : data that was received # puts "writing #{args[2].length} bytes to #{args[0].local} starting at #{args[1]}" # when :close then # # args[0] : file metadata # puts "finished with #{args[0].remote}" # when :mkdir then # # args[0] : local path name # puts "creating directory #{args[0]}" # when :finish then # puts "all done!" # end # end # # However, for more complex implementations (e.g., GUI interfaces and such) # a block can become cumbersome. In those cases, you can create custom # handler objects that respond to certain methods, and then pass your handler # to the downloader: # # class CustomHandler # def on_open(downloader, file) # puts "starting download: #{file.remote} -> #{file.local} (#{file.size} bytes)" # end # # def on_get(downloader, file, offset, data) # puts "writing #{data.length} bytes to #{file.local} starting at #{offset}" # end # # def on_close(downloader, file) # puts "finished with #{file.remote}" # end # # def on_mkdir(downloader, path) # puts "creating directory #{path}" # end # # def on_finish(downloader) # puts "all done!" # end # end # # sftp.download!("remote", "local", :progress => CustomHandler.new) # # If you omit any of those methods, the progress updates for those missing # events will be ignored. You can create a catchall method named "call" for # those, instead. class Download include Net::SSH::Loggable # The destination of the download (the name of a file or directory on # the local server, or an IO object) attr_reader :local # The source of the download (the name of a file or directory on the # remote server) attr_reader :remote # The hash of options that was given to this Download instance. attr_reader :options # The SFTP session instance that drives this download. attr_reader :sftp # The properties hash for this object attr_reader :properties # Instantiates a new downloader process on top of the given SFTP session. # +local+ is either an IO object that should receive the data, or a string # identifying the target file or directory on the local host. +remote+ is # a string identifying the location on the remote host that the download # should source. # # This will return immediately, and requires that the SSH event loop be # run in order to effect the download. (See #wait.) def initialize(sftp, local, remote, options={}, &progress) @sftp = sftp @local = local @remote = remote @progress = progress || options[:progress] @options = options @active = 0 @properties = options[:properties] || {} self.logger = sftp.logger if recursive? && local.respond_to?(:write) raise ArgumentError, "cannot download a directory tree in-memory" end @stack = [Entry.new(remote, local, recursive?)] process_next_entry end # Returns the value of the :recursive key in the options hash that was # given when the object was instantiated. def recursive? options[:recursive] end # Returns true if there are any active requests or pending files or # directories. def active? @active > 0 || stack.any? end # Forces the transfer to stop. def abort! @active = 0 @stack.clear end # Runs the SSH event loop for as long as the downloader is active (see # #active?). This can be used to block until the download completes. def wait sftp.loop { active? } self end # Returns the property with the given name. This allows Download instances # to store their own state when used as part of a state machine. def [](name) @properties[name.to_sym] end # Sets the given property to the given name. This allows Download instances # to store their own state when used as part of a state machine. def []=(name, value) @properties[name.to_sym] = value end private # A simple struct for encapsulating information about a single remote # file or directory that needs to be downloaded. Entry = Struct.new(:remote, :local, :directory, :size, :handle, :offset, :sink) #-- # "ruby -w" hates private attributes, so we have to do these longhand #++ # The stack of Entry instances, indicating which files and directories # on the remote host remain to be downloaded. def stack; @stack; end # The progress handler for this instance. Possibly nil. def progress; @progress; end # The default read size. DEFAULT_READ_SIZE = 32_000 # The number of bytes to read at a time from remote files. def read_size options[:read_size] || DEFAULT_READ_SIZE end # The number of simultaneou SFTP requests to use to effect the download. # Defaults to 16 for recursive downloads. def requests options[:requests] || (recursive? ? 16 : 2) end # Enqueues as many files and directories from the stack as possible # (see #requests). def process_next_entry while stack.any? && requests > @active entry = stack.shift @active += 1 if entry.directory update_progress(:mkdir, entry.local) ::Dir.mkdir(entry.local) unless ::File.directory?(entry.local) request = sftp.opendir(entry.remote, &method(:on_opendir)) request[:entry] = entry else open_file(entry) end end update_progress(:finish) if !active? end # Called when a remote directory is "opened" for reading, e.g. to # enumerate its contents. Starts an readdir operation if the opendir # operation was successful. def on_opendir(response) entry = response.request[:entry] raise StatusException.new(response, "opendir #{entry.remote}") unless response.ok? entry.handle = response[:handle] request = sftp.readdir(response[:handle], &method(:on_readdir)) request[:parent] = entry end # Called when the next batch of items is read from a directory on the # remote server. If any items were read, they are added to the queue # and #process_next_entry is called. def on_readdir(response) entry = response.request[:parent] if response.eof? request = sftp.close(entry.handle, &method(:on_closedir)) request[:parent] = entry elsif !response.ok? raise StatusException.new(response, "readdir #{entry.remote}") else response[:names].each do |item| next if item.name == "." || item.name == ".." stack << Entry.new(::File.join(entry.remote, item.name), ::File.join(entry.local, item.name), item.directory?, item.attributes.size) end # take this opportunity to enqueue more requests process_next_entry request = sftp.readdir(entry.handle, &method(:on_readdir)) request[:parent] = entry end end # Called when a file is to be opened for reading from the remote server. def open_file(entry) update_progress(:open, entry) request = sftp.open(entry.remote, &method(:on_open)) request[:entry] = entry end # Called when a directory handle is closed. def on_closedir(response) @active -= 1 entry = response.request[:parent] raise StatusException.new(response, "close #{entry.remote}") unless response.ok? process_next_entry end # Called when a file has been opened. This will call #download_next_chunk # to initiate the data transfer. def on_open(response) entry = response.request[:entry] raise StatusException.new(response, "open #{entry.remote}") unless response.ok? entry.handle = response[:handle] entry.sink = entry.local.respond_to?(:write) ? entry.local : ::File.open(entry.local, "wb") entry.offset = 0 download_next_chunk(entry) end # Initiates a read of the next #read_size bytes from the file. def download_next_chunk(entry) request = sftp.read(entry.handle, entry.offset, read_size, &method(:on_read)) request[:entry] = entry request[:offset] = entry.offset end # Called when a read from a file finishes. If the read was successful # and returned data, this will call #download_next_chunk to read the # next bit from the file. Otherwise the file will be closed. def on_read(response) entry = response.request[:entry] if response.eof? update_progress(:close, entry) entry.sink.close request = sftp.close(entry.handle, &method(:on_close)) request[:entry] = entry elsif !response.ok? raise StatusException.new(response, "read #{entry.remote}") else entry.offset += response[:data].bytesize update_progress(:get, entry, response.request[:offset], response[:data]) entry.sink.write(response[:data]) download_next_chunk(entry) end end # Called when a file handle is closed. def on_close(response) @active -= 1 entry = response.request[:entry] raise StatusException.new(response, "close #{entry.remote}") unless response.ok? process_next_entry end # If a progress callback or object has been set, this will report # the progress to that callback or object. def update_progress(hook, *args) on = "on_#{hook}" if progress.respond_to?(on) progress.send(on, self, *args) elsif progress.respond_to?(:call) progress.call(hook, self, *args) end end end end; end; end net-sftp-3.0.0/lib/net/sftp/protocol/0000755000004100000410000000000013672727541017471 5ustar www-datawww-datanet-sftp-3.0.0/lib/net/sftp/protocol/01/0000755000004100000410000000000013672727541017711 5ustar www-datawww-datanet-sftp-3.0.0/lib/net/sftp/protocol/01/base.rb0000644000004100000410000002367513672727541021165 0ustar www-datawww-datarequire 'net/ssh/loggable' require 'net/sftp/constants' require 'net/sftp/packet' require 'net/sftp/protocol/base' require 'net/sftp/protocol/01/attributes' require 'net/sftp/protocol/01/name' module Net; module SFTP; module Protocol; module V01 # Wraps the low-level SFTP calls for version 1 of the SFTP protocol. Also # implements the packet parsing as defined by version 1 of the protocol. # # None of these protocol methods block--all of them return immediately, # requiring the SSH event loop to be run while the server response is # pending. # # You will almost certainly never need to use this driver directly. Please # see Net::SFTP::Session for the recommended interface. class Base < Protocol::Base include Net::SFTP::Constants::OpenFlags # Returns the protocol version implemented by this driver. (1, in this # case) def version 1 end # Parses the given FXP_HANDLE packet and returns a hash with one key, # :handle, which references the handle. def parse_handle_packet(packet) { :handle => packet.read_string } end # Parses the given FXP_STATUS packet and returns a hash with one key, # :code, which references the status code returned by the server. def parse_status_packet(packet) { :code => packet.read_long } end # Parses the given FXP_DATA packet and returns a hash with one key, # :data, which references the data returned in the packet. def parse_data_packet(packet) { :data => packet.read_string } end # Parses the given FXP_ATTRS packet and returns a hash with one key, # :attrs, which references an Attributes object. def parse_attrs_packet(packet) { :attrs => attribute_factory.from_buffer(packet) } end # Parses the given FXP_NAME packet and returns a hash with one key, :names, # which references an array of Name objects. def parse_name_packet(packet) names = [] packet.read_long.times do filename = packet.read_string longname = packet.read_string attrs = attribute_factory.from_buffer(packet) names << name_factory.new(filename, longname, attrs) end { :names => names } end # Sends a FXP_OPEN packet to the server and returns the packet identifier. # The +flags+ parameter is either an integer (in which case it must be # a combination of the IO constants) or a string (in which case it must # be one of the mode strings that IO::open accepts). The +options+ # parameter is a hash that is used to construct a new Attribute object, # to pass as part of the FXP_OPEN request. def open(path, flags, options) flags = normalize_open_flags(flags) if flags & (IO::WRONLY | IO::RDWR) != 0 sftp_flags = FV1::WRITE sftp_flags |= FV1::READ if flags & IO::RDWR != 0 sftp_flags |= FV1::APPEND if flags & IO::APPEND != 0 else sftp_flags = FV1::READ end sftp_flags |= FV1::CREAT if flags & IO::CREAT != 0 sftp_flags |= FV1::TRUNC if flags & IO::TRUNC != 0 sftp_flags |= FV1::EXCL if flags & IO::EXCL != 0 attributes = attribute_factory.new(options) send_request(FXP_OPEN, :string, path, :long, sftp_flags, :raw, attributes.to_s) end # Sends a FXP_CLOSE packet to the server for the given +handle+ (such as # would be returned via a FXP_HANDLE packet). Returns the new packet id. def close(handle) send_request(FXP_CLOSE, :string, handle) end # Sends a FXP_READ packet to the server, requesting that +length+ bytes # be read from the file identified by +handle+, starting at +offset+ bytes # within the file. The handle must be one that was returned via a # FXP_HANDLE packet. Returns the new packet id. def read(handle, offset, length) send_request(FXP_READ, :string, handle, :int64, offset, :long, length) end # Sends a FXP_WRITE packet to the server, requesting that +data+ (a string), # be written to the file identified by +handle+, starting at +offset+ bytes # from the beginning of the file. The handle must be one that was returned # via a FXP_HANDLE packet. Returns the new packet id. def write(handle, offset, data) send_request(FXP_WRITE, :string, handle, :int64, offset, :string, data) end # Sends a FXP_LSTAT packet to the server, requesting a FXP_ATTR response # for the file at the given remote +path+ (a string). The +flags+ parameter # is ignored in this version of the protocol. #lstat will not follow # symbolic links; see #stat for a version that will. def lstat(path, flags=nil) send_request(FXP_LSTAT, :string, path) end # Sends a FXP_FSTAT packet to the server, requesting a FXP_ATTR response # for the file represented by the given +handle+ (which must have been # obtained from a FXP_HANDLE packet). The +flags+ parameter is ignored in # this version of the protocol. def fstat(handle, flags=nil) send_request(FXP_FSTAT, :string, handle) end # Sends a FXP_SETSTAT packet to the server, to update the attributes for # the file at the given remote +path+ (a string). The +attrs+ parameter is # a hash that defines the attributes to set. def setstat(path, attrs) send_request(FXP_SETSTAT, :string, path, :raw, attribute_factory.new(attrs).to_s) end # Sends a FXP_FSETSTAT packet to the server, to update the attributes for # the file represented by the given +handle+ (which must have been obtained # from a FXP_HANDLE packet). The +attrs+ parameter is a hash that defines # the attributes to set. def fsetstat(handle, attrs) send_request(FXP_FSETSTAT, :string, handle, :raw, attribute_factory.new(attrs).to_s) end # Sends a FXP_OPENDIR packet to the server, to request a handle for # manipulating the directory at the given remote +path+. def opendir(path) send_request(FXP_OPENDIR, :string, path) end # Sends a FXP_READDIR packet to the server, to request a batch of # directory name entries in the directory identified by +handle+ (which # must have been obtained via a FXP_OPENDIR request). def readdir(handle) send_request(FXP_READDIR, :string, handle) end # Sends a FXP_REMOTE packet to the server, to request that the given # file be deleted from the remote server. def remove(filename) send_request(FXP_REMOVE, :string, filename) end # Sends a FXP_MKDIR packet to the server, to request that a new directory # at +path+ on the remote server be created, and with +attrs+ (a hash) # describing the attributes of the new directory. def mkdir(path, attrs) send_request(FXP_MKDIR, :string, path, :raw, attribute_factory.new(attrs).to_s) end # Sends a FXP_RMDIR packet to the server, to request that the directory # at +path+ on the remote server be deleted. def rmdir(path) send_request(FXP_RMDIR, :string, path) end # Sends a FXP_REALPATH packet to the server, to request that the given # +path+ be canonicalized, taking into account path segments like "..". def realpath(path) send_request(FXP_REALPATH, :string, path) end # Sends a FXP_STAT packet to the server, requesting a FXP_ATTR response # for the file at the given remote +path+ (a string). The +flags+ parameter # is ignored in this version of the protocol. #stat will follow # symbolic links; see #lstat for a version that will not. def stat(path, flags=nil) send_request(FXP_STAT, :string, path) end # Not implemented in version 1 of the SFTP protocol. Raises a # NotImplementedError if called. def rename(name, new_name, flags=nil) not_implemented! :rename end # Not implemented in version 1 of the SFTP protocol. Raises a # NotImplementedError if called. def readlink(path) not_implemented! :readlink end # Not implemented in version 1 of the SFTP protocol. Raises a # NotImplementedError if called. def symlink(path, target) not_implemented! :symlink end # Not implemented in version 1 of the SFTP protocol. Raises a # NotImplementedError if called. def link(*args) not_implemented! :link end # Not implemented in version 1 of the SFTP protocol. Raises a # NotImplementedError if called. def block(handle, offset, length, mask) not_implemented! :block end # Not implemented in version 1 of the SFTP protocol. Raises a # NotImplementedError if called. def unblock(handle, offset, length) not_implemented! :unblock end protected # A helper method for implementing wrappers for operations that are # not implemented by the current SFTP protocol version. Simply raises # NotImplementedError with a message based on the given operation name. def not_implemented!(operation) raise NotImplementedError, "the #{operation} operation is not available in the version of the SFTP protocol supported by your server" end # Normalizes the given flags parameter, converting it into a combination # of IO constants. def normalize_open_flags(flags) if String === flags case flags.tr("b", "") when "r" then IO::RDONLY when "r+" then IO::RDWR when "w" then IO::WRONLY | IO::TRUNC | IO::CREAT when "w+" then IO::RDWR | IO::TRUNC | IO::CREAT when "a" then IO::APPEND | IO::CREAT | IO::WRONLY when "a+" then IO::APPEND | IO::CREAT | IO::RDWR else raise ArgumentError, "unsupported flags: #{flags.inspect}" end else flags.to_i end end # Returns the Attributes class used by this version of the protocol # (Net::SFTP::Protocol::V01::Attributes, in this case) def attribute_factory V01::Attributes end # Returns the Name class used by this version of the protocol # (Net::SFTP::Protocol::V01::Name, in this case) def name_factory V01::Name end end end; end; end; endnet-sftp-3.0.0/lib/net/sftp/protocol/01/attributes.rb0000644000004100000410000002426513672727541022435 0ustar www-datawww-datarequire 'net/ssh/buffer' module Net; module SFTP; module Protocol; module V01 # A class representing the attributes of a file or directory on the server. # It may be used to specify new attributes, or to query existing attributes. # # To specify new attributes, just pass a hash as the argument to the # constructor. The following keys are supported: # # * :size:: the size of the file # * :uid:: the user-id that owns the file (integer) # * :gid:: the group-id that owns the file (integer) # * :owner:: the name of the user that owns the file (string) # * :group:: the name of the group that owns the file (string) # * :permissions:: the permissions on the file (integer, e.g. 0755) # * :atime:: the access time of the file (integer, seconds since epoch) # * :mtime:: the modification time of the file (integer, seconds since epoch) # * :extended:: a hash of name/value pairs identifying extended info # # Likewise, when the server sends an Attributes object, all of the # above attributes are exposed as methods (though not all will be set with # non-nil values from the server). class Attributes F_SIZE = 0x00000001 F_UIDGID = 0x00000002 F_PERMISSIONS = 0x00000004 F_ACMODTIME = 0x00000008 F_EXTENDED = 0x80000000 T_REGULAR = 1 T_DIRECTORY = 2 T_SYMLINK = 3 T_SPECIAL = 4 T_UNKNOWN = 5 T_SOCKET = 6 T_CHAR_DEVICE = 7 T_BLOCK_DEVICE = 8 T_FIFO = 9 class < names } end # Sends a FXP_STAT packet to the server for the given +path+, and with the # given +flags+. If +flags+ is nil, it defaults to F_SIZE | F_PERMISSIONS | # F_ACCESSTIME | F_CREATETIME | F_MODIFYTIME | F_ACL | F_OWNERGROUP | # F_SUBSECOND_TIMES | F_EXTENDED (see Net::SFTP::Protocol::V04::Attributes # for those constants). def stat(path, flags=nil) send_request(FXP_STAT, :string, path, :long, flags || DEFAULT_FLAGS) end # Sends a FXP_LSTAT packet to the server for the given +path+, and with the # given +flags+. If +flags+ is nil, it defaults to F_SIZE | F_PERMISSIONS | # F_ACCESSTIME | F_CREATETIME | F_MODIFYTIME | F_ACL | F_OWNERGROUP | # F_SUBSECOND_TIMES | F_EXTENDED (see Net::SFTP::Protocol::V04::Attributes # for those constants). def lstat(path, flags=nil) send_request(FXP_LSTAT, :string, path, :long, flags || DEFAULT_FLAGS) end # Sends a FXP_FSTAT packet to the server for the given +path+, and with the # given +flags+. If +flags+ is nil, it defaults to F_SIZE | F_PERMISSIONS | # F_ACCESSTIME | F_CREATETIME | F_MODIFYTIME | F_ACL | F_OWNERGROUP | # F_SUBSECOND_TIMES | F_EXTENDED (see Net::SFTP::Protocol::V04::Attributes # for those constants). def fstat(handle, flags=nil) send_request(FXP_FSTAT, :string, handle, :long, flags || DEFAULT_FLAGS) end protected # The default flags used if the +flags+ parameter is nil for any of the # #stat, #lstat, or #fstat operations. DEFAULT_FLAGS = Attributes::F_SIZE | Attributes::F_PERMISSIONS | Attributes::F_ACCESSTIME | Attributes::F_CREATETIME | Attributes::F_MODIFYTIME | Attributes::F_ACL | Attributes::F_OWNERGROUP | Attributes::F_SUBSECOND_TIMES | Attributes::F_EXTENDED # Returns the Attributes class used by this version of the protocol # (Net::SFTP::Protocol::V04::Attributes, in this case) def attribute_factory V04::Attributes end # Returns the Name class used by this version of the protocol # (Net::SFTP::Protocol::V04::Name, in this case) def name_factory V04::Name end end end; end; end; endnet-sftp-3.0.0/lib/net/sftp/protocol/04/attributes.rb0000644000004100000410000001423213672727541022431 0ustar www-datawww-datarequire 'net/sftp/protocol/01/attributes' module Net; module SFTP; module Protocol; module V04 # A class representing the attributes of a file or directory on the server. # It may be used to specify new attributes, or to query existing attributes. # This particular class is specific to versions 4 and 5 of the SFTP # protocol. # # To specify new attributes, just pass a hash as the argument to the # constructor. The following keys are supported: # # * :type:: the type of the item (integer, one of the T_ constants) # * :size:: the size of the item (integer) # * :uid:: the user-id that owns the file (integer) # * :gid:: the group-id that owns the file (integer) # * :owner:: the name of the user that owns the file (string) # * :group:: the name of the group that owns the file (string) # * :permissions:: the permissions on the file (integer, e.g. 0755) # * :atime:: the access time of the file (integer, seconds since epoch) # * :atime_nseconds:: the nanosecond component of atime (integer) # * :createtime:: the time at which the file was created (integer, seconds since epoch) # * :createtime_nseconds:: the nanosecond component of createtime (integer) # * :mtime:: the modification time of the file (integer, seconds since epoch) # * :mtime_nseconds:: the nanosecond component of mtime (integer) # * :acl:: an array of ACL entries for the item # * :extended:: a hash of name/value pairs identifying extended info # # Likewise, when the server sends an Attributes object, all of the # above attributes are exposed as methods (though not all will be set with # non-nil values from the server). class Attributes < V01::Attributes F_ACCESSTIME = 0x00000008 F_CREATETIME = 0x00000010 F_MODIFYTIME = 0x00000020 F_ACL = 0x00000040 F_OWNERGROUP = 0x00000080 F_SUBSECOND_TIMES = 0x00000100 # A simple struct for representing a single entry in an Access Control # List. (See Net::SFTP::Constants::ACE) ACL = Struct.new(:type, :flag, :mask, :who) class <Please note: this project is in maintenance mode. It is not under active development but pull requests are very much welcome. Just be sure to include tests! -- delano * Docs: http://net-ssh.github.com/net-sftp * Issues: https://github.com/net-ssh/net-sftp/issues * Codes: https://github.com/net-ssh/net-sftp * Email: net-ssh@solutious.com As of v2.1.0, all gem releases are signed. See INSTALL. == DESCRIPTION: Net::SFTP is a pure-Ruby implementation of the SFTP protocol (specifically, versions 1 through 6 of the SFTP protocol). Note that this is the "Secure File Transfer Protocol", typically run over an SSH connection, and has nothing to do with the FTP protocol. == FEATURES/PROBLEMS: * Transfer files or even entire directory trees to or from a remote host via SFTP * Completely supports all six protocol versions * Asynchronous and synchronous operation * Read and write files using an IO-like interface == SYNOPSIS: In a nutshell: require 'net/sftp' Net::SFTP.start('host', 'username', :password => 'password') do |sftp| # upload a file or directory to the remote host sftp.upload!("/path/to/local", "/path/to/remote") # download a file or directory from the remote host sftp.download!("/path/to/remote", "/path/to/local") # grab data off the remote host directly to a buffer data = sftp.download!("/path/to/remote") # open and write to a pseudo-IO for a remote file sftp.file.open("/path/to/remote", "w") do |f| f.puts "Hello, world!\n" end # open and read from a pseudo-IO for a remote file sftp.file.open("/path/to/remote", "r") do |f| puts f.gets end # create a directory sftp.mkdir! "/path/to/directory" # list the entries in a directory sftp.dir.foreach("/path/to/directory") do |entry| puts entry.longname end end For the full documentation, start with Net::SFTP::Session. Also see Net::SFTP::Operations::Upload, Net::SFTP::Operations::Download, Net::SFTP::Operations::FileFactory, Net::SFTP::Operations::File, and Net::SFTP::Operations::Dir. == REQUIREMENTS: * Net::SSH 2 If you wish to run the tests, you'll need: * Echoe (if you want to use the Rakefile) * Mocha == INSTALL: * gem install net-sftp (might need sudo privileges) However, in order to be sure the code you're installing hasn't been tampered with, it's recommended that you verify the signature[http://docs.rubygems.org/read/chapter/21]. To do this, you need to add my public key as a trusted certificate (you only need to do this once): # Add the public key as a trusted certificate # (You only need to do this once) $ curl -O https://raw.github.com/net-ssh/net-ssh/master/gem-public_cert.pem $ gem cert --add gem-public_cert.pem Then, when install the gem, do so with high security: $ gem install net-sftp -P HighSecurity If you don't add the public key, you'll see an error like "Couldn't verify data signature". If you're still having trouble let me know and I'll give you a hand. Or, if you prefer to do it the hard way (sans Rubygems): * tar xzf net-ssh-*.tgz * cd net-ssh-* * ruby setup.rb config * ruby setup.rb install (might need sudo privileges) == LICENSE: (The MIT License) Copyright (c) 2008 Jamis Buck Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. net-sftp-3.0.0/Gemfile0000644000004100000410000000055213672727541014615 0ustar www-datawww-datasource 'https://rubygems.org' # Specify your gem's dependencies in mygem.gemspec gemspec # TODO: add to gemspec gem "bundler", "~> 2.1" gem "rake", "~> 12.0" gem 'byebug', group: %i[development test] if !Gem.win_platform? && RUBY_ENGINE == "ruby" if ENV["CI"] gem 'codecov', require: false, group: :test gem 'simplecov', require: false, group: :test end net-sftp-3.0.0/setup.rb0000644000004100000410000006723713672727541015024 0ustar www-datawww-data# # setup.rb # # Copyright (c) 2000-2004 Minero Aoki # # This program is free software. # You can distribute/modify this program under the terms of # the GNU Lesser General Public License version 2.1. # # # For backward compatibility # unless Enumerable.method_defined?(:map) module Enumerable alias map collect end end unless Enumerable.method_defined?(:detect) module Enumerable alias detect find end end unless Enumerable.method_defined?(:select) module Enumerable alias select find_all end end unless Enumerable.method_defined?(:reject) module Enumerable def reject result = [] each do |i| result.push i unless yield(i) end result end end end unless Enumerable.method_defined?(:inject) module Enumerable def inject(result) each do |i| result = yield(result, i) end result end end end unless Enumerable.method_defined?(:any?) module Enumerable def any? each do |i| return true if yield(i) end false end end end unless File.respond_to?(:read) def File.read(fname) open(fname) {|f| return f.read } end end # # Application independent utilities # def File.binread(fname) open(fname, 'rb') {|f| return f.read } end # for corrupted windows stat(2) def File.dir?(path) File.directory?((path[-1,1] == '/') ? path : path + '/') end # # Config # if arg = ARGV.detect{|arg| /\A--rbconfig=/ =~ arg } ARGV.delete(arg) require arg.split(/=/, 2)[1] $".push 'rbconfig.rb' else require 'rbconfig' end def multipackage_install? FileTest.directory?(File.dirname($0) + '/packages') end class ConfigTable c = ::Config::CONFIG rubypath = c['bindir'] + '/' + c['ruby_install_name'] major = c['MAJOR'].to_i minor = c['MINOR'].to_i teeny = c['TEENY'].to_i version = "#{major}.#{minor}" # ruby ver. >= 1.4.4? newpath_p = ((major >= 2) or ((major == 1) and ((minor >= 5) or ((minor == 4) and (teeny >= 4))))) subprefix = lambda {|path| path.sub(/\A#{Regexp.quote(c['prefix'])}/o, '$prefix') } if c['rubylibdir'] # V < 1.6.3 stdruby = subprefix.call(c['rubylibdir']) siteruby = subprefix.call(c['sitedir']) versite = subprefix.call(c['sitelibdir']) sodir = subprefix.call(c['sitearchdir']) elsif newpath_p # 1.4.4 <= V <= 1.6.3 stdruby = "$prefix/lib/ruby/#{version}" siteruby = subprefix.call(c['sitedir']) versite = siteruby + '/' + version sodir = "$site-ruby/#{c['arch']}" else # V < 1.4.4 stdruby = "$prefix/lib/ruby/#{version}" siteruby = "$prefix/lib/ruby/#{version}/site_ruby" versite = siteruby sodir = "$site-ruby/#{c['arch']}" end if arg = c['configure_args'].split.detect {|arg| /--with-make-prog=/ =~ arg } makeprog = arg.sub(/'/, '').split(/=/, 2)[1] else makeprog = 'make' end common_descripters = [ [ 'prefix', [ c['prefix'], 'path', 'path prefix of target environment' ] ], [ 'std-ruby', [ stdruby, 'path', 'the directory for standard ruby libraries' ] ], [ 'site-ruby-common', [ siteruby, 'path', 'the directory for version-independent non-standard ruby libraries' ] ], [ 'site-ruby', [ versite, 'path', 'the directory for non-standard ruby libraries' ] ], [ 'bin-dir', [ '$prefix/bin', 'path', 'the directory for commands' ] ], [ 'rb-dir', [ '$site-ruby', 'path', 'the directory for ruby scripts' ] ], [ 'so-dir', [ sodir, 'path', 'the directory for ruby extentions' ] ], [ 'data-dir', [ '$prefix/share', 'path', 'the directory for shared data' ] ], [ 'ruby-path', [ rubypath, 'path', 'path to set to #! line' ] ], [ 'ruby-prog', [ rubypath, 'name', 'the ruby program using for installation' ] ], [ 'make-prog', [ makeprog, 'name', 'the make program to compile ruby extentions' ] ], [ 'without-ext', [ 'no', 'yes/no', 'does not compile/install ruby extentions' ] ] ] multipackage_descripters = [ [ 'with', [ '', 'name,name...', 'package names that you want to install', 'ALL' ] ], [ 'without', [ '', 'name,name...', 'package names that you do not want to install', 'NONE' ] ] ] if multipackage_install? DESCRIPTER = common_descripters + multipackage_descripters else DESCRIPTER = common_descripters end SAVE_FILE = 'config.save' def ConfigTable.each_name(&block) keys().each(&block) end def ConfigTable.keys DESCRIPTER.map {|name, *dummy| name } end def ConfigTable.each_definition(&block) DESCRIPTER.each(&block) end def ConfigTable.get_entry(name) name, ent = DESCRIPTER.assoc(name) ent end def ConfigTable.get_entry!(name) get_entry(name) or raise ArgumentError, "no such config: #{name}" end def ConfigTable.add_entry(name, vals) ConfigTable::DESCRIPTER.push [name,vals] end def ConfigTable.remove_entry(name) get_entry(name) or raise ArgumentError, "no such config: #{name}" DESCRIPTER.delete_if {|n, arr| n == name } end def ConfigTable.config_key?(name) get_entry(name) ? true : false end def ConfigTable.bool_config?(name) ent = get_entry(name) or return false ent[1] == 'yes/no' end def ConfigTable.value_config?(name) ent = get_entry(name) or return false ent[1] != 'yes/no' end def ConfigTable.path_config?(name) ent = get_entry(name) or return false ent[1] == 'path' end class << self alias newobj new end def ConfigTable.new c = newobj() c.initialize_from_table c end def ConfigTable.load c = newobj() c.initialize_from_file c end def initialize_from_table @table = {} DESCRIPTER.each do |k, (default, vname, desc, default2)| @table[k] = default end end def initialize_from_file raise InstallError, "#{File.basename $0} config first"\ unless File.file?(SAVE_FILE) @table = {} File.foreach(SAVE_FILE) do |line| k, v = line.split(/=/, 2) @table[k] = v.strip end end def save File.open(SAVE_FILE, 'w') {|f| @table.each do |k, v| f.printf "%s=%s\n", k, v if v end } end def []=(k, v) raise InstallError, "unknown config option #{k}"\ unless ConfigTable.config_key?(k) @table[k] = v end def [](key) return nil unless @table[key] @table[key].gsub(%r<\$([^/]+)>) { self[$1] } end def set_raw(key, val) @table[key] = val end def get_raw(key) @table[key] end end module MetaConfigAPI def eval_file_ifexist(fname) instance_eval File.read(fname), fname, 1 if File.file?(fname) end def config_names ConfigTable.keys end def config?(name) ConfigTable.config_key?(name) end def bool_config?(name) ConfigTable.bool_config?(name) end def value_config?(name) ConfigTable.value_config?(name) end def path_config?(name) ConfigTable.path_config?(name) end def add_config(name, argname, default, desc) ConfigTable.add_entry name,[default,argname,desc] end def add_path_config(name, default, desc) add_config name, 'path', default, desc end def add_bool_config(name, default, desc) add_config name, 'yes/no', default ? 'yes' : 'no', desc end def set_config_default(name, default) if bool_config?(name) ConfigTable.get_entry!(name)[0] = (default ? 'yes' : 'no') else ConfigTable.get_entry!(name)[0] = default end end def remove_config(name) ent = ConfigTable.get_entry(name) ConfigTable.remove_entry name ent end end # # File Operations # module FileOperations def mkdir_p(dirname, prefix = nil) dirname = prefix + dirname if prefix $stderr.puts "mkdir -p #{dirname}" if verbose? return if no_harm? # does not check '/'... it's too abnormal case dirs = dirname.split(%r<(?=/)>) if /\A[a-z]:\z/i =~ dirs[0] disk = dirs.shift dirs[0] = disk + dirs[0] end dirs.each_index do |idx| path = dirs[0..idx].join('') Dir.mkdir path unless File.dir?(path) end end def rm_f(fname) $stderr.puts "rm -f #{fname}" if verbose? return if no_harm? if File.exist?(fname) or File.symlink?(fname) File.chmod 0777, fname File.unlink fname end end def rm_rf(dn) $stderr.puts "rm -rf #{dn}" if verbose? return if no_harm? Dir.chdir dn Dir.foreach('.') do |fn| next if fn == '.' next if fn == '..' if File.dir?(fn) verbose_off { rm_rf fn } else verbose_off { rm_f fn } end end Dir.chdir '..' Dir.rmdir dn end def move_file(src, dest) File.unlink dest if File.exist?(dest) begin File.rename src, dest rescue File.open(dest, 'wb') {|f| f.write File.binread(src) } File.chmod File.stat(src).mode, dest File.unlink src end end def install(from, dest, mode, prefix = nil) $stderr.puts "install #{from} #{dest}" if verbose? return if no_harm? realdest = prefix + dest if prefix realdest = File.join(realdest, File.basename(from)) if File.dir?(realdest) str = File.binread(from) if diff?(str, realdest) verbose_off { rm_f realdest if File.exist?(realdest) } File.open(realdest, 'wb') {|f| f.write str } File.chmod mode, realdest File.open("#{objdir_root()}/InstalledFiles", 'a') {|f| if prefix f.puts realdest.sub(prefix, '') else f.puts realdest end } end end def diff?(new_content, path) return true unless File.exist?(path) new_content != File.binread(path) end def command(str) $stderr.puts str if verbose? system str or raise RuntimeError, "'system #{str}' failed" end def ruby(str) command config('ruby-prog') + ' ' + str end def make(task = '') command config('make-prog') + ' ' + task end def extdir?(dir) File.exist?(dir + '/MANIFEST') end def all_files_in(dirname) Dir.open(dirname) {|d| return d.select {|ent| File.file?("#{dirname}/#{ent}") } } end REJECT_DIRS = %w( CVS SCCS RCS CVS.adm ) def all_dirs_in(dirname) Dir.open(dirname) {|d| return d.select {|n| File.dir?("#{dirname}/#{n}") } - %w(. ..) - REJECT_DIRS } end end # # Main Installer # class InstallError < StandardError; end module HookUtils def run_hook(name) try_run_hook "#{curr_srcdir()}/#{name}" or try_run_hook "#{curr_srcdir()}/#{name}.rb" end def try_run_hook(fname) return false unless File.file?(fname) begin instance_eval File.read(fname), fname, 1 rescue raise InstallError, "hook #{fname} failed:\n" + $!.message end true end end module HookScriptAPI def get_config(key) @config[key] end alias config get_config def set_config(key, val) @config[key] = val end # # srcdir/objdir (works only in the package directory) # #abstract srcdir_root #abstract objdir_root #abstract relpath def curr_srcdir "#{srcdir_root()}/#{relpath()}" end def curr_objdir "#{objdir_root()}/#{relpath()}" end def srcfile(path) "#{curr_srcdir()}/#{path}" end def srcexist?(path) File.exist?(srcfile(path)) end def srcdirectory?(path) File.dir?(srcfile(path)) end def srcfile?(path) File.file? srcfile(path) end def srcentries(path = '.') Dir.open("#{curr_srcdir()}/#{path}") {|d| return d.to_a - %w(. ..) } end def srcfiles(path = '.') srcentries(path).select {|fname| File.file?(File.join(curr_srcdir(), path, fname)) } end def srcdirectories(path = '.') srcentries(path).select {|fname| File.dir?(File.join(curr_srcdir(), path, fname)) } end end class ToplevelInstaller Version = '3.2.4' Copyright = 'Copyright (c) 2000-2004 Minero Aoki' TASKS = [ [ 'config', 'saves your configurations' ], [ 'show', 'shows current configuration' ], [ 'setup', 'compiles ruby extentions and others' ], [ 'install', 'installs files' ], [ 'clean', "does `make clean' for each extention" ], [ 'distclean',"does `make distclean' for each extention" ] ] def ToplevelInstaller.invoke instance().invoke end @singleton = nil def ToplevelInstaller.instance @singleton ||= new(File.dirname($0)) @singleton end include MetaConfigAPI def initialize(ardir_root) @config = nil @options = { 'verbose' => true } @ardir = File.expand_path(ardir_root) end def inspect "#<#{self.class} #{__id__()}>" end def invoke run_metaconfigs task = parsearg_global() @config = load_config(task) __send__ "parsearg_#{task}" init_installers __send__ "exec_#{task}" end def run_metaconfigs eval_file_ifexist "#{@ardir}/metaconfig" end def load_config(task) case task when 'config' ConfigTable.new when 'clean', 'distclean' if File.exist?('config.save') then ConfigTable.load else ConfigTable.new end else ConfigTable.load end end def init_installers @installer = Installer.new(@config, @options, @ardir, File.expand_path('.')) end # # Hook Script API bases # def srcdir_root @ardir end def objdir_root '.' end def relpath '.' end # # Option Parsing # def parsearg_global valid_task = /\A(?:#{TASKS.map {|task,desc| task }.join '|'})\z/ while arg = ARGV.shift case arg when /\A\w+\z/ raise InstallError, "invalid task: #{arg}" unless valid_task =~ arg return arg when '-q', '--quiet' @options['verbose'] = false when '--verbose' @options['verbose'] = true when '-h', '--help' print_usage $stdout exit 0 when '-v', '--version' puts "#{File.basename($0)} version #{Version}" exit 0 when '--copyright' puts Copyright exit 0 else raise InstallError, "unknown global option '#{arg}'" end end raise InstallError, <" out.puts " ruby #{File.basename $0} [] []" fmt = " %-20s %s\n" out.puts out.puts 'Global options:' out.printf fmt, '-q,--quiet', 'suppress message outputs' out.printf fmt, ' --verbose', 'output messages verbosely' out.printf fmt, '-h,--help', 'print this message' out.printf fmt, '-v,--version', 'print version and quit' out.printf fmt, ' --copyright', 'print copyright and quit' out.puts out.puts 'Tasks:' TASKS.each do |name, desc| out.printf " %-10s %s\n", name, desc end out.puts out.puts 'Options for config:' ConfigTable.each_definition do |name, (default, arg, desc, default2)| out.printf " %-20s %s [%s]\n", '--'+ name + (ConfigTable.bool_config?(name) ? '' : '='+arg), desc, default2 || default end out.printf " %-20s %s [%s]\n", '--rbconfig=path', 'your rbconfig.rb to load', "running ruby's" out.puts out.puts 'Options for install:' out.printf " %-20s %s [%s]\n", '--no-harm', 'only display what to do if given', 'off' out.printf " %-20s %s [%s]\n", '--prefix', 'install path prefix', '$prefix' out.puts end # # Task Handlers # def exec_config @installer.exec_config @config.save # must be final end def exec_setup @installer.exec_setup end def exec_install @installer.exec_install end def exec_show ConfigTable.each_name do |k| v = @config.get_raw(k) if not v or v.empty? v = '(not specified)' end printf "%-10s %s\n", k, v end end def exec_clean @installer.exec_clean end def exec_distclean @installer.exec_distclean end end class ToplevelInstallerMulti < ToplevelInstaller include HookUtils include HookScriptAPI include FileOperations def initialize(ardir) super @packages = all_dirs_in("#{@ardir}/packages") raise 'no package exists' if @packages.empty? end def run_metaconfigs eval_file_ifexist "#{@ardir}/metaconfig" @packages.each do |name| eval_file_ifexist "#{@ardir}/packages/#{name}/metaconfig" end end def init_installers @installers = {} @packages.each do |pack| @installers[pack] = Installer.new(@config, @options, "#{@ardir}/packages/#{pack}", "packages/#{pack}") end with = extract_selection(config('with')) without = extract_selection(config('without')) @selected = @installers.keys.select {|name| (with.empty? or with.include?(name)) \ and not without.include?(name) } end def extract_selection(list) a = list.split(/,/) a.each do |name| raise InstallError, "no such package: #{name}" \ unless @installers.key?(name) end a end def print_usage(f) super f.puts 'Inluded packages:' f.puts ' ' + @packages.sort.join(' ') f.puts end # # multi-package metaconfig API # attr_reader :packages def declare_packages(list) raise 'package list is empty' if list.empty? list.each do |name| raise "directory packages/#{name} does not exist"\ unless File.dir?("#{@ardir}/packages/#{name}") end @packages = list end # # Task Handlers # def exec_config run_hook 'pre-config' each_selected_installers {|inst| inst.exec_config } run_hook 'post-config' @config.save # must be final end def exec_setup run_hook 'pre-setup' each_selected_installers {|inst| inst.exec_setup } run_hook 'post-setup' end def exec_install run_hook 'pre-install' each_selected_installers {|inst| inst.exec_install } run_hook 'post-install' end def exec_clean rm_f 'config.save' run_hook 'pre-clean' each_selected_installers {|inst| inst.exec_clean } run_hook 'post-clean' end def exec_distclean rm_f 'config.save' run_hook 'pre-distclean' each_selected_installers {|inst| inst.exec_distclean } run_hook 'post-distclean' end # # lib # def each_selected_installers Dir.mkdir 'packages' unless File.dir?('packages') @selected.each do |pack| $stderr.puts "Processing the package `#{pack}' ..." if @options['verbose'] Dir.mkdir "packages/#{pack}" unless File.dir?("packages/#{pack}") Dir.chdir "packages/#{pack}" yield @installers[pack] Dir.chdir '../..' end end def verbose? @options['verbose'] end def no_harm? @options['no-harm'] end end class Installer FILETYPES = %w( bin lib ext data ) include HookScriptAPI include HookUtils include FileOperations def initialize(config, opt, srcroot, objroot) @config = config @options = opt @srcdir = File.expand_path(srcroot) @objdir = File.expand_path(objroot) @currdir = '.' end def inspect "#<#{self.class} #{File.basename(@srcdir)}>" end # # Hook Script API bases # def srcdir_root @srcdir end def objdir_root @objdir end def relpath @currdir end # # configs/options # def no_harm? @options['no-harm'] end def verbose? @options['verbose'] end def verbose_off begin save, @options['verbose'] = @options['verbose'], false yield ensure @options['verbose'] = save end end # # TASK config # def exec_config exec_task_traverse 'config' end def config_dir_bin(rel) end def config_dir_lib(rel) end def config_dir_ext(rel) extconf if extdir?(curr_srcdir()) end def extconf opt = @options['config-opt'].join(' ') command "#{config('ruby-prog')} #{curr_srcdir()}/extconf.rb #{opt}" end def config_dir_data(rel) end # # TASK setup # def exec_setup exec_task_traverse 'setup' end def setup_dir_bin(rel) all_files_in(curr_srcdir()).each do |fname| adjust_shebang "#{curr_srcdir()}/#{fname}" end end # modify: #!/usr/bin/ruby # modify: #! /usr/bin/ruby # modify: #!ruby # not modify: #!/usr/bin/env ruby SHEBANG_RE = /\A\#!\s*\S*ruby\S*/ def adjust_shebang(path) return if no_harm? tmpfile = File.basename(path) + '.tmp' begin File.open(path, 'rb') {|r| File.open(tmpfile, 'wb') {|w| first = r.gets return unless SHEBANG_RE =~ first $stderr.puts "adjusting shebang: #{File.basename path}" if verbose? w.print first.sub(SHEBANG_RE, '#!' + config('ruby-path')) w.write r.read } } move_file tmpfile, File.basename(path) ensure File.unlink tmpfile if File.exist?(tmpfile) end end def setup_dir_lib(rel) end def setup_dir_ext(rel) make if extdir?(curr_srcdir()) end def setup_dir_data(rel) end # # TASK install # def exec_install exec_task_traverse 'install' end def install_dir_bin(rel) install_files collect_filenames_auto(), "#{config('bin-dir')}/#{rel}", 0755 end def install_dir_lib(rel) install_files ruby_scripts(), "#{config('rb-dir')}/#{rel}", 0644 end def install_dir_ext(rel) return unless extdir?(curr_srcdir()) install_files ruby_extentions('.'), "#{config('so-dir')}/#{File.dirname(rel)}", 0555 end def install_dir_data(rel) install_files collect_filenames_auto(), "#{config('data-dir')}/#{rel}", 0644 end def install_files(list, dest, mode) mkdir_p dest, @options['install-prefix'] list.each do |fname| install fname, dest, mode, @options['install-prefix'] end end def ruby_scripts collect_filenames_auto().select {|n| /\.rb\z/ =~ n || "module.yml" == n } end # picked up many entries from cvs-1.11.1/src/ignore.c reject_patterns = %w( core RCSLOG tags TAGS .make.state .nse_depinfo #* .#* cvslog.* ,* .del-* *.olb *~ *.old *.bak *.BAK *.orig *.rej _$* *$ *.org *.in .* ) mapping = { '.' => '\.', '$' => '\$', '#' => '\#', '*' => '.*' } REJECT_PATTERNS = Regexp.new('\A(?:' + reject_patterns.map {|pat| pat.gsub(/[\.\$\#\*]/) {|ch| mapping[ch] } }.join('|') + ')\z') def collect_filenames_auto mapdir((existfiles() - hookfiles()).reject {|fname| REJECT_PATTERNS =~ fname }) end def existfiles all_files_in(curr_srcdir()) | all_files_in('.') end def hookfiles %w( pre-%s post-%s pre-%s.rb post-%s.rb ).map {|fmt| %w( config setup install clean ).map {|t| sprintf(fmt, t) } }.flatten end def mapdir(filelist) filelist.map {|fname| if File.exist?(fname) # objdir fname else # srcdir File.join(curr_srcdir(), fname) end } end def ruby_extentions(dir) _ruby_extentions(dir) or raise InstallError, "no ruby extention exists: 'ruby #{$0} setup' first" end DLEXT = /\.#{ ::Config::CONFIG['DLEXT'] }\z/ def _ruby_extentions(dir) Dir.open(dir) {|d| return d.select {|fname| DLEXT =~ fname } } end # # TASK clean # def exec_clean exec_task_traverse 'clean' rm_f 'config.save' rm_f 'InstalledFiles' end def clean_dir_bin(rel) end def clean_dir_lib(rel) end def clean_dir_ext(rel) return unless extdir?(curr_srcdir()) make 'clean' if File.file?('Makefile') end def clean_dir_data(rel) end # # TASK distclean # def exec_distclean exec_task_traverse 'distclean' rm_f 'config.save' rm_f 'InstalledFiles' end def distclean_dir_bin(rel) end def distclean_dir_lib(rel) end def distclean_dir_ext(rel) return unless extdir?(curr_srcdir()) make 'distclean' if File.file?('Makefile') end # # lib # def exec_task_traverse(task) run_hook "pre-#{task}" FILETYPES.each do |type| if config('without-ext') == 'yes' and type == 'ext' $stderr.puts 'skipping ext/* by user option' if verbose? next end traverse task, type, "#{task}_dir_#{type}" end run_hook "post-#{task}" end def traverse(task, rel, mid) dive_into(rel) { run_hook "pre-#{task}" __send__ mid, rel.sub(%r[\A.*?(?:/|\z)], '') all_dirs_in(curr_srcdir()).each do |d| traverse task, "#{rel}/#{d}", mid end run_hook "post-#{task}" } end def dive_into(rel) return unless File.dir?("#{@srcdir}/#{rel}") dir = File.basename(rel) Dir.mkdir dir unless File.dir?(dir) prevdir = Dir.pwd Dir.chdir dir $stderr.puts '---> ' + rel if verbose? @currdir = rel yield Dir.chdir prevdir $stderr.puts '<--- ' + rel if verbose? @currdir = File.dirname(rel) end end if $0 == __FILE__ begin if multipackage_install? ToplevelInstallerMulti.invoke else ToplevelInstaller.invoke end rescue raise if $DEBUG $stderr.puts $!.message $stderr.puts "Try 'ruby #{$0} --help' for detailed usage." exit 1 end end net-sftp-3.0.0/CHANGES.txt0000644000004100000410000000370313672727541015134 0ustar www-datawww-data=== 3.0.0 * Pass protocol version via Net::SFTP.start [#107] * Net-ssh 6.0 support [#106] === 2.1.2 / 07 May 2013 * Fix fragmentation download failure [accardi] === 2.1.0 / 06 Feb 2013 * Added public cert. All gem releases are now signed. See INSTALL in readme. * Remove self-require, it causes a warning in Ruby 1.9.2. [jbarnette] * Allow for upload to use the filename of the local file by default [czarneckid] * Properly handle receiving less data than requested. [thedarkone] * Added option to create directory on directory upload [Pablo Merino] * Remove a warnings in tests [kachick] === 2.0.5 / 19 Aug 2010 * Fixed missing StringIO exception in download! [Toby Bryans, Delano Mandelbaum] === 2.0.4 / 23 Nov 2009 * Fixed frozen string issue in StatusException.message [appoxy] === 2.0.3 / 17 Nov 2009 * Reference the correct Exception class when rescuing errors [Scott Tadman] === 2.0.2 / 1 Feb 2009 * Avoid using 'ensure' in Net::SFTP.start since it causes unfriendly behavior when exceptions are raised [Jamis Buck] === 2.0.1 / 29 May 2008 * Open files in binary mode to appease Windows [Jamis Buck] === 2.0.0 / 1 May 2008 * Make Net::SSH::Connection::Session#sftp accept an argument determining whether or not to block while the SFTP subsystem initializes (defaults to true) [Jamis Buck] * Allow Session#connect to be useful even in the open/opening states by invoking or queuing the callback block [Jamis Buck] * Allow custom properties to be set on upload/download initiation via the :properties option [Jamis Buck] * Custom properties on Download instances [Jamis Buck] * Add #abort! method to Upload and Download operations [Jamis Buck] * More complete support for file-type detection in protocol versions 1-3 [Jamis Buck] === 2.0 Preview Release 2 (1.99.1) / 10 Apr 2008 * Custom properties on Upload instances [Jamis Buck] === 2.0 Preview Release 1 (1.99.0) / 22 Mar 2008 * Rewritten! (Never, ever, do this at home.) New and improved API. net-sftp-3.0.0/LICENSE.txt0000644000004100000410000000204513672727541015144 0ustar www-datawww-dataCopyright © 2008 Jamis Buck Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the ‘Software’), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED ‘AS IS’, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.