├── .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 |
--------------------------------------------------------------------------------