├── .github └── workflows │ └── ci.yml ├── .gitignore ├── CHANGES.txt ├── Gemfile ├── LICENSE.txt ├── Manifest ├── README.rdoc ├── Rakefile ├── lib └── net │ ├── sftp.rb │ └── sftp │ ├── constants.rb │ ├── errors.rb │ ├── operations │ ├── dir.rb │ ├── download.rb │ ├── file.rb │ ├── file_factory.rb │ └── upload.rb │ ├── packet.rb │ ├── protocol.rb │ ├── protocol │ ├── 01 │ │ ├── attributes.rb │ │ ├── base.rb │ │ └── name.rb │ ├── 02 │ │ └── base.rb │ ├── 03 │ │ └── base.rb │ ├── 04 │ │ ├── attributes.rb │ │ ├── base.rb │ │ └── name.rb │ ├── 05 │ │ └── base.rb │ ├── 06 │ │ ├── attributes.rb │ │ └── base.rb │ └── base.rb │ ├── request.rb │ ├── response.rb │ ├── session.rb │ └── version.rb ├── net-sftp-public_cert.pem ├── net-sftp.gemspec ├── setup.rb └── test ├── common.rb ├── protocol ├── 01 │ ├── test_attributes.rb │ ├── test_base.rb │ └── test_name.rb ├── 02 │ └── test_base.rb ├── 03 │ └── test_base.rb ├── 04 │ ├── test_attributes.rb │ ├── test_base.rb │ └── test_name.rb ├── 05 │ └── test_base.rb ├── 06 │ ├── test_attributes.rb │ └── test_base.rb └── test_base.rb ├── test_all.rb ├── test_dir.rb ├── test_download.rb ├── test_file.rb ├── test_file_factory.rb ├── test_packet.rb ├── test_protocol.rb ├── test_request.rb ├── test_response.rb ├── test_session.rb ├── test_start.rb └── test_upload.rb /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: [push, pull_request] 3 | jobs: 4 | test: 5 | runs-on: ubuntu-18.04 6 | continue-on-error: ${{ matrix.flaky }} 7 | strategy: 8 | matrix: 9 | ruby-version: ["2.5", "2.6", "2.7", "3.0", "3.1", "truffleruby-22", "truffleruby-21"] 10 | flaky: [false] 11 | include: 12 | - ruby-version: "ruby-head" 13 | flaky: true 14 | - ruby-version: "jruby-9.2" 15 | flaky: true 16 | - ruby-version: "jruby-9.3" 17 | flaky: true 18 | - ruby-version: "jruby-head" 19 | flaky: true 20 | - ruby-version: "truffleruby-head" 21 | flaky: true 22 | steps: 23 | - uses: actions/checkout@v1 24 | 25 | - name: Set up Ruby ${{ matrix.ruby-version }} 26 | uses: ruby/setup-ruby@v1 27 | with: 28 | ruby-version: ${{ matrix.ruby-version }} 29 | 30 | - name: Bundle install 31 | run: | 32 | gem install bundler 33 | bundle install 34 | - name: Run Tests 35 | run: bundle exec rake test 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | pkg 2 | doc 3 | coverage 4 | ri 5 | *.swp 6 | Gemfile.lock 7 | -------------------------------------------------------------------------------- /CHANGES.txt: -------------------------------------------------------------------------------- 1 | === 3.0.0 2 | 3 | * Pass protocol version via Net::SFTP.start [#107] 4 | * Net-ssh 6.0 support [#106] 5 | 6 | === 2.1.2 / 07 May 2013 7 | 8 | * Fix fragmentation download failure [accardi] 9 | 10 | === 2.1.0 / 06 Feb 2013 11 | 12 | * Added public cert. All gem releases are now signed. See INSTALL in readme. 13 | * Remove self-require, it causes a warning in Ruby 1.9.2. [jbarnette] 14 | * Allow for upload to use the filename of the local file by default [czarneckid] 15 | * Properly handle receiving less data than requested. [thedarkone] 16 | * Added option to create directory on directory upload [Pablo Merino] 17 | * Remove a warnings in tests [kachick] 18 | 19 | 20 | === 2.0.5 / 19 Aug 2010 21 | 22 | * Fixed missing StringIO exception in download! [Toby Bryans, Delano Mandelbaum] 23 | 24 | 25 | === 2.0.4 / 23 Nov 2009 26 | 27 | * Fixed frozen string issue in StatusException.message [appoxy] 28 | 29 | 30 | === 2.0.3 / 17 Nov 2009 31 | 32 | * Reference the correct Exception class when rescuing errors [Scott Tadman] 33 | 34 | 35 | === 2.0.2 / 1 Feb 2009 36 | 37 | * Avoid using 'ensure' in Net::SFTP.start since it causes unfriendly behavior when exceptions are raised [Jamis Buck] 38 | 39 | 40 | === 2.0.1 / 29 May 2008 41 | 42 | * Open files in binary mode to appease Windows [Jamis Buck] 43 | 44 | 45 | === 2.0.0 / 1 May 2008 46 | 47 | * 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] 48 | 49 | * Allow Session#connect to be useful even in the open/opening states by invoking or queuing the callback block [Jamis Buck] 50 | 51 | * Allow custom properties to be set on upload/download initiation via the :properties option [Jamis Buck] 52 | 53 | * Custom properties on Download instances [Jamis Buck] 54 | 55 | * Add #abort! method to Upload and Download operations [Jamis Buck] 56 | 57 | * More complete support for file-type detection in protocol versions 1-3 [Jamis Buck] 58 | 59 | 60 | === 2.0 Preview Release 2 (1.99.1) / 10 Apr 2008 61 | 62 | * Custom properties on Upload instances [Jamis Buck] 63 | 64 | 65 | === 2.0 Preview Release 1 (1.99.0) / 22 Mar 2008 66 | 67 | * Rewritten! (Never, ever, do this at home.) New and improved API. 68 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in mygem.gemspec 4 | gemspec 5 | 6 | # TODO: add to gemspec 7 | gem "bundler", "~> 2.1" 8 | gem "rake", "~> 12.0" 9 | 10 | gem 'byebug', group: %i[development test] if !Gem.win_platform? && RUBY_ENGINE == "ruby" 11 | 12 | if ENV["CI"] 13 | gem 'codecov', require: false, group: :test 14 | gem 'simplecov', require: false, group: :test 15 | end 16 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright © 2008 Jamis Buck 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the ‘Software’), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 7 | of the Software, and to permit persons to whom the Software is furnished to do 8 | so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED ‘AS IS’, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /Manifest: -------------------------------------------------------------------------------- 1 | CHANGELOG.rdoc 2 | lib/net/sftp/constants.rb 3 | lib/net/sftp/errors.rb 4 | lib/net/sftp/operations/dir.rb 5 | lib/net/sftp/operations/download.rb 6 | lib/net/sftp/operations/file.rb 7 | lib/net/sftp/operations/file_factory.rb 8 | lib/net/sftp/operations/upload.rb 9 | lib/net/sftp/packet.rb 10 | lib/net/sftp/protocol/01/attributes.rb 11 | lib/net/sftp/protocol/01/base.rb 12 | lib/net/sftp/protocol/01/name.rb 13 | lib/net/sftp/protocol/02/base.rb 14 | lib/net/sftp/protocol/03/base.rb 15 | lib/net/sftp/protocol/04/attributes.rb 16 | lib/net/sftp/protocol/04/base.rb 17 | lib/net/sftp/protocol/04/name.rb 18 | lib/net/sftp/protocol/05/base.rb 19 | lib/net/sftp/protocol/06/attributes.rb 20 | lib/net/sftp/protocol/06/base.rb 21 | lib/net/sftp/protocol/base.rb 22 | lib/net/sftp/protocol.rb 23 | lib/net/sftp/request.rb 24 | lib/net/sftp/response.rb 25 | lib/net/sftp/session.rb 26 | lib/net/sftp/version.rb 27 | lib/net/sftp.rb 28 | Rakefile 29 | README.rdoc 30 | setup.rb 31 | test/common.rb 32 | test/protocol/01/test_attributes.rb 33 | test/protocol/01/test_base.rb 34 | test/protocol/01/test_name.rb 35 | test/protocol/02/test_base.rb 36 | test/protocol/03/test_base.rb 37 | test/protocol/04/test_attributes.rb 38 | test/protocol/04/test_base.rb 39 | test/protocol/04/test_name.rb 40 | test/protocol/05/test_base.rb 41 | test/protocol/06/test_attributes.rb 42 | test/protocol/06/test_base.rb 43 | test/protocol/test_base.rb 44 | test/test_all.rb 45 | test/test_dir.rb 46 | test/test_download.rb 47 | test/test_file.rb 48 | test/test_file_factory.rb 49 | test/test_packet.rb 50 | test/test_protocol.rb 51 | test/test_request.rb 52 | test/test_response.rb 53 | test/test_session.rb 54 | test/test_upload.rb 55 | Manifest 56 | -------------------------------------------------------------------------------- /README.rdoc: -------------------------------------------------------------------------------- 1 | = Net::SFTP 2 | 3 | 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 4 | 5 | 6 | * Docs: http://net-ssh.github.io/net-sftp 7 | * Issues: https://github.com/net-ssh/net-sftp/issues 8 | * Codes: https://github.com/net-ssh/net-sftp 9 | * Email: net-ssh@solutious.com 10 | 11 | As of v2.1.0, all gem releases are signed. See INSTALL. 12 | 13 | 14 | == DESCRIPTION: 15 | 16 | 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. 17 | 18 | == FEATURES/PROBLEMS: 19 | 20 | * Transfer files or even entire directory trees to or from a remote host via SFTP 21 | * Completely supports all six protocol versions 22 | * Asynchronous and synchronous operation 23 | * Read and write files using an IO-like interface 24 | 25 | == SYNOPSIS: 26 | 27 | In a nutshell: 28 | 29 | require 'net/sftp' 30 | 31 | Net::SFTP.start('host', 'username', :password => 'password') do |sftp| 32 | # upload a file or directory to the remote host 33 | sftp.upload!("/path/to/local", "/path/to/remote") 34 | 35 | # download a file or directory from the remote host 36 | sftp.download!("/path/to/remote", "/path/to/local") 37 | 38 | # grab data off the remote host directly to a buffer 39 | data = sftp.download!("/path/to/remote") 40 | 41 | # open and write to a pseudo-IO for a remote file 42 | sftp.file.open("/path/to/remote", "w") do |f| 43 | f.puts "Hello, world!\n" 44 | end 45 | 46 | # open and read from a pseudo-IO for a remote file 47 | sftp.file.open("/path/to/remote", "r") do |f| 48 | puts f.gets 49 | end 50 | 51 | # create a directory 52 | sftp.mkdir! "/path/to/directory" 53 | 54 | # list the entries in a directory 55 | sftp.dir.foreach("/path/to/directory") do |entry| 56 | puts entry.longname 57 | end 58 | end 59 | 60 | 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. 61 | 62 | == REQUIREMENTS: 63 | 64 | * Net::SSH 2 65 | 66 | If you wish to run the tests, you'll need: 67 | 68 | * Echoe (if you want to use the Rakefile) 69 | * Mocha 70 | 71 | == INSTALL: 72 | 73 | * gem install net-sftp (might need sudo privileges) 74 | 75 | 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): 76 | 77 | # Add the public key as a trusted certificate 78 | # (You only need to do this once) 79 | $ curl -O https://raw.githubusercontent.com/net-ssh/net-sftp/master/net-sftp-public_cert.pem 80 | $ gem cert --add net-sftp-public_cert.pem 81 | 82 | Then, when install the gem, do so with high security: 83 | 84 | $ gem install net-sftp -P HighSecurity 85 | 86 | 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. 87 | 88 | Or, if you prefer to do it the hard way (sans Rubygems): 89 | 90 | * tar xzf net-ssh-*.tgz 91 | * cd net-ssh-* 92 | * ruby setup.rb config 93 | * ruby setup.rb install (might need sudo privileges) 94 | 95 | == LICENSE: 96 | 97 | (The MIT License) 98 | 99 | Copyright (c) 2008 Jamis Buck 100 | 101 | Permission is hereby granted, free of charge, to any person obtaining 102 | a copy of this software and associated documentation files (the 103 | 'Software'), to deal in the Software without restriction, including 104 | without limitation the rights to use, copy, modify, merge, publish, 105 | distribute, sublicense, and/or sell copies of the Software, and to 106 | permit persons to whom the Software is furnished to do so, subject to 107 | the following conditions: 108 | 109 | The above copyright notice and this permission notice shall be 110 | included in all copies or substantial portions of the Software. 111 | 112 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 113 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 114 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 115 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 116 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 117 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 118 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 119 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "rubygems" 2 | require "rake" 3 | require "rake/clean" 4 | require "rdoc/task" 5 | require "bundler/gem_tasks" 6 | 7 | desc "When releasing make sure NET_SSH_BUILDGEM_SIGNED is set" 8 | task :check_NET_SSH_BUILDGEM_SIGNED do 9 | raise "NET_SSH_BUILDGEM_SIGNED should be set to release" unless ENV['NET_SSH_BUILDGEM_SIGNED'] 10 | end 11 | 12 | Rake::Task[:release].enhance [:check_NET_SSH_BUILDGEM_SIGNED] 13 | Rake::Task[:release].prerequisites.unshift(:check_NET_SSH_BUILDGEM_SIGNED) 14 | 15 | task :default => ["build"] 16 | CLEAN.include [ 'pkg', 'rdoc' ] 17 | name = "net-sftp" 18 | 19 | require_relative "lib/net/sftp/version" 20 | version = Net::SFTP::Version::CURRENT 21 | 22 | namespace :cert do 23 | desc "Update public cert from private - only run if public is expired" 24 | task :update_public_when_expired do 25 | require 'openssl' 26 | require 'time' 27 | raw = File.read "net-sftp-public_cert.pem" 28 | certificate = OpenSSL::X509::Certificate.new raw 29 | raise Exception, "Not yet expired: #{certificate.not_after}" unless certificate.not_after < Time.now 30 | sh "gem cert --build netssh@solutious.com --days 365*5 --private-key /mnt/gem/net-ssh-private_key.pem" 31 | sh "mv gem-public_cert.pem net-sftp-public_cert.pem" 32 | sh "gem cert --add net-sftp-public_cert.pem" 33 | end 34 | end 35 | 36 | require 'rake/testtask' 37 | Rake::TestTask.new do |t| 38 | t.libs = ["lib", "test"] 39 | end 40 | 41 | extra_files = %w[LICENSE.txt THANKS.txt CHANGES.txt ] 42 | RDoc::Task.new do |rdoc| 43 | rdoc.rdoc_dir = "rdoc" 44 | rdoc.title = "#{name} #{version}" 45 | rdoc.generator = 'hanna' # gem install hanna-nouveau 46 | rdoc.main = 'README.rdoc' 47 | rdoc.rdoc_files.include("README*") 48 | rdoc.rdoc_files.include("bin/*.rb") 49 | rdoc.rdoc_files.include("lib/**/*.rb") 50 | extra_files.each { |file| 51 | rdoc.rdoc_files.include(file) if File.exist?(file) 52 | } 53 | end 54 | -------------------------------------------------------------------------------- /lib/net/sftp.rb: -------------------------------------------------------------------------------- 1 | require 'net/ssh' 2 | require 'net/sftp/session' 3 | 4 | module Net 5 | 6 | # Net::SFTP is a pure-Ruby module for programmatically interacting with a 7 | # remote host via the SFTP protocol (that's SFTP as in "Secure File Transfer 8 | # Protocol" produced by the Secure Shell Working Group, not "Secure FTP" 9 | # and certainly not "Simple FTP"). 10 | # 11 | # See Net::SFTP#start for an introduction to the library. Also, see 12 | # Net::SFTP::Session for further documentation. 13 | module SFTP 14 | # A convenience method for starting a standalone SFTP session. It will 15 | # start up an SSH session using the given arguments (see the documentation 16 | # for Net::SSH::Session for details), and will then start a new SFTP session 17 | # with the SSH session. This will block until the new SFTP is fully open 18 | # and initialized before returning it. 19 | # 20 | # sftp = Net::SFTP.start("localhost", "user") 21 | # sftp.upload! "/local/file.tgz", "/remote/file.tgz" 22 | # 23 | # If a block is given, it will be passed to the SFTP session and will be 24 | # called once the SFTP session is fully open and initialized. When the 25 | # block terminates, the new SSH session will automatically be closed. 26 | # 27 | # Net::SFTP.start("localhost", "user") do |sftp| 28 | # sftp.upload! "/local/file.tgz", "/remote/file.tgz" 29 | # end 30 | # 31 | # Extra parameters can be passed: 32 | # - The Net::SSH connection options (see Net::SSH for more information) 33 | # - The Net::SFTP connection options (only :version is supported, to let you 34 | # set the SFTP protocol version to be used) 35 | def self.start(host, user, ssh_options={}, sftp_options={}, &block) 36 | session = Net::SSH.start(host, user, ssh_options) 37 | # We only use a single option here, but this leaves room for more later 38 | # without breaking the external API. 39 | version = sftp_options.fetch(:version, nil) 40 | sftp = Net::SFTP::Session.new(session, version, &block).connect! 41 | 42 | if block_given? 43 | sftp.loop 44 | session.close 45 | return nil 46 | end 47 | 48 | sftp 49 | rescue Object => anything 50 | begin 51 | session.shutdown! 52 | rescue ::Exception 53 | # swallow exceptions that occur while trying to shutdown 54 | end 55 | 56 | raise anything 57 | end 58 | end 59 | 60 | end 61 | 62 | class Net::SSH::Connection::Session 63 | # A convenience method for starting up a new SFTP connection on the current 64 | # SSH session. Blocks until the SFTP session is fully open, and then 65 | # returns the SFTP session. 66 | # 67 | # Net::SSH.start("localhost", "user", :password => "password") do |ssh| 68 | # ssh.sftp.upload!("/local/file.tgz", "/remote/file.tgz") 69 | # ssh.exec! "cd /some/path && tar xf /remote/file.tgz && rm /remote/file.tgz" 70 | # end 71 | def sftp(wait=true) 72 | @sftp ||= begin 73 | sftp = Net::SFTP::Session.new(self) 74 | sftp.connect! if wait 75 | sftp 76 | end 77 | end 78 | end 79 | -------------------------------------------------------------------------------- /lib/net/sftp/constants.rb: -------------------------------------------------------------------------------- 1 | module Net module SFTP 2 | 3 | # The packet types and other general constants used by the SFTP protocol. 4 | # See the specification for the SFTP protocol for a full discussion of their 5 | # meaning and usage. 6 | module Constants 7 | 8 | # The various packet types supported by SFTP protocol versions 1 through 6. 9 | # The FXP_EXTENDED and FXP_EXTENDED_REPLY packet types are not currently 10 | # understood by Net::SFTP. 11 | module PacketTypes 12 | FXP_INIT = 1 13 | FXP_VERSION = 2 14 | 15 | FXP_OPEN = 3 16 | FXP_CLOSE = 4 17 | FXP_READ = 5 18 | FXP_WRITE = 6 19 | FXP_LSTAT = 7 20 | FXP_FSTAT = 8 21 | FXP_SETSTAT = 9 22 | FXP_FSETSTAT = 10 23 | FXP_OPENDIR = 11 24 | FXP_READDIR = 12 25 | FXP_REMOVE = 13 26 | FXP_MKDIR = 14 27 | FXP_RMDIR = 15 28 | FXP_REALPATH = 16 29 | FXP_STAT = 17 30 | FXP_RENAME = 18 31 | FXP_READLINK = 19 32 | FXP_SYMLINK = 20 33 | FXP_LINK = 21 34 | FXP_BLOCK = 22 35 | FXP_UNBLOCK = 23 36 | 37 | FXP_STATUS = 101 38 | FXP_HANDLE = 102 39 | FXP_DATA = 103 40 | FXP_NAME = 104 41 | FXP_ATTRS = 105 42 | 43 | FXP_EXTENDED = 200 44 | FXP_EXTENDED_REPLY = 201 45 | end 46 | 47 | # Beginning in version 5 of the protocol, Net::SFTP::Session#rename accepts 48 | # an optional +flags+ argument that must be either 0 or a combination of 49 | # these constants. 50 | module RenameFlags 51 | OVERWRITE = 0x00000001 52 | ATOMIC = 0x00000002 53 | NATIVE = 0x00000004 54 | end 55 | 56 | # When an FXP_STATUS packet is received from the server, the +code+ will 57 | # be one of the following constants. 58 | module StatusCodes 59 | FX_OK = 0 60 | FX_EOF = 1 61 | FX_NO_SUCH_FILE = 2 62 | FX_PERMISSION_DENIED = 3 63 | FX_FAILURE = 4 64 | FX_BAD_MESSAGE = 5 65 | FX_NO_CONNECTION = 6 66 | FX_CONNECTION_LOST = 7 67 | FX_OP_UNSUPPORTED = 8 68 | FX_INVALID_HANDLE = 9 69 | FX_NO_SUCH_PATH = 10 70 | FX_FILE_ALREADY_EXISTS = 11 71 | FX_WRITE_PROTECT = 12 72 | FX_NO_MEDIA = 13 73 | FX_NO_SPACE_ON_FILESYSTEM = 14 74 | FX_QUOTA_EXCEEDED = 15 75 | FX_UNKNOWN_PRINCIPLE = 16 76 | FX_LOCK_CONFlICT = 17 77 | FX_DIR_NOT_EMPTY = 18 78 | FX_NOT_A_DIRECTORY = 19 79 | FX_INVALID_FILENAME = 20 80 | FX_LINK_LOOP = 21 81 | end 82 | 83 | # The Net::SFTP::Session#open operation is one of the worst casualties of 84 | # the revisions between SFTP protocol versions. The flags change considerably 85 | # between version 1 and version 6. Net::SFTP tries to shield programmers 86 | # from the differences, so you'll almost never need to use these flags 87 | # directly, but if you ever need to specify some flag that isn't exposed 88 | # by the higher-level API, these are the ones that are available to you. 89 | module OpenFlags 90 | # These are the flags that are understood by versions 1-4 of the the 91 | # open operation. 92 | module FV1 93 | READ = 0x00000001 94 | WRITE = 0x00000002 95 | APPEND = 0x00000004 96 | CREAT = 0x00000008 97 | TRUNC = 0x00000010 98 | EXCL = 0x00000020 99 | end 100 | 101 | # Version 5 of the open operation totally discarded the flags understood 102 | # by versions 1-4, and replaced them with these. 103 | module FV5 104 | CREATE_NEW = 0x00000000 105 | CREATE_TRUNCATE = 0x00000001 106 | OPEN_EXISTING = 0x00000002 107 | OPEN_OR_CREATE = 0x00000003 108 | TRUNCATE_EXISTING = 0x00000004 109 | 110 | APPEND_DATA = 0x00000008 111 | APPEND_DATA_ATOMIC = 0x00000010 112 | TEXT_MODE = 0x00000020 113 | READ_LOCK = 0x00000040 114 | WRITE_LOCK = 0x00000080 115 | DELETE_LOCK = 0x00000100 116 | end 117 | 118 | # Version 6 of the open operation added these flags, in addition to the 119 | # flags understood by version 5. 120 | module FV6 121 | ADVISORY_LOCK = 0x00000200 122 | NOFOLLOW = 0x00000400 123 | DELETE_ON_CLOSE = 0x00000800 124 | ACCESS_AUDIT_ALARM_INFO = 0x00001000 125 | ACCESS_BACKUP = 0x00002000 126 | BACKUP_STREAM = 0x00004000 127 | OVERRIDE_OWNER = 0x00008000 128 | end 129 | end 130 | 131 | # The Net::SFTP::Session#block operation, implemented in version 6 of 132 | # the protocol, understands these constants for the +mask+ parameter. 133 | module LockTypes 134 | READ = OpenFlags::FV5::READ_LOCK 135 | WRITE = OpenFlags::FV5::WRITE_LOCK 136 | DELETE = OpenFlags::FV5::DELETE_LOCK 137 | ADVISORY = OpenFlags::FV6::ADVISORY_LOCK 138 | end 139 | 140 | module ACE 141 | # Access control entry types, used from version 4 of the protocol, 142 | # onward. See Net::SFTP::Protocol::V04::Attributes::ACL. 143 | module Type 144 | ACCESS_ALLOWED = 0x00000000 145 | ACCESS_DENIED = 0x00000001 146 | SYSTEM_AUDIT = 0x00000002 147 | SYSTEM_ALARM = 0x00000003 148 | end 149 | 150 | # Access control entry flags, used from version 4 of the protocol, 151 | # onward. See Net::SFTP::Protocol::V04::Attributes::ACL. 152 | module Flag 153 | FILE_INHERIT = 0x00000001 154 | DIRECTORY_INHERIT = 0x00000002 155 | NO_PROPAGATE_INHERIT = 0x00000004 156 | INHERIT_ONLY = 0x00000008 157 | SUCCESSFUL_ACCESS = 0x00000010 158 | FAILED_ACCESS = 0x00000020 159 | IDENTIFIER_GROUP = 0x00000040 160 | end 161 | 162 | # Access control entry masks, used from version 4 of the protocol, 163 | # onward. See Net::SFTP::Protocol::V04::Attributes::ACL. 164 | module Mask 165 | READ_DATA = 0x00000001 166 | LIST_DIRECTORY = 0x00000001 167 | WRITE_DATA = 0x00000002 168 | ADD_FILE = 0x00000002 169 | APPEND_DATA = 0x00000004 170 | ADD_SUBDIRECTORY = 0x00000004 171 | READ_NAMED_ATTRS = 0x00000008 172 | WRITE_NAMED_ATTRS = 0x00000010 173 | EXECUTE = 0x00000020 174 | DELETE_CHILD = 0x00000040 175 | READ_ATTRIBUTES = 0x00000080 176 | WRITE_ATTRIBUTES = 0x00000100 177 | DELETE = 0x00010000 178 | READ_ACL = 0x00020000 179 | WRITE_ACL = 0x00040000 180 | WRITE_OWNER = 0x00080000 181 | SYNCHRONIZE = 0x00100000 182 | end 183 | end 184 | 185 | end 186 | 187 | end end 188 | -------------------------------------------------------------------------------- /lib/net/sftp/errors.rb: -------------------------------------------------------------------------------- 1 | module Net; module SFTP 2 | 3 | # The base exception class for the SFTP system. 4 | class Exception < RuntimeError; end 5 | 6 | # A exception class for reporting a non-success result of an operation. 7 | class StatusException < Net::SFTP::Exception 8 | 9 | # The response object that caused the exception. 10 | attr_reader :response 11 | 12 | # The error code (numeric) 13 | attr_reader :code 14 | 15 | # The description of the error 16 | attr_reader :description 17 | 18 | # Any incident-specific text given when the exception was raised 19 | attr_reader :text 20 | 21 | # Create a new status exception that reports the given code and 22 | # description. 23 | def initialize(response, text=nil) 24 | @response, @text = response, text 25 | @code = response.code 26 | @description = response.message 27 | @description = Response::MAP[@code] if @description.nil? || @description.empty? 28 | end 29 | 30 | # Override the default message format, to include the code and 31 | # description. 32 | def message 33 | m = super.dup 34 | m << " #{text}" if text 35 | m << " (#{code}, #{description.inspect})" 36 | end 37 | 38 | end 39 | end; end 40 | -------------------------------------------------------------------------------- /lib/net/sftp/operations/dir.rb: -------------------------------------------------------------------------------- 1 | require 'net/ssh/loggable' 2 | 3 | module Net; module SFTP; module Operations 4 | 5 | # A convenience class for working with remote directories. It provides methods 6 | # for searching and enumerating directory entries, similarly to the standard 7 | # ::Dir class. 8 | # 9 | # sftp.dir.foreach("/remote/path") do |entry| 10 | # puts entry.name 11 | # end 12 | # 13 | # p sftp.dir.entries("/remote/path").map { |e| e.name } 14 | # 15 | # sftp.dir.glob("/remote/path", "**/*.rb") do |entry| 16 | # puts entry.name 17 | # end 18 | class Dir 19 | # The SFTP session object that drives this directory factory. 20 | attr_reader :sftp 21 | 22 | # Create a new instance on top of the given SFTP session instance. 23 | def initialize(sftp) 24 | @sftp = sftp 25 | end 26 | 27 | # Calls the block once for each entry in the named directory on the 28 | # remote server. Yields a Name object to the block, rather than merely 29 | # the name of the entry. 30 | def foreach(path) 31 | handle = sftp.opendir!(path) 32 | while entries = sftp.readdir!(handle) 33 | entries.each { |entry| yield entry } 34 | end 35 | return nil 36 | ensure 37 | sftp.close!(handle) if handle 38 | end 39 | 40 | # Returns an array of Name objects representing the items in the given 41 | # remote directory, +path+. 42 | def entries(path) 43 | results = [] 44 | foreach(path) { |entry| results << entry } 45 | return results 46 | end 47 | 48 | # Works as ::Dir.glob, matching (possibly recursively) all directory 49 | # entries under +path+ against +pattern+. If a block is given, matches 50 | # will be yielded to the block as they are found; otherwise, they will 51 | # be returned in an array when the method finishes. 52 | # 53 | # Because working over an SFTP connection is always going to be slower than 54 | # working purely locally, don't expect this method to perform with the 55 | # same level of alacrity that ::Dir.glob does; it will work best for 56 | # shallow directory hierarchies with relatively few directories, though 57 | # it should be able to handle modest numbers of files in each directory. 58 | def glob(path, pattern, flags=0) 59 | flags |= ::File::FNM_PATHNAME 60 | path = path.chop if path.end_with?('/') && path != '/' 61 | 62 | results = [] unless block_given? 63 | queue = entries(path).reject { |e| %w(. ..).include?(e.name) } 64 | while queue.any? 65 | entry = queue.shift 66 | 67 | if entry.directory? && !%w(. ..).include?(::File.basename(entry.name)) 68 | queue += entries("#{path}/#{entry.name}").map do |e| 69 | e.name.replace("#{entry.name}/#{e.name}") 70 | e 71 | end 72 | end 73 | 74 | if ::File.fnmatch(pattern, entry.name, flags) 75 | if block_given? 76 | yield entry 77 | else 78 | results << entry 79 | end 80 | end 81 | end 82 | 83 | return results unless block_given? 84 | end 85 | 86 | # Identical to calling #glob with a +flags+ parameter of 0 and no block. 87 | # Simply returns the matched entries as an array. 88 | def [](path, pattern) 89 | glob(path, pattern, 0) 90 | end 91 | end 92 | 93 | end; end; end 94 | -------------------------------------------------------------------------------- /lib/net/sftp/operations/download.rb: -------------------------------------------------------------------------------- 1 | require 'net/ssh/loggable' 2 | 3 | module Net; module SFTP; module Operations 4 | 5 | # A general purpose downloader module for Net::SFTP. It can download files 6 | # into IO objects, or directly to files on the local file system. It can 7 | # even download entire directory trees via SFTP, and provides a flexible 8 | # progress reporting mechanism. 9 | # 10 | # To download a single file from the remote server, simply specify both the 11 | # remote and local paths: 12 | # 13 | # downloader = sftp.download("/path/to/remote.txt", "/path/to/local.txt") 14 | # 15 | # By default, this operates asynchronously, so if you want to block until 16 | # the download finishes, you can use the 'bang' variant: 17 | # 18 | # sftp.download!("/path/to/remote.txt", "/path/to/local.txt") 19 | # 20 | # Or, if you have multiple downloads that you want to run in parallel, you can 21 | # employ the #wait method of the returned object: 22 | # 23 | # dls = %w(file1 file2 file3).map { |f| sftp.download("remote/#{f}", f) } 24 | # dls.each { |d| d.wait } 25 | # 26 | # To download an entire directory tree, recursively, simply specify :recursive => true: 27 | # 28 | # sftp.download!("/path/to/remotedir", "/path/to/local", :recursive => true) 29 | # 30 | # This will download "/path/to/remotedir", its contents, its subdirectories, 31 | # and their contents, recursively, to "/path/to/local" on the local host. 32 | # (If you specify :recursive => true and the source is not a directory, 33 | # you'll get an error!) 34 | # 35 | # If you want to pull the contents of a file on the remote server, and store 36 | # the data in memory rather than immediately to disk, you can pass an IO 37 | # object as the destination: 38 | # 39 | # require 'stringio' 40 | # io = StringIO.new 41 | # sftp.download!("/path/to/remote", io) 42 | # 43 | # This will only work for single-file downloads. Trying to do so with 44 | # :recursive => true will cause an error. 45 | # 46 | # The following options are supported: 47 | # 48 | # * :progress - either a block or an object to act as a progress 49 | # callback. See the discussion of "progress monitoring" below. 50 | # * :requests - the number of pending SFTP requests to allow at 51 | # any given time. When downloading an entire directory tree recursively, 52 | # this will default to 16. Setting this higher might improve throughput. 53 | # Reducing it will reduce throughput. 54 | # * :read_size - the maximum number of bytes to read at a time 55 | # from the source. Increasing this value might improve throughput. It 56 | # defaults to 32,000 bytes. 57 | # 58 | # == Progress Monitoring 59 | # 60 | # Sometimes it is desirable to track the progress of a download. There are 61 | # two ways to do this: either using a callback block, or a special custom 62 | # object. 63 | # 64 | # Using a block it's pretty straightforward: 65 | # 66 | # sftp.download!("remote", "local") do |event, downloader, *args| 67 | # case event 68 | # when :open then 69 | # # args[0] : file metadata 70 | # puts "starting download: #{args[0].remote} -> #{args[0].local} (#{args[0].size} bytes}" 71 | # when :get then 72 | # # args[0] : file metadata 73 | # # args[1] : byte offset in remote file 74 | # # args[2] : data that was received 75 | # puts "writing #{args[2].length} bytes to #{args[0].local} starting at #{args[1]}" 76 | # when :close then 77 | # # args[0] : file metadata 78 | # puts "finished with #{args[0].remote}" 79 | # when :mkdir then 80 | # # args[0] : local path name 81 | # puts "creating directory #{args[0]}" 82 | # when :finish then 83 | # puts "all done!" 84 | # end 85 | # end 86 | # 87 | # However, for more complex implementations (e.g., GUI interfaces and such) 88 | # a block can become cumbersome. In those cases, you can create custom 89 | # handler objects that respond to certain methods, and then pass your handler 90 | # to the downloader: 91 | # 92 | # class CustomHandler 93 | # def on_open(downloader, file) 94 | # puts "starting download: #{file.remote} -> #{file.local} (#{file.size} bytes)" 95 | # end 96 | # 97 | # def on_get(downloader, file, offset, data) 98 | # puts "writing #{data.length} bytes to #{file.local} starting at #{offset}" 99 | # end 100 | # 101 | # def on_close(downloader, file) 102 | # puts "finished with #{file.remote}" 103 | # end 104 | # 105 | # def on_mkdir(downloader, path) 106 | # puts "creating directory #{path}" 107 | # end 108 | # 109 | # def on_finish(downloader) 110 | # puts "all done!" 111 | # end 112 | # end 113 | # 114 | # sftp.download!("remote", "local", :progress => CustomHandler.new) 115 | # 116 | # If you omit any of those methods, the progress updates for those missing 117 | # events will be ignored. You can create a catchall method named "call" for 118 | # those, instead. 119 | class Download 120 | include Net::SSH::Loggable 121 | 122 | # The destination of the download (the name of a file or directory on 123 | # the local server, or an IO object) 124 | attr_reader :local 125 | 126 | # The source of the download (the name of a file or directory on the 127 | # remote server) 128 | attr_reader :remote 129 | 130 | # The hash of options that was given to this Download instance. 131 | attr_reader :options 132 | 133 | # The SFTP session instance that drives this download. 134 | attr_reader :sftp 135 | 136 | # The properties hash for this object 137 | attr_reader :properties 138 | 139 | # Instantiates a new downloader process on top of the given SFTP session. 140 | # +local+ is either an IO object that should receive the data, or a string 141 | # identifying the target file or directory on the local host. +remote+ is 142 | # a string identifying the location on the remote host that the download 143 | # should source. 144 | # 145 | # This will return immediately, and requires that the SSH event loop be 146 | # run in order to effect the download. (See #wait.) 147 | def initialize(sftp, local, remote, options={}, &progress) 148 | @sftp = sftp 149 | @local = local 150 | @remote = remote 151 | @progress = progress || options[:progress] 152 | @options = options 153 | @active = 0 154 | @properties = options[:properties] || {} 155 | 156 | self.logger = sftp.logger 157 | 158 | if recursive? && local.respond_to?(:write) 159 | raise ArgumentError, "cannot download a directory tree in-memory" 160 | end 161 | 162 | @stack = [Entry.new(remote, local, recursive?)] 163 | process_next_entry 164 | end 165 | 166 | # Returns the value of the :recursive key in the options hash that was 167 | # given when the object was instantiated. 168 | def recursive? 169 | options[:recursive] 170 | end 171 | 172 | # Returns true if there are any active requests or pending files or 173 | # directories. 174 | def active? 175 | @active > 0 || stack.any? 176 | end 177 | 178 | # Forces the transfer to stop. 179 | def abort! 180 | @active = 0 181 | @stack.clear 182 | end 183 | 184 | # Runs the SSH event loop for as long as the downloader is active (see 185 | # #active?). This can be used to block until the download completes. 186 | def wait 187 | sftp.loop { active? } 188 | self 189 | end 190 | 191 | # Returns the property with the given name. This allows Download instances 192 | # to store their own state when used as part of a state machine. 193 | def [](name) 194 | @properties[name.to_sym] 195 | end 196 | 197 | # Sets the given property to the given name. This allows Download instances 198 | # to store their own state when used as part of a state machine. 199 | def []=(name, value) 200 | @properties[name.to_sym] = value 201 | end 202 | 203 | private 204 | 205 | # A simple struct for encapsulating information about a single remote 206 | # file or directory that needs to be downloaded. 207 | Entry = Struct.new(:remote, :local, :directory, :size, :handle, :offset, :sink) 208 | 209 | #-- 210 | # "ruby -w" hates private attributes, so we have to do these longhand 211 | #++ 212 | 213 | # The stack of Entry instances, indicating which files and directories 214 | # on the remote host remain to be downloaded. 215 | def stack; @stack; end 216 | 217 | # The progress handler for this instance. Possibly nil. 218 | def progress; @progress; end 219 | 220 | # The default read size. 221 | DEFAULT_READ_SIZE = 32_000 222 | 223 | # The number of bytes to read at a time from remote files. 224 | def read_size 225 | options[:read_size] || DEFAULT_READ_SIZE 226 | end 227 | 228 | # The number of simultaneou SFTP requests to use to effect the download. 229 | # Defaults to 16 for recursive downloads. 230 | def requests 231 | options[:requests] || (recursive? ? 16 : 2) 232 | end 233 | 234 | # Enqueues as many files and directories from the stack as possible 235 | # (see #requests). 236 | def process_next_entry 237 | while stack.any? && requests > @active 238 | entry = stack.shift 239 | @active += 1 240 | 241 | if entry.directory 242 | update_progress(:mkdir, entry.local) 243 | ::Dir.mkdir(entry.local) unless ::File.directory?(entry.local) 244 | request = sftp.opendir(entry.remote, &method(:on_opendir)) 245 | request[:entry] = entry 246 | else 247 | open_file(entry) 248 | end 249 | end 250 | 251 | update_progress(:finish) if !active? 252 | end 253 | 254 | # Called when a remote directory is "opened" for reading, e.g. to 255 | # enumerate its contents. Starts an readdir operation if the opendir 256 | # operation was successful. 257 | def on_opendir(response) 258 | entry = response.request[:entry] 259 | raise StatusException.new(response, "opendir #{entry.remote}") unless response.ok? 260 | entry.handle = response[:handle] 261 | request = sftp.readdir(response[:handle], &method(:on_readdir)) 262 | request[:parent] = entry 263 | end 264 | 265 | # Called when the next batch of items is read from a directory on the 266 | # remote server. If any items were read, they are added to the queue 267 | # and #process_next_entry is called. 268 | def on_readdir(response) 269 | entry = response.request[:parent] 270 | if response.eof? 271 | request = sftp.close(entry.handle, &method(:on_closedir)) 272 | request[:parent] = entry 273 | elsif !response.ok? 274 | raise StatusException.new(response, "readdir #{entry.remote}") 275 | else 276 | response[:names].each do |item| 277 | next if item.name == "." || item.name == ".." 278 | stack << Entry.new(::File.join(entry.remote, item.name), ::File.join(entry.local, item.name), item.directory?, item.attributes.size) 279 | end 280 | 281 | # take this opportunity to enqueue more requests 282 | process_next_entry 283 | 284 | request = sftp.readdir(entry.handle, &method(:on_readdir)) 285 | request[:parent] = entry 286 | end 287 | end 288 | 289 | # Called when a file is to be opened for reading from the remote server. 290 | def open_file(entry) 291 | update_progress(:open, entry) 292 | request = sftp.open(entry.remote, &method(:on_open)) 293 | request[:entry] = entry 294 | end 295 | 296 | # Called when a directory handle is closed. 297 | def on_closedir(response) 298 | @active -= 1 299 | entry = response.request[:parent] 300 | raise StatusException.new(response, "close #{entry.remote}") unless response.ok? 301 | process_next_entry 302 | end 303 | 304 | # Called when a file has been opened. This will call #download_next_chunk 305 | # to initiate the data transfer. 306 | def on_open(response) 307 | entry = response.request[:entry] 308 | raise StatusException.new(response, "open #{entry.remote}") unless response.ok? 309 | 310 | entry.handle = response[:handle] 311 | entry.sink = entry.local.respond_to?(:write) ? entry.local : ::File.open(entry.local, "wb") 312 | entry.offset = 0 313 | 314 | download_next_chunk(entry) 315 | end 316 | 317 | # Initiates a read of the next #read_size bytes from the file. 318 | def download_next_chunk(entry) 319 | request = sftp.read(entry.handle, entry.offset, read_size, &method(:on_read)) 320 | request[:entry] = entry 321 | request[:offset] = entry.offset 322 | end 323 | 324 | # Called when a read from a file finishes. If the read was successful 325 | # and returned data, this will call #download_next_chunk to read the 326 | # next bit from the file. Otherwise the file will be closed. 327 | def on_read(response) 328 | entry = response.request[:entry] 329 | 330 | if response.eof? 331 | update_progress(:close, entry) 332 | entry.sink.close 333 | request = sftp.close(entry.handle, &method(:on_close)) 334 | request[:entry] = entry 335 | elsif !response.ok? 336 | raise StatusException.new(response, "read #{entry.remote}") 337 | else 338 | entry.offset += response[:data].bytesize 339 | update_progress(:get, entry, response.request[:offset], response[:data]) 340 | entry.sink.write(response[:data]) 341 | download_next_chunk(entry) 342 | end 343 | end 344 | 345 | # Called when a file handle is closed. 346 | def on_close(response) 347 | @active -= 1 348 | entry = response.request[:entry] 349 | raise StatusException.new(response, "close #{entry.remote}") unless response.ok? 350 | process_next_entry 351 | end 352 | 353 | # If a progress callback or object has been set, this will report 354 | # the progress to that callback or object. 355 | def update_progress(hook, *args) 356 | on = "on_#{hook}" 357 | if progress.respond_to?(on) 358 | progress.send(on, self, *args) 359 | elsif progress.respond_to?(:call) 360 | progress.call(hook, self, *args) 361 | end 362 | end 363 | end 364 | 365 | end; end; end 366 | -------------------------------------------------------------------------------- /lib/net/sftp/operations/file.rb: -------------------------------------------------------------------------------- 1 | require 'net/ssh/loggable' 2 | 3 | module Net; module SFTP; module Operations 4 | 5 | # A wrapper around an SFTP file handle, that exposes an IO-like interface 6 | # for interacting with the remote file. All operations are synchronous 7 | # (blocking), making this a very convenient way to deal with remote files. 8 | # 9 | # A wrapper is usually created via the Net::SFTP::Session#file factory: 10 | # 11 | # file = sftp.file.open("/path/to/remote") 12 | # puts file.gets 13 | # file.close 14 | class File 15 | # A reference to the Net::SFTP::Session instance that drives this wrapper 16 | attr_reader :sftp 17 | 18 | # The SFTP file handle object that this object wraps 19 | attr_reader :handle 20 | 21 | # The current position within the remote file 22 | attr_reader :pos 23 | 24 | # Creates a new wrapper that encapsulates the given +handle+ (such as 25 | # would be returned by Net::SFTP::Session#open!). The +sftp+ parameter 26 | # must be the same Net::SFTP::Session instance that opened the file. 27 | def initialize(sftp, handle) 28 | @sftp = sftp 29 | @handle = handle 30 | @pos = 0 31 | @real_pos = 0 32 | @real_eof = false 33 | @buffer = "" 34 | end 35 | 36 | # Repositions the file pointer to the given offset (relative to the 37 | # start of the file). This will also reset the EOF flag. 38 | def pos=(offset) 39 | @real_pos = @pos = offset 40 | @buffer = "" 41 | @real_eof = false 42 | end 43 | 44 | # Closes the underlying file and sets the handle to +nil+. Subsequent 45 | # operations on this object will fail. 46 | def close 47 | sftp.close!(handle) 48 | @handle = nil 49 | end 50 | 51 | # Returns true if the end of the file has been encountered by a previous 52 | # read. Setting the current file position via #pos= will reset this 53 | # flag (useful if the file's contents have changed since the EOF was 54 | # encountered). 55 | def eof? 56 | @real_eof && @buffer.empty? 57 | end 58 | 59 | # Reads up to +n+ bytes of data from the stream. Fewer bytes will be 60 | # returned if EOF is encountered before the requested number of bytes 61 | # could be read. Without an argument (or with a nil argument) all data 62 | # to the end of the file will be read and returned. 63 | # 64 | # This will advance the file pointer (#pos). 65 | def read(n=nil) 66 | loop do 67 | break if n && @buffer.length >= n 68 | break unless fill 69 | end 70 | 71 | if n 72 | result, @buffer = @buffer[0,n], (@buffer[n..-1] || "") 73 | else 74 | result, @buffer = @buffer, "" 75 | end 76 | 77 | @pos += result.length 78 | return result 79 | end 80 | 81 | # Reads up to the next instance of +sep_string+ in the stream, and 82 | # returns the bytes read (including +sep_string+). If +sep_string+ is 83 | # omitted, it defaults to +$/+. If EOF is encountered before any data 84 | # could be read, #gets will return +nil+. If the first argument is an 85 | # integer, or optional second argument is given, the returning string 86 | # would not be longer than the given value in bytes. 87 | def gets(sep_or_limit=$/, limit=Float::INFINITY) 88 | if sep_or_limit.is_a? Integer 89 | sep_string = $/ 90 | lim = sep_or_limit 91 | else 92 | sep_string = sep_or_limit 93 | lim = limit 94 | end 95 | 96 | delim = if sep_string && sep_string.length == 0 97 | "#{$/}#{$/}" 98 | else 99 | sep_string 100 | end 101 | 102 | loop do 103 | at = @buffer.index(delim) if delim 104 | if at 105 | offset = [at + delim.length, lim].min 106 | @pos += offset 107 | line, @buffer = @buffer[0,offset], @buffer[offset..-1] 108 | return line 109 | elsif lim < @buffer.length 110 | @pos += lim 111 | line, @buffer = @buffer[0,lim], @buffer[lim..-1] 112 | return line 113 | elsif !fill 114 | return nil if @buffer.empty? 115 | @pos += @buffer.length 116 | line, @buffer = @buffer, "" 117 | return line 118 | end 119 | end 120 | end 121 | 122 | # Same as #gets, but raises EOFError if EOF is encountered before any 123 | # data could be read. 124 | def readline(sep_or_limit=$/, limit=Float::INFINITY) 125 | line = gets(sep_or_limit, limit) 126 | raise EOFError if line.nil? 127 | return line 128 | end 129 | 130 | # Writes the given data to the stream, incrementing the file position and 131 | # returning the number of bytes written. 132 | def write(data) 133 | data = data.to_s 134 | sftp.write!(handle, @real_pos, data) 135 | @real_pos += data.bytes.length 136 | @pos = @real_pos 137 | data.bytes.length 138 | end 139 | 140 | # Writes each argument to the stream. If +$\+ is set, it will be written 141 | # after all arguments have been written. 142 | def print(*items) 143 | items.each { |item| write(item) } 144 | write($\) if $\ 145 | nil 146 | end 147 | 148 | def size 149 | stat.size 150 | end 151 | 152 | # Resets position to beginning of file 153 | def rewind 154 | self.pos = 0 155 | end 156 | 157 | # Writes each argument to the stream, appending a newline to any item 158 | # that does not already end in a newline. Array arguments are flattened. 159 | def puts(*items) 160 | items.each do |item| 161 | if Array === item 162 | puts(*item) 163 | else 164 | write(item) 165 | write("\n") unless item[-1] == ?\n 166 | end 167 | end 168 | nil 169 | end 170 | 171 | # Performs an fstat operation on the handle and returns the attribute 172 | # object (Net::SFTP::Protocol::V01::Attributes, Net::SFTP::Protool::V04::Attributes, 173 | # or Net::SFTP::Protocol::V06::Attributes, depending on the SFTP protocol 174 | # version in use). 175 | def stat 176 | sftp.fstat!(handle) 177 | end 178 | 179 | private 180 | 181 | # Fills the buffer. Returns +true+ if it succeeded, and +false+ if 182 | # EOF was encountered before any data was read. 183 | def fill 184 | data = sftp.read!(handle, @real_pos, 8192) 185 | 186 | if data.nil? 187 | @real_eof = true 188 | return false 189 | else 190 | @real_pos += data.length 191 | @buffer << data 192 | end 193 | 194 | !@real_eof 195 | end 196 | end 197 | 198 | end; end; end 199 | -------------------------------------------------------------------------------- /lib/net/sftp/operations/file_factory.rb: -------------------------------------------------------------------------------- 1 | require 'net/ssh/loggable' 2 | require 'net/sftp/operations/file' 3 | 4 | module Net; module SFTP; module Operations 5 | 6 | # A factory class for opening files and returning Operations::File instances 7 | # that wrap the SFTP handles that represent them. This is a convenience 8 | # class for use when working with files synchronously. Rather than relying 9 | # on the programmer to provide callbacks that define a state machine that 10 | # describes the behavior of the program, this class (and Operations::File) 11 | # provide an interface where calls will block until they return, mimicking 12 | # the IO class' interface. 13 | class FileFactory 14 | # The SFTP session object that drives this file factory. 15 | attr_reader :sftp 16 | 17 | # Create a new instance on top of the given SFTP session instance. 18 | def initialize(sftp) 19 | @sftp = sftp 20 | end 21 | 22 | # :call-seq: 23 | # open(name, flags="r", mode=nil) -> file 24 | # open(name, flags="r", mode=nil) { |file| ... } 25 | # 26 | # Attempt to open a file on the remote server. The +flags+ parameter 27 | # accepts the same values as the standard Ruby ::File#open method. The 28 | # +mode+ parameter must be an integer describing the permissions to use 29 | # if a new file is being created. 30 | # 31 | # If a block is given, the new Operations::File instance will be yielded 32 | # to it, and closed automatically when the block terminates. Otherwise 33 | # the object will be returned, and it is the caller's responsibility to 34 | # close the file. 35 | # 36 | # sftp.file.open("/tmp/names.txt", "w") do |f| 37 | # # ... 38 | # end 39 | def open(name, flags="r", mode=nil, &block) 40 | handle = sftp.open!(name, flags, :permissions => mode) 41 | file = Operations::File.new(sftp, handle) 42 | 43 | if block_given? 44 | begin 45 | yield file 46 | ensure 47 | file.close 48 | end 49 | else 50 | return file 51 | end 52 | end 53 | 54 | # Returns +true+ if the argument refers to a directory on the remote host. 55 | def directory?(path) 56 | sftp.lstat!(path).directory? 57 | end 58 | end 59 | 60 | end; end; end 61 | -------------------------------------------------------------------------------- /lib/net/sftp/operations/upload.rb: -------------------------------------------------------------------------------- 1 | require 'net/ssh/loggable' 2 | 3 | module Net; module SFTP; module Operations 4 | 5 | # A general purpose uploader module for Net::SFTP. It can upload IO objects, 6 | # files, and even entire directory trees via SFTP, and provides a flexible 7 | # progress reporting mechanism. 8 | # 9 | # To upload a single file to the remote server, simply specify both the 10 | # local and remote paths: 11 | # 12 | # uploader = sftp.upload("/path/to/local.txt", "/path/to/remote.txt") 13 | # 14 | # By default, this operates asynchronously, so if you want to block until 15 | # the upload finishes, you can use the 'bang' variant: 16 | # 17 | # sftp.upload!("/path/to/local.txt", "/path/to/remote.txt") 18 | # 19 | # Or, if you have multiple uploads that you want to run in parallel, you can 20 | # employ the #wait method of the returned object: 21 | # 22 | # uploads = %w(file1 file2 file3).map { |f| sftp.upload(f, "remote/#{f}") } 23 | # uploads.each { |u| u.wait } 24 | # 25 | # To upload an entire directory tree, recursively, simply pass the directory 26 | # path as the first parameter: 27 | # 28 | # sftp.upload!("/path/to/directory", "/path/to/remote") 29 | # 30 | # This will upload "/path/to/directory", its contents, its subdirectories, 31 | # and their contents, recursively, to "/path/to/remote" on the remote server. 32 | # 33 | # For uploading a directory without creating it, do 34 | # sftp.upload!("/path/to/directory", "/path/to/remote", :mkdir => false) 35 | # 36 | # If you want to send data to a file on the remote server, but the data is 37 | # in memory, you can pass an IO object and upload its contents: 38 | # 39 | # require 'stringio' 40 | # io = StringIO.new(data) 41 | # sftp.upload!(io, "/path/to/remote") 42 | # 43 | # The following options are supported: 44 | # 45 | # * :progress - either a block or an object to act as a progress 46 | # callback. See the discussion of "progress monitoring" below. 47 | # * :requests - the number of pending SFTP requests to allow at 48 | # any given time. When uploading an entire directory tree recursively, 49 | # this will default to 16, otherwise it will default to 2. Setting this 50 | # higher might improve throughput. Reducing it will reduce throughput. 51 | # * :read_size - the maximum number of bytes to read at a time 52 | # from the source. Increasing this value might improve throughput. It 53 | # defaults to 32,000 bytes. 54 | # * :name - the filename to report to the progress monitor when 55 | # an IO object is given as +local+. This defaults to "". 56 | # 57 | # == Progress Monitoring 58 | # 59 | # Sometimes it is desirable to track the progress of an upload. There are 60 | # two ways to do this: either using a callback block, or a special custom 61 | # object. 62 | # 63 | # Using a block it's pretty straightforward: 64 | # 65 | # sftp.upload!("local", "remote") do |event, uploader, *args| 66 | # case event 67 | # when :open then 68 | # # args[0] : file metadata 69 | # puts "starting upload: #{args[0].local} -> #{args[0].remote} (#{args[0].size} bytes}" 70 | # when :put then 71 | # # args[0] : file metadata 72 | # # args[1] : byte offset in remote file 73 | # # args[2] : data being written (as string) 74 | # puts "writing #{args[2].length} bytes to #{args[0].remote} starting at #{args[1]}" 75 | # when :close then 76 | # # args[0] : file metadata 77 | # puts "finished with #{args[0].remote}" 78 | # when :mkdir then 79 | # # args[0] : remote path name 80 | # puts "creating directory #{args[0]}" 81 | # when :finish then 82 | # puts "all done!" 83 | # end 84 | # 85 | # However, for more complex implementations (e.g., GUI interfaces and such) 86 | # a block can become cumbersome. In those cases, you can create custom 87 | # handler objects that respond to certain methods, and then pass your handler 88 | # to the uploader: 89 | # 90 | # class CustomHandler 91 | # def on_open(uploader, file) 92 | # puts "starting upload: #{file.local} -> #{file.remote} (#{file.size} bytes)" 93 | # end 94 | # 95 | # def on_put(uploader, file, offset, data) 96 | # puts "writing #{data.length} bytes to #{file.remote} starting at #{offset}" 97 | # end 98 | # 99 | # def on_close(uploader, file) 100 | # puts "finished with #{file.remote}" 101 | # end 102 | # 103 | # def on_mkdir(uploader, path) 104 | # puts "creating directory #{path}" 105 | # end 106 | # 107 | # def on_finish(uploader) 108 | # puts "all done!" 109 | # end 110 | # end 111 | # 112 | # sftp.upload!("local", "remote", :progress => CustomHandler.new) 113 | # 114 | # If you omit any of those methods, the progress updates for those missing 115 | # events will be ignored. You can create a catchall method named "call" for 116 | # those, instead. 117 | class Upload 118 | include Net::SSH::Loggable 119 | 120 | # The source of the upload (on the local server) 121 | attr_reader :local 122 | 123 | # The destination of the upload (on the remote server) 124 | attr_reader :remote 125 | 126 | # The hash of options that were given when the object was instantiated 127 | attr_reader :options 128 | 129 | # The SFTP session object used by this upload instance 130 | attr_reader :sftp 131 | 132 | # The properties hash for this object 133 | attr_reader :properties 134 | 135 | # Instantiates a new uploader process on top of the given SFTP session. 136 | # +local+ is either an IO object containing data to upload, or a string 137 | # identifying a file or directory on the local host. +remote+ is a string 138 | # identifying the location on the remote host that the upload should 139 | # target. 140 | # 141 | # This will return immediately, and requires that the SSH event loop be 142 | # run in order to effect the upload. (See #wait.) 143 | def initialize(sftp, local, remote, options={}, &progress) #:nodoc: 144 | @sftp = sftp 145 | @local = local 146 | @remote = remote 147 | @progress = progress || options[:progress] 148 | @options = options 149 | @properties = options[:properties] || {} 150 | @active = 0 151 | 152 | self.logger = sftp.logger 153 | 154 | @uploads = [] 155 | @recursive = local.respond_to?(:read) ? false : ::File.directory?(local) 156 | 157 | if recursive? 158 | @stack = [entries_for(local)] 159 | @local_cwd = local 160 | @remote_cwd = remote 161 | 162 | @active += 1 163 | if @options[:mkdir] 164 | sftp.mkdir(remote) do |response| 165 | @active -= 1 166 | raise StatusException.new(response, "mkdir `#{remote}'") unless response.ok? 167 | (options[:requests] || RECURSIVE_READERS).to_i.times do 168 | break unless process_next_entry 169 | end 170 | end 171 | else 172 | @active -= 1 173 | process_next_entry 174 | end 175 | else 176 | raise ArgumentError, "expected a file to upload" unless local.respond_to?(:read) || ::File.exist?(local) 177 | @stack = [[local]] 178 | process_next_entry 179 | end 180 | end 181 | 182 | # Returns true if a directory tree is being uploaded, and false if only a 183 | # single file is being uploaded. 184 | def recursive? 185 | @recursive 186 | end 187 | 188 | # Returns true if the uploader is currently running. When this is false, 189 | # the uploader has finished processing. 190 | def active? 191 | @active > 0 || @stack.any? 192 | end 193 | 194 | # Forces the transfer to stop. 195 | def abort! 196 | @active = 0 197 | @stack.clear 198 | @uploads.clear 199 | end 200 | 201 | # Blocks until the upload has completed. 202 | def wait 203 | sftp.loop { active? } 204 | self 205 | end 206 | 207 | # Returns the property with the given name. This allows Upload instances 208 | # to store their own state when used as part of a state machine. 209 | def [](name) 210 | @properties[name.to_sym] 211 | end 212 | 213 | # Sets the given property to the given name. This allows Upload instances 214 | # to store their own state when used as part of a state machine. 215 | def []=(name, value) 216 | @properties[name.to_sym] = value 217 | end 218 | 219 | private 220 | 221 | #-- 222 | # "ruby -w" hates private attributes, so we have to do this longhand. 223 | #++ 224 | 225 | # The progress handler for this instance. Possibly nil. 226 | def progress; @progress; end 227 | 228 | # A simple struct for recording metadata about the file currently being 229 | # uploaded. 230 | LiveFile = Struct.new(:local, :remote, :io, :size, :handle) 231 | 232 | # The default # of bytes to read from disk at a time. 233 | DEFAULT_READ_SIZE = 32_000 234 | 235 | # The number of readers to use when uploading a single file. 236 | SINGLE_FILE_READERS = 2 237 | 238 | # The number of readers to use when uploading a directory. 239 | RECURSIVE_READERS = 16 240 | 241 | # Examines the stack and determines what action to take. This is the 242 | # starting point of the state machine. 243 | def process_next_entry 244 | if @stack.empty? 245 | if @uploads.any? 246 | write_next_chunk(@uploads.first) 247 | elsif !active? 248 | update_progress(:finish) 249 | end 250 | return false 251 | elsif @stack.last.empty? 252 | @stack.pop 253 | @local_cwd = ::File.dirname(@local_cwd) 254 | @remote_cwd = ::File.dirname(@remote_cwd) 255 | process_next_entry 256 | elsif recursive? 257 | entry = @stack.last.shift 258 | lpath = ::File.join(@local_cwd, entry) 259 | rpath = ::File.join(@remote_cwd, entry) 260 | 261 | if ::File.directory?(lpath) 262 | @stack.push(entries_for(lpath)) 263 | @local_cwd = lpath 264 | @remote_cwd = rpath 265 | 266 | @active += 1 267 | update_progress(:mkdir, rpath) 268 | request = sftp.mkdir(rpath, &method(:on_mkdir)) 269 | request[:dir] = rpath 270 | else 271 | open_file(lpath, rpath) 272 | end 273 | else 274 | open_file(@stack.pop.first, remote) 275 | end 276 | return true 277 | end 278 | 279 | # Prepares to send +local+ to +remote+. 280 | def open_file(local, remote) 281 | @active += 1 282 | 283 | if local.respond_to?(:read) 284 | file = local 285 | name = options[:name] || "" 286 | else 287 | file = ::File.open(local, "rb") 288 | name = local 289 | end 290 | 291 | if file.respond_to?(:stat) 292 | size = file.stat.size 293 | else 294 | size = file.size 295 | end 296 | 297 | metafile = LiveFile.new(name, remote, file, size) 298 | update_progress(:open, metafile) 299 | 300 | request = sftp.open(remote, "w", &method(:on_open)) 301 | request[:file] = metafile 302 | end 303 | 304 | # Called when a +mkdir+ request finishes, successfully or otherwise. 305 | # If the request failed, this will raise a StatusException, otherwise 306 | # it will call #process_next_entry to continue the state machine. 307 | def on_mkdir(response) 308 | @active -= 1 309 | dir = response.request[:dir] 310 | raise StatusException.new(response, "mkdir #{dir}") unless response.ok? 311 | 312 | process_next_entry 313 | end 314 | 315 | # Called when an +open+ request finishes. Raises StatusException if the 316 | # open failed, otherwise it calls #write_next_chunk to begin sending 317 | # data to the remote server. 318 | def on_open(response) 319 | @active -= 1 320 | file = response.request[:file] 321 | raise StatusException.new(response, "open #{file.remote}") unless response.ok? 322 | 323 | file.handle = response[:handle] 324 | 325 | @uploads << file 326 | write_next_chunk(file) 327 | 328 | if !recursive? 329 | (options[:requests] || SINGLE_FILE_READERS).to_i.times { write_next_chunk(file) } 330 | end 331 | end 332 | 333 | # Called when a +write+ request finishes. Raises StatusException if the 334 | # write failed, otherwise it calls #write_next_chunk to continue the 335 | # write. 336 | def on_write(response) 337 | @active -= 1 338 | file = response.request[:file] 339 | raise StatusException.new(response, "write #{file.remote}") unless response.ok? 340 | write_next_chunk(file) 341 | end 342 | 343 | # Called when a +close+ request finishes. Raises a StatusException if the 344 | # close failed, otherwise it calls #process_next_entry to continue the 345 | # state machine. 346 | def on_close(response) 347 | @active -= 1 348 | file = response.request[:file] 349 | raise StatusException.new(response, "close #{file.remote}") unless response.ok? 350 | process_next_entry 351 | end 352 | 353 | # Attempts to send the next chunk from the given file (where +file+ is 354 | # a LiveFile instance). 355 | def write_next_chunk(file) 356 | if file.io.nil? 357 | process_next_entry 358 | else 359 | @active += 1 360 | offset = file.io.pos 361 | data = file.io.read(options[:read_size] || DEFAULT_READ_SIZE) 362 | if data.nil? 363 | update_progress(:close, file) 364 | request = sftp.close(file.handle, &method(:on_close)) 365 | request[:file] = file 366 | file.io.close 367 | file.io = nil 368 | @uploads.delete(file) 369 | else 370 | update_progress(:put, file, offset, data) 371 | request = sftp.write(file.handle, offset, data, &method(:on_write)) 372 | request[:file] = file 373 | end 374 | end 375 | end 376 | 377 | # Returns all directory entries for the given path, removing the '.' 378 | # and '..' relative paths. 379 | def entries_for(local) 380 | ::Dir.entries(local).reject { |v| %w(. ..).include?(v) } 381 | end 382 | 383 | # Attempts to notify the progress monitor (if one was given) about 384 | # progress made for the given event. 385 | def update_progress(event, *args) 386 | on = "on_#{event}" 387 | if progress.respond_to?(on) 388 | progress.send(on, self, *args) 389 | elsif progress.respond_to?(:call) 390 | progress.call(event, self, *args) 391 | end 392 | end 393 | end 394 | 395 | end; end; end 396 | -------------------------------------------------------------------------------- /lib/net/sftp/packet.rb: -------------------------------------------------------------------------------- 1 | require 'net/ssh/buffer' 2 | 3 | module Net; module SFTP 4 | 5 | # A specialization of the Net::SSH::Buffer class, which simply auto-reads 6 | # the type byte from the front of every packet it represents. 7 | class Packet < Net::SSH::Buffer 8 | # The (intger) type of this packet. See Net::SFTP::Constants for all 9 | # possible packet types. 10 | attr_reader :type 11 | 12 | # Create a new Packet object that wraps the given +data+ (which should be 13 | # a String). The first byte of the data will be consumed automatically and 14 | # interpreted as the #type of this packet. 15 | def initialize(data) 16 | super 17 | @type = read_byte 18 | end 19 | end 20 | 21 | end; end -------------------------------------------------------------------------------- /lib/net/sftp/protocol.rb: -------------------------------------------------------------------------------- 1 | require 'net/sftp/protocol/01/base' 2 | require 'net/sftp/protocol/02/base' 3 | require 'net/sftp/protocol/03/base' 4 | require 'net/sftp/protocol/04/base' 5 | require 'net/sftp/protocol/05/base' 6 | require 'net/sftp/protocol/06/base' 7 | 8 | module Net; module SFTP 9 | 10 | # The Protocol module contains the definitions for all supported SFTP 11 | # protocol versions. 12 | module Protocol 13 | 14 | # Instantiates and returns a new protocol driver instance for the given 15 | # protocol version. +session+ must be a valid SFTP session object, and 16 | # +version+ must be an integer. If an unsupported version is given, 17 | # an exception will be raised. 18 | def self.load(session, version) 19 | case version 20 | when 1 then V01::Base.new(session) 21 | when 2 then V02::Base.new(session) 22 | when 3 then V03::Base.new(session) 23 | when 4 then V04::Base.new(session) 24 | when 5 then V05::Base.new(session) 25 | when 6 then V06::Base.new(session) 26 | else raise NotImplementedError, "unsupported SFTP version #{version.inspect}" 27 | end 28 | end 29 | 30 | end 31 | 32 | end; end -------------------------------------------------------------------------------- /lib/net/sftp/protocol/01/attributes.rb: -------------------------------------------------------------------------------- 1 | require 'net/ssh/buffer' 2 | 3 | module Net; module SFTP; module Protocol; module V01 4 | 5 | # A class representing the attributes of a file or directory on the server. 6 | # It may be used to specify new attributes, or to query existing attributes. 7 | # 8 | # To specify new attributes, just pass a hash as the argument to the 9 | # constructor. The following keys are supported: 10 | # 11 | # * :size:: the size of the file 12 | # * :uid:: the user-id that owns the file (integer) 13 | # * :gid:: the group-id that owns the file (integer) 14 | # * :owner:: the name of the user that owns the file (string) 15 | # * :group:: the name of the group that owns the file (string) 16 | # * :permissions:: the permissions on the file (integer, e.g. 0755) 17 | # * :atime:: the access time of the file (integer, seconds since epoch) 18 | # * :mtime:: the modification time of the file (integer, seconds since epoch) 19 | # * :extended:: a hash of name/value pairs identifying extended info 20 | # 21 | # Likewise, when the server sends an Attributes object, all of the 22 | # above attributes are exposed as methods (though not all will be set with 23 | # non-nil values from the server). 24 | class Attributes 25 | 26 | F_SIZE = 0x00000001 27 | F_UIDGID = 0x00000002 28 | F_PERMISSIONS = 0x00000004 29 | F_ACMODTIME = 0x00000008 30 | F_EXTENDED = 0x80000000 31 | 32 | T_REGULAR = 1 33 | T_DIRECTORY = 2 34 | T_SYMLINK = 3 35 | T_SPECIAL = 4 36 | T_UNKNOWN = 5 37 | T_SOCKET = 6 38 | T_CHAR_DEVICE = 7 39 | T_BLOCK_DEVICE = 8 40 | T_FIFO = 9 41 | 42 | class < packet.read_string } 32 | end 33 | 34 | # Parses the given FXP_STATUS packet and returns a hash with one key, 35 | # :code, which references the status code returned by the server. 36 | def parse_status_packet(packet) 37 | { :code => packet.read_long } 38 | end 39 | 40 | # Parses the given FXP_DATA packet and returns a hash with one key, 41 | # :data, which references the data returned in the packet. 42 | def parse_data_packet(packet) 43 | { :data => packet.read_string } 44 | end 45 | 46 | # Parses the given FXP_ATTRS packet and returns a hash with one key, 47 | # :attrs, which references an Attributes object. 48 | def parse_attrs_packet(packet) 49 | { :attrs => attribute_factory.from_buffer(packet) } 50 | end 51 | 52 | # Parses the given FXP_NAME packet and returns a hash with one key, :names, 53 | # which references an array of Name objects. 54 | def parse_name_packet(packet) 55 | names = [] 56 | 57 | packet.read_long.times do 58 | filename = packet.read_string 59 | longname = packet.read_string 60 | attrs = attribute_factory.from_buffer(packet) 61 | names << name_factory.new(filename, longname, attrs) 62 | end 63 | 64 | { :names => names } 65 | end 66 | 67 | # Sends a FXP_OPEN packet to the server and returns the packet identifier. 68 | # The +flags+ parameter is either an integer (in which case it must be 69 | # a combination of the IO constants) or a string (in which case it must 70 | # be one of the mode strings that IO::open accepts). The +options+ 71 | # parameter is a hash that is used to construct a new Attribute object, 72 | # to pass as part of the FXP_OPEN request. 73 | def open(path, flags, options) 74 | flags = normalize_open_flags(flags) 75 | 76 | if flags & (IO::WRONLY | IO::RDWR) != 0 77 | sftp_flags = FV1::WRITE 78 | sftp_flags |= FV1::READ if flags & IO::RDWR != 0 79 | sftp_flags |= FV1::APPEND if flags & IO::APPEND != 0 80 | else 81 | sftp_flags = FV1::READ 82 | end 83 | 84 | sftp_flags |= FV1::CREAT if flags & IO::CREAT != 0 85 | sftp_flags |= FV1::TRUNC if flags & IO::TRUNC != 0 86 | sftp_flags |= FV1::EXCL if flags & IO::EXCL != 0 87 | 88 | attributes = attribute_factory.new(options) 89 | 90 | send_request(FXP_OPEN, :string, path, :long, sftp_flags, :raw, attributes.to_s) 91 | end 92 | 93 | # Sends a FXP_CLOSE packet to the server for the given +handle+ (such as 94 | # would be returned via a FXP_HANDLE packet). Returns the new packet id. 95 | def close(handle) 96 | send_request(FXP_CLOSE, :string, handle) 97 | end 98 | 99 | # Sends a FXP_READ packet to the server, requesting that +length+ bytes 100 | # be read from the file identified by +handle+, starting at +offset+ bytes 101 | # within the file. The handle must be one that was returned via a 102 | # FXP_HANDLE packet. Returns the new packet id. 103 | def read(handle, offset, length) 104 | send_request(FXP_READ, :string, handle, :int64, offset, :long, length) 105 | end 106 | 107 | # Sends a FXP_WRITE packet to the server, requesting that +data+ (a string), 108 | # be written to the file identified by +handle+, starting at +offset+ bytes 109 | # from the beginning of the file. The handle must be one that was returned 110 | # via a FXP_HANDLE packet. Returns the new packet id. 111 | def write(handle, offset, data) 112 | send_request(FXP_WRITE, :string, handle, :int64, offset, :string, data) 113 | end 114 | 115 | # Sends a FXP_LSTAT packet to the server, requesting a FXP_ATTR response 116 | # for the file at the given remote +path+ (a string). The +flags+ parameter 117 | # is ignored in this version of the protocol. #lstat will not follow 118 | # symbolic links; see #stat for a version that will. 119 | def lstat(path, flags=nil) 120 | send_request(FXP_LSTAT, :string, path) 121 | end 122 | 123 | # Sends a FXP_FSTAT packet to the server, requesting a FXP_ATTR response 124 | # for the file represented by the given +handle+ (which must have been 125 | # obtained from a FXP_HANDLE packet). The +flags+ parameter is ignored in 126 | # this version of the protocol. 127 | def fstat(handle, flags=nil) 128 | send_request(FXP_FSTAT, :string, handle) 129 | end 130 | 131 | # Sends a FXP_SETSTAT packet to the server, to update the attributes for 132 | # the file at the given remote +path+ (a string). The +attrs+ parameter is 133 | # a hash that defines the attributes to set. 134 | def setstat(path, attrs) 135 | send_request(FXP_SETSTAT, :string, path, :raw, attribute_factory.new(attrs).to_s) 136 | end 137 | 138 | # Sends a FXP_FSETSTAT packet to the server, to update the attributes for 139 | # the file represented by the given +handle+ (which must have been obtained 140 | # from a FXP_HANDLE packet). The +attrs+ parameter is a hash that defines 141 | # the attributes to set. 142 | def fsetstat(handle, attrs) 143 | send_request(FXP_FSETSTAT, :string, handle, :raw, attribute_factory.new(attrs).to_s) 144 | end 145 | 146 | # Sends a FXP_OPENDIR packet to the server, to request a handle for 147 | # manipulating the directory at the given remote +path+. 148 | def opendir(path) 149 | send_request(FXP_OPENDIR, :string, path) 150 | end 151 | 152 | # Sends a FXP_READDIR packet to the server, to request a batch of 153 | # directory name entries in the directory identified by +handle+ (which 154 | # must have been obtained via a FXP_OPENDIR request). 155 | def readdir(handle) 156 | send_request(FXP_READDIR, :string, handle) 157 | end 158 | 159 | # Sends a FXP_REMOTE packet to the server, to request that the given 160 | # file be deleted from the remote server. 161 | def remove(filename) 162 | send_request(FXP_REMOVE, :string, filename) 163 | end 164 | 165 | # Sends a FXP_MKDIR packet to the server, to request that a new directory 166 | # at +path+ on the remote server be created, and with +attrs+ (a hash) 167 | # describing the attributes of the new directory. 168 | def mkdir(path, attrs) 169 | send_request(FXP_MKDIR, :string, path, :raw, attribute_factory.new(attrs).to_s) 170 | end 171 | 172 | # Sends a FXP_RMDIR packet to the server, to request that the directory 173 | # at +path+ on the remote server be deleted. 174 | def rmdir(path) 175 | send_request(FXP_RMDIR, :string, path) 176 | end 177 | 178 | # Sends a FXP_REALPATH packet to the server, to request that the given 179 | # +path+ be canonicalized, taking into account path segments like "..". 180 | def realpath(path) 181 | send_request(FXP_REALPATH, :string, path) 182 | end 183 | 184 | # Sends a FXP_STAT packet to the server, requesting a FXP_ATTR response 185 | # for the file at the given remote +path+ (a string). The +flags+ parameter 186 | # is ignored in this version of the protocol. #stat will follow 187 | # symbolic links; see #lstat for a version that will not. 188 | def stat(path, flags=nil) 189 | send_request(FXP_STAT, :string, path) 190 | end 191 | 192 | # Not implemented in version 1 of the SFTP protocol. Raises a 193 | # NotImplementedError if called. 194 | def rename(name, new_name, flags=nil) 195 | not_implemented! :rename 196 | end 197 | 198 | # Not implemented in version 1 of the SFTP protocol. Raises a 199 | # NotImplementedError if called. 200 | def readlink(path) 201 | not_implemented! :readlink 202 | end 203 | 204 | # Not implemented in version 1 of the SFTP protocol. Raises a 205 | # NotImplementedError if called. 206 | def symlink(path, target) 207 | not_implemented! :symlink 208 | end 209 | 210 | # Not implemented in version 1 of the SFTP protocol. Raises a 211 | # NotImplementedError if called. 212 | def link(*args) 213 | not_implemented! :link 214 | end 215 | 216 | # Not implemented in version 1 of the SFTP protocol. Raises a 217 | # NotImplementedError if called. 218 | def block(handle, offset, length, mask) 219 | not_implemented! :block 220 | end 221 | 222 | # Not implemented in version 1 of the SFTP protocol. Raises a 223 | # NotImplementedError if called. 224 | def unblock(handle, offset, length) 225 | not_implemented! :unblock 226 | end 227 | 228 | protected 229 | 230 | # A helper method for implementing wrappers for operations that are 231 | # not implemented by the current SFTP protocol version. Simply raises 232 | # NotImplementedError with a message based on the given operation name. 233 | def not_implemented!(operation) 234 | raise NotImplementedError, "the #{operation} operation is not available in the version of the SFTP protocol supported by your server" 235 | end 236 | 237 | # Normalizes the given flags parameter, converting it into a combination 238 | # of IO constants. 239 | def normalize_open_flags(flags) 240 | if String === flags 241 | case flags.tr("b", "") 242 | when "r" then IO::RDONLY 243 | when "r+" then IO::RDWR 244 | when "w" then IO::WRONLY | IO::TRUNC | IO::CREAT 245 | when "w+" then IO::RDWR | IO::TRUNC | IO::CREAT 246 | when "a" then IO::APPEND | IO::CREAT | IO::WRONLY 247 | when "a+" then IO::APPEND | IO::CREAT | IO::RDWR 248 | else raise ArgumentError, "unsupported flags: #{flags.inspect}" 249 | end 250 | else 251 | flags.to_i 252 | end 253 | end 254 | 255 | # Returns the Attributes class used by this version of the protocol 256 | # (Net::SFTP::Protocol::V01::Attributes, in this case) 257 | def attribute_factory 258 | V01::Attributes 259 | end 260 | 261 | # Returns the Name class used by this version of the protocol 262 | # (Net::SFTP::Protocol::V01::Name, in this case) 263 | def name_factory 264 | V01::Name 265 | end 266 | end 267 | 268 | end; end; end; end -------------------------------------------------------------------------------- /lib/net/sftp/protocol/01/name.rb: -------------------------------------------------------------------------------- 1 | module Net; module SFTP; module Protocol; module V01 2 | 3 | # Represents a single named item on the remote server. This includes the 4 | # name, attributes about the item, and the "longname", which is intended 5 | # for use when displaying directory data, and has no specified format. 6 | class Name 7 | # The name of the item on the remote server. 8 | attr_reader :name 9 | 10 | # The display-ready name of the item, possibly with other attributes. 11 | attr_reader :longname 12 | 13 | # The Attributes object describing this item. 14 | attr_reader :attributes 15 | 16 | # Create a new Name object with the given name, longname, and attributes. 17 | def initialize(name, longname, attributes) 18 | @name, @longname, @attributes = name, longname, attributes 19 | end 20 | 21 | # Returns +true+ if the item appears to be a directory. It does this by 22 | # examining the attributes. If there is insufficient information in the 23 | # attributes, this will return nil, rather than a boolean. 24 | def directory? 25 | attributes.directory? 26 | end 27 | 28 | # Returns +true+ if the item appears to be a symlink. It does this by 29 | # examining the attributes. If there is insufficient information in the 30 | # attributes, this will return nil, rather than a boolean. 31 | def symlink? 32 | attributes.symlink? 33 | end 34 | 35 | # Returns +true+ if the item appears to be a regular file. It does this by 36 | # examining the attributes. If there is insufficient information in the 37 | # attributes, this will return nil, rather than a boolean. 38 | def file? 39 | attributes.file? 40 | end 41 | end 42 | 43 | end; end; end; end -------------------------------------------------------------------------------- /lib/net/sftp/protocol/02/base.rb: -------------------------------------------------------------------------------- 1 | require 'net/sftp/protocol/01/base' 2 | 3 | module Net; module SFTP; module Protocol; module V02 4 | 5 | # Wraps the low-level SFTP calls for version 2 of the SFTP protocol. 6 | # 7 | # None of these protocol methods block--all of them return immediately, 8 | # requiring the SSH event loop to be run while the server response is 9 | # pending. 10 | # 11 | # You will almost certainly never need to use this driver directly. Please 12 | # see Net::SFTP::Session for the recommended interface. 13 | class Base < V01::Base 14 | 15 | # Returns the protocol version implemented by this driver. (2, in this 16 | # case) 17 | def version 18 | 2 19 | end 20 | 21 | # Sends a FXP_RENAME packet to the server to request that the file or 22 | # directory with the given +name+ (must be a full path) be changed to 23 | # +new_name+ (which must also be a path). The +flags+ parameter is 24 | # ignored in this version of the protocol. 25 | def rename(name, new_name, flags=nil) 26 | send_request(FXP_RENAME, :string, name, :string, new_name) 27 | end 28 | 29 | end 30 | 31 | end; end; end; end 32 | -------------------------------------------------------------------------------- /lib/net/sftp/protocol/03/base.rb: -------------------------------------------------------------------------------- 1 | require 'net/sftp/protocol/02/base' 2 | 3 | module Net; module SFTP; module Protocol; module V03 4 | 5 | # Wraps the low-level SFTP calls for version 3 of the SFTP protocol. 6 | # 7 | # None of these protocol methods block--all of them return immediately, 8 | # requiring the SSH event loop to be run while the server response is 9 | # pending. 10 | # 11 | # You will almost certainly never need to use this driver directly. Please 12 | # see Net::SFTP::Session for the recommended interface. 13 | class Base < V02::Base 14 | 15 | # Returns the protocol version implemented by this driver. (3, in this 16 | # case) 17 | def version 18 | 3 19 | end 20 | 21 | # Sends a FXP_READLINK packet to the server to request that the target of 22 | # the given symlink on the remote host (+path+) be returned. 23 | def readlink(path) 24 | send_request(FXP_READLINK, :string, path) 25 | end 26 | 27 | # Sends a FXP_SYMLINK packet to the server to request that a symlink at the 28 | # given +path+ be created, pointing at +target+.. 29 | def symlink(path, target) 30 | send_request(FXP_SYMLINK, :string, path, :string, target) 31 | end 32 | 33 | end 34 | 35 | end; end; end; end -------------------------------------------------------------------------------- /lib/net/sftp/protocol/04/attributes.rb: -------------------------------------------------------------------------------- 1 | require 'net/sftp/protocol/01/attributes' 2 | 3 | module Net; module SFTP; module Protocol; module V04 4 | 5 | # A class representing the attributes of a file or directory on the server. 6 | # It may be used to specify new attributes, or to query existing attributes. 7 | # This particular class is specific to versions 4 and 5 of the SFTP 8 | # protocol. 9 | # 10 | # To specify new attributes, just pass a hash as the argument to the 11 | # constructor. The following keys are supported: 12 | # 13 | # * :type:: the type of the item (integer, one of the T_ constants) 14 | # * :size:: the size of the item (integer) 15 | # * :uid:: the user-id that owns the file (integer) 16 | # * :gid:: the group-id that owns the file (integer) 17 | # * :owner:: the name of the user that owns the file (string) 18 | # * :group:: the name of the group that owns the file (string) 19 | # * :permissions:: the permissions on the file (integer, e.g. 0755) 20 | # * :atime:: the access time of the file (integer, seconds since epoch) 21 | # * :atime_nseconds:: the nanosecond component of atime (integer) 22 | # * :createtime:: the time at which the file was created (integer, seconds since epoch) 23 | # * :createtime_nseconds:: the nanosecond component of createtime (integer) 24 | # * :mtime:: the modification time of the file (integer, seconds since epoch) 25 | # * :mtime_nseconds:: the nanosecond component of mtime (integer) 26 | # * :acl:: an array of ACL entries for the item 27 | # * :extended:: a hash of name/value pairs identifying extended info 28 | # 29 | # Likewise, when the server sends an Attributes object, all of the 30 | # above attributes are exposed as methods (though not all will be set with 31 | # non-nil values from the server). 32 | class Attributes < V01::Attributes 33 | 34 | F_ACCESSTIME = 0x00000008 35 | F_CREATETIME = 0x00000010 36 | F_MODIFYTIME = 0x00000020 37 | F_ACL = 0x00000040 38 | F_OWNERGROUP = 0x00000080 39 | F_SUBSECOND_TIMES = 0x00000100 40 | 41 | # A simple struct for representing a single entry in an Access Control 42 | # List. (See Net::SFTP::Constants::ACE) 43 | ACL = Struct.new(:type, :flag, :mask, :who) 44 | 45 | class < names } 38 | end 39 | 40 | # Sends a FXP_STAT packet to the server for the given +path+, and with the 41 | # given +flags+. If +flags+ is nil, it defaults to F_SIZE | F_PERMISSIONS | 42 | # F_ACCESSTIME | F_CREATETIME | F_MODIFYTIME | F_ACL | F_OWNERGROUP | 43 | # F_SUBSECOND_TIMES | F_EXTENDED (see Net::SFTP::Protocol::V04::Attributes 44 | # for those constants). 45 | def stat(path, flags=nil) 46 | send_request(FXP_STAT, :string, path, :long, flags || DEFAULT_FLAGS) 47 | end 48 | 49 | # Sends a FXP_LSTAT packet to the server for the given +path+, and with the 50 | # given +flags+. If +flags+ is nil, it defaults to F_SIZE | F_PERMISSIONS | 51 | # F_ACCESSTIME | F_CREATETIME | F_MODIFYTIME | F_ACL | F_OWNERGROUP | 52 | # F_SUBSECOND_TIMES | F_EXTENDED (see Net::SFTP::Protocol::V04::Attributes 53 | # for those constants). 54 | def lstat(path, flags=nil) 55 | send_request(FXP_LSTAT, :string, path, :long, flags || DEFAULT_FLAGS) 56 | end 57 | 58 | # Sends a FXP_FSTAT packet to the server for the given +path+, and with the 59 | # given +flags+. If +flags+ is nil, it defaults to F_SIZE | F_PERMISSIONS | 60 | # F_ACCESSTIME | F_CREATETIME | F_MODIFYTIME | F_ACL | F_OWNERGROUP | 61 | # F_SUBSECOND_TIMES | F_EXTENDED (see Net::SFTP::Protocol::V04::Attributes 62 | # for those constants). 63 | def fstat(handle, flags=nil) 64 | send_request(FXP_FSTAT, :string, handle, :long, flags || DEFAULT_FLAGS) 65 | end 66 | 67 | protected 68 | 69 | # The default flags used if the +flags+ parameter is nil for any of the 70 | # #stat, #lstat, or #fstat operations. 71 | DEFAULT_FLAGS = Attributes::F_SIZE | 72 | Attributes::F_PERMISSIONS | 73 | Attributes::F_ACCESSTIME | 74 | Attributes::F_CREATETIME | 75 | Attributes::F_MODIFYTIME | 76 | Attributes::F_ACL | 77 | Attributes::F_OWNERGROUP | 78 | Attributes::F_SUBSECOND_TIMES | 79 | Attributes::F_EXTENDED 80 | 81 | # Returns the Attributes class used by this version of the protocol 82 | # (Net::SFTP::Protocol::V04::Attributes, in this case) 83 | def attribute_factory 84 | V04::Attributes 85 | end 86 | 87 | # Returns the Name class used by this version of the protocol 88 | # (Net::SFTP::Protocol::V04::Name, in this case) 89 | def name_factory 90 | V04::Name 91 | end 92 | end 93 | 94 | end; end; end; end -------------------------------------------------------------------------------- /lib/net/sftp/protocol/04/name.rb: -------------------------------------------------------------------------------- 1 | module Net; module SFTP; module Protocol; module V04 2 | 3 | # Represents a single named item on the remote server. This includes the 4 | # name, and attributes about the item, and the "longname". 5 | # 6 | # For backwards compatibility with the format and interface of the Name 7 | # structure from previous protocol versions, this also exposes a #longname 8 | # method, which returns a string that can be used to display this item in 9 | # a directory listing. 10 | class Name 11 | # The name of the item on the remote server. 12 | attr_reader :name 13 | 14 | # Attributes instance describing this item. 15 | attr_reader :attributes 16 | 17 | # Create a new Name object with the given name and attributes. 18 | def initialize(name, attributes) 19 | @name, @attributes = name, attributes 20 | end 21 | 22 | # Returns +true+ if the item is a directory. 23 | def directory? 24 | attributes.directory? 25 | end 26 | 27 | # Returns +true+ if the item is a symlink. 28 | def symlink? 29 | attributes.symlink? 30 | end 31 | 32 | # Returns +true+ if the item is a regular file. 33 | def file? 34 | attributes.file? 35 | end 36 | 37 | # Returns a string representing this file, in a format similar to that 38 | # used by the unix "ls" utility. 39 | def longname 40 | @longname ||= begin 41 | longname = if directory? 42 | "d" 43 | elsif symlink? 44 | "l" 45 | else 46 | "-" 47 | end 48 | 49 | longname << (attributes.permissions & 0400 != 0 ? "r" : "-") 50 | longname << (attributes.permissions & 0200 != 0 ? "w" : "-") 51 | longname << (attributes.permissions & 0100 != 0 ? "x" : "-") 52 | longname << (attributes.permissions & 0040 != 0 ? "r" : "-") 53 | longname << (attributes.permissions & 0020 != 0 ? "w" : "-") 54 | longname << (attributes.permissions & 0010 != 0 ? "x" : "-") 55 | longname << (attributes.permissions & 0004 != 0 ? "r" : "-") 56 | longname << (attributes.permissions & 0002 != 0 ? "w" : "-") 57 | longname << (attributes.permissions & 0001 != 0 ? "x" : "-") 58 | 59 | longname << (" %-8s %-8s %8d " % [attributes.owner, attributes.group, attributes.size]) 60 | 61 | longname << Time.at(attributes.mtime).strftime("%b %e %H:%M ") 62 | longname << name 63 | end 64 | end 65 | end 66 | 67 | end; end; end; end -------------------------------------------------------------------------------- /lib/net/sftp/protocol/05/base.rb: -------------------------------------------------------------------------------- 1 | require 'net/sftp/protocol/04/base' 2 | 3 | module Net; module SFTP; module Protocol; module V05 4 | 5 | # Wraps the low-level SFTP calls for version 5 of the SFTP protocol. 6 | # 7 | # None of these protocol methods block--all of them return immediately, 8 | # requiring the SSH event loop to be run while the server response is 9 | # pending. 10 | # 11 | # You will almost certainly never need to use this driver directly. Please 12 | # see Net::SFTP::Session for the recommended interface. 13 | class Base < V04::Base 14 | # Returns the protocol version implemented by this driver. (5, in this 15 | # case) 16 | def version 17 | 5 18 | end 19 | 20 | # Sends a FXP_RENAME packet to the server to request that the file or 21 | # directory with the given +name+ (must be a full path) be changed to 22 | # +new_name+ (which must also be a path). The +flags+ parameter must be 23 | # either +nil+ or 0 (the default), or some combination of the 24 | # Net::SFTP::Constants::RenameFlags constants. 25 | def rename(name, new_name, flags=nil) 26 | send_request(FXP_RENAME, :string, name, :string, new_name, :long, flags || 0) 27 | end 28 | 29 | # Sends a FXP_OPEN packet to the server and returns the packet identifier. 30 | # The +flags+ parameter is either an integer (in which case it must be 31 | # a combination of the IO constants) or a string (in which case it must 32 | # be one of the mode strings that IO::open accepts). The +options+ 33 | # parameter is a hash that is used to construct a new Attribute object, 34 | # to pass as part of the FXP_OPEN request. 35 | def open(path, flags, options) 36 | flags = normalize_open_flags(flags) 37 | 38 | sftp_flags, desired_access = if flags & (IO::WRONLY | IO::RDWR) != 0 39 | open = if flags & (IO::CREAT | IO::EXCL) == (IO::CREAT | IO::EXCL) 40 | FV5::CREATE_NEW 41 | elsif flags & (IO::CREAT | IO::TRUNC) == (IO::CREAT | IO::TRUNC) 42 | FV5::CREATE_TRUNCATE 43 | elsif flags & IO::CREAT == IO::CREAT 44 | FV5::OPEN_OR_CREATE 45 | else 46 | FV5::OPEN_EXISTING 47 | end 48 | access = ACE::Mask::WRITE_DATA | ACE::Mask::WRITE_ATTRIBUTES 49 | access |= ACE::Mask::READ_DATA | ACE::Mask::READ_ATTRIBUTES if (flags & IO::RDWR) == IO::RDWR 50 | if flags & IO::APPEND == IO::APPEND 51 | open |= FV5::APPEND_DATA 52 | access |= ACE::Mask::APPEND_DATA 53 | end 54 | [open, access] 55 | else 56 | [FV5::OPEN_EXISTING, ACE::Mask::READ_DATA | ACE::Mask::READ_ATTRIBUTES] 57 | end 58 | 59 | attributes = attribute_factory.new(options) 60 | 61 | send_request(FXP_OPEN, :string, path, :long, desired_access, :long, sftp_flags, :raw, attributes.to_s) 62 | end 63 | 64 | end 65 | 66 | end; end; end; end -------------------------------------------------------------------------------- /lib/net/sftp/protocol/06/attributes.rb: -------------------------------------------------------------------------------- 1 | require 'net/sftp/protocol/04/attributes' 2 | 3 | module Net; module SFTP; module Protocol; module V06 4 | 5 | # A class representing the attributes of a file or directory on the server. 6 | # It may be used to specify new attributes, or to query existing attributes. 7 | # This particular class is specific to versions 6 and higher of the SFTP 8 | # protocol. 9 | # 10 | # To specify new attributes, just pass a hash as the argument to the 11 | # constructor. The following keys are supported: 12 | # 13 | # * :type:: the type of the item (integer, one of the T_ constants) 14 | # * :size:: the size of the item (integer) 15 | # * :allocation_size:: the actual number of bytes that the item uses on disk (integer) 16 | # * :uid:: the user-id that owns the file (integer) 17 | # * :gid:: the group-id that owns the file (integer) 18 | # * :owner:: the name of the user that owns the file (string) 19 | # * :group:: the name of the group that owns the file (string) 20 | # * :permissions:: the permissions on the file (integer, e.g. 0755) 21 | # * :atime:: the access time of the file (integer, seconds since epoch) 22 | # * :atime_nseconds:: the nanosecond component of atime (integer) 23 | # * :createtime:: the time at which the file was created (integer, seconds since epoch) 24 | # * :createtime_nseconds:: the nanosecond component of createtime (integer) 25 | # * :mtime:: the modification time of the file (integer, seconds since epoch) 26 | # * :mtime_nseconds:: the nanosecond component of mtime (integer) 27 | # * :ctime:: the time that the file's attributes were last changed (integer) 28 | # * :ctime_nseconds:: the nanosecond component of ctime (integer) 29 | # * :acl:: an array of ACL entries for the item 30 | # * :attrib_bits:: other attributes of the file or directory (as a bit field) (integer) 31 | # * :attrib_bits_valid:: a mask describing which bits in attrib_bits are valid (integer) 32 | # * :text_hint:: whether the file may or may not contain textual data (integer) 33 | # * :mime_type:: the mime type of the file (string) 34 | # * :link_count:: the hard link count of the file (integer) 35 | # * :untranslated_name:: the value of the filename before filename translation was attempted (string) 36 | # * :extended:: a hash of name/value pairs identifying extended info 37 | # 38 | # Likewise, when the server sends an Attributes object, all of the 39 | # above attributes are exposed as methods (though not all will be set with 40 | # non-nil values from the server). 41 | class Attributes < V04::Attributes 42 | F_BITS = 0x00000200 43 | F_ALLOCATION_SIZE = 0x00000400 44 | F_TEXT_HINT = 0x00000800 45 | F_MIME_TYPE = 0x00001000 46 | F_LINK_COUNT = 0x00002000 47 | F_UNTRANSLATED_NAME = 0x00004000 48 | F_CTIME = 0x00008000 49 | 50 | # The array of elements that describe this structure, in order. Used when 51 | # parsing and serializing attribute objects. 52 | def self.elements #:nodoc: 53 | @elements ||= [ 54 | [:type, :byte, 0], 55 | [:size, :int64, F_SIZE], 56 | [:allocation_size, :int64, F_ALLOCATION_SIZE], 57 | [:owner, :string, F_OWNERGROUP], 58 | [:group, :string, F_OWNERGROUP], 59 | [:permissions, :long, F_PERMISSIONS], 60 | [:atime, :int64, F_ACCESSTIME], 61 | [:atime_nseconds, :long, F_ACCESSTIME | F_SUBSECOND_TIMES], 62 | [:createtime, :int64, F_CREATETIME], 63 | [:createtime_nseconds, :long, F_CREATETIME | F_SUBSECOND_TIMES], 64 | [:mtime, :int64, F_MODIFYTIME], 65 | [:mtime_nseconds, :long, F_MODIFYTIME | F_SUBSECOND_TIMES], 66 | [:ctime, :int64, F_CTIME], 67 | [:ctime_nseconds, :long, F_CTIME | F_SUBSECOND_TIMES], 68 | [:acl, :special, F_ACL], 69 | [:attrib_bits, :long, F_BITS], 70 | [:attrib_bits_valid, :long, F_BITS], 71 | [:text_hint, :byte, F_TEXT_HINT], 72 | [:mime_type, :string, F_MIME_TYPE], 73 | [:link_count, :long, F_LINK_COUNT], 74 | [:untranslated_name, :string, F_UNTRANSLATED_NAME], 75 | [:extended, :special, F_EXTENDED] 76 | ] 77 | end 78 | 79 | # The size on-disk of the file 80 | attr_accessor :allocation_size 81 | 82 | # The time at which the file's attributes were last changed 83 | attr_accessor :ctime 84 | 85 | # The nanosecond component of #ctime 86 | attr_accessor :ctime_nseconds 87 | 88 | # Other attributes of this file or directory (as a bit field) 89 | attr_accessor :attrib_bits 90 | 91 | # A bit mask describing which bits in #attrib_bits are valid 92 | attr_accessor :attrib_bits_valid 93 | 94 | # Describes whether the file may or may not contain textual data 95 | attr_accessor :text_hint 96 | 97 | # The mime-type of the file 98 | attr_accessor :mime_type 99 | 100 | # The hard link count for the file 101 | attr_accessor :link_count 102 | 103 | # The value of the file name before filename translation was attempted 104 | attr_accessor :untranslated_name 105 | end 106 | 107 | end; end; end; end -------------------------------------------------------------------------------- /lib/net/sftp/protocol/06/base.rb: -------------------------------------------------------------------------------- 1 | require 'net/sftp/protocol/05/base' 2 | require 'net/sftp/protocol/06/attributes' 3 | 4 | module Net; module SFTP; module Protocol; module V06 5 | 6 | # Wraps the low-level SFTP calls for version 6 of the SFTP protocol. 7 | # 8 | # None of these protocol methods block--all of them return immediately, 9 | # requiring the SSH event loop to be run while the server response is 10 | # pending. 11 | # 12 | # You will almost certainly never need to use this driver directly. Please 13 | # see Net::SFTP::Session for the recommended interface. 14 | class Base < V05::Base 15 | 16 | # Returns the protocol version implemented by this driver. (6, in this 17 | # case) 18 | def version 19 | 6 20 | end 21 | 22 | # Sends a FXP_LINK packet to the server to request that a link be created 23 | # at +new_link_path+, pointing to +existing_path+. If +symlink+ is true, a 24 | # symbolic link will be created; otherwise a hard link will be created. 25 | def link(new_link_path, existing_path, symlink) 26 | send_request(FXP_LINK, :string, new_link_path, :string, existing_path, :bool, symlink) 27 | end 28 | 29 | # Provided for backwards compatibility; v6 of the SFTP protocol removes the 30 | # older FXP_SYMLINK packet type, so this method simply calls the #link 31 | # method. 32 | def symlink(path, target) 33 | link(path, target, true) 34 | end 35 | 36 | # Sends a FXP_BLOCK packet to the server to request that a byte-range lock 37 | # be obtained on the given +handle+, for the given byte +offset+ and 38 | # +length+. The +mask+ parameter is a bitfield indicating what kind of 39 | # lock to acquire, and must be a combination of one or more of the 40 | # Net::SFTP::Constants::LockTypes constants. 41 | def block(handle, offset, length, mask) 42 | send_request(FXP_BLOCK, :string, handle, :int64, offset, :int64, length, :long, mask) 43 | end 44 | 45 | # Sends a FXP_UNBLOCK packet to the server to request that a previously 46 | # acquired byte-range lock be released on the given +handle+, for the 47 | # given byte +offset+ and +length+. The +handle+, +offset+, and +length+ 48 | # must all exactly match the parameters that were given when the lock was 49 | # originally acquired (see #block). 50 | def unblock(handle, offset, length) 51 | send_request(FXP_UNBLOCK, :string, handle, :int64, offset, :int64, length) 52 | end 53 | 54 | protected 55 | 56 | # Returns the Attributes class used by this version of the protocol 57 | # (Net::SFTP::Protocol::V06::Attributes, in this case) 58 | def attribute_factory 59 | V06::Attributes 60 | end 61 | end 62 | 63 | end; end; end; end -------------------------------------------------------------------------------- /lib/net/sftp/protocol/base.rb: -------------------------------------------------------------------------------- 1 | require 'net/ssh/loggable' 2 | require 'net/sftp/constants' 3 | 4 | module Net; module SFTP; module Protocol 5 | 6 | # The abstract superclass of the specific implementations for each supported 7 | # SFTP protocol version. It implements general packet parsing logic, and 8 | # provides a way for subclasses to send requests. 9 | class Base 10 | include Net::SSH::Loggable 11 | include Net::SFTP::Constants 12 | include Net::SFTP::Constants::PacketTypes 13 | 14 | # The SFTP session object that acts as client to this protocol instance 15 | attr_reader :session 16 | 17 | # Create a new instance of a protocol driver, servicing the given session. 18 | def initialize(session) 19 | @session = session 20 | self.logger = session.logger 21 | @request_id_counter = -1 22 | end 23 | 24 | # Attept to parse the given packet. If the packet is of an unsupported 25 | # type, an exception will be raised. Returns the parsed data as a hash 26 | # (the keys in the hash are packet-type specific). 27 | def parse(packet) 28 | case packet.type 29 | when FXP_STATUS then parse_status_packet(packet) 30 | when FXP_HANDLE then parse_handle_packet(packet) 31 | when FXP_DATA then parse_data_packet(packet) 32 | when FXP_NAME then parse_name_packet(packet) 33 | when FXP_ATTRS then parse_attrs_packet(packet) 34 | else raise NotImplementedError, "unknown packet type: #{packet.type}" 35 | end 36 | end 37 | 38 | private 39 | 40 | # Send a new packet of the given type, and with the given data arguments. 41 | # A new request identifier will be allocated to this request, and will 42 | # be returned. 43 | def send_request(type, *args) 44 | @request_id_counter += 1 45 | session.send_packet(type, :long, @request_id_counter, *args) 46 | return @request_id_counter 47 | end 48 | end 49 | 50 | end; end; end -------------------------------------------------------------------------------- /lib/net/sftp/request.rb: -------------------------------------------------------------------------------- 1 | require 'net/sftp/constants' 2 | require 'net/sftp/response' 3 | 4 | module Net; module SFTP 5 | 6 | # Encapsulates a single active SFTP request. This is instantiated 7 | # automatically by the Net::SFTP::Session class when an operation is 8 | # executed. 9 | # 10 | # request = sftp.open("/path/to/file") 11 | # puts request.pending? #-> true 12 | # request.wait 13 | # puts request.pending? #-> false 14 | # result = request.response 15 | class Request 16 | include Constants::PacketTypes 17 | 18 | # The Net::SFTP session object that is servicing this request 19 | attr_reader :session 20 | 21 | # The SFTP packet identifier for this request 22 | attr_reader :id 23 | 24 | # The type of this request (e.g., :open, :symlink, etc.) 25 | attr_reader :type 26 | 27 | # The callback (if any) associated with this request. When the response 28 | # is recieved for this request, the callback will be invoked. 29 | attr_reader :callback 30 | 31 | # The hash of properties associated with this request. Properties allow 32 | # programmers to associate arbitrary data with a request, making state 33 | # machines richer. 34 | attr_reader :properties 35 | 36 | # The response that was received for this request (see Net::SFTP::Response) 37 | attr_reader :response 38 | 39 | # Instantiate a new Request object, serviced by the given +session+, and 40 | # being of the given +type+. The +id+ is the packet identifier for this 41 | # request. 42 | def initialize(session, type, id, &callback) #:nodoc: 43 | @session, @id, @type, @callback = session, id, type, callback 44 | @response = nil 45 | @properties = {} 46 | end 47 | 48 | # Returns the value of property with the given +key+. If +key+ is not a 49 | # symbol, it will be converted to a symbol before lookup. 50 | def [](key) 51 | properties[key.to_sym] 52 | end 53 | 54 | # Sets the value of the property with name +key+ to +value+. If +key+ is 55 | # not a symbol, it will be converted to a symbol before lookup. 56 | def []=(key, value) 57 | properties[key.to_sym] = value 58 | end 59 | 60 | # Returns +true+ if the request is still waiting for a response from the 61 | # server, and +false+ otherwise. The SSH event loop must be run in order 62 | # for a request to be processed; see #wait. 63 | def pending? 64 | session.pending_requests.key?(id) 65 | end 66 | 67 | # Waits (blocks) until the server responds to this packet. If prior 68 | # SFTP packets were also pending, they will be processed as well (since 69 | # SFTP packets are processed in the order in which they are received by 70 | # the server). Returns the request object itself. 71 | def wait 72 | session.loop { pending? } 73 | self 74 | end 75 | 76 | public # but not "published". Internal use only 77 | 78 | # When the server responds to this request, the packet is passed to 79 | # this method, which parses the packet and builds a Net::SFTP::Response 80 | # object to encapsulate it. If a #callback has been provided for this 81 | # request, the callback is invoked with the new response object. 82 | def respond_to(packet) #:nodoc: 83 | data = session.protocol.parse(packet) 84 | data[:type] = packet.type 85 | @response = Response.new(self, data) 86 | 87 | callback.call(@response) if callback 88 | end 89 | end 90 | 91 | end; end -------------------------------------------------------------------------------- /lib/net/sftp/response.rb: -------------------------------------------------------------------------------- 1 | require 'net/sftp/constants' 2 | 3 | module Net; module SFTP 4 | 5 | # Encapsulates a response from the remote server, to a specific client 6 | # request. Response objects are passed as parameters to callbacks when you 7 | # are performing asynchronous operations; when you call Net::SFTP::Request#wait, 8 | # you can get the corresponding response object via Net::SFTP::Request#response. 9 | # 10 | # sftp.open("/path/to/file") do |response| 11 | # p response.ok? 12 | # p response[:handle] 13 | # end 14 | # 15 | # sftp.loop 16 | class Response 17 | include Net::SFTP::Constants::StatusCodes 18 | 19 | # The request object that this object is in response to 20 | attr_reader :request 21 | 22 | # A hash of request-specific data, such as a file handle or attribute information 23 | attr_reader :data 24 | 25 | # The numeric code, one of the FX_* constants 26 | attr_reader :code 27 | 28 | # The textual message for this response (possibly blank) 29 | attr_reader :message 30 | 31 | # Create a new Response object for the given Net::SFTP::Request instance, 32 | # and with the given data. If there is no :code key in the data, the 33 | # code is assumed to be FX_OK. 34 | def initialize(request, data={}) #:nodoc: 35 | @request, @data = request, data 36 | @code, @message = data[:code] || FX_OK, data[:message] 37 | end 38 | 39 | # Retrieve the data item with the given +key+. The key is converted to a 40 | # symbol before being used to lookup the value. 41 | def [](key) 42 | data[key.to_sym] 43 | end 44 | 45 | # Returns a textual description of this response, including the status 46 | # code and name. 47 | def to_s 48 | if message && !message.empty? && message.downcase != MAP[code] 49 | "#{message} (#{MAP[code]}, #{code})" 50 | else 51 | "#{MAP[code]} (#{code})" 52 | end 53 | end 54 | 55 | alias :to_str :to_s 56 | 57 | # Returns +true+ if the status code is FX_OK; +false+ otherwise. 58 | def ok? 59 | code == FX_OK 60 | end 61 | 62 | # Returns +true+ if the status code is FX_EOF; +false+ otherwise. 63 | def eof? 64 | code == FX_EOF 65 | end 66 | 67 | #-- 68 | MAP = constants.inject({}) do |memo, name| 69 | next memo unless name =~ /^FX_(.*)/ 70 | memo[const_get(name)] = $1.downcase.tr("_", " ") 71 | memo 72 | end 73 | #++ 74 | end 75 | 76 | end; end -------------------------------------------------------------------------------- /lib/net/sftp/version.rb: -------------------------------------------------------------------------------- 1 | module Net 2 | module SFTP 3 | # A class for describing the current version of a library. The version 4 | # consists of three parts: the +major+ number, the +minor+ number, and the 5 | # +tiny+ (or +patch+) number. 6 | # 7 | # Two Version instances may be compared, so that you can test that a version 8 | # of a library is what you require: 9 | # 10 | # require 'net/sftp/version' 11 | # 12 | # if Net::SFTP::Version::CURRENT < Net::SFTP::Version[2,1,0] 13 | # abort "your software is too old!" 14 | # end 15 | class Version 16 | include Comparable 17 | 18 | # A convenience method for instantiating a new Version instance with the 19 | # given +major+, +minor+, and +tiny+ components. 20 | def self.[](major, minor, tiny, pre = nil) 21 | new(major, minor, tiny, pre) 22 | end 23 | 24 | attr_reader :major, :minor, :tiny 25 | 26 | # Create a new Version object with the given components. 27 | def initialize(major, minor, tiny, pre = nil) 28 | @major, @minor, @tiny, @pre = major, minor, tiny, pre 29 | end 30 | 31 | # Compare this version to the given +version+ object. 32 | def <=>(version) 33 | to_i <=> version.to_i 34 | end 35 | 36 | # Converts this version object to a string, where each of the three 37 | # version components are joined by the '.' character. E.g., 2.0.0. 38 | def to_s 39 | @to_s ||= [@major, @minor, @tiny, @pre].compact.join(".") 40 | end 41 | 42 | # Converts this version to a canonical integer that may be compared 43 | # against other version objects. 44 | def to_i 45 | @to_i ||= @major * 1_000_000 + @minor * 1_000 + @tiny 46 | end 47 | 48 | # The major component of this version of the Net::SFTP library 49 | MAJOR = 4 50 | 51 | # The minor component of this version of the Net::SFTP library 52 | MINOR = 0 53 | 54 | # The tiny component of this version of the Net::SFTP library 55 | TINY = 0 56 | 57 | # The prerelease component of this version of the Net::SFTP library 58 | # nil allowed 59 | PRE = nil 60 | 61 | # The current version of the Net::SFTP library as a Version instance 62 | CURRENT = new(*[MAJOR, MINOR, TINY, PRE].compact) 63 | 64 | # The current version of the Net::SFTP library as a String 65 | STRING = CURRENT.to_s 66 | end 67 | end 68 | end 69 | -------------------------------------------------------------------------------- /net-sftp-public_cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDQDCCAiigAwIBAgIBATANBgkqhkiG9w0BAQsFADAlMSMwIQYDVQQDDBpuZXRz 3 | c2gvREM9c29sdXRpb3VzL0RDPWNvbTAeFw0yMjA5MjIxMTUwMDJaFw0yMzA5MjIx 4 | MTUwMDJaMCUxIzAhBgNVBAMMGm5ldHNzaC9EQz1zb2x1dGlvdXMvREM9Y29tMIIB 5 | IjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxieE22fR/qmdPKUHyYTyUx2g 6 | wskLwrCkxay+Tvc97ZZUOwf85LDDDPqhQaTWLvRwnIOMgQE2nBPzwalVclK6a+pW 7 | x/18KDeZY15vm3Qn5p42b0wi9hUxOqPm3J2hdCLCcgtENgdX21nVzejn39WVqFJO 8 | lntgSDNW5+kCS8QaRsmIbzj17GKKkrsw39kiQw7FhWfJFeTjddzoZiWwc59KA/Bx 9 | fBbmDnsMLAtAtauMOxORrbx3EOY7sHku/kSrMg3FXFay7jc6BkbbUij+MjJ/k82l 10 | 4o8o0YO4BAnya90xgEmgOG0LCCxRhuXQFnMDuDjK2XnUe0h4/6NCn94C+z9GsQID 11 | AQABo3sweTAJBgNVHRMEAjAAMAsGA1UdDwQEAwIEsDAdBgNVHQ4EFgQUBfKiwO2e 12 | M4NEiRrVG793qEPLYyMwHwYDVR0RBBgwFoEUbmV0c3NoQHNvbHV0aW91cy5jb20w 13 | HwYDVR0SBBgwFoEUbmV0c3NoQHNvbHV0aW91cy5jb20wDQYJKoZIhvcNAQELBQAD 14 | ggEBABI2ORK5kzUL7uOF0EHI4ECMWxQMiN+pURyGp9u7DU0H8eSdZN52jbUGHzSB 15 | j7bB6GpqElEWjOe0IbH3vR52IVXq2bOF4P4vFchGAb4OuzJD8aJmrC/SPLHbWBuV 16 | 2GpbRQRJyYPWN6Rt/4EHOxoFnhXOBEB6CGIy0dt7YezycVbzqtHoiI2Qf/bIFJQZ 17 | mpJAAUBkRiWksE7zrsE5DGK8kL2GVos7f8kdM71zT8p7VBwkMdY277T29TG2xD0D 18 | 66Oev0C3/x89NXqCHkl1JElSzEFbOoxan16z7xNEf2MKcBKGhsYfzWVNyEtJm785 19 | g+97rn/AuO6dcxJnW2qBGYQa7pQ= 20 | -----END CERTIFICATE----- 21 | -------------------------------------------------------------------------------- /net-sftp.gemspec: -------------------------------------------------------------------------------- 1 | require_relative 'lib/net/sftp/version' 2 | 3 | Gem::Specification.new do |spec| 4 | spec.name = "net-sftp" 5 | spec.version = Net::SFTP::Version::STRING 6 | spec.authors = ["Jamis Buck", "Delano Mandelbaum", "Mikl\u{f3}s Fazekas"] 7 | spec.email = ["net-ssh@solutious.com"] 8 | 9 | if ENV['NET_SSH_BUILDGEM_SIGNED'] 10 | spec.cert_chain = ["net-sftp-public_cert.pem"] 11 | spec.signing_key = "/mnt/gem/net-ssh-private_key.pem" 12 | end 13 | 14 | spec.summary = %q{A pure Ruby implementation of the SFTP client protocol.} 15 | spec.description = %q{A pure Ruby implementation of the SFTP client protocol} 16 | spec.homepage = "https://github.com/net-ssh/net-sftp" 17 | spec.license = "MIT" 18 | spec.required_rubygems_version = Gem::Requirement.new(">= 0") if spec.respond_to? :required_rubygems_version= 19 | 20 | spec.extra_rdoc_files = [ 21 | "LICENSE.txt", 22 | "README.rdoc" 23 | ] 24 | 25 | spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) } 26 | spec.bindir = "exe" 27 | spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } 28 | spec.require_paths = ["lib"] 29 | 30 | if spec.respond_to? :specification_version then 31 | spec.specification_version = 3 32 | 33 | if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then 34 | spec.add_runtime_dependency(%q, [">= 5.0.0", "< 8.0.0"]) 35 | spec.add_development_dependency(%q, [">= 5"]) 36 | spec.add_development_dependency(%q, [">= 0"]) 37 | else 38 | spec.add_dependency(%q, [">= 5.0.0", "< 8.0.0"]) 39 | spec.add_dependency(%q, [">= 5"]) 40 | spec.add_dependency(%q, [">= 0"]) 41 | end 42 | else 43 | spec.add_dependency(%q, [">= 5.0.0", "< 8.0.0"]) 44 | spec.add_dependency(%q, [">= 5"]) 45 | spec.add_dependency(%q, [">= 0"]) 46 | spec.add_dependency(%q, [">= 0"]) 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /test/common.rb: -------------------------------------------------------------------------------- 1 | require 'minitest/autorun' 2 | require 'mocha/minitest' 3 | require 'stringio' 4 | 5 | begin 6 | require 'net/ssh' 7 | require 'net/ssh/version' 8 | raise LoadError, "wrong version" unless Net::SSH::Version::STRING >= '1.99.0' 9 | rescue LoadError 10 | begin 11 | gem 'net-ssh', ">= 2.0.0" 12 | require 'net/ssh' 13 | rescue LoadError => e 14 | abort "could not load net/ssh v2 (#{e.inspect})" 15 | end 16 | end 17 | 18 | $LOAD_PATH.unshift "#{File.dirname(__FILE__)}/../lib" 19 | require 'net/sftp' 20 | require 'net/sftp/constants' 21 | require 'net/ssh/test' 22 | 23 | class Net::SFTP::TestCase < Minitest::Test 24 | include Net::SFTP::Constants::PacketTypes 25 | include Net::SSH::Test 26 | 27 | def default_test 28 | # do nothing, this is just hacky-hack to work around Test::Unit's 29 | # insistence that all TestCase subclasses have at least one test 30 | # method defined. 31 | end 32 | 33 | protected 34 | 35 | def raw(*args) 36 | Net::SSH::Buffer.from(*args).to_s 37 | end 38 | 39 | def sftp(options={}, version=nil) 40 | @sftp ||= Net::SFTP::Session.new(connection(options), version) 41 | end 42 | 43 | def expect_sftp_session(opts={}) 44 | story do |session| 45 | channel = session.opens_channel 46 | channel.sends_subsystem("sftp") 47 | channel.sends_packet(FXP_INIT, :long, opts[:client_version] || Net::SFTP::Session::HIGHEST_PROTOCOL_VERSION_SUPPORTED) 48 | channel.gets_packet(FXP_VERSION, :long, opts[:server_version] || Net::SFTP::Session::HIGHEST_PROTOCOL_VERSION_SUPPORTED) 49 | yield channel if block_given? 50 | end 51 | end 52 | 53 | def assert_scripted_command 54 | assert_scripted do 55 | sftp.connect! 56 | yield 57 | sftp.loop 58 | end 59 | end 60 | 61 | def assert_progress_reported_open(expect={}) 62 | assert_progress_reported(:open, expect) 63 | end 64 | 65 | def assert_progress_reported_put(offset, data, expect={}) 66 | assert_equal offset, current_event[3] if offset 67 | assert_equal data, current_event[4] if data 68 | assert_progress_reported(:put, expect) 69 | end 70 | 71 | def assert_progress_reported_get(offset, data, expect={}) 72 | assert_equal offset, current_event[3] if offset 73 | if data.is_a?(0.class) 74 | assert_equal data, current_event[4].length 75 | elsif data 76 | assert_equal data, current_event[4] 77 | end 78 | assert_progress_reported(:get, expect) 79 | end 80 | 81 | def assert_progress_reported_close(expect={}) 82 | assert_progress_reported(:close, expect) 83 | end 84 | 85 | def assert_progress_reported_mkdir(dir) 86 | assert_equal dir, current_event[2] 87 | assert_progress_reported(:mkdir) 88 | end 89 | 90 | def assert_progress_reported_finish 91 | assert_progress_reported(:finish) 92 | end 93 | 94 | def assert_progress_reported(event, expect={}) 95 | assert_equal event, current_event[0] 96 | expect.each do |key, value| 97 | assert_equal value, current_event[2].send(key) 98 | end 99 | next_event! 100 | end 101 | 102 | def assert_no_more_reported_events 103 | assert @progress.empty?, "expected #{@progress.empty?} to be empty" 104 | end 105 | 106 | def prepare_progress! 107 | @progress = [] 108 | end 109 | 110 | def record_progress(event) 111 | @progress << event 112 | end 113 | 114 | def current_event 115 | @progress.first 116 | end 117 | 118 | def next_event! 119 | @progress.shift 120 | end 121 | end 122 | 123 | class Net::SSH::Test::Channel 124 | def gets_packet(type, *args) 125 | gets_data(sftp_packet(type, *args)) 126 | end 127 | 128 | def gets_packet_in_two(fragment_len, type, *args) 129 | fragment_len ||= 0 130 | whole_packet = sftp_packet(type, *args) 131 | 132 | if 0 < fragment_len && fragment_len < whole_packet.length 133 | gets_data(whole_packet[0, whole_packet.length - fragment_len]) 134 | gets_data(whole_packet[-fragment_len..-1]) 135 | else 136 | gets_data(whole_packet) 137 | end 138 | end 139 | 140 | def sends_packet(type, *args) 141 | sends_data(sftp_packet(type, *args)) 142 | end 143 | 144 | private 145 | 146 | def sftp_packet(type, *args) 147 | data = Net::SSH::Buffer.from(*args) 148 | Net::SSH::Buffer.from(:long, data.length+1, :byte, type, :raw, data).to_s 149 | end 150 | end 151 | 152 | class ProgressHandler 153 | def initialize(progress_ref) 154 | @progress = progress_ref 155 | end 156 | 157 | def on_open(*args) 158 | @progress << [:open, *args] 159 | end 160 | 161 | def on_put(*args) 162 | @progress << [:put, *args] 163 | end 164 | 165 | def on_close(*args) 166 | @progress << [:close, *args] 167 | end 168 | 169 | def on_finish(*args) 170 | @progress << [:finish, *args] 171 | end 172 | end 173 | 174 | # "prime the pump", so to speak: predefine the modules we need so we can 175 | # define the test classes in a more elegant short-hand. 176 | 177 | module Protocol 178 | module V01; end 179 | module V02; end 180 | module V03; end 181 | module V04; end 182 | module V05; end 183 | module V06; end 184 | end 185 | -------------------------------------------------------------------------------- /test/protocol/01/test_attributes.rb: -------------------------------------------------------------------------------- 1 | require 'common' 2 | 3 | module Etc; end 4 | 5 | class Protocol::V01::TestAttributes < Net::SFTP::TestCase 6 | def test_from_buffer_should_correctly_parse_buffer_and_return_attribute_object 7 | attributes = attributes_factory.from_buffer(full_buffer) 8 | 9 | assert_equal 1234567890, attributes.size 10 | assert_equal 100, attributes.uid 11 | assert_equal 200, attributes.gid 12 | assert_equal 0755, attributes.permissions 13 | assert_equal 1234567890, attributes.atime 14 | assert_equal 2345678901, attributes.mtime 15 | assert_equal "second", attributes.extended["first"] 16 | end 17 | 18 | def test_from_buffer_should_correctly_parse_buffer_with_attribute_subset_and_return_attribute_object 19 | buffer = Net::SSH::Buffer.from(:long, 0x4, :long, 0755) 20 | 21 | attributes = attributes_factory.from_buffer(buffer) 22 | 23 | assert_equal 0755, attributes.permissions 24 | 25 | assert_nil attributes.size 26 | assert_nil attributes.uid 27 | assert_nil attributes.gid 28 | assert_nil attributes.atime 29 | assert_nil attributes.mtime 30 | assert_nil attributes.extended 31 | end 32 | 33 | def test_attributes_to_s_should_build_binary_representation 34 | attributes = attributes_factory.new( 35 | :size => 1234567890, 36 | :uid => 100, :gid => 200, 37 | :permissions => 0755, 38 | :atime => 1234567890, :mtime => 2345678901, 39 | :extended => { "first" => "second" }) 40 | 41 | assert_equal full_buffer.to_s, attributes.to_s 42 | end 43 | 44 | def test_attributes_to_s_should_build_binary_representation_when_subset_is_present 45 | attributes = attributes_factory.new(:permissions => 0755) 46 | assert_equal Net::SSH::Buffer.from(:long, 0x4, :long, 0755).to_s, attributes.to_s 47 | end 48 | 49 | def test_attributes_to_s_with_owner_and_group_should_translate_to_uid_and_gid 50 | attributes = attributes_factory.new(:owner => "jamis", :group => "sftp") 51 | attributes.expects(:require).with("etc").times(2) 52 | Etc.expects(:getpwnam).with("jamis").returns(mock('user', :uid => 100)) 53 | Etc.expects(:getgrnam).with("sftp").returns(mock('group', :gid => 200)) 54 | assert_equal Net::SSH::Buffer.from(:long, 0x2, :long, 100, :long, 200).to_s, attributes.to_s 55 | end 56 | 57 | def test_owner_should_translate_from_uid 58 | attributes = attributes_factory.new(:uid => 100) 59 | attributes.expects(:require).with("etc") 60 | Etc.expects(:getpwuid).with(100).returns(mock('user', :name => "jamis")) 61 | assert_equal "jamis", attributes.owner 62 | end 63 | 64 | def test_group_should_translate_from_gid 65 | attributes = attributes_factory.new(:gid => 200) 66 | attributes.expects(:require).with("etc") 67 | Etc.expects(:getgrgid).with(200).returns(mock('group', :name => "sftp")) 68 | assert_equal "sftp", attributes.group 69 | end 70 | 71 | def test_type_should_infer_type_from_permissions 72 | assert_equal af::T_SOCKET, af.new(:permissions => 0140755).type 73 | assert_equal af::T_SYMLINK, af.new(:permissions => 0120755).type 74 | assert_equal af::T_REGULAR, af.new(:permissions => 0100755).type 75 | assert_equal af::T_BLOCK_DEVICE, af.new(:permissions => 060755).type 76 | assert_equal af::T_DIRECTORY, af.new(:permissions => 040755).type 77 | assert_equal af::T_CHAR_DEVICE, af.new(:permissions => 020755).type 78 | assert_equal af::T_FIFO, af.new(:permissions => 010755).type 79 | assert_equal af::T_UNKNOWN, af.new(:permissions => 0755).type 80 | assert_equal af::T_UNKNOWN, af.new.type 81 | end 82 | 83 | private 84 | 85 | def full_buffer 86 | Net::SSH::Buffer.from(:long, 0x8000000f, 87 | :int64, 1234567890, :long, 100, :long, 200, 88 | :long, 0755, :long, 1234567890, :long, 2345678901, 89 | :long, 1, :string, "first", :string, "second") 90 | end 91 | 92 | def attributes_factory 93 | Net::SFTP::Protocol::V01::Attributes 94 | end 95 | 96 | alias af attributes_factory 97 | end -------------------------------------------------------------------------------- /test/protocol/01/test_base.rb: -------------------------------------------------------------------------------- 1 | require 'common' 2 | 3 | # NOTE: these tests assume that the interface to Net::SFTP::Session#send_packet 4 | # will remain constant. If that interface ever changes, these tests will need 5 | # to be updated! 6 | 7 | class Protocol::V01::TestBase < Net::SFTP::TestCase 8 | include Net::SFTP::Constants 9 | include Net::SFTP::Constants::PacketTypes 10 | include Net::SFTP::Constants::OpenFlags 11 | 12 | def setup 13 | @session = stub('session', :logger => nil) 14 | @base = driver.new(@session) 15 | end 16 | 17 | def test_version 18 | assert_equal 1, @base.version 19 | end 20 | 21 | def test_parse_handle_packet_should_read_string_from_packet_and_return_handle_in_hash 22 | packet = Net::SSH::Buffer.from(:string, "here is a string") 23 | assert_equal({ :handle => "here is a string" }, @base.parse_handle_packet(packet)) 24 | end 25 | 26 | def test_parse_status_packet_should_read_long_from_packet_and_return_code_in_hash 27 | packet = Net::SSH::Buffer.from(:long, 15) 28 | assert_equal({ :code => 15 }, @base.parse_status_packet(packet)) 29 | end 30 | 31 | def test_parse_data_packet_should_read_string_from_packet_and_return_data_in_hash 32 | packet = Net::SSH::Buffer.from(:string, "here is a string") 33 | assert_equal({ :data => "here is a string" }, @base.parse_data_packet(packet)) 34 | end 35 | 36 | def test_parse_attrs_packet_should_use_correct_attributes_class 37 | Net::SFTP::Protocol::V01::Attributes.expects(:from_buffer).with(:packet).returns(:result) 38 | assert_equal({ :attrs => :result }, @base.parse_attrs_packet(:packet)) 39 | end 40 | 41 | def test_parse_name_packet_should_use_correct_name_class 42 | packet = Net::SSH::Buffer.from(:long, 2, 43 | :string, "name1", :string, "long1", :long, 0x4, :long, 0755, 44 | :string, "name2", :string, "long2", :long, 0x4, :long, 0550) 45 | names = @base.parse_name_packet(packet)[:names] 46 | 47 | refute_nil names 48 | assert_equal 2, names.length 49 | assert_instance_of Net::SFTP::Protocol::V01::Name, names.first 50 | 51 | assert_equal "name1", names.first.name 52 | assert_equal "long1", names.first.longname 53 | assert_equal 0755, names.first.attributes.permissions 54 | 55 | assert_equal "name2", names.last.name 56 | assert_equal "long2", names.last.longname 57 | assert_equal 0550, names.last.attributes.permissions 58 | end 59 | 60 | def test_open_with_numeric_flag_should_accept_IO_constants 61 | @session.expects(:send_packet).with(FXP_OPEN, :long, 0, 62 | :string, "/path/to/file", 63 | :long, FV1::READ | FV1::WRITE | FV1::CREAT | FV1::EXCL, 64 | :raw, attributes.new.to_s) 65 | 66 | assert_equal 0, @base.open("/path/to/file", IO::RDWR | IO::CREAT | IO::EXCL, {}) 67 | end 68 | 69 | { "r" => FV1::READ, 70 | "rb" => FV1::READ, 71 | "r+" => FV1::READ | FV1::WRITE, 72 | "w" => FV1::WRITE | FV1::TRUNC | FV1::CREAT, 73 | "w+" => FV1::WRITE | FV1::READ | FV1::TRUNC | FV1::CREAT, 74 | "a" => FV1::APPEND | FV1::WRITE | FV1::CREAT, 75 | "a+" => FV1::APPEND | FV1::WRITE | FV1::READ | FV1::CREAT 76 | }.each do |flags, options| 77 | safe_name = flags.sub(/\+/, "_plus") 78 | define_method("test_open_with_#{safe_name}_should_translate_correctly") do 79 | @session.expects(:send_packet).with(FXP_OPEN, :long, 0, 80 | :string, "/path/to/file", :long, options, :raw, attributes.new.to_s) 81 | 82 | assert_equal 0, @base.open("/path/to/file", flags, {}) 83 | end 84 | end 85 | 86 | def test_open_with_attributes_converts_hash_to_attribute_packet 87 | @session.expects(:send_packet).with(FXP_OPEN, :long, 0, 88 | :string, "/path/to/file", :long, FV1::READ, :raw, attributes.new(:permissions => 0755).to_s) 89 | @base.open("/path/to/file", "r", :permissions => 0755) 90 | end 91 | 92 | def test_close_should_send_close_packet 93 | @session.expects(:send_packet).with(FXP_CLOSE, :long, 0, :string, "handle") 94 | assert_equal 0, @base.close("handle") 95 | end 96 | 97 | def test_read_should_send_read_packet 98 | @session.expects(:send_packet).with(FXP_READ, :long, 0, :string, "handle", :int64, 1234, :long, 5678) 99 | assert_equal 0, @base.read("handle", 1234, 5678) 100 | end 101 | 102 | def test_write_should_send_write_packet 103 | @session.expects(:send_packet).with(FXP_WRITE, :long, 0, :string, "handle", :int64, 1234, :string, "data") 104 | assert_equal 0, @base.write("handle", 1234, "data") 105 | end 106 | 107 | def test_lstat_should_send_lstat_packet 108 | @session.expects(:send_packet).with(FXP_LSTAT, :long, 0, :string, "/path/to/file") 109 | assert_equal 0, @base.lstat("/path/to/file") 110 | end 111 | 112 | def test_lstat_should_ignore_flags_parameter 113 | @session.expects(:send_packet).with(FXP_LSTAT, :long, 0, :string, "/path/to/file") 114 | assert_equal 0, @base.lstat("/path/to/file", 12345) 115 | end 116 | 117 | def test_fstat_should_send_fstat_packet 118 | @session.expects(:send_packet).with(FXP_FSTAT, :long, 0, :string, "handle") 119 | assert_equal 0, @base.fstat("handle") 120 | end 121 | 122 | def test_fstat_should_ignore_flags_parameter 123 | @session.expects(:send_packet).with(FXP_FSTAT, :long, 0, :string, "handle") 124 | assert_equal 0, @base.fstat("handle", 12345) 125 | end 126 | 127 | def test_setstat_should_translate_hash_to_attributes_and_send_setstat_packet 128 | @session.expects(:send_packet).with(FXP_SETSTAT, :long, 0, :string, "/path/to/file", :raw, attributes.new(:atime => 1, :mtime => 2, :permissions => 0755).to_s) 129 | assert_equal 0, @base.setstat("/path/to/file", :atime => 1, :mtime => 2, :permissions => 0755) 130 | end 131 | 132 | def test_fsetstat_should_translate_hash_to_attributes_and_send_fsetstat_packet 133 | @session.expects(:send_packet).with(FXP_FSETSTAT, :long, 0, :string, "handle", :raw, attributes.new(:atime => 1, :mtime => 2, :permissions => 0755).to_s) 134 | assert_equal 0, @base.fsetstat("handle", :atime => 1, :mtime => 2, :permissions => 0755) 135 | end 136 | 137 | def test_opendir_should_send_opendir_packet 138 | @session.expects(:send_packet).with(FXP_OPENDIR, :long, 0, :string, "/path/to/dir") 139 | assert_equal 0, @base.opendir("/path/to/dir") 140 | end 141 | 142 | def test_readdir_should_send_readdir_packet 143 | @session.expects(:send_packet).with(FXP_READDIR, :long, 0, :string, "handle") 144 | assert_equal 0, @base.readdir("handle") 145 | end 146 | 147 | def test_remove_should_send_remove_packet 148 | @session.expects(:send_packet).with(FXP_REMOVE, :long, 0, :string, "/path/to/file") 149 | assert_equal 0, @base.remove("/path/to/file") 150 | end 151 | 152 | def test_mkdir_should_translate_hash_to_attributes_and_send_mkdir_packet 153 | @session.expects(:send_packet).with(FXP_MKDIR, :long, 0, :string, "/path/to/dir", :raw, attributes.new(:atime => 1, :mtime => 2, :permissions => 0755).to_s) 154 | assert_equal 0, @base.mkdir("/path/to/dir", :atime => 1, :mtime => 2, :permissions => 0755) 155 | end 156 | 157 | def test_rmdir_should_send_rmdir_packet 158 | @session.expects(:send_packet).with(FXP_RMDIR, :long, 0, :string, "/path/to/dir") 159 | assert_equal 0, @base.rmdir("/path/to/dir") 160 | end 161 | 162 | def test_realpath_should_send_realpath_packet 163 | @session.expects(:send_packet).with(FXP_REALPATH, :long, 0, :string, "/path/to/file") 164 | assert_equal 0, @base.realpath("/path/to/file") 165 | end 166 | 167 | def test_stat_should_send_stat_packet 168 | @session.expects(:send_packet).with(FXP_STAT, :long, 0, :string, "/path/to/file") 169 | assert_equal 0, @base.stat("/path/to/file") 170 | end 171 | 172 | def test_stat_should_ignore_flags_parameter 173 | @session.expects(:send_packet).with(FXP_STAT, :long, 0, :string, "/path/to/file") 174 | assert_equal 0, @base.stat("/path/to/file", 12345) 175 | end 176 | 177 | def test_rename_should_raise_not_implemented_error 178 | assert_raises(NotImplementedError) { @base.rename("/path/to/old", "/path/to/new") } 179 | end 180 | 181 | def test_readlink_should_raise_not_implemented_error 182 | assert_raises(NotImplementedError) { @base.readlink("/path/to/link") } 183 | end 184 | 185 | def test_symlink_should_raise_not_implemented_error 186 | assert_raises(NotImplementedError) { @base.symlink("/path/to/link", "/path/to/file") } 187 | end 188 | 189 | def test_link_should_raise_not_implemented_error 190 | assert_raises(NotImplementedError) { @base.link("/path/to/link", "/path/to/file", true) } 191 | end 192 | 193 | def test_block_should_raise_not_implemented_error 194 | assert_raises(NotImplementedError) { @base.block("handle", 100, 200, 0) } 195 | end 196 | 197 | def test_unblock_should_raise_not_implemented_error 198 | assert_raises(NotImplementedError) { @base.unblock("handle", 100, 200) } 199 | end 200 | 201 | private 202 | 203 | def driver 204 | Net::SFTP::Protocol::V01::Base 205 | end 206 | 207 | def attributes 208 | Net::SFTP::Protocol::V01::Attributes 209 | end 210 | end 211 | -------------------------------------------------------------------------------- /test/protocol/01/test_name.rb: -------------------------------------------------------------------------------- 1 | require 'common' 2 | 3 | class Protocol::V01::TestName < Net::SFTP::TestCase 4 | def setup 5 | @directory = Net::SFTP::Protocol::V01::Name.new("test", "drwxr-x-r-x 89 test test 3026 Mar 10 17:45 test", Net::SFTP::Protocol::V01::Attributes.new(:permissions => 040755)) 6 | @link = Net::SFTP::Protocol::V01::Name.new("test", "lrwxr-x-r-x 89 test test 3026 Mar 10 17:45 test", Net::SFTP::Protocol::V01::Attributes.new(:permissions => 0120755)) 7 | @file = Net::SFTP::Protocol::V01::Name.new("test", "-rwxr-x-r-x 89 test test 3026 Mar 10 17:45 test", Net::SFTP::Protocol::V01::Attributes.new(:permissions => 0100755)) 8 | end 9 | 10 | def test_directory? 11 | assert @directory.directory? 12 | assert !@link.directory? 13 | assert !@file.directory? 14 | end 15 | 16 | def test_symlink? 17 | assert !@directory.symlink? 18 | assert @link.symlink? 19 | assert !@file.symlink? 20 | end 21 | 22 | def test_file? 23 | assert !@directory.file? 24 | assert !@link.file? 25 | assert @file.file? 26 | end 27 | end -------------------------------------------------------------------------------- /test/protocol/02/test_base.rb: -------------------------------------------------------------------------------- 1 | require 'common' 2 | require 'protocol/01/test_base' 3 | 4 | class Protocol::V02::TestBase < Protocol::V01::TestBase 5 | def test_version 6 | assert_equal 2, @base.version 7 | end 8 | 9 | undef test_rename_should_raise_not_implemented_error 10 | 11 | def test_rename_should_send_rename_packet 12 | @session.expects(:send_packet).with(FXP_RENAME, :long, 0, :string, "/old/file", :string, "/new/file") 13 | assert_equal 0, @base.rename("/old/file", "/new/file") 14 | end 15 | 16 | def test_rename_should_ignore_flags_parameter 17 | @session.expects(:send_packet).with(FXP_RENAME, :long, 0, :string, "/old/file", :string, "/new/file") 18 | assert_equal 0, @base.rename("/old/file", "/new/file", 1234) 19 | end 20 | 21 | private 22 | 23 | def driver 24 | Net::SFTP::Protocol::V02::Base 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /test/protocol/03/test_base.rb: -------------------------------------------------------------------------------- 1 | require 'common' 2 | require 'protocol/02/test_base' 3 | 4 | class Protocol::V03::TestBase < Protocol::V02::TestBase 5 | def test_version 6 | assert_equal 3, @base.version 7 | end 8 | 9 | undef test_readlink_should_raise_not_implemented_error 10 | undef test_symlink_should_raise_not_implemented_error 11 | 12 | def test_readlink_should_send_readlink_packet 13 | @session.expects(:send_packet).with(FXP_READLINK, :long, 0, :string, "/path/to/link") 14 | assert_equal 0, @base.readlink("/path/to/link") 15 | end 16 | 17 | def test_symlink_should_send_symlink_packet 18 | @session.expects(:send_packet).with(FXP_SYMLINK, :long, 0, :string, "/path/to/link", :string, "/path/to/file") 19 | assert_equal 0, @base.symlink("/path/to/link", "/path/to/file") 20 | end 21 | 22 | private 23 | 24 | def driver 25 | Net::SFTP::Protocol::V03::Base 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /test/protocol/04/test_attributes.rb: -------------------------------------------------------------------------------- 1 | require 'common' 2 | 3 | module Etc; end 4 | 5 | class Protocol::V04::TestAttributes < Net::SFTP::TestCase 6 | def setup 7 | @directory = attributes_factory.new(:type => attributes_factory::T_DIRECTORY) 8 | @symlink = attributes_factory.new(:type => attributes_factory::T_SYMLINK) 9 | @file = attributes_factory.new(:type => attributes_factory::T_REGULAR) 10 | end 11 | 12 | def test_from_buffer_should_correctly_parse_buffer_and_return_attribute_object 13 | attributes = attributes_factory.from_buffer(full_buffer) 14 | 15 | assert_equal 9, attributes.type 16 | assert_equal 1234567890, attributes.size 17 | assert_equal "jamis", attributes.owner 18 | assert_equal "users", attributes.group 19 | assert_equal 0755, attributes.permissions 20 | assert_equal 1234567890, attributes.atime 21 | assert_equal 12345, attributes.atime_nseconds 22 | assert_equal 2345678901, attributes.createtime 23 | assert_equal 23456, attributes.createtime_nseconds 24 | assert_equal 3456789012, attributes.mtime 25 | assert_equal 34567, attributes.mtime_nseconds 26 | 27 | assert_equal 2, attributes.acl.length 28 | 29 | assert_equal 1, attributes.acl.first.type 30 | assert_equal 2, attributes.acl.first.flag 31 | assert_equal 3, attributes.acl.first.mask 32 | assert_equal "foo", attributes.acl.first.who 33 | 34 | assert_equal 4, attributes.acl.last.type 35 | assert_equal 5, attributes.acl.last.flag 36 | assert_equal 6, attributes.acl.last.mask 37 | assert_equal "bar", attributes.acl.last.who 38 | 39 | assert_equal "second", attributes.extended["first"] 40 | end 41 | 42 | def test_from_buffer_should_correctly_parse_buffer_with_attribute_subset_and_return_attribute_object 43 | buffer = Net::SSH::Buffer.from(:long, 0x4, :byte, 1, :long, 0755) 44 | 45 | attributes = attributes_factory.from_buffer(buffer) 46 | 47 | assert_equal 1, attributes.type 48 | assert_equal 0755, attributes.permissions 49 | 50 | assert_nil attributes.size 51 | assert_nil attributes.owner 52 | assert_nil attributes.group 53 | assert_nil attributes.atime 54 | assert_nil attributes.atime_nseconds 55 | assert_nil attributes.createtime 56 | assert_nil attributes.createtime_nseconds 57 | assert_nil attributes.mtime 58 | assert_nil attributes.mtime_nseconds 59 | assert_nil attributes.acl 60 | assert_nil attributes.extended 61 | end 62 | 63 | def test_attributes_to_s_should_build_binary_representation 64 | attributes = attributes_factory.new( 65 | :type => 9, 66 | :size => 1234567890, 67 | :owner => "jamis", :group => "users", 68 | :permissions => 0755, 69 | :atime => 1234567890, :atime_nseconds => 12345, 70 | :createtime => 2345678901, :createtime_nseconds => 23456, 71 | :mtime => 3456789012, :mtime_nseconds => 34567, 72 | :acl => [attributes_factory::ACL.new(1,2,3,"foo"), 73 | attributes_factory::ACL.new(4,5,6,"bar")], 74 | :extended => { "first" => "second" }) 75 | 76 | assert_equal full_buffer.to_s, attributes.to_s 77 | end 78 | 79 | def test_attributes_to_s_should_build_binary_representation_when_subset_is_present 80 | attributes = attributes_factory.new(:permissions => 0755) 81 | assert_equal Net::SSH::Buffer.from(:long, 0x4, :byte, 1, :long, 0755).to_s, attributes.to_s 82 | end 83 | 84 | def test_attributes_to_s_with_uid_and_gid_should_translate_to_owner_and_group 85 | attributes = attributes_factory.new(:uid => 100, :gid => 200) 86 | attributes.expects(:require).with("etc").times(2) 87 | Etc.expects(:getpwuid).with(100).returns(mock('user', :name => "jamis")) 88 | Etc.expects(:getgrgid).with(200).returns(mock('group', :name => "sftp")) 89 | assert_equal Net::SSH::Buffer.from(:long, 0x80, :byte, 1, :string, "jamis", :string, "sftp").to_s, attributes.to_s 90 | end 91 | 92 | def test_uid_should_translate_from_owner 93 | attributes = attributes_factory.new(:owner => "jamis") 94 | attributes.expects(:require).with("etc") 95 | Etc.expects(:getpwnam).with("jamis").returns(mock('user', :uid => 100)) 96 | assert_equal 100, attributes.uid 97 | end 98 | 99 | def test_gid_should_translate_from_group 100 | attributes = attributes_factory.new(:group => "sftp") 101 | attributes.expects(:require).with("etc") 102 | Etc.expects(:getgrnam).with("sftp").returns(mock('group', :gid => 200)) 103 | assert_equal 200, attributes.gid 104 | end 105 | 106 | def test_attributes_without_subsecond_times_should_serialize_without_subsecond_times 107 | attributes = attributes_factory.new(:atime => 100) 108 | assert_equal Net::SSH::Buffer.from(:long, 0x8, :byte, 1, :int64, 100).to_s, attributes.to_s 109 | end 110 | 111 | def test_directory_should_be_true_only_when_type_is_directory 112 | assert @directory.directory? 113 | assert !@symlink.directory? 114 | assert !@file.directory? 115 | end 116 | 117 | def test_symlink_should_be_true_only_when_type_is_symlink 118 | assert !@directory.symlink? 119 | assert @symlink.symlink? 120 | assert !@file.symlink? 121 | end 122 | 123 | def test_file_should_be_true_only_when_type_is_file 124 | assert !@directory.file? 125 | assert !@symlink.file? 126 | assert @file.file? 127 | end 128 | 129 | private 130 | 131 | def full_buffer 132 | Net::SSH::Buffer.from(:long, 0x800001fd, 133 | :byte, 9, :int64, 1234567890, 134 | :string, "jamis", :string, "users", 135 | :long, 0755, 136 | :int64, 1234567890, :long, 12345, 137 | :int64, 2345678901, :long, 23456, 138 | :int64, 3456789012, :long, 34567, 139 | :string, raw(:long, 2, 140 | :long, 1, :long, 2, :long, 3, :string, "foo", 141 | :long, 4, :long, 5, :long, 6, :string, "bar"), 142 | :long, 1, :string, "first", :string, "second") 143 | end 144 | 145 | def attributes_factory 146 | Net::SFTP::Protocol::V04::Attributes 147 | end 148 | end -------------------------------------------------------------------------------- /test/protocol/04/test_base.rb: -------------------------------------------------------------------------------- 1 | require 'common' 2 | require 'protocol/03/test_base' 3 | 4 | class Protocol::V04::TestBase < Protocol::V03::TestBase 5 | def test_version 6 | assert_equal 4, @base.version 7 | end 8 | 9 | def test_parse_attrs_packet_should_use_correct_attributes_class 10 | Net::SFTP::Protocol::V04::Attributes.expects(:from_buffer).with(:packet).returns(:result) 11 | assert_equal({ :attrs => :result }, @base.parse_attrs_packet(:packet)) 12 | end 13 | 14 | def test_parse_name_packet_should_use_correct_name_class 15 | packet = Net::SSH::Buffer.from(:long, 2, 16 | :string, "name1", :long, 0x4, :byte, 1, :long, 0755, 17 | :string, "name2", :long, 0x4, :byte, 1, :long, 0550) 18 | names = @base.parse_name_packet(packet)[:names] 19 | 20 | refute_nil names 21 | assert_equal 2, names.length 22 | assert_instance_of Net::SFTP::Protocol::V04::Name, names.first 23 | 24 | assert_equal "name1", names.first.name 25 | assert_equal 0755, names.first.attributes.permissions 26 | 27 | assert_equal "name2", names.last.name 28 | assert_equal 0550, names.last.attributes.permissions 29 | end 30 | 31 | undef test_fstat_should_ignore_flags_parameter 32 | undef test_lstat_should_ignore_flags_parameter 33 | undef test_stat_should_ignore_flags_parameter 34 | 35 | def test_lstat_should_send_lstat_packet 36 | @session.expects(:send_packet).with(FXP_LSTAT, :long, 0, :string, "/path/to/file", :long, 0x800001fd) 37 | assert_equal 0, @base.lstat("/path/to/file") 38 | end 39 | 40 | def test_lstat_with_custom_flags_should_send_lstat_packet_with_given_flags 41 | @session.expects(:send_packet).with(FXP_LSTAT, :long, 0, :string, "/path/to/file", :long, 1234) 42 | assert_equal 0, @base.lstat("/path/to/file", 1234) 43 | end 44 | 45 | def test_fstat_should_send_fstat_packet 46 | @session.expects(:send_packet).with(FXP_FSTAT, :long, 0, :string, "handle", :long, 0x800001fd) 47 | assert_equal 0, @base.fstat("handle") 48 | end 49 | 50 | def test_fstat_with_custom_flags_should_send_fstat_packet_with_given_flags 51 | @session.expects(:send_packet).with(FXP_FSTAT, :long, 0, :string, "handle", :long, 1234) 52 | assert_equal 0, @base.fstat("handle", 1234) 53 | end 54 | 55 | def test_stat_should_send_stat_packet 56 | @session.expects(:send_packet).with(FXP_STAT, :long, 0, :string, "/path/to/file", :long, 0x800001fd) 57 | assert_equal 0, @base.stat("/path/to/file") 58 | end 59 | 60 | def test_stat_with_custom_flags_should_send_stat_packet_with_given_flags 61 | @session.expects(:send_packet).with(FXP_STAT, :long, 0, :string, "/path/to/file", :long, 1234) 62 | assert_equal 0, @base.stat("/path/to/file", 1234) 63 | end 64 | 65 | private 66 | 67 | def driver 68 | Net::SFTP::Protocol::V04::Base 69 | end 70 | 71 | def attributes 72 | Net::SFTP::Protocol::V04::Attributes 73 | end 74 | end 75 | -------------------------------------------------------------------------------- /test/protocol/04/test_name.rb: -------------------------------------------------------------------------------- 1 | require 'common' 2 | 3 | class Protocol::V04::TestName < Net::SFTP::TestCase 4 | def setup 5 | @save_tz = ENV['TZ'] 6 | ENV['TZ'] = 'UTC' 7 | 8 | @directory = Net::SFTP::Protocol::V04::Name.new("test", Net::SFTP::Protocol::V04::Attributes.new(:type => 2, :mtime => 1205293237, :owner => "jamis", :group => "users", :size => 1024, :permissions => 0755)) 9 | @link = Net::SFTP::Protocol::V04::Name.new("test", Net::SFTP::Protocol::V04::Attributes.new(:type => 3, :mtime => 1205293237, :owner => "jamis", :group => "users", :size => 32, :permissions => 0755)) 10 | @file = Net::SFTP::Protocol::V04::Name.new("test", Net::SFTP::Protocol::V04::Attributes.new(:type => 1, :mtime => 1205293237, :owner => "jamis", :group => "users", :size => 10240, :permissions => 0755)) 11 | end 12 | 13 | def teardown 14 | if @save_tz 15 | ENV['TZ'] = @save_tz 16 | else 17 | ENV.delete('TZ') 18 | end 19 | end 20 | 21 | def test_directory? 22 | assert @directory.directory? 23 | assert !@link.directory? 24 | assert !@file.directory? 25 | end 26 | 27 | def test_symlink? 28 | assert !@directory.symlink? 29 | assert @link.symlink? 30 | assert !@file.symlink? 31 | end 32 | 33 | def test_file? 34 | assert !@directory.file? 35 | assert !@link.file? 36 | assert @file.file? 37 | end 38 | 39 | def test_longname_for_directory_should_format_as_directory 40 | assert_equal "drwxr-xr-x jamis users 1024 Mar 12 03:40 test", 41 | @directory.longname 42 | end 43 | 44 | def test_longname_for_symlink_should_format_as_symlink 45 | assert_equal "lrwxr-xr-x jamis users 32 Mar 12 03:40 test", 46 | @link.longname 47 | end 48 | 49 | def test_longname_for_file_should_format_as_file 50 | assert_equal "-rwxr-xr-x jamis users 10240 Mar 12 03:40 test", 51 | @file.longname 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /test/protocol/05/test_base.rb: -------------------------------------------------------------------------------- 1 | require 'common' 2 | require 'protocol/04/test_base' 3 | 4 | class Protocol::V05::TestBase < Protocol::V04::TestBase 5 | include Net::SFTP::Constants::OpenFlags 6 | include Net::SFTP::Constants 7 | 8 | def test_version 9 | assert_equal 5, @base.version 10 | end 11 | 12 | undef test_rename_should_ignore_flags_parameter 13 | 14 | def test_rename_should_send_rename_packet 15 | @session.expects(:send_packet).with(FXP_RENAME, :long, 0, :string, "/old/file", :string, "/new/file", :long, 0) 16 | assert_equal 0, @base.rename("/old/file", "/new/file") 17 | end 18 | 19 | def test_rename_with_flags_should_send_rename_packet_with_flags 20 | @session.expects(:send_packet).with(FXP_RENAME, :long, 0, :string, "/old/file", :string, "/new/file", :long, RenameFlags::ATOMIC) 21 | assert_equal 0, @base.rename("/old/file", "/new/file", RenameFlags::ATOMIC) 22 | end 23 | 24 | def test_open_with_numeric_flag_should_accept_IO_constants 25 | @session.expects(:send_packet).with(FXP_OPEN, :long, 0, 26 | :string, "/path/to/file", 27 | :long, ACE::Mask::READ_DATA | ACE::Mask::READ_ATTRIBUTES | ACE::Mask::WRITE_DATA | ACE::Mask::WRITE_ATTRIBUTES, 28 | :long, FV5::CREATE_NEW, 29 | :raw, attributes.new.to_s) 30 | 31 | assert_equal 0, @base.open("/path/to/file", IO::RDWR | IO::CREAT | IO::EXCL, {}) 32 | end 33 | 34 | { "r" => [FV5::OPEN_EXISTING, ACE::Mask::READ_DATA | ACE::Mask::READ_ATTRIBUTES], 35 | "rb" => [FV5::OPEN_EXISTING, ACE::Mask::READ_DATA | ACE::Mask::READ_ATTRIBUTES], 36 | "r+" => [FV5::OPEN_EXISTING, ACE::Mask::READ_DATA | ACE::Mask::READ_ATTRIBUTES | ACE::Mask::WRITE_DATA | ACE::Mask::WRITE_ATTRIBUTES], 37 | "w" => [FV5::CREATE_TRUNCATE, ACE::Mask::WRITE_DATA | ACE::Mask::WRITE_ATTRIBUTES], 38 | "w+" => [FV5::CREATE_TRUNCATE, ACE::Mask::READ_DATA | ACE::Mask::READ_ATTRIBUTES | ACE::Mask::WRITE_DATA | ACE::Mask::WRITE_ATTRIBUTES], 39 | "a" => [FV5::OPEN_OR_CREATE | FV5::APPEND_DATA, ACE::Mask::WRITE_DATA | ACE::Mask::WRITE_ATTRIBUTES | ACE::Mask::APPEND_DATA], 40 | "a+" => [FV5::OPEN_OR_CREATE | FV5::APPEND_DATA, ACE::Mask::READ_DATA | ACE::Mask::READ_ATTRIBUTES | ACE::Mask::WRITE_DATA | ACE::Mask::WRITE_ATTRIBUTES | ACE::Mask::APPEND_DATA] 41 | }.each do |mode_string, (flags, access)| 42 | define_method("test_open_with_#{mode_string.sub(/\+/, '_plus')}_should_translate_correctly") do 43 | @session.expects(:send_packet).with(FXP_OPEN, :long, 0, 44 | :string, "/path/to/file", :long, access, :long, flags, :raw, attributes.new.to_s) 45 | 46 | assert_equal 0, @base.open("/path/to/file", mode_string, {}) 47 | end 48 | end 49 | 50 | def test_open_with_attributes_converts_hash_to_attribute_packet 51 | @session.expects(:send_packet).with(FXP_OPEN, :long, 0, 52 | :string, "/path/to/file", :long, ACE::Mask::READ_DATA | ACE::Mask::READ_ATTRIBUTES, 53 | :long, FV5::OPEN_EXISTING, :raw, attributes.new(:permissions => 0755).to_s) 54 | @base.open("/path/to/file", "r", :permissions => 0755) 55 | end 56 | 57 | private 58 | 59 | def driver 60 | Net::SFTP::Protocol::V05::Base 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /test/protocol/06/test_attributes.rb: -------------------------------------------------------------------------------- 1 | require 'common' 2 | 3 | module Etc; end 4 | 5 | class Protocol::V06::TestAttributes < Net::SFTP::TestCase 6 | def test_from_buffer_should_correctly_parse_buffer_and_return_attribute_object 7 | attributes = attributes_factory.from_buffer(full_buffer) 8 | 9 | assert_equal 9, attributes.type 10 | assert_equal 1234567890, attributes.size 11 | assert_equal 2345678901, attributes.allocation_size 12 | assert_equal "jamis", attributes.owner 13 | assert_equal "users", attributes.group 14 | assert_equal 0755, attributes.permissions 15 | assert_equal 1234567890, attributes.atime 16 | assert_equal 12345, attributes.atime_nseconds 17 | assert_equal 2345678901, attributes.createtime 18 | assert_equal 23456, attributes.createtime_nseconds 19 | assert_equal 3456789012, attributes.mtime 20 | assert_equal 34567, attributes.mtime_nseconds 21 | assert_equal 4567890123, attributes.ctime 22 | assert_equal 45678, attributes.ctime_nseconds 23 | 24 | assert_equal 2, attributes.acl.length 25 | 26 | assert_equal 1, attributes.acl.first.type 27 | assert_equal 2, attributes.acl.first.flag 28 | assert_equal 3, attributes.acl.first.mask 29 | assert_equal "foo", attributes.acl.first.who 30 | 31 | assert_equal 4, attributes.acl.last.type 32 | assert_equal 5, attributes.acl.last.flag 33 | assert_equal 6, attributes.acl.last.mask 34 | assert_equal "bar", attributes.acl.last.who 35 | 36 | assert_equal 0x12341234, attributes.attrib_bits 37 | assert_equal 0x23452345, attributes.attrib_bits_valid 38 | assert_equal 0x3, attributes.text_hint 39 | assert_equal "text/html", attributes.mime_type 40 | assert_equal 144, attributes.link_count 41 | assert_equal "an untranslated name", attributes.untranslated_name 42 | 43 | assert_equal "second", attributes.extended["first"] 44 | end 45 | 46 | def test_from_buffer_should_correctly_parse_buffer_with_attribute_subset_and_return_attribute_object 47 | buffer = Net::SSH::Buffer.from(:long, 0x4, :byte, 1, :long, 0755) 48 | 49 | attributes = attributes_factory.from_buffer(buffer) 50 | 51 | assert_equal 1, attributes.type 52 | assert_equal 0755, attributes.permissions 53 | 54 | assert_nil attributes.size 55 | assert_nil attributes.allocation_size 56 | assert_nil attributes.owner 57 | assert_nil attributes.group 58 | assert_nil attributes.atime 59 | assert_nil attributes.atime_nseconds 60 | assert_nil attributes.createtime 61 | assert_nil attributes.createtime_nseconds 62 | assert_nil attributes.mtime 63 | assert_nil attributes.mtime_nseconds 64 | assert_nil attributes.ctime 65 | assert_nil attributes.ctime_nseconds 66 | assert_nil attributes.acl 67 | assert_nil attributes.attrib_bits 68 | assert_nil attributes.attrib_bits_valid 69 | assert_nil attributes.text_hint 70 | assert_nil attributes.mime_type 71 | assert_nil attributes.link_count 72 | assert_nil attributes.untranslated_name 73 | assert_nil attributes.extended 74 | end 75 | 76 | def test_attributes_to_s_should_build_binary_representation 77 | attributes = attributes_factory.new( 78 | :type => 9, 79 | :size => 1234567890, :allocation_size => 2345678901, 80 | :owner => "jamis", :group => "users", 81 | :permissions => 0755, 82 | :atime => 1234567890, :atime_nseconds => 12345, 83 | :createtime => 2345678901, :createtime_nseconds => 23456, 84 | :mtime => 3456789012, :mtime_nseconds => 34567, 85 | :ctime => 4567890123, :ctime_nseconds => 45678, 86 | :acl => [attributes_factory::ACL.new(1,2,3,"foo"), 87 | attributes_factory::ACL.new(4,5,6,"bar")], 88 | :attrib_bits => 0x12341234, :attrib_bits_valid => 0x23452345, 89 | :text_hint => 0x3, :mime_type => "text/html", 90 | :link_count => 144, :untranslated_name => "an untranslated name", 91 | :extended => { "first" => "second" }) 92 | 93 | assert_equal full_buffer.to_s, attributes.to_s 94 | end 95 | 96 | def test_attributes_to_s_should_build_binary_representation_when_subset_is_present 97 | attributes = attributes_factory.new(:permissions => 0755) 98 | assert_equal Net::SSH::Buffer.from(:long, 0x4, :byte, 1, :long, 0755).to_s, attributes.to_s 99 | end 100 | 101 | private 102 | 103 | def full_buffer 104 | Net::SSH::Buffer.from(:long, 0x8000fffd, 105 | :byte, 9, :int64, 1234567890, :int64, 2345678901, 106 | :string, "jamis", :string, "users", 107 | :long, 0755, 108 | :int64, 1234567890, :long, 12345, 109 | :int64, 2345678901, :long, 23456, 110 | :int64, 3456789012, :long, 34567, 111 | :int64, 4567890123, :long, 45678, 112 | :string, raw(:long, 2, 113 | :long, 1, :long, 2, :long, 3, :string, "foo", 114 | :long, 4, :long, 5, :long, 6, :string, "bar"), 115 | :long, 0x12341234, :long, 0x23452345, 116 | :byte, 0x3, :string, "text/html", :long, 144, 117 | :string, "an untranslated name", 118 | :long, 1, :string, "first", :string, "second") 119 | end 120 | 121 | def attributes_factory 122 | Net::SFTP::Protocol::V06::Attributes 123 | end 124 | end -------------------------------------------------------------------------------- /test/protocol/06/test_base.rb: -------------------------------------------------------------------------------- 1 | require 'common' 2 | require 'protocol/05/test_base' 3 | 4 | class Protocol::V06::TestBase < Protocol::V05::TestBase 5 | include Net::SFTP::Constants::OpenFlags 6 | include Net::SFTP::Constants 7 | 8 | def test_version 9 | assert_equal 6, @base.version 10 | end 11 | 12 | def test_parse_attrs_packet_should_use_correct_attributes_class 13 | Net::SFTP::Protocol::V06::Attributes.expects(:from_buffer).with(:packet).returns(:result) 14 | assert_equal({ :attrs => :result }, @base.parse_attrs_packet(:packet)) 15 | end 16 | 17 | undef test_link_should_raise_not_implemented_error 18 | undef test_block_should_raise_not_implemented_error 19 | undef test_unblock_should_raise_not_implemented_error 20 | undef test_symlink_should_send_symlink_packet 21 | 22 | def test_link_should_send_link_packet 23 | @session.expects(:send_packet).with(FXP_LINK, :long, 0, :string, "/path/to/link", :string, "/path/to/file", :bool, true) 24 | assert_equal 0, @base.link("/path/to/link", "/path/to/file", true) 25 | end 26 | 27 | def test_symlink_should_send_link_packet_as_symlink 28 | @session.expects(:send_packet).with(FXP_LINK, :long, 0, :string, "/path/to/link", :string, "/path/to/file", :bool, true) 29 | assert_equal 0, @base.symlink("/path/to/link", "/path/to/file") 30 | end 31 | 32 | def test_block_should_send_block_packet 33 | @session.expects(:send_packet).with(FXP_BLOCK, :long, 0, :string, "handle", :int64, 1234, :int64, 4567, :long, 0x40) 34 | assert_equal 0, @base.block("handle", 1234, 4567, 0x40) 35 | end 36 | 37 | def test_unblock_should_send_unblock_packet 38 | @session.expects(:send_packet).with(FXP_UNBLOCK, :long, 0, :string, "handle", :int64, 1234, :int64, 4567) 39 | assert_equal 0, @base.unblock("handle", 1234, 4567) 40 | end 41 | 42 | private 43 | 44 | def driver 45 | Net::SFTP::Protocol::V06::Base 46 | end 47 | 48 | def attributes 49 | Net::SFTP::Protocol::V06::Attributes 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /test/protocol/test_base.rb: -------------------------------------------------------------------------------- 1 | require 'common' 2 | 3 | class Protocol::TestBase < Net::SFTP::TestCase 4 | def setup 5 | @base = Net::SFTP::Protocol::Base.new(stub('session', :logger => nil)) 6 | end 7 | 8 | def test_parse_with_status_packet_should_delegate_to_parse_status_packet 9 | packet = stub('packet', :type => FXP_STATUS) 10 | @base.expects(:parse_status_packet).with(packet).returns(:result) 11 | assert_equal :result, @base.parse(packet) 12 | end 13 | 14 | def test_parse_with_handle_packet_should_delegate_to_parse_handle_packet 15 | packet = stub('packet', :type => FXP_HANDLE) 16 | @base.expects(:parse_handle_packet).with(packet).returns(:result) 17 | assert_equal :result, @base.parse(packet) 18 | end 19 | 20 | def test_parse_with_data_packet_should_delegate_to_parse_data_packet 21 | packet = stub('packet', :type => FXP_DATA) 22 | @base.expects(:parse_data_packet).with(packet).returns(:result) 23 | assert_equal :result, @base.parse(packet) 24 | end 25 | 26 | def test_parse_with_name_packet_should_delegate_to_parse_name_packet 27 | packet = stub('packet', :type => FXP_NAME) 28 | @base.expects(:parse_name_packet).with(packet).returns(:result) 29 | assert_equal :result, @base.parse(packet) 30 | end 31 | 32 | def test_parse_with_attrs_packet_should_delegate_to_parse_attrs_packet 33 | packet = stub('packet', :type => FXP_ATTRS) 34 | @base.expects(:parse_attrs_packet).with(packet).returns(:result) 35 | assert_equal :result, @base.parse(packet) 36 | end 37 | 38 | def test_parse_with_unknown_packet_should_raise_exception 39 | packet = stub('packet', :type => FXP_WRITE) 40 | assert_raises(NotImplementedError) { @base.parse(packet) } 41 | end 42 | end -------------------------------------------------------------------------------- /test/test_all.rb: -------------------------------------------------------------------------------- 1 | # $ ruby -I../net-ssh/lib -Ilib -Itest -rrubygems test/test_all.rb 2 | #require 'net/ssh' 3 | #puts Net::SSH::Version::CURRENT 4 | require 'common' 5 | Dir.chdir(File.dirname(__FILE__)) do 6 | Dir['**/test_*.rb'].each { |file| require(file) unless file == File.basename(__FILE__) } 7 | end -------------------------------------------------------------------------------- /test/test_dir.rb: -------------------------------------------------------------------------------- 1 | require 'common' 2 | 3 | class DirOperationsTest < Net::SFTP::TestCase 4 | def setup 5 | @sftp = mock("sftp") 6 | @dir = Net::SFTP::Operations::Dir.new(@sftp) 7 | end 8 | 9 | def test_foreach_should_iterate_over_all_entries_in_directory 10 | @sftp.expects(:opendir!).with("/path/to/remote").returns("handle") 11 | @sftp.expects(:readdir!).with("handle").returns([:e1, :e2, :e3], [:e4, :e5], nil).times(3) 12 | @sftp.expects(:close!).with("handle") 13 | 14 | entries = [] 15 | @dir.foreach("/path/to/remote") { |entry| entries << entry } 16 | assert_equal [:e1, :e2, :e3, :e4, :e5], entries 17 | end 18 | 19 | def test_entries_should_return_all_entries_in_a_single_array 20 | @sftp.expects(:opendir!).with("/path/to/remote").returns("handle") 21 | @sftp.expects(:readdir!).with("handle").returns([:e1, :e2, :e3], [:e4, :e5], nil).times(3) 22 | @sftp.expects(:close!).with("handle") 23 | 24 | assert_equal [:e1, :e2, :e3, :e4, :e5], @dir.entries("/path/to/remote") 25 | end 26 | 27 | def test_glob_should_search_under_path_for_matching_entries 28 | @sftp.expects(:opendir!).with("/path/to/remote").returns("handle") 29 | @sftp.expects(:opendir!).with("/path/to/remote/e3").returns("handle-e3") 30 | @sftp.expects(:opendir!).with("/path/to/remote/e5").returns("handle-e5") 31 | @sftp.expects(:readdir!).with("handle").returns([n(".", true), n("..", true), n("e1"), n("e2"), n("e3", true)], [n("e4"), n("e5", true)], nil).times(3) 32 | @sftp.expects(:readdir!).with("handle-e3").returns([n(".", true), n("..", true), n("e3e1"), n("e3e2")], nil).times(2) 33 | @sftp.expects(:readdir!).with("handle-e5").returns([n(".", true), n("..", true), n("e5e1"), n("e5e2"), n("e5e3")], nil).times(2) 34 | @sftp.expects(:close!).with("handle") 35 | @sftp.expects(:close!).with("handle-e3") 36 | @sftp.expects(:close!).with("handle-e5") 37 | 38 | assert_equal %w(e3/e3e2 e5/e5e2), @dir.glob("/path/to/remote", "**/e?e2").map { |e| e.name } 39 | end 40 | 41 | private 42 | 43 | def n(name, directory=false) 44 | Net::SFTP::Protocol::V01::Name.new(name.to_s, "longname for #{name}", 45 | Net::SFTP::Protocol::V01::Attributes.new(:permissions => directory ? 040755 : 0100644)) 46 | end 47 | end -------------------------------------------------------------------------------- /test/test_download.rb: -------------------------------------------------------------------------------- 1 | require "common" 2 | 3 | class DownloadTest < Net::SFTP::TestCase 4 | FXP_DATA_CHUNK_SIZE = 1024 5 | 6 | def setup 7 | prepare_progress! 8 | end 9 | 10 | def test_download_file_should_transfer_remote_to_local 11 | local = "/path/to/local" 12 | remote = "/path/to/remote" 13 | text = "this is some text\n" 14 | 15 | expect_file_transfer(remote, text) 16 | 17 | file = StringIO.new 18 | File.stubs(:open).with(local, "wb").returns(file) 19 | 20 | assert_scripted_command { sftp.download(remote, local) } 21 | assert_equal text, file.string 22 | end 23 | 24 | def test_download_file_should_transfer_remote_to_local_in_spite_of_fragmentation 25 | local = "/path/to/local" 26 | remote = "/path/to/remote" 27 | text = "this is some text\n" 28 | 29 | expect_file_transfer(remote, text, :fragment_len => 1) 30 | 31 | file = StringIO.new 32 | File.stubs(:open).with(local, "wb").returns(file) 33 | 34 | assert_scripted_command { sftp.download(remote, local) } 35 | assert_equal text, file.string 36 | end 37 | 38 | def test_download_large_file_should_transfer_remote_to_local 39 | local = "/path/to/local" 40 | remote = "/path/to/remote" 41 | text = "0123456789" * 1024 42 | 43 | file = prepare_large_file_download(local, remote, text) 44 | 45 | assert_scripted_command { sftp.download(remote, local, :read_size => 1024) } 46 | assert_equal text, file.string 47 | end 48 | 49 | def test_download_large_file_should_handle_too_large_read_size 50 | local = "/path/to/local" 51 | remote = "/path/to/remote" 52 | text = "0123456789" * 1024 53 | 54 | # some servers put upper bound on the max read_size value and send less data than requested 55 | too_large_read_size = FXP_DATA_CHUNK_SIZE + 1 56 | file = prepare_large_file_download(local, remote, text, too_large_read_size) 57 | 58 | assert_scripted_command { sftp.download(remote, local, :read_size => too_large_read_size) } 59 | assert_equal text, file.string 60 | end 61 | 62 | def test_download_large_file_with_progress_should_report_progress 63 | local = "/path/to/local" 64 | remote = "/path/to/remote" 65 | text = "0123456789" * 1024 66 | 67 | file = prepare_large_file_download(local, remote, text) 68 | 69 | assert_scripted_command do 70 | sftp.download(remote, local, :read_size => 1024) do |*args| 71 | record_progress(args) 72 | end 73 | end 74 | 75 | assert_equal text, file.string 76 | 77 | assert_progress_reported_open :remote => "/path/to/remote" 78 | assert_progress_reported_get 0, 1024 79 | assert_progress_reported_get 1024, 1024 80 | assert_progress_reported_get 2048, 1024 81 | assert_progress_reported_get 3072, 1024 82 | assert_progress_reported_get 4096, 1024 83 | assert_progress_reported_get 5120, 1024 84 | assert_progress_reported_get 6144, 1024 85 | assert_progress_reported_get 7168, 1024 86 | assert_progress_reported_get 8192, 1024 87 | assert_progress_reported_get 9216, 1024 88 | assert_progress_reported_close 89 | assert_progress_reported_finish 90 | assert_no_more_reported_events 91 | end 92 | 93 | def test_download_directory_should_mirror_directory_locally 94 | file1, file2 = prepare_directory_tree_download("/path/to/local", "/path/to/remote") 95 | 96 | assert_scripted_command do 97 | sftp.download("/path/to/remote", "/path/to/local", :recursive => true) 98 | end 99 | 100 | assert_equal "contents of file1", file1.string 101 | assert_equal "contents of file2", file2.string 102 | end 103 | 104 | def test_download_directory_with_progress_should_report_progress 105 | file1, file2 = prepare_directory_tree_download("/path/to/local", "/path/to/remote") 106 | 107 | assert_scripted_command do 108 | sftp.download("/path/to/remote", "/path/to/local", :recursive => true) do |*args| 109 | record_progress(args) 110 | end 111 | end 112 | 113 | assert_equal "contents of file1", file1.string 114 | assert_equal "contents of file2", file2.string 115 | 116 | assert_progress_reported_mkdir "/path/to/local" 117 | assert_progress_reported_mkdir "/path/to/local/subdir1" 118 | assert_progress_reported_open :remote => "/path/to/remote/file1" 119 | assert_progress_reported_open :remote => "/path/to/remote/subdir1/file2" 120 | assert_progress_reported_get 0, "contents of file1" 121 | assert_progress_reported_close :remote => "/path/to/remote/file1" 122 | assert_progress_reported_get 0, "contents of file2" 123 | assert_progress_reported_close :remote => "/path/to/remote/subdir1/file2" 124 | assert_progress_reported_finish 125 | assert_no_more_reported_events 126 | end 127 | 128 | def test_download_file_should_transfer_remote_to_local_buffer 129 | remote = "/path/to/remote" 130 | text = "this is some text\n" 131 | 132 | expect_file_transfer(remote, text) 133 | 134 | local = StringIO.new 135 | 136 | assert_scripted_command { sftp.download(remote, local) } 137 | assert_equal text, local.string 138 | end 139 | 140 | def test_download_directory_to_buffer_should_fail 141 | expect_sftp_session :server_version => 3 142 | Net::SSH::Test::Extensions::IO.with_test_extension do 143 | assert_raises(ArgumentError) { sftp.download("/path/to/remote", StringIO.new, :recursive => true) } 144 | end 145 | end 146 | 147 | private 148 | 149 | def expect_file_transfer(remote, text, opts={}) 150 | expect_sftp_session :server_version => 3 do |channel| 151 | channel.sends_packet(FXP_OPEN, :long, 0, :string, remote, :long, 0x01, :long, 0) 152 | channel.gets_packet(FXP_HANDLE, :long, 0, :string, "handle") 153 | channel.sends_packet(FXP_READ, :long, 1, :string, "handle", :int64, 0, :long, 32_000) 154 | channel.gets_packet_in_two(opts[:fragment_len], FXP_DATA, :long, 1, :string, text) 155 | channel.sends_packet(FXP_READ, :long, 2, :string, "handle", :int64, text.bytesize, :long, 32_000) 156 | channel.gets_packet(FXP_STATUS, :long, 2, :long, 1) 157 | channel.sends_packet(FXP_CLOSE, :long, 3, :string, "handle") 158 | channel.gets_packet(FXP_STATUS, :long, 3, :long, 0) 159 | end 160 | end 161 | 162 | def prepare_large_file_download(local, remote, text, requested_chunk_size = FXP_DATA_CHUNK_SIZE) 163 | expect_sftp_session :server_version => 3 do |channel| 164 | channel.sends_packet(FXP_OPEN, :long, 0, :string, remote, :long, 0x01, :long, 0) 165 | channel.gets_packet(FXP_HANDLE, :long, 0, :string, "handle") 166 | offset = 0 167 | data_packet_count = (text.bytesize / FXP_DATA_CHUNK_SIZE.to_f).ceil 168 | data_packet_count.times do |n| 169 | payload = text[n*FXP_DATA_CHUNK_SIZE,FXP_DATA_CHUNK_SIZE] 170 | channel.sends_packet(FXP_READ, :long, n+1, :string, "handle", :int64, offset, :long, requested_chunk_size) 171 | offset += payload.bytesize 172 | channel.gets_packet(FXP_DATA, :long, n+1, :string, payload) 173 | end 174 | channel.sends_packet(FXP_READ, :long, data_packet_count + 1, :string, "handle", :int64, offset, :long, requested_chunk_size) 175 | channel.gets_packet(FXP_STATUS, :long, data_packet_count + 1, :long, 1) 176 | channel.sends_packet(FXP_CLOSE, :long, data_packet_count + 2, :string, "handle") 177 | channel.gets_packet(FXP_STATUS, :long, data_packet_count + 2, :long, 0) 178 | end 179 | 180 | file = StringIO.new 181 | File.stubs(:open).with(local, "wb").returns(file) 182 | 183 | return file 184 | end 185 | 186 | # 0:OPENDIR(remote) -> 187 | # <- 0:HANDLE("dir1") 188 | # 1:READDIR("dir1") -> 189 | # <- 1:NAME("..", ".", "subdir1", "file1") 190 | # 2:OPENDIR(remote/subdir1) -> 191 | # 3:OPEN(remote/file1) -> 192 | # 4:READDIR("dir1") -> 193 | # <- 2:HANDLE("dir2") 194 | # 5:READDIR("dir2") -> 195 | # <- 3:HANDLE("file1") 196 | # 6:READ("file1", 0, 32k) -> 197 | # <- 4:STATUS(1) 198 | # 7:CLOSE("dir1") -> 199 | # <- 5:NAME("..", ".", "file2") 200 | # 8:OPEN(remote/subdir1/file2) -> 201 | # 9:READDIR("dir2") -> 202 | # <- 6:DATA("blah blah blah") 203 | # 10:READ("file1", n, 32k) 204 | # <- 7:STATUS(0) 205 | # <- 8:HANDLE("file2") 206 | # 11:READ("file2", 0, 32k) -> 207 | # <- 9:STATUS(1) 208 | # 12:CLOSE("dir2") -> 209 | # <- 10:STATUS(1) 210 | # 13:CLOSE("file1") -> 211 | # <- 11:DATA("blah blah blah") 212 | # 14:READ("file2", n, 32k) -> 213 | # <- 12:STATUS(0) 214 | # <- 13:STATUS(0) 215 | # <- 14:STATUS(1) 216 | # 15:CLOSE("file2") -> 217 | # <- 15:STATUS(0) 218 | 219 | def prepare_directory_tree_download(local, remote) 220 | file1_contents = "contents of file1" 221 | file2_contents = "contents of file2" 222 | expect_sftp_session :server_version => 3 do |channel| 223 | channel.sends_packet(FXP_OPENDIR, :long, 0, :string, remote) 224 | channel.gets_packet(FXP_HANDLE, :long, 0, :string, "dir1") 225 | 226 | channel.sends_packet(FXP_READDIR, :long, 1, :string, "dir1") 227 | channel.gets_packet(FXP_NAME, :long, 1, :long, 4, 228 | :string, "..", :string, "drwxr-xr-x 4 bob bob 136 Aug 1 ..", :long, 0x04, :long, 040755, 229 | :string, ".", :string, "drwxr-xr-x 4 bob bob 136 Aug 1 .", :long, 0x04, :long, 040755, 230 | :string, "subdir1", :string, "drwxr-xr-x 4 bob bob 136 Aug 1 subdir1", :long, 0x04, :long, 040755, 231 | :string, "file1", :string, "-rw-rw-r-- 1 bob bob 100 Aug 1 file1", :long, 0x04, :long, 0100644) 232 | 233 | channel.sends_packet(FXP_OPENDIR, :long, 2, :string, File.join(remote, "subdir1")) 234 | channel.sends_packet(FXP_OPEN, :long, 3, :string, File.join(remote, "file1"), :long, 0x01, :long, 0) 235 | channel.sends_packet(FXP_READDIR, :long, 4, :string, "dir1") 236 | 237 | channel.gets_packet(FXP_HANDLE, :long, 2, :string, "dir2") 238 | channel.sends_packet(FXP_READDIR, :long, 5, :string, "dir2") 239 | 240 | channel.gets_packet(FXP_HANDLE, :long, 3, :string, "file1") 241 | channel.sends_packet(FXP_READ, :long, 6, :string, "file1", :int64, 0, :long, 32_000) 242 | 243 | channel.gets_packet(FXP_STATUS, :long, 4, :long, 1) 244 | channel.sends_packet(FXP_CLOSE, :long, 7, :string, "dir1") 245 | 246 | channel.gets_packet(FXP_NAME, :long, 5, :long, 3, 247 | :string, "..", :string, "drwxr-xr-x 4 bob bob 136 Aug 1 ..", :long, 0x04, :long, 040755, 248 | :string, ".", :string, "drwxr-xr-x 4 bob bob 136 Aug 1 .", :long, 0x04, :long, 040755, 249 | :string, "file2", :string, "-rw-rw-r-- 1 bob bob 100 Aug 1 file2", :long, 0x04, :long, 0100644) 250 | 251 | channel.sends_packet(FXP_OPEN, :long, 8, :string, File.join(remote, "subdir1", "file2"), :long, 0x01, :long, 0) 252 | channel.sends_packet(FXP_READDIR, :long, 9, :string, "dir2") 253 | 254 | channel.gets_packet(FXP_DATA, :long, 6, :string, file1_contents) 255 | channel.sends_packet(FXP_READ, :long, 10, :string, "file1", :int64, file1_contents.bytesize, :long, 32_000) 256 | 257 | channel.gets_packet(FXP_STATUS, :long, 7, :long, 0) 258 | channel.gets_packet(FXP_HANDLE, :long, 8, :string, "file2") 259 | channel.sends_packet(FXP_READ, :long, 11, :string, "file2", :int64, 0, :long, 32_000) 260 | 261 | channel.gets_packet(FXP_STATUS, :long, 9, :long, 1) 262 | channel.sends_packet(FXP_CLOSE, :long, 12, :string, "dir2") 263 | 264 | channel.gets_packet(FXP_STATUS, :long, 10, :long, 1) 265 | channel.sends_packet(FXP_CLOSE, :long, 13, :string, "file1") 266 | 267 | channel.gets_packet(FXP_DATA, :long, 11, :string, file2_contents) 268 | channel.sends_packet(FXP_READ, :long, 14, :string, "file2", :int64, file2_contents.bytesize, :long, 32_000) 269 | 270 | channel.gets_packet(FXP_STATUS, :long, 12, :long, 0) 271 | channel.gets_packet(FXP_STATUS, :long, 13, :long, 0) 272 | channel.gets_packet(FXP_STATUS, :long, 14, :long, 1) 273 | channel.sends_packet(FXP_CLOSE, :long, 15, :string, "file2") 274 | channel.gets_packet(FXP_STATUS, :long, 15, :long, 0) 275 | end 276 | 277 | File.expects(:directory?).with(local).returns(false) 278 | File.expects(:directory?).with(File.join(local, "subdir1")).returns(false) 279 | Dir.expects(:mkdir).with(local) 280 | Dir.expects(:mkdir).with(File.join(local, "subdir1")) 281 | 282 | file1 = StringIO.new 283 | file2 = StringIO.new 284 | File.expects(:open).with(File.join(local, "file1"), "wb").returns(file1) 285 | File.expects(:open).with(File.join(local, "subdir1", "file2"), "wb").returns(file2) 286 | 287 | [file1, file2] 288 | end 289 | end -------------------------------------------------------------------------------- /test/test_file.rb: -------------------------------------------------------------------------------- 1 | require 'common' 2 | 3 | class FileOperationsTest < Net::SFTP::TestCase 4 | def setup 5 | @sftp = mock("sftp") 6 | @file = Net::SFTP::Operations::File.new(@sftp, "handle") 7 | @save_dollar_fslash, $/ = $/, "\n" 8 | @save_dollar_bslash, $\ = $\, nil 9 | end 10 | 11 | def teardown 12 | $/ = @save_dollar_fslash 13 | $\ = @save_dollar_bslash 14 | end 15 | 16 | def test_pos_assignment_should_set_position 17 | @file.pos = 15 18 | assert_equal 15, @file.pos 19 | end 20 | 21 | def test_pos_assignment_should_reset_eof 22 | @sftp.expects(:read!).with("handle", 0, 8192).returns(nil) 23 | assert !@file.eof? 24 | @file.read 25 | assert @file.eof? 26 | @file.pos = 0 27 | assert !@file.eof? 28 | end 29 | 30 | def test_close_should_close_handle_and_set_handle_to_nil 31 | assert_equal "handle", @file.handle 32 | @sftp.expects(:close!).with("handle") 33 | @file.close 34 | assert_nil @file.handle 35 | end 36 | 37 | def test_eof_should_be_false_if_at_eof_but_data_remains_in_buffer 38 | @sftp.expects(:read!).returns("hello world", nil) 39 | @file.read(1) 40 | assert !@file.eof? 41 | end 42 | 43 | def test_eof_should_be_true_if_at_eof_and_no_data_in_buffer 44 | @sftp.expects(:read!).times(2).returns("hello world", nil) 45 | @file.read 46 | assert @file.eof? 47 | end 48 | 49 | def test_read_without_argument_should_read_and_return_remainder_of_file_and_set_pos 50 | @sftp.expects(:read!).times(2).returns("hello world", nil) 51 | assert_equal "hello world", @file.read 52 | assert_equal 11, @file.pos 53 | end 54 | 55 | def test_read_with_argument_should_read_and_return_n_bytes_and_set_pos 56 | @sftp.expects(:read!).returns("hello world") 57 | assert_equal "hello", @file.read(5) 58 | assert_equal 5, @file.pos 59 | end 60 | 61 | def test_read_after_pos_assignment_should_read_from_specified_position 62 | @sftp.expects(:read!).with("handle", 5, 8192).returns("hello world") 63 | @file.pos = 5 64 | assert_equal "hello", @file.read(5) 65 | assert_equal 10, @file.pos 66 | end 67 | 68 | def test_gets_without_argument_should_read_until_first_dollar_fslash 69 | @sftp.expects(:read!).returns("hello world\ngoodbye world\n\nfarewell!\n") 70 | assert_equal "\n", $/ 71 | assert_equal "hello world\n", @file.gets 72 | assert_equal 12, @file.pos 73 | end 74 | 75 | def test_gets_with_empty_argument_should_read_until_double_dollar_fslash 76 | @sftp.expects(:read!).returns("hello world\ngoodbye world\n\nfarewell!\n") 77 | assert_equal "\n", $/ 78 | assert_equal "hello world\ngoodbye world\n\n", @file.gets("") 79 | assert_equal 27, @file.pos 80 | end 81 | 82 | def test_gets_with_argument_should_read_until_first_instance_of_argument 83 | @sftp.expects(:read!).returns("hello world\ngoodbye world\n\nfarewell!\n") 84 | assert_equal "hello w", @file.gets("w") 85 | assert_equal 7, @file.pos 86 | end 87 | 88 | def test_gets_when_no_such_delimiter_exists_in_stream_should_read_to_EOF 89 | @sftp.expects(:read!).times(2).returns("hello world\ngoodbye world\n\nfarewell!\n", nil) 90 | assert_equal "hello world\ngoodbye world\n\nfarewell!\n", @file.gets("X") 91 | assert @file.eof? 92 | end 93 | 94 | def test_gets_when_nil_delimiter_should_fread_to_EOF 95 | @sftp.expects(:read!).times(2).returns("hello world\ngoodbye world\n\nfarewell!\n", nil) 96 | assert_equal "hello world\ngoodbye world\n\nfarewell!\n", @file.gets(nil) 97 | assert @file.eof? 98 | end 99 | 100 | def test_gets_with_integer_argument_should_read_number_of_bytes 101 | @sftp.expects(:read!).returns("hello world\ngoodbye world\n\nfarewell!\n") 102 | assert_equal "hello w", @file.gets(7) 103 | end 104 | 105 | def test_gets_with_delimiter_and_limit_should_read_to_delimiter_if_less_than_limit 106 | @sftp.expects(:read!).returns("hello world\ngoodbye world\n\nfarewell!\n") 107 | assert_equal "hello w", @file.gets("w", 11) 108 | end 109 | 110 | def test_gets_with_delimiter_and_limit_should_read_to_limit_if_less_than_delimiter 111 | @sftp.expects(:read!).returns("hello world\ngoodbye world\n\nfarewell!\n") 112 | assert_equal "hello", @file.gets("w", 5) 113 | end 114 | 115 | def test_gets_when_no_such_delimiter_exists_in_stream_but_limit_provided_should_read_to_limit 116 | @sftp.expects(:read!).returns("hello world\ngoodbye world\n\nfarewell!\n") 117 | assert_equal "hello w", @file.gets("z", 7) 118 | end 119 | 120 | def test_gets_when_nil_delimiter_and_limit_provided_should_read_to_limit 121 | @sftp.expects(:read!).returns("hello world\ngoodbye world\n\nfarewell!\n") 122 | assert_equal "hello w", @file.gets(nil, 7) 123 | end 124 | 125 | def test_gets_at_EOF_should_return_nil 126 | @sftp.expects(:read!).returns(nil) 127 | assert_nil @file.gets 128 | assert @file.eof? 129 | end 130 | 131 | def test_readline_should_raise_exception_on_EOF 132 | @sftp.expects(:read!).returns(nil) 133 | assert_raises(EOFError) { @file.readline } 134 | end 135 | 136 | def test_rewind_should_reset_to_beginning_of_file 137 | @sftp.expects(:read!).times(2).returns("hello world", nil) 138 | @file.read 139 | assert @file.eof? 140 | @file.rewind 141 | assert !@file.eof? 142 | assert_equal 0, @file.pos 143 | end 144 | 145 | def test_rewind 146 | @sftp.expects(:write!).with("handle", 0, "hello world\n") 147 | @sftp.expects(:read!).with("handle", 12, 8192).returns("hello world\n") 148 | @sftp.expects(:read!).with("handle", 0, 8192).returns("hello world\n") 149 | @file.puts "hello world\n" 150 | assert_equal "hello", @file.read(5) 151 | @file.rewind 152 | assert_equal "hello world", @file.read(11) 153 | end 154 | 155 | def test_write_should_write_data_and_increment_pos_and_return_data_length 156 | @sftp.expects(:write!).with("handle", 0, "hello world") 157 | assert_equal 11, @file.write("hello world") 158 | assert_equal 11, @file.pos 159 | end 160 | 161 | def test_write_after_pos_assignment_should_write_at_position 162 | @sftp.expects(:write!).with("handle", 15, "hello world") 163 | @file.pos = 15 164 | assert_equal 11, @file.write("hello world") 165 | assert_equal 26, @file.pos 166 | end 167 | 168 | def test_write_should_write_mutlibyte_string_data_and_increment_pos_and_return_data_length 169 | @sftp.expects(:write!).with("handle", 0, "hello world, 你好,世界") 170 | assert_equal 28, @file.write("hello world, 你好,世界") 171 | assert_equal 28, @file.pos 172 | end 173 | 174 | def test_print_with_no_arguments_should_write_nothing_if_dollar_bslash_is_nil 175 | assert_nil $\ 176 | @sftp.expects(:write!).never 177 | @file.print 178 | end 179 | 180 | def test_print_with_no_arguments_should_write_dollar_bslash_if_dollar_bslash_is_not_nil 181 | $\ = "-" 182 | @sftp.expects(:write!).with("handle", 0, "-") 183 | @file.print 184 | end 185 | 186 | def test_print_with_arguments_should_write_all_arguments 187 | @sftp.expects(:write!).with("handle", 0, "hello") 188 | @sftp.expects(:write!).with("handle", 5, " ") 189 | @sftp.expects(:write!).with("handle", 6, "world") 190 | @file.print("hello", " ", "world") 191 | end 192 | 193 | def test_puts_should_recursively_puts_array_arguments 194 | 10.times do |i| 195 | @sftp.expects(:write!).with("handle", i*2, i.to_s) 196 | @sftp.expects(:write!).with("handle", i*2+1, "\n") 197 | end 198 | @file.puts 0, [1, [2, 3], 4, [5, [6, 7, 8]]], 9 199 | end 200 | 201 | def test_puts_should_not_append_newline_if_argument_ends_in_newline 202 | @sftp.expects(:write!).with("handle", 0, "a") 203 | @sftp.expects(:write!).with("handle", 1, "\n") 204 | @sftp.expects(:write!).with("handle", 2, "b\n") 205 | @sftp.expects(:write!).with("handle", 4, "c") 206 | @sftp.expects(:write!).with("handle", 5, "\n") 207 | @file.puts "a", "b\n", "c" 208 | end 209 | 210 | def test_stat_should_return_attributes_object_for_handle 211 | stat = stub("stat") 212 | @sftp.expects(:fstat!).with("handle").returns(stat) 213 | assert_equal stat, @file.stat 214 | end 215 | 216 | def test_size_should_return_size_from_stat 217 | stat = stub(size: 1024) 218 | @sftp.expects(:fstat!).with("handle").returns(stat) 219 | assert_equal 1024, @file.size 220 | end 221 | end 222 | -------------------------------------------------------------------------------- /test/test_file_factory.rb: -------------------------------------------------------------------------------- 1 | require "common" 2 | 3 | class FileFactoryTest < Net::SFTP::TestCase 4 | def setup 5 | @sftp = stub('sftp') 6 | @factory = Net::SFTP::Operations::FileFactory.new(@sftp) 7 | end 8 | 9 | def test_open_with_block_should_yield_and_close_handle 10 | @sftp.expects(:open!).with("/path/to/remote", "r", :permissions => nil).returns("handle") 11 | @sftp.expects(:close!).with("handle") 12 | 13 | called = false 14 | @factory.open("/path/to/remote") do |f| 15 | called = true 16 | assert_instance_of Net::SFTP::Operations::File, f 17 | end 18 | 19 | assert called 20 | end 21 | 22 | def test_open_with_block_should_close_file_even_if_exception_is_raised 23 | @sftp.expects(:open!).with("/path/to/remote", "r", :permissions => nil).returns("handle") 24 | @sftp.expects(:close!).with("handle") 25 | 26 | assert_raises(RuntimeError) do 27 | @factory.open("/path/to/remote") { |f| raise RuntimeError, "b00m" } 28 | end 29 | end 30 | 31 | def test_open_without_block_should_return_new_file 32 | @sftp.expects(:open!).with("/path/to/remote", "r", :permissions => nil).returns("handle") 33 | @sftp.expects(:close!).never 34 | 35 | f = @factory.open("/path/to/remote") 36 | assert_instance_of Net::SFTP::Operations::File, f 37 | end 38 | 39 | def test_directory_should_be_true_for_directory 40 | @sftp.expects(:lstat!).with("/path/to/dir").returns(mock('attrs', :directory? => true)) 41 | assert @factory.directory?("/path/to/dir") 42 | end 43 | 44 | def test_directory_should_be_false_for_non_directory 45 | @sftp.expects(:lstat!).with("/path/to/file").returns(mock('attrs', :directory? => false)) 46 | assert !@factory.directory?("/path/to/file") 47 | end 48 | end -------------------------------------------------------------------------------- /test/test_packet.rb: -------------------------------------------------------------------------------- 1 | require 'common' 2 | 3 | class PacketTest < Net::SFTP::TestCase 4 | def test_packet_should_auto_read_type_byte 5 | packet = Net::SFTP::Packet.new("\001rest-of-packet-here") 6 | assert_equal 1, packet.type 7 | assert_equal "rest-of-packet-here", packet.content[packet.position..-1] 8 | end 9 | end -------------------------------------------------------------------------------- /test/test_protocol.rb: -------------------------------------------------------------------------------- 1 | require 'common' 2 | 3 | class ProtocolTest < Net::SFTP::TestCase 4 | 1.upto(6) do |version| 5 | define_method("test_load_version_#{version}_should_return_v#{version}_driver") do 6 | session = stub('session', :logger => nil) 7 | driver = Net::SFTP::Protocol.load(session, version) 8 | assert_instance_of Net::SFTP::Protocol.const_get("V%02d" % version)::Base, driver 9 | end 10 | end 11 | 12 | def test_load_version_7_should_be_unsupported 13 | assert_raises(NotImplementedError) do 14 | Net::SFTP::Protocol.load(stub('session'), 7) 15 | end 16 | end 17 | end -------------------------------------------------------------------------------- /test/test_request.rb: -------------------------------------------------------------------------------- 1 | require 'common' 2 | 3 | class RequestTest < Net::SFTP::TestCase 4 | def test_property_setter_should_symbolize_key 5 | request = Net::SFTP::Request.new(stub("session"), :open, 1) 6 | request["key"] = :value 7 | assert_equal :value, request['key'] 8 | assert_equal :value, request[:key] 9 | assert_equal :value, request.properties[:key] 10 | assert_nil request.properties['key'] 11 | end 12 | 13 | def test_pending_should_query_pending_requests_of_session 14 | session = stub("session", :pending_requests => {1 => true}) 15 | request = Net::SFTP::Request.new(session, :open, 1) 16 | assert request.pending? 17 | request = Net::SFTP::Request.new(session, :open, 2) 18 | assert !request.pending? 19 | end 20 | 21 | def test_wait_should_run_loop_while_pending_and_return_self 22 | session = MockSession.new 23 | request = Net::SFTP::Request.new(session, :open, 1) 24 | request.expects(:pending?).times(4).returns(true, true, true, false) 25 | assert_equal 0, session.loops 26 | assert_equal request, request.wait 27 | assert_equal 4, session.loops 28 | end 29 | 30 | def test_respond_to_should_set_response_property 31 | packet = stub("packet", :type => 1) 32 | session = stub("session", :protocol => mock("protocol")) 33 | session.protocol.expects(:parse).with(packet).returns({}) 34 | request = Net::SFTP::Request.new(session, :open, 1) 35 | assert_nil request.response 36 | request.respond_to(packet) 37 | assert_instance_of Net::SFTP::Response, request.response 38 | end 39 | 40 | def test_respond_to_with_callback_should_invoke_callback 41 | packet = stub("packet", :type => 1) 42 | session = stub("session", :protocol => mock("protocol")) 43 | session.protocol.expects(:parse).with(packet).returns({}) 44 | 45 | called = false 46 | request = Net::SFTP::Request.new(session, :open, 1) do |response| 47 | called = true 48 | assert_equal request.response, response 49 | end 50 | 51 | request.respond_to(packet) 52 | assert called 53 | end 54 | 55 | private 56 | 57 | class MockSession 58 | attr_reader :loops 59 | 60 | def initialize 61 | @loops = 0 62 | end 63 | 64 | def loop 65 | while true 66 | @loops += 1 67 | break unless yield 68 | end 69 | end 70 | end 71 | end -------------------------------------------------------------------------------- /test/test_response.rb: -------------------------------------------------------------------------------- 1 | require 'common' 2 | 3 | class ResponseTest < Net::SFTP::TestCase 4 | def test_code_should_default_to_FX_OK 5 | response = Net::SFTP::Response.new(mock("response")) 6 | assert_equal Net::SFTP::Response::FX_OK, response.code 7 | end 8 | 9 | def test_brackets_should_symbolize_key 10 | response = Net::SFTP::Response.new(mock("response"), :handle => "foo") 11 | assert_equal "foo", response['handle'] 12 | end 13 | 14 | def test_to_s_with_nil_message_should_show_default_message 15 | response = Net::SFTP::Response.new(mock("response"), :code => 14) 16 | assert_equal "no space on filesystem (14)", response.to_s 17 | end 18 | 19 | def test_to_s_with_empty_message_should_show_default_message 20 | response = Net::SFTP::Response.new(mock("response"), :code => 14, :message => "") 21 | assert_equal "no space on filesystem (14)", response.to_s 22 | end 23 | 24 | def test_to_s_with_default_message_should_show_default_message 25 | response = Net::SFTP::Response.new(mock("response"), :code => 14, :message => "no space on filesystem") 26 | assert_equal "no space on filesystem (14)", response.to_s 27 | end 28 | 29 | def test_to_s_with_explicit_message_should_show_explicit_message 30 | response = Net::SFTP::Response.new(mock("response"), :code => 14, :message => "out of space") 31 | assert_equal "out of space (no space on filesystem, 14)", response.to_s 32 | end 33 | 34 | def test_ok_should_be_true_when_code_is_FX_OK 35 | response = Net::SFTP::Response.new(mock("response")) 36 | assert_equal true, response.ok? 37 | end 38 | 39 | def test_ok_should_be_false_when_code_is_not_FX_OK 40 | response = Net::SFTP::Response.new(mock("response"), :code => 14) 41 | assert_equal false, response.ok? 42 | end 43 | 44 | def test_eof_should_be_true_when_code_is_FX_EOF 45 | response = Net::SFTP::Response.new(mock("response"), :code => 1) 46 | assert_equal true, response.eof? 47 | end 48 | 49 | def test_eof_should_be_false_when_code_is_not_FX_EOF 50 | response = Net::SFTP::Response.new(mock("response"), :code => 14) 51 | assert_equal false, response.eof? 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /test/test_start.rb: -------------------------------------------------------------------------------- 1 | require 'common' 2 | 3 | class StartTest < Net::SFTP::TestCase 4 | def test_with_block 5 | ssh = mock('ssh') 6 | ssh.expects(:close) 7 | Net::SSH.expects(:start).with('host', 'user', {}).returns(ssh) 8 | 9 | sftp = mock('sftp') 10 | # TODO: figure out how to verify a block is passed, and call it later. 11 | # I suspect this is hard to do properly with mocha. 12 | Net::SFTP::Session.expects(:new).with(ssh, nil).returns(sftp) 13 | sftp.expects(:connect!).returns(sftp) 14 | sftp.expects(:loop) 15 | 16 | Net::SFTP.start('host', 'user') do 17 | # NOTE: currently not called! 18 | end 19 | end 20 | 21 | def test_with_block_and_options 22 | ssh = mock('ssh') 23 | ssh.expects(:close) 24 | Net::SSH.expects(:start).with('host', 'user', auth_methods: ["password"]).returns(ssh) 25 | 26 | sftp = mock('sftp') 27 | Net::SFTP::Session.expects(:new).with(ssh, 3).returns(sftp) 28 | sftp.expects(:connect!).returns(sftp) 29 | sftp.expects(:loop) 30 | 31 | Net::SFTP.start('host', 'user', {auth_methods: ["password"]}, {version: 3}) do 32 | # NOTE: currently not called! 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /test/test_upload.rb: -------------------------------------------------------------------------------- 1 | require "common" 2 | 3 | class UploadTest < Net::SFTP::TestCase 4 | def setup 5 | prepare_progress! 6 | end 7 | 8 | def test_upload_file_should_send_file_contents 9 | expect_file_transfer("/path/to/local", "/path/to/remote", "here are the contents") 10 | assert_scripted_command { sftp.upload("/path/to/local", "/path/to/remote") } 11 | end 12 | 13 | def test_upload_file_without_remote_uses_filename_of_local_file 14 | expect_file_transfer("/path/to/local", "local", "here are the contents") 15 | 16 | assert_scripted_command do 17 | sftp.upload("/path/to/local") { |*args| record_progress(args) } 18 | end 19 | 20 | assert_progress_reported_open(:remote => "local") 21 | assert_progress_reported_put(0, "here are the contents", :remote => "local") 22 | assert_progress_reported_close(:remote => "local") 23 | assert_progress_reported_finish 24 | assert_no_more_reported_events 25 | end 26 | 27 | def test_upload_file_with_progress_should_report_progress 28 | expect_file_transfer("/path/to/local", "/path/to/remote", "here are the contents") 29 | 30 | assert_scripted_command do 31 | sftp.upload("/path/to/local", "/path/to/remote") { |*args| record_progress(args) } 32 | end 33 | 34 | assert_progress_reported_open(:remote => "/path/to/remote") 35 | assert_progress_reported_put(0, "here are the contents", :remote => "/path/to/remote") 36 | assert_progress_reported_close(:remote => "/path/to/remote") 37 | assert_progress_reported_finish 38 | assert_no_more_reported_events 39 | end 40 | 41 | def test_upload_file_with_progress_handler_should_report_progress 42 | expect_file_transfer("/path/to/local", "/path/to/remote", "here are the contents") 43 | 44 | assert_scripted_command do 45 | sftp.upload("/path/to/local", "/path/to/remote", :progress => ProgressHandler.new(@progress)) 46 | end 47 | 48 | assert_progress_reported_open(:remote => "/path/to/remote") 49 | assert_progress_reported_put(0, "here are the contents", :remote => "/path/to/remote") 50 | assert_progress_reported_close(:remote => "/path/to/remote") 51 | assert_progress_reported_finish 52 | assert_no_more_reported_events 53 | end 54 | 55 | def test_upload_file_should_read_chunks_of_size(requested_size=nil) 56 | size = requested_size || Net::SFTP::Operations::Upload::DEFAULT_READ_SIZE 57 | expect_sftp_session :server_version => 3 do |channel| 58 | channel.sends_packet(FXP_OPEN, :long, 0, :string, "/path/to/remote", :long, 0x1A, :long, 0) 59 | channel.gets_packet(FXP_HANDLE, :long, 0, :string, "handle") 60 | channel.sends_packet(FXP_WRITE, :long, 1, :string, "handle", :int64, 0, :string, "a" * size) 61 | channel.sends_packet(FXP_WRITE, :long, 2, :string, "handle", :int64, size, :string, "b" * size) 62 | channel.sends_packet(FXP_WRITE, :long, 3, :string, "handle", :int64, size*2, :string, "c" * size) 63 | channel.gets_packet(FXP_STATUS, :long, 1, :long, 0) 64 | channel.sends_packet(FXP_WRITE, :long, 4, :string, "handle", :int64, size*3, :string, "d" * size) 65 | channel.gets_packet(FXP_STATUS, :long, 2, :long, 0) 66 | channel.sends_packet(FXP_CLOSE, :long, 5, :string, "handle") 67 | channel.gets_packet(FXP_STATUS, :long, 3, :long, 0) 68 | channel.gets_packet(FXP_STATUS, :long, 4, :long, 0) 69 | channel.gets_packet(FXP_STATUS, :long, 5, :long, 0) 70 | end 71 | 72 | expect_file("/path/to/local", "a" * size + "b" * size + "c" * size + "d" * size) 73 | 74 | assert_scripted_command do 75 | opts = {} 76 | opts[:read_size] = size if requested_size 77 | sftp.upload("/path/to/local", "/path/to/remote", opts) 78 | end 79 | end 80 | 81 | def test_upload_file_with_custom_read_size_should_read_chunks_of_that_size 82 | test_upload_file_should_read_chunks_of_size(100) 83 | end 84 | 85 | def test_upload_file_with_custom_requests_should_start_that_many_writes 86 | size = Net::SFTP::Operations::Upload::DEFAULT_READ_SIZE 87 | expect_sftp_session :server_version => 3 do |channel| 88 | channel.sends_packet(FXP_OPEN, :long, 0, :string, "/path/to/remote", :long, 0x1A, :long, 0) 89 | channel.gets_packet(FXP_HANDLE, :long, 0, :string, "handle") 90 | channel.sends_packet(FXP_WRITE, :long, 1, :string, "handle", :int64, 0, :string, "a" * size) 91 | channel.sends_packet(FXP_WRITE, :long, 2, :string, "handle", :int64, size, :string, "b" * size) 92 | channel.sends_packet(FXP_WRITE, :long, 3, :string, "handle", :int64, size*2, :string, "c" * size) 93 | channel.sends_packet(FXP_WRITE, :long, 4, :string, "handle", :int64, size*3, :string, "d" * size) 94 | channel.gets_packet(FXP_STATUS, :long, 1, :long, 0) 95 | channel.sends_packet(FXP_CLOSE, :long, 5, :string, "handle") 96 | channel.gets_packet(FXP_STATUS, :long, 2, :long, 0) 97 | channel.gets_packet(FXP_STATUS, :long, 3, :long, 0) 98 | channel.gets_packet(FXP_STATUS, :long, 4, :long, 0) 99 | channel.gets_packet(FXP_STATUS, :long, 5, :long, 0) 100 | end 101 | 102 | expect_file("/path/to/local", "a" * size + "b" * size + "c" * size + "d" * size) 103 | 104 | assert_scripted_command do 105 | sftp.upload("/path/to/local", "/path/to/remote", :requests => 3) 106 | end 107 | end 108 | 109 | def test_upload_directory_should_mirror_directory_structure_remotely 110 | prepare_directory 111 | 112 | assert_scripted_command do 113 | sftp.upload("/path/to/local", "/path/to/remote", :mkdir => true) 114 | end 115 | end 116 | 117 | def test_upload_directory_with_handler_should_report_progress 118 | prepare_directory 119 | 120 | assert_scripted_command do 121 | sftp.upload("/path/to/local", "/path/to/remote", :mkdir => true) { |*args| record_progress(args) } 122 | end 123 | 124 | assert_progress_reported_open(:remote => "/path/to/remote/file1") 125 | assert_progress_reported_open(:remote => "/path/to/remote/file2") 126 | assert_progress_reported_open(:remote => "/path/to/remote/file3") 127 | assert_progress_reported_mkdir("/path/to/remote/subdir") 128 | assert_progress_reported_open(:remote => "/path/to/remote/subdir/other1") 129 | assert_progress_reported_open(:remote => "/path/to/remote/subdir/other2") 130 | assert_progress_reported_put(0, "contents of file1", :remote => "/path/to/remote/file1") 131 | assert_progress_reported_put(0, "contents of file2", :remote => "/path/to/remote/file2") 132 | assert_progress_reported_put(0, "contents of file3", :remote => "/path/to/remote/file3") 133 | assert_progress_reported_close(:remote => "/path/to/remote/file1") 134 | assert_progress_reported_put(0, "contents of other1", :remote => "/path/to/remote/subdir/other1") 135 | assert_progress_reported_put(0, "contents of other2", :remote => "/path/to/remote/subdir/other2") 136 | assert_progress_reported_close(:remote => "/path/to/remote/file2") 137 | assert_progress_reported_close(:remote => "/path/to/remote/file3") 138 | assert_progress_reported_close(:remote => "/path/to/remote/subdir/other1") 139 | assert_progress_reported_close(:remote => "/path/to/remote/subdir/other2") 140 | assert_progress_reported_finish 141 | assert_no_more_reported_events 142 | end 143 | 144 | def test_upload_io_should_send_io_as_file 145 | expect_sftp_session :server_version => 3 do |channel| 146 | channel.sends_packet(FXP_OPEN, :long, 0, :string, "/path/to/remote", :long, 0x1A, :long, 0) 147 | channel.gets_packet(FXP_HANDLE, :long, 0, :string, "handle") 148 | channel.sends_packet(FXP_WRITE, :long, 1, :string, "handle", :int64, 0, :string, "this is some text") 149 | channel.sends_packet(FXP_CLOSE, :long, 2, :string, "handle") 150 | channel.gets_packet(FXP_STATUS, :long, 1, :long, 0) 151 | channel.gets_packet(FXP_STATUS, :long, 2, :long, 0) 152 | end 153 | 154 | assert_scripted_command do 155 | sftp.upload(StringIO.new("this is some text"), "/path/to/remote") 156 | end 157 | end 158 | 159 | private 160 | 161 | def prepare_directory 162 | expect_directory("/path/to/local", %w(. .. file1 file2 file3 subdir)) 163 | expect_directory("/path/to/local/subdir", %w(. .. other1 other2)) 164 | expect_file("/path/to/local/file1", "contents of file1") 165 | expect_file("/path/to/local/file2", "contents of file2") 166 | expect_file("/path/to/local/file3", "contents of file3") 167 | expect_file("/path/to/local/subdir/other1", "contents of other1") 168 | expect_file("/path/to/local/subdir/other2", "contents of other2") 169 | 170 | expect_sftp_session :server_version => 3 do |ch| 171 | ch.sends_packet(FXP_MKDIR, :long, 0, :string, "/path/to/remote", :long, 0) 172 | ch.gets_packet(FXP_STATUS, :long, 0, :long, 0) 173 | ch.sends_packet(FXP_OPEN, :long, 1, :string, "/path/to/remote/file1", :long, 0x1A, :long, 0) 174 | ch.sends_packet(FXP_OPEN, :long, 2, :string, "/path/to/remote/file2", :long, 0x1A, :long, 0) 175 | ch.sends_packet(FXP_OPEN, :long, 3, :string, "/path/to/remote/file3", :long, 0x1A, :long, 0) 176 | ch.sends_packet(FXP_MKDIR, :long, 4, :string, "/path/to/remote/subdir", :long, 0) 177 | ch.sends_packet(FXP_OPEN, :long, 5, :string, "/path/to/remote/subdir/other1", :long, 0x1A, :long, 0) 178 | ch.sends_packet(FXP_OPEN, :long, 6, :string, "/path/to/remote/subdir/other2", :long, 0x1A, :long, 0) 179 | ch.gets_packet(FXP_HANDLE, :long, 1, :string, "hfile1") 180 | ch.sends_packet(FXP_WRITE, :long, 7, :string, "hfile1", :int64, 0, :string, "contents of file1") 181 | ch.gets_packet(FXP_HANDLE, :long, 2, :string, "hfile2") 182 | ch.sends_packet(FXP_WRITE, :long, 8, :string, "hfile2", :int64, 0, :string, "contents of file2") 183 | ch.gets_packet(FXP_HANDLE, :long, 3, :string, "hfile3") 184 | ch.sends_packet(FXP_WRITE, :long, 9, :string, "hfile3", :int64, 0, :string, "contents of file3") 185 | ch.gets_packet(FXP_STATUS, :long, 4, :long, 0) 186 | ch.gets_packet(FXP_HANDLE, :long, 5, :string, "hother1") 187 | ch.sends_packet(FXP_CLOSE, :long, 10, :string, "hfile1") 188 | ch.sends_packet(FXP_WRITE, :long, 11, :string, "hother1", :int64, 0, :string, "contents of other1") 189 | ch.gets_packet(FXP_HANDLE, :long, 6, :string, "hother2") 190 | ch.sends_packet(FXP_WRITE, :long, 12, :string, "hother2", :int64, 0, :string, "contents of other2") 191 | ch.gets_packet(FXP_STATUS, :long, 7, :long, 0) 192 | ch.sends_packet(FXP_CLOSE, :long, 13, :string, "hfile2") 193 | ch.gets_packet(FXP_STATUS, :long, 8, :long, 0) 194 | ch.sends_packet(FXP_CLOSE, :long, 14, :string, "hfile3") 195 | ch.gets_packet(FXP_STATUS, :long, 9, :long, 0) 196 | ch.sends_packet(FXP_CLOSE, :long, 15, :string, "hother1") 197 | ch.gets_packet(FXP_STATUS, :long, 10, :long, 0) 198 | ch.sends_packet(FXP_CLOSE, :long, 16, :string, "hother2") 199 | ch.gets_packet(FXP_STATUS, :long, 11, :long, 0) 200 | ch.gets_packet(FXP_STATUS, :long, 12, :long, 0) 201 | ch.gets_packet(FXP_STATUS, :long, 13, :long, 0) 202 | ch.gets_packet(FXP_STATUS, :long, 14, :long, 0) 203 | ch.gets_packet(FXP_STATUS, :long, 15, :long, 0) 204 | ch.gets_packet(FXP_STATUS, :long, 16, :long, 0) 205 | end 206 | end 207 | 208 | def expect_file(path, data) 209 | File.stubs(:directory?).with(path).returns(false) 210 | File.stubs(:exist?).with(path).returns(true) 211 | file = StringIO.new(data) 212 | file.stubs(:stat).returns(stub("stat", :size => data.length)) 213 | File.stubs(:open).with(path, "rb").returns(file) 214 | end 215 | 216 | def expect_directory(path, entries) 217 | Dir.stubs(:entries).with(path).returns(entries) 218 | File.stubs(:directory?).with(path).returns(true) 219 | end 220 | 221 | def expect_file_transfer(local, remote, data) 222 | expect_sftp_session :server_version => 3 do |channel| 223 | channel.sends_packet(FXP_OPEN, :long, 0, :string, remote, :long, 0x1A, :long, 0) 224 | channel.gets_packet(FXP_HANDLE, :long, 0, :string, "handle") 225 | channel.sends_packet(FXP_WRITE, :long, 1, :string, "handle", :int64, 0, :string, data) 226 | channel.sends_packet(FXP_CLOSE, :long, 2, :string, "handle") 227 | channel.gets_packet(FXP_STATUS, :long, 1, :long, 0) 228 | channel.gets_packet(FXP_STATUS, :long, 2, :long, 0) 229 | end 230 | 231 | expect_file(local, data) 232 | end 233 | end 234 | --------------------------------------------------------------------------------