├── .github └── workflows │ └── ci.yml ├── .gitignore ├── CHANGES.txt ├── Gemfile ├── LICENSE.txt ├── Manifest ├── README.md ├── Rakefile ├── SECURITY.md ├── lib ├── net │ ├── scp.rb │ └── scp │ │ ├── download.rb │ │ ├── errors.rb │ │ ├── upload.rb │ │ └── version.rb └── uri │ ├── open-scp.rb │ └── scp.rb ├── net-scp-public_cert.pem ├── net-scp.gemspec ├── setup.rb └── test ├── common.rb ├── test_all.rb ├── test_download.rb ├── test_scp.rb └── test_upload.rb /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | pull_request: 4 | push: { branches: master } 5 | permissions: 6 | contents: read 7 | jobs: 8 | test: 9 | runs-on: ubuntu-latest 10 | strategy: 11 | fail-fast: false 12 | matrix: 13 | ruby-version: ['2.3', '2.4', '2.5', '2.6', '2.7', '3.0', '3.1', '3.2', '3.3', 'ruby-head'] 14 | continue-on-error: ${{ matrix.ruby-version == 'ruby-head' }} 15 | steps: 16 | - uses: actions/checkout@v3 17 | - name: Set up Ruby ${{ matrix.ruby-version }} 18 | uses: ruby/setup-ruby@v1 19 | with: 20 | ruby-version: ${{ matrix.ruby-version }} 21 | bundler-cache: true # runs 'bundle install' and caches installed gems automatically 22 | - name: Run Tests 23 | run: bundle exec rake test 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | coverage 2 | pkg 3 | doc 4 | *.swp 5 | 6 | .DS_Store 7 | 8 | # Bundle local files 9 | /vendor/ 10 | .bundle/config 11 | Gemfile.lock 12 | 13 | lib/net/ssh/version.rb.old 14 | -------------------------------------------------------------------------------- /CHANGES.txt: -------------------------------------------------------------------------------- 1 | === 3.0.0 2 | 3 | * NetSHH 6.* support 4 | 5 | 6 | === 2.0.0 7 | 8 | * NetSSH 5.* support 9 | 10 | === 1.2.1 / 30 Apr 2014 11 | 12 | * Resign gem with new pubkey 13 | 14 | === 1.2.0 / 11 Apr 2014 15 | 16 | * Get the error string during download [jkeiser] 17 | 18 | === 1.1.2 / 6 Jul 2013 19 | 20 | * Explicit convert to string in shellescape [jwils] 21 | 22 | === 1.1.1 / 13 May 2013 23 | 24 | * Allow passing a shell to use when executing scp. [Arthur Schreiber] 25 | 26 | === 1.1.0 / 06 Feb 2013 27 | 28 | * Added public cert. All gem releases are now signed. See INSTALL in readme. 29 | 30 | === 1.0.4 / 16 Sep 2010 31 | 32 | * maintain filename sanitization compatibility with ruby 1.8.6 [Sung Pae, Tim Charper] 33 | 34 | === 1.0.3 / 17 Aug 2010 35 | 36 | * replace :sanitize_file_name with a call to String#shellescape [Sung Pae] 37 | * Added gemspec file and removed echoe dependency [Miron Cuperman, Delano Mandelbaum] 38 | * Removed Hanna dependency in Rakefile [Delano Mandelbaum] 39 | 40 | 41 | === 1.0.2 / 4 Feb 2009 42 | 43 | * Escape spaces in file names on remote server [Jamis Buck] 44 | 45 | 46 | === 1.0.1 / 29 May 2008 47 | 48 | * Make sure downloads open the file in binary mode to appease Windows [Jamis Buck] 49 | 50 | 51 | === 1.0.0 / 1 May 2008 52 | 53 | * Pass the channel object as the first argument to the progress callback [Jamis Buck] 54 | 55 | 56 | === 1.0 Preview Release 1 (0.99.0) / 22 Mar 2008 57 | 58 | * Birthday! 59 | -------------------------------------------------------------------------------- /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", ">= 1.11" 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/scp/download.rb 3 | lib/net/scp/errors.rb 4 | lib/net/scp/upload.rb 5 | lib/net/scp/version.rb 6 | lib/net/scp.rb 7 | lib/uri/open-scp.rb 8 | lib/uri/scp.rb 9 | Rakefile 10 | README.md 11 | setup.rb 12 | test/common.rb 13 | test/test_all.rb 14 | test/test_download.rb 15 | test/test_scp.rb 16 | test/test_upload.rb 17 | Manifest 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Net::SCP 2 | 3 | ***Please note: this project is in maintenance mode. It is not under active 4 | development but pull requests are very much welcome. Just be sure to include 5 | tests! -- delano*** 6 | 7 | * Docs: http://net-ssh.github.io/net-scp 8 | * Issues: https://github.com/net-ssh/net-scp/issues 9 | * Codes: https://github.com/net-ssh/net-scp 10 | * Email: net-ssh@solutious.com 11 | 12 | 13 | *As of v1.0.5, all gem releases are signed. See INSTALL.* 14 | 15 | ## DESCRIPTION: 16 | 17 | Net::SCP is a pure-Ruby implementation of the SCP protocol. This operates over 18 | SSH (and requires the Net::SSH library), and allows files and directory trees 19 | to be copied to and from a remote server. 20 | 21 | ## FEATURES/PROBLEMS: 22 | 23 | * Transfer files or entire directory trees to or from a remote host via SCP 24 | * Can preserve file attributes across transfers 25 | * Can download files in-memory, or direct-to-disk 26 | * Support for SCP URI's, and OpenURI 27 | 28 | 29 | ## SYNOPSIS: 30 | 31 | In a nutshell: 32 | 33 | ```ruby 34 | require 'net/scp' 35 | 36 | # upload a file to a remote server 37 | Net::SCP.upload!("remote.host.com", "username", 38 | "/local/path", "/remote/path", 39 | :ssh => { :password => "password" }) 40 | 41 | # upload recursively 42 | Net::SCP.upload!("remote.host", "username", "/path/to/local", "/path/to/remote", 43 | :ssh => { :password => "foo" }, :recursive => true) 44 | 45 | # download a file from a remote server 46 | Net::SCP.download!("remote.host.com", "username", 47 | "/remote/path", "/local/path", 48 | :ssh => { :password => "password" }) 49 | 50 | # download a file to an in-memory buffer 51 | data = Net::SCP::download!("remote.host.com", "username", "/remote/path") 52 | 53 | # use a persistent connection to transfer files 54 | Net::SCP.start("remote.host.com", "username", :password => "password") do |scp| 55 | # upload a file to a remote server 56 | scp.upload! "/local/path", "/remote/path" 57 | 58 | # upload from an in-memory buffer 59 | scp.upload! StringIO.new("some data to upload"), "/remote/path" 60 | 61 | # run multiple downloads in parallel 62 | d1 = scp.download("/remote/path", "/local/path") 63 | d2 = scp.download("/remote/path2", "/local/path2") 64 | [d1, d2].each { |d| d.wait } 65 | end 66 | 67 | # You can also use open-uri to grab data via scp: 68 | require 'uri/open-scp' 69 | data = open("scp://user@host/path/to/file.txt").read 70 | ``` 71 | 72 | For more information, see Net::SCP. 73 | 74 | ## REQUIREMENTS: 75 | 76 | * Net::SSH 2 77 | 78 | If you wish to run the tests, you'll also need: 79 | 80 | * Echoe (for Rakefile use) 81 | * Mocha (for tests) 82 | 83 | 84 | ## INSTALL: 85 | 86 | * ```gem install net-scp (might need sudo privileges)``` 87 | 88 | 89 | However, in order to be sure the code you're installing hasn't been tampered 90 | with, it's recommended that you verify the 91 | [signature](http://docs.seattlerb.org/rubygems/Gem/Security.html). To do this, 92 | you need to add my public key as a trusted certificate (you only need to do 93 | this once): 94 | 95 | ```sh 96 | # Add the public key as a trusted certificate 97 | # (You only need to do this once) 98 | $ curl -O https://raw.githubusercontent.com/net-ssh/net-ssh/master/net-ssh-public_cert.pem 99 | $ gem cert --add net-ssh-public_cert.pem 100 | ``` 101 | 102 | Then- when installing the gem - do so with high security: 103 | 104 | $ gem install net-scp -P HighSecurity 105 | 106 | If you don't add the public key, you'll see an error like "Couldn't verify 107 | data signature". If you're still having trouble let me know and I'll give you 108 | a hand. 109 | 110 | Or, you can do it the hard way (without Rubygems): 111 | 112 | * tar xzf net-scp-*.tgz 113 | * cd net-scp-* 114 | * ruby setup.rb config 115 | * ruby setup.rb install (might need sudo privileges) 116 | 117 | ## Security contact information 118 | 119 | See [SECURITY.md](SECURITY.md) 120 | 121 | ## LICENSE: 122 | 123 | (The MIT License) 124 | 125 | Copyright (c) 2008 Jamis Buck 126 | 127 | Permission is hereby granted, free of charge, to any person obtaining a copy 128 | of this software and associated documentation files (the 'Software'), to deal 129 | in the Software without restriction, including without limitation the rights 130 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 131 | copies of the Software, and to permit persons to whom the Software is 132 | furnished to do so, subject to the following conditions: 133 | 134 | The above copyright notice and this permission notice shall be included in all 135 | copies or substantial portions of the Software. 136 | 137 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 138 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 139 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 140 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 141 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 142 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 143 | SOFTWARE. 144 | -------------------------------------------------------------------------------- /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-scp" 18 | 19 | require_relative "lib/net/scp/version" 20 | version = Net::SCP::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-scp-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-scp-public_cert.pem" 32 | sh "gem cert --add net-scp-public_cert.pem" 33 | end 34 | end 35 | 36 | if false 37 | begin 38 | require "jeweler" 39 | Jeweler::Tasks.new do |s| 40 | s.version = version 41 | s.name = name 42 | s.summary = "A pure Ruby implementation of the SCP client protocol" 43 | s.description = s.summary 44 | s.email = "net-ssh@solutious.com" 45 | s.homepage = "https://github.com/net-ssh/net-scp" 46 | s.authors = ["Jamis Buck", "Delano Mandelbaum"] 47 | 48 | s.add_dependency 'net-ssh', ">=2.6.5" 49 | 50 | s.add_development_dependency 'test-unit' 51 | s.add_development_dependency 'mocha' 52 | 53 | s.license = "MIT" 54 | 55 | s.signing_key = File.join('/mnt/gem/', 'gem-private_key.pem') 56 | s.cert_chain = ['gem-public_cert.pem'] 57 | end 58 | Jeweler::GemcutterTasks.new 59 | rescue LoadError 60 | puts "Jeweler (or a dependency) not available. Install it with: sudo gem install jeweler" 61 | end 62 | end 63 | 64 | require 'rake/testtask' 65 | Rake::TestTask.new do |t| 66 | t.libs = ["lib", "test"] 67 | end 68 | 69 | extra_files = %w[LICENSE.txt THANKS.txt CHANGES.txt ] 70 | RDoc::Task.new do |rdoc| 71 | rdoc.rdoc_dir = "rdoc" 72 | rdoc.title = "#{name} #{version}" 73 | rdoc.generator = 'hanna' # gem install hanna-nouveau 74 | rdoc.main = 'README.md' 75 | rdoc.rdoc_files.include("README*") 76 | rdoc.rdoc_files.include("bin/*.rb") 77 | rdoc.rdoc_files.include("lib/**/*.rb") 78 | extra_files.each { |file| 79 | rdoc.rdoc_files.include(file) if File.exist?(file) 80 | } 81 | end 82 | 83 | def change_version(&block) 84 | version_file = 'lib/net/scp/version.rb' 85 | require_relative version_file 86 | pre = Net::SCP::Version::PRE 87 | tiny = Net::SCP::Version::TINY 88 | result = block[pre: pre, tiny: Net::SCP::Version::TINY] 89 | raise ArgumentError, "Version change logic should always return a pre" unless result.key?(:pre) 90 | 91 | new_pre = result[:pre] 92 | new_tiny = result[:tiny] || tiny 93 | found = { pre: false, tiny: false } 94 | File.open("#{version_file}.new", "w") do |f| 95 | File.readlines(version_file).each do |line| 96 | match = 97 | if pre.nil? 98 | /^(\s+PRE\s+=\s+)nil(\s*)$/.match(line) 99 | else 100 | /^(\s+PRE\s+=\s+")#{pre}("\s*)$/.match(line) 101 | end 102 | if match 103 | prefix = match[1] 104 | postfix = match[2] 105 | prefix.delete_suffix!('"') 106 | postfix.delete_prefix!('"') 107 | new_line = "#{prefix}#{new_pre.inspect}#{postfix}" 108 | puts "Changing:\n - #{line} + #{new_line}" 109 | line = new_line 110 | found[:pre] = true 111 | end 112 | 113 | if new_tiny != tiny 114 | match = /^(\s+TINY\s+=\s+)#{tiny}(\s*)$/.match(line) 115 | if match 116 | prefix = match[1] 117 | postfix = match[2] 118 | new_line = "#{prefix}#{new_tiny}#{postfix}" 119 | puts "Changing:\n - #{line} + #{new_line}" 120 | line = new_line 121 | found[:tiny] = true 122 | end 123 | end 124 | 125 | f.write(line) 126 | end 127 | raise ArgumentError, "Cound not find line: PRE = \"#{pre}\" in #{version_file}" unless found[:pre] 128 | raise ArgumentError, "Cound not find line: TINY = \"#{tiny}\" in #{version_file}" unless found[:tiny] || new_tiny == tiny 129 | end 130 | 131 | FileUtils.mv version_file, "#{version_file}.old" 132 | FileUtils.mv "#{version_file}.new", version_file 133 | end 134 | 135 | namespace :vbump do 136 | desc "Final release" 137 | task :final do 138 | change_version do |pre:, tiny:| 139 | _ = tiny 140 | if pre.nil? 141 | { tiny: tiny + 1, pre: nil } 142 | else 143 | raise ArgumentError, "Unexpected pre: #{pre}" if pre.nil? 144 | 145 | { pre: nil } 146 | end 147 | end 148 | end 149 | 150 | desc "Increment prerelease" 151 | task :pre, [:type] do |_t, args| 152 | change_version do |pre:, tiny:| 153 | puts " PRE => #{pre.inspect}" 154 | match = /^([a-z]+)(\d+)/.match(pre) 155 | raise ArgumentError, "Unexpected pre: #{pre}" if match.nil? && args[:type].nil? 156 | 157 | if match.nil? || (!args[:type].nil? && args[:type] != match[1]) 158 | if pre.nil? 159 | { pre: "#{args[:type]}1", tiny: tiny + 1 } 160 | else 161 | { pre: "#{args[:type]}1" } 162 | end 163 | else 164 | { pre: "#{match[1]}#{match[2].to_i + 1}" } 165 | end 166 | end 167 | end 168 | end 169 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | ## Security contact information 2 | 3 | To report a security vulnerability, please use the 4 | [GitHub vulnerability reporting feature](https://docs.github.com/en/code-security/security-advisories/guidance-on-reporting-and-writing/privately-reporting-a-security-vulnerability). 5 | -------------------------------------------------------------------------------- /lib/net/scp.rb: -------------------------------------------------------------------------------- 1 | require 'stringio' 2 | require 'shellwords' 3 | 4 | require 'net/ssh' 5 | require 'net/scp/errors' 6 | require 'net/scp/upload' 7 | require 'net/scp/download' 8 | 9 | module Net 10 | 11 | # Net::SCP implements the SCP (Secure CoPy) client protocol, allowing Ruby 12 | # programs to securely and programmatically transfer individual files or 13 | # entire directory trees to and from remote servers. It provides support for 14 | # multiple simultaneous SCP copies working in parallel over the same 15 | # connection, as well as for synchronous, serial copies. 16 | # 17 | # Basic usage: 18 | # 19 | # require 'net/scp' 20 | # 21 | # Net::SCP.start("remote.host", "username", :password => "passwd") do |scp| 22 | # # synchronous (blocking) upload; call blocks until upload completes 23 | # scp.upload! "/local/path", "/remote/path" 24 | # 25 | # # asynchronous upload; call returns immediately and requires SSH 26 | # # event loop to run 27 | # channel = scp.upload("/local/path", "/remote/path") 28 | # channel.wait 29 | # end 30 | # 31 | # Net::SCP also provides an open-uri tie-in, so you can use the Kernel#open 32 | # method to open and read a remote file: 33 | # 34 | # # if you just want to parse SCP URL's: 35 | # require 'uri/scp' 36 | # url = URI.parse("scp://user@remote.host/path/to/file") 37 | # 38 | # # if you want to read from a URL voa SCP: 39 | # require 'uri/open-scp' 40 | # puts open("scp://user@remote.host/path/to/file").read 41 | # 42 | # Lastly, Net::SCP adds a method to the Net::SSH::Connection::Session class, 43 | # allowing you to easily grab a Net::SCP reference from an existing Net::SSH 44 | # session: 45 | # 46 | # require 'net/ssh' 47 | # require 'net/scp' 48 | # 49 | # Net::SSH.start("remote.host", "username", :password => "passwd") do |ssh| 50 | # ssh.scp.download! "/remote/path", "/local/path" 51 | # end 52 | # 53 | # == Progress Reporting 54 | # 55 | # By default, uploading and downloading proceed silently, without any 56 | # outward indication of their progress. For long running uploads or downloads 57 | # (and especially in interactive environments) it is desirable to report 58 | # to the user the progress of the current operation. 59 | # 60 | # To receive progress reports for the current operation, just pass a block 61 | # to #upload or #download (or one of their variants): 62 | # 63 | # scp.upload!("/path/to/local", "/path/to/remote") do |ch, name, sent, total| 64 | # puts "#{name}: #{sent}/#{total}" 65 | # end 66 | # 67 | # Whenever a new chunk of data is recieved for or sent to a file, the callback 68 | # will be invoked, indicating the name of the file (local for downloads, 69 | # remote for uploads), the number of bytes that have been sent or received 70 | # so far for the file, and the size of the file. 71 | # 72 | #-- 73 | # = Protocol Description 74 | # 75 | # Although this information has zero relevance to consumers of the Net::SCP 76 | # library, I'm documenting it here so that anyone else looking for documentation 77 | # of the SCP protocol won't be left high-and-dry like I was. The following is 78 | # reversed engineered from the OpenSSH SCP implementation, and so may 79 | # contain errors. You have been warned! 80 | # 81 | # The first step is to invoke the "scp" command on the server. It accepts 82 | # the following parameters, which must be set correctly to avoid errors: 83 | # 84 | # * "-t" -- tells the remote scp process that data will be sent "to" it, 85 | # e.g., that data will be uploaded and it should initialize itself 86 | # accordingly. 87 | # * "-f" -- tells the remote scp process that data should come "from" it, 88 | # e.g., that data will be downloaded and it should initialize itself 89 | # accordingly. 90 | # * "-v" -- verbose mode; the remote scp process should chatter about what 91 | # it is doing via stderr. 92 | # * "-p" -- preserve timestamps. 'T' directives (see below) should be/will 93 | # be sent to indicate the modification and access times of each file. 94 | # * "-r" -- recursive transfers should be allowed. Without this, it is an 95 | # error to upload or download a directory. 96 | # 97 | # After those flags, the name of the remote file/directory should be passed 98 | # as the sole non-switch argument to scp. 99 | # 100 | # Then the fun begins. If you're doing a download, enter the download_start_state. 101 | # Otherwise, look for upload_start_state. 102 | # 103 | # == Net::SCP::Download#download_start_state 104 | # 105 | # This is the start state for downloads. It simply sends a 0-byte to the 106 | # server. The next state is Net::SCP::Download#read_directive_state. 107 | # 108 | # == Net::SCP::Upload#upload_start_state 109 | # 110 | # Sets up the initial upload scaffolding and waits for a 0-byte from the 111 | # server, and then switches to Net::SCP::Upload#upload_current_state. 112 | # 113 | # == Net::SCP::Download#read_directive_state 114 | # 115 | # Reads a directive line from the input. The following directives are 116 | # recognized: 117 | # 118 | # * T%d %d %d %d -- a "times" packet. Indicates that the next file to be 119 | # downloaded must have mtime/usec/atime/usec attributes preserved. 120 | # * D%o %d %s -- a directory change. The process is changing to a directory 121 | # with the given permissions/size/name, and the recipient should create 122 | # a directory with the same name and permissions. Subsequent files and 123 | # directories will be children of this directory, until a matching 'E' 124 | # directive. 125 | # * C%o %d %s -- a file is being sent next. The file will have the given 126 | # permissions/size/name. Immediately following this line, +size+ bytes 127 | # will be sent, raw. 128 | # * E -- terminator directive. Indicates the end of a directory, and subsequent 129 | # files and directories should be received by the parent of the current 130 | # directory. 131 | # * \0 -- indicates a successful response from the other end. 132 | # * \1 -- warning directive. Indicates a warning from the other end. Text from 133 | # this warning will be reported if the SCP results in an error. 134 | # * \2 -- error directive. Indicates an error from the other end. Text from 135 | # this error will be reported if the SCP results in an error. 136 | # 137 | # If a 'C' directive is received, we switch over to 138 | # Net::SCP::Download#read_data_state. If an 'E' directive is received, and 139 | # there is no parent directory, we switch over to Net::SCP#finish_state. 140 | # 141 | # Regardless of what the next state is, we send a 0-byte to the server 142 | # before moving to the next state. 143 | # 144 | # == Net::SCP::Download#read_data_state 145 | # 146 | # Bytes are read to satisfy the size of the incoming file. When all pending 147 | # data has been read, we wait for the server to send a 0-byte, and then we 148 | # switch to the Net::SCP::Download#finish_read_state. 149 | # 150 | # == Net::SCP::Download#finish_read_state 151 | # 152 | # We sent a 0-byte to the server to indicate that the file was successfully 153 | # received. If there is no parent directory, then we're downloading a single 154 | # file and we switch to Net::SCP#finish_state. Otherwise we jump back to the 155 | # Net::SCP::Download#read_directive state to see what we get to download next. 156 | # 157 | # == Net::SCP::Upload#upload_current_state 158 | # 159 | # If the current item is a file, send a file. Sending a file starts with a 160 | # 'T' directive (if :preserve is true), then a wait for the server to respond, 161 | # and then a 'C' directive, and then a wait for the server to respond, and 162 | # then a jump to Net::SCP::Upload#send_data_state. 163 | # 164 | # If current item is a directory, send a 'D' directive, and wait for the 165 | # server to respond with a 0-byte. Then jump to Net::SCP::Upload#next_item_state. 166 | # 167 | # == Net::SCP::Upload#send_data_state 168 | # 169 | # Reads and sends the next chunk of data to the server. The state machine 170 | # remains in this state until all data has been sent, at which point we 171 | # send a 0-byte to the server, and wait for the server to respond with a 172 | # 0-byte of its own. Then we jump back to Net::SCP::Upload#next_item_state. 173 | # 174 | # == Net::SCP::Upload#next_item_state 175 | # 176 | # If there is nothing left to upload, and there is no parent directory, 177 | # jump to Net::SCP#finish_state. 178 | # 179 | # If there is nothing left to upload from the current directory, send an 180 | # 'E' directive and wait for the server to respond with a 0-byte. Then go 181 | # to Net::SCP::Upload#next_item_state. 182 | # 183 | # Otherwise, set the current upload source and go to 184 | # Net::SCP::Upload#upload_current_state. 185 | # 186 | # == Net::SCP#finish_state 187 | # 188 | # Tells the server that no more data is forthcoming from this end of the 189 | # pipe (via Net::SSH::Connection::Channel#eof!) and leaves the pipe to drain. 190 | # It will be terminated when the remote process closes with an exit status 191 | # of zero. 192 | #++ 193 | class SCP 194 | include Net::SSH::Loggable 195 | include Upload, Download 196 | 197 | # Starts up a new SSH connection and instantiates a new SCP session on 198 | # top of it. If a block is given, the SCP session is yielded, and the 199 | # SSH session is closed automatically when the block terminates. If no 200 | # block is given, the SCP session is returned. 201 | def self.start(host, username, options={}) 202 | session = Net::SSH.start(host, username, options) 203 | scp = new(session) 204 | 205 | if block_given? 206 | begin 207 | yield scp 208 | session.loop 209 | ensure 210 | session.close 211 | end 212 | else 213 | return scp 214 | end 215 | end 216 | 217 | # Starts up a new SSH connection using the +host+ and +username+ parameters, 218 | # instantiates a new SCP session on top of it, and then begins an 219 | # upload from +local+ to +remote+. If the +options+ hash includes an 220 | # :ssh key, the value for that will be passed to the SSH connection as 221 | # options (e.g., to set the password, etc.). All other options are passed 222 | # to the #upload! method. If a block is given, it will be used to report 223 | # progress (see "Progress Reporting", under Net::SCP). 224 | def self.upload!(host, username, local, remote, options={}, &progress) 225 | options = options.dup 226 | start(host, username, options.delete(:ssh) || {}) do |scp| 227 | scp.upload!(local, remote, options, &progress) 228 | end 229 | end 230 | 231 | # Starts up a new SSH connection using the +host+ and +username+ parameters, 232 | # instantiates a new SCP session on top of it, and then begins a 233 | # download from +remote+ to +local+. If the +options+ hash includes an 234 | # :ssh key, the value for that will be passed to the SSH connection as 235 | # options (e.g., to set the password, etc.). All other options are passed 236 | # to the #download! method. If a block is given, it will be used to report 237 | # progress (see "Progress Reporting", under Net::SCP). 238 | def self.download!(host, username, remote, local=nil, options={}, &progress) 239 | options = options.dup 240 | start(host, username, options.delete(:ssh) || {}) do |scp| 241 | return scp.download!(remote, local, options, &progress) 242 | end 243 | end 244 | 245 | # The underlying Net::SSH session that acts as transport for the SCP 246 | # packets. 247 | attr_reader :session 248 | 249 | # Creates a new Net::SCP session on top of the given Net::SSH +session+ 250 | # object. 251 | def initialize(session) 252 | @session = session 253 | self.logger = session.logger 254 | end 255 | 256 | # Inititiate a synchronous (non-blocking) upload from +local+ to +remote+. 257 | # The following options are recognized: 258 | # 259 | # * :recursive - the +local+ parameter refers to a local directory, which 260 | # should be uploaded to a new directory named +remote+ on the remote 261 | # server. 262 | # * :preserve - the atime and mtime of the file should be preserved. 263 | # * :verbose - the process should result in verbose output on the server 264 | # end (useful for debugging). 265 | # * :chunk_size - the size of each "chunk" that should be sent. Defaults 266 | # to 2048. Changing this value may improve throughput at the expense 267 | # of decreasing interactivity. 268 | # 269 | # This method will return immediately, returning the Net::SSH::Connection::Channel 270 | # object that will support the upload. To wait for the upload to finish, 271 | # you can either call the #wait method on the channel, or otherwise run 272 | # the Net::SSH event loop until the channel's #active? method returns false. 273 | # 274 | # channel = scp.upload("/local/path", "/remote/path") 275 | # channel.wait 276 | def upload(local, remote, options={}, &progress) 277 | start_command(:upload, local, remote, options, &progress) 278 | end 279 | 280 | # Same as #upload, but blocks until the upload finishes. Identical to 281 | # calling #upload and then calling the #wait method on the channel object 282 | # that is returned. The return value is not defined. 283 | def upload!(local, remote, options={}, &progress) 284 | upload(local, remote, options, &progress).wait 285 | end 286 | 287 | # Inititiate a synchronous (non-blocking) download from +remote+ to +local+. 288 | # The following options are recognized: 289 | # 290 | # * :recursive - the +remote+ parameter refers to a remote directory, which 291 | # should be downloaded to a new directory named +local+ on the local 292 | # machine. 293 | # * :preserve - the atime and mtime of the file should be preserved. 294 | # * :verbose - the process should result in verbose output on the server 295 | # end (useful for debugging). 296 | # 297 | # This method will return immediately, returning the Net::SSH::Connection::Channel 298 | # object that will support the download. To wait for the download to finish, 299 | # you can either call the #wait method on the channel, or otherwise run 300 | # the Net::SSH event loop until the channel's #active? method returns false. 301 | # 302 | # channel = scp.download("/remote/path", "/local/path") 303 | # channel.wait 304 | def download(remote, local, options={}, &progress) 305 | start_command(:download, local, remote, options, &progress) 306 | end 307 | 308 | # Same as #download, but blocks until the download finishes. Identical to 309 | # calling #download and then calling the #wait method on the channel 310 | # object that is returned. 311 | # 312 | # scp.download!("/remote/path", "/local/path") 313 | # 314 | # If +local+ is nil, and the download is not recursive (e.g., it is downloading 315 | # only a single file), the file will be downloaded to an in-memory buffer 316 | # and the resulting string returned. 317 | # 318 | # data = download!("/remote/path") 319 | def download!(remote, local=nil, options={}, &progress) 320 | destination = local ? local : StringIO.new.tap { |io| io.set_encoding('BINARY') } 321 | download(remote, destination, options, &progress).wait 322 | local ? true : destination.string 323 | end 324 | 325 | private 326 | 327 | # Constructs the scp command line needed to initiate and SCP session 328 | # for the given +mode+ (:upload or :download) and with the given options 329 | # (:verbose, :recursive, :preserve). Returns the command-line as a 330 | # string, ready to execute. 331 | def scp_command(mode, options) 332 | command = "scp " 333 | command << (mode == :upload ? "-t" : "-f") 334 | command << " -v" if options[:verbose] 335 | command << " -r" if options[:recursive] 336 | command << " -p" if options[:preserve] 337 | command 338 | end 339 | 340 | # Opens a new SSH channel and executes the necessary SCP command over 341 | # it (see #scp_command). It then sets up the necessary callbacks, and 342 | # sets up a state machine to use to process the upload or download. 343 | # (See Net::SCP::Upload and Net::SCP::Download). 344 | def start_command(mode, local, remote, options={}, &callback) 345 | session.open_channel do |channel| 346 | 347 | if options[:shell] 348 | escaped_file = shellescape(remote).gsub(/'/) { |m| "'\\''" } 349 | command = "#{options[:shell]} -c '#{scp_command(mode, options)} #{escaped_file}'" 350 | else 351 | command = "#{scp_command(mode, options)} #{shellescape remote}" 352 | end 353 | 354 | channel.exec(command) do |ch, success| 355 | if success 356 | channel[:local ] = local 357 | channel[:remote ] = remote 358 | channel[:options ] = options.dup 359 | channel[:callback] = callback 360 | channel[:buffer ] = Net::SSH::Buffer.new 361 | channel[:state ] = "#{mode}_start" 362 | channel[:stack ] = [] 363 | channel[:error_string] = '' 364 | 365 | channel.on_close do 366 | # If we got an exit-status and it is not 0, something went wrong 367 | if !channel[:exit].nil? && channel[:exit] != 0 368 | raise Net::SCP::Error, 'SCP did not finish successfully ' \ 369 | "(#{channel[:exit]}): #{channel[:error_string]}" 370 | end 371 | # We may get no exit-status at all as returning a status is only RECOMENDED 372 | # in RFC4254. But if our state is not :finish, something went wrong 373 | if channel[:exit].nil? && channel[:state] != :finish 374 | raise Net::SCP::Error, 'SCP did not finish successfully ' \ 375 | '(channel closed before end of transmission)' 376 | end 377 | # At this point, :state can be :finish or :next_item 378 | send("#{channel[:state]}_state", channel) 379 | end 380 | channel.on_data { |ch2, data| channel[:buffer].append(data) } 381 | channel.on_extended_data { |ch2, type, data| debug { data.chomp } } 382 | channel.on_request("exit-status") { |ch2, data| channel[:exit] = data.read_long } 383 | channel.on_process { send("#{channel[:state]}_state", channel) } 384 | else 385 | channel.close 386 | raise Net::SCP::Error, "could not exec scp on the remote host" 387 | end 388 | end 389 | end 390 | end 391 | 392 | # Causes the state machine to enter the "await response" state, where 393 | # things just pause until the server replies with a 0 (see 394 | # #await_response_state), at which point the state machine will pick up 395 | # at +next_state+ and continue processing. 396 | def await_response(channel, next_state) 397 | channel[:state] = :await_response 398 | channel[:next ] = next_state.to_sym 399 | # check right away, to see if the response is immediately available 400 | await_response_state(channel) 401 | end 402 | 403 | # The action invoked while the state machine remains in the "await 404 | # response" state. As long as there is no data ready to process, the 405 | # machine will remain in this state. As soon as the server replies with 406 | # an integer 0 as the only byte, the state machine is kicked into the 407 | # next state (see +await_response+). If the response is not a 0, an 408 | # exception is raised. 409 | def await_response_state(channel) 410 | return if channel[:buffer].available == 0 411 | c = channel[:buffer].read_byte 412 | raise Net::SCP::Error, "#{c.chr}#{channel[:buffer].read}" if c != 0 413 | channel[:next], channel[:state] = nil, channel[:next] 414 | send("#{channel[:state]}_state", channel) 415 | end 416 | 417 | # The action invoked when the state machine is in the "finish" state. 418 | # It just tells the server not to expect any more data from this end 419 | # of the pipe, and allows the pipe to drain until the server closes it. 420 | def finish_state(channel) 421 | channel.eof! 422 | end 423 | 424 | # Invoked to report progress back to the client. If a callback was not 425 | # set, this does nothing. 426 | def progress_callback(channel, name, sent, total) 427 | channel[:callback].call(channel, name, sent, total) if channel[:callback] 428 | end 429 | 430 | # Imported from ruby 1.9.2 shellwords.rb 431 | def shellescape(path) 432 | # Convert path to a string if it isn't already one. 433 | str = path.to_s 434 | 435 | # ruby 1.8.7+ implements String#shellescape 436 | return str.shellescape if str.respond_to? :shellescape 437 | 438 | # An empty argument will be skipped, so return empty quotes. 439 | return "''" if str.empty? 440 | 441 | str = str.dup 442 | 443 | # Process as a single byte sequence because not all shell 444 | # implementations are multibyte aware. 445 | str.gsub!(/([^A-Za-z0-9_\-.,:\/@\n])/n, "\\\\\\1") 446 | 447 | # A LF cannot be escaped with a backslash because a backslash + LF 448 | # combo is regarded as line continuation and simply ignored. 449 | str.gsub!(/\n/, "'\n'") 450 | 451 | return str 452 | end 453 | end 454 | end 455 | 456 | class Net::SSH::Connection::Session 457 | # Provides a convenient way to initialize a SCP session given a Net::SSH 458 | # session. Returns the Net::SCP instance, ready to use. 459 | def scp 460 | @scp ||= Net::SCP.new(self) 461 | end 462 | end 463 | -------------------------------------------------------------------------------- /lib/net/scp/download.rb: -------------------------------------------------------------------------------- 1 | require 'net/scp/errors' 2 | 3 | module Net; class SCP 4 | 5 | # This module implements the state machine for downloading information from 6 | # a remote server. It exposes no public methods. See Net::SCP#download for 7 | # a discussion of how to use Net::SCP to download data. 8 | module Download 9 | private 10 | 11 | # This is the starting state for the download state machine. The 12 | # #start_command method puts the state machine into this state the first 13 | # time the channel is processed. This state does some basic error checking 14 | # and scaffolding and then sends a 0-byte to the remote server, indicating 15 | # readiness to proceed. Then, the state machine is placed into the 16 | # "read directive" state (see #read_directive_state). 17 | def download_start_state(channel) 18 | if channel[:local].respond_to?(:write) && channel[:options][:recursive] 19 | raise Net::SCP::Error, "cannot recursively download to an in-memory location" 20 | elsif channel[:local].respond_to?(:write) && channel[:options][:preserve] 21 | lwarn { ":preserve option is ignored when downloading to an in-memory buffer" } 22 | channel[:options].delete(:preserve) 23 | elsif channel[:options][:recursive] && !File.exist?(channel[:local]) 24 | Dir.mkdir(channel[:local]) 25 | end 26 | 27 | channel.send_data("\0") 28 | channel[:state] = :read_directive 29 | end 30 | 31 | # This state parses the next full line (up to a new-line) for the next 32 | # directive. (See the SCP protocol documentation in Net::SCP for the 33 | # possible directives). 34 | def read_directive_state(channel) 35 | return unless line = channel[:buffer].read_to("\n") 36 | channel[:buffer].consume! 37 | 38 | directive = parse_directive(line) 39 | case directive[:type] 40 | when :OK 41 | return 42 | when :warning 43 | channel[:error_string] << directive[:message] 44 | when :error 45 | channel[:error_string] << directive[:message] 46 | when :times 47 | channel[:times] = directive 48 | when :directory 49 | read_directory(channel, directive) 50 | when :file 51 | read_file(channel, directive) 52 | when :end 53 | channel[:local] = File.dirname(channel[:local]) 54 | channel[:stack].pop 55 | channel[:state] = :finish if channel[:stack].empty? 56 | end 57 | 58 | channel.send_data("\0") 59 | end 60 | 61 | # Reads data from the channel for as long as there is data remaining to 62 | # be read. As soon as there is no more data to read for the current file, 63 | # the state machine switches to #finish_read_state. 64 | def read_data_state(channel) 65 | return if channel[:buffer].empty? 66 | data = channel[:buffer].read!(channel[:remaining]) 67 | channel[:io].write(data) 68 | channel[:remaining] -= data.length 69 | progress_callback(channel, channel[:file][:name], channel[:file][:size] - channel[:remaining], channel[:file][:size]) 70 | await_response(channel, :finish_read) if channel[:remaining] <= 0 71 | end 72 | 73 | # Finishes off the read, sets the times for the file (if any), and then 74 | # jumps to either #finish_state (for single-file downloads) or 75 | # #read_directive_state (for recursive downloads). A 0-byte is sent to the 76 | # server to indicate that the file was recieved successfully. 77 | def finish_read_state(channel) 78 | channel[:io].close unless channel[:io] == channel[:local] 79 | 80 | if channel[:options][:preserve] && channel[:file][:times] 81 | File.utime(channel[:file][:times][:atime], 82 | channel[:file][:times][:mtime], channel[:file][:name]) 83 | end 84 | 85 | channel[:file] = nil 86 | channel[:state] = channel[:stack].empty? ? :finish : :read_directive 87 | channel.send_data("\0") 88 | end 89 | 90 | # Parses the given +text+ to extract which SCP directive it contains. It 91 | # then returns a hash with at least one key, :type, which describes what 92 | # type of directive it is. The hash may also contain other, directive-specific 93 | # data. 94 | def parse_directive(text) 95 | case type = text[0] 96 | when "\x00" 97 | # Success 98 | { :type => :OK } 99 | when "\x01" 100 | { :type => :warning, 101 | :message => text[1..-1] } 102 | when "\x02" 103 | { :type => :error, 104 | :message => text[1..-1] } 105 | when ?T 106 | parts = text[1..-1].split(/ /, 4).map { |i| i.to_i } 107 | { :type => :times, 108 | :mtime => Time.at(parts[0], parts[1]), 109 | :atime => Time.at(parts[2], parts[3]) } 110 | when ?C, ?D 111 | parts = text[1..-1].split(/ /, 3) 112 | { :type => (type == ?C ? :file : :directory), 113 | :mode => parts[0].to_i(8), 114 | :size => parts[1].to_i, 115 | :name => parts[2].chomp } 116 | when ?E 117 | { :type => :end } 118 | else raise ArgumentError, "unknown directive: #{text.inspect}" 119 | end 120 | end 121 | 122 | # Sets the new directory as the current directory, creates the directory 123 | # if it does not exist, and then falls back into #read_directive_state. 124 | def read_directory(channel, directive) 125 | if !channel[:options][:recursive] 126 | raise Net::SCP::Error, ":recursive not specified for directory download" 127 | end 128 | 129 | channel[:local] = File.join(channel[:local], directive[:name]) 130 | 131 | if File.exist?(channel[:local]) && !File.directory?(channel[:local]) 132 | raise Net::SCP::Error, "#{channel[:local]} already exists and is not a directory" 133 | elsif !File.exist?(channel[:local]) 134 | Dir.mkdir(channel[:local], directive[:mode] | 0700) 135 | end 136 | 137 | if channel[:options][:preserve] && channel[:times] 138 | File.utime(channel[:times][:atime], channel[:times][:mtime], channel[:local]) 139 | end 140 | 141 | channel[:stack] << directive 142 | channel[:times] = nil 143 | end 144 | 145 | # Opens the given file locally, and switches to #read_data_state to do the 146 | # actual read. 147 | def read_file(channel, directive) 148 | if !channel[:local].respond_to?(:write) 149 | directive[:name] = (channel[:options][:recursive] || File.directory?(channel[:local])) ? 150 | File.join(channel[:local], directive[:name]) : 151 | channel[:local] 152 | end 153 | 154 | channel[:file] = directive.merge(:times => channel[:times]) 155 | channel[:io] = channel[:local].respond_to?(:write) ? channel[:local] : 156 | File.new(directive[:name], "wb", directive[:mode] | 0600) 157 | channel[:times] = nil 158 | channel[:remaining] = channel[:file][:size] 159 | channel[:state] = :read_data 160 | 161 | progress_callback(channel, channel[:file][:name], 0, channel[:file][:size]) 162 | end 163 | end 164 | 165 | end; end 166 | -------------------------------------------------------------------------------- /lib/net/scp/errors.rb: -------------------------------------------------------------------------------- 1 | module Net; class SCP 2 | 3 | class Error < RuntimeError; end 4 | 5 | end; end -------------------------------------------------------------------------------- /lib/net/scp/upload.rb: -------------------------------------------------------------------------------- 1 | require 'net/scp/errors' 2 | 3 | module Net; class SCP 4 | 5 | # This module implements the state machine for uploading information to 6 | # a remote server. It exposes no public methods. See Net::SCP#upload for 7 | # a discussion of how to use Net::SCP to upload data. 8 | module Upload 9 | private 10 | 11 | # The default read chunk size, if an explicit chunk-size is not specified 12 | # by the client. 13 | DEFAULT_CHUNK_SIZE = 16384 14 | 15 | # The start state for uploads. Simply sets up the upload scaffolding, 16 | # sets the current item to upload, and jumps to #upload_current_state. 17 | def upload_start_state(channel) 18 | if channel[:local].respond_to?(:read) 19 | channel[:options].delete(:recursive) 20 | channel[:options].delete(:preserve) 21 | end 22 | 23 | channel[:chunk_size] = channel[:options][:chunk_size] || DEFAULT_CHUNK_SIZE 24 | set_current(channel, channel[:local]) 25 | await_response(channel, :upload_current) 26 | end 27 | 28 | # Determines what the next thing to upload is, and branches. If the next 29 | # item is a file, goes to #upload_file_state. If it is a directory, goes 30 | # to #upload_directory_state. 31 | def upload_current_state(channel) 32 | if channel[:current].respond_to?(:read) 33 | upload_file_state(channel) 34 | elsif File.directory?(channel[:current]) 35 | raise Net::SCP::Error, "can't upload directories unless :recursive" unless channel[:options][:recursive] 36 | upload_directory_state(channel) 37 | elsif File.file?(channel[:current]) 38 | upload_file_state(channel) 39 | else 40 | raise Net::SCP::Error, "not a directory or a regular file: #{channel[:current].inspect}" 41 | end 42 | end 43 | 44 | # After transferring attributes (if requested), sends a 'D' directive and 45 | # awaites the server's 0-byte response. Then goes to #next_item_state. 46 | def upload_directory_state(channel) 47 | if preserve_attributes_if_requested(channel) 48 | mode = channel[:stat].mode & 07777 49 | directive = "D%04o %d %s\n" % [mode, 0, File.basename(channel[:current])] 50 | channel.send_data(directive) 51 | channel[:cwd] = channel[:current] 52 | channel[:stack] << Dir.entries(channel[:current]).reject { |i| i == "." || i == ".." } 53 | await_response(channel, :next_item) 54 | end 55 | end 56 | 57 | # After transferring attributes (if requested), sends a 'C' directive and 58 | # awaits the server's 0-byte response. Then goes to #send_data_state. 59 | def upload_file_state(channel) 60 | if preserve_attributes_if_requested(channel) 61 | mode = channel[:stat] ? channel[:stat].mode & 07777 : channel[:options][:mode] 62 | channel[:name] = channel[:current].respond_to?(:read) ? channel[:remote] : channel[:current] 63 | directive = "C%04o %d %s\n" % [mode || 0640, channel[:size], File.basename(channel[:name])] 64 | channel.send_data(directive) 65 | channel[:io] = channel[:current].respond_to?(:read) ? channel[:current] : File.open(channel[:current], "rb") 66 | channel[:sent] = 0 67 | progress_callback(channel, channel[:name], channel[:sent], channel[:size]) 68 | await_response(channel, :send_data) 69 | end 70 | end 71 | 72 | # If any data remains to be transferred from the current file, sends it. 73 | # Otherwise, sends a 0-byte and transfers to #next_item_state. 74 | def send_data_state(channel) 75 | data = channel[:io].read(channel[:chunk_size]) 76 | if data.nil? 77 | channel[:io].close unless channel[:local].respond_to?(:read) 78 | channel.send_data("\0") 79 | await_response(channel, :next_item) 80 | else 81 | channel[:sent] += data.length 82 | progress_callback(channel, channel[:name], channel[:sent], channel[:size]) 83 | channel.send_data(data) 84 | end 85 | end 86 | 87 | # Checks the work queue to see what needs to be done next. If there is 88 | # nothing to do, calls Net::SCP#finish_state. If we're at the end of a 89 | # directory, sends an 'E' directive and waits for the server to respond 90 | # before moving to #next_item_state. Otherwise, sets the next thing to 91 | # upload and moves to #upload_current_state. 92 | def next_item_state(channel) 93 | if channel[:stack].empty? 94 | finish_state(channel) 95 | else 96 | next_item = channel[:stack].last.shift 97 | if next_item.nil? 98 | channel[:stack].pop 99 | channel[:cwd] = File.dirname(channel[:cwd]) 100 | channel.send_data("E\n") 101 | await_response(channel, channel[:stack].empty? ? :finish : :next_item) 102 | else 103 | set_current(channel, next_item) 104 | upload_current_state(channel) 105 | end 106 | end 107 | end 108 | 109 | # Sets the given +path+ as the new current item to upload. 110 | def set_current(channel, path) 111 | path = channel[:cwd] ? File.join(channel[:cwd], path) : path 112 | channel[:current] = path 113 | 114 | if channel[:current].respond_to?(:read) 115 | channel[:stat] = channel[:current].stat if channel[:current].respond_to?(:stat) 116 | else 117 | channel[:stat] = File.stat(channel[:current]) 118 | end 119 | 120 | channel[:size] = channel[:stat] ? channel[:stat].size : channel[:current].size 121 | end 122 | 123 | # If the :preserve option is set, send a 'T' directive and wait for the 124 | # server to respond before proceeding to either #upload_file_state or 125 | # #upload_directory_state, depending on what is being uploaded. 126 | def preserve_attributes_if_requested(channel) 127 | if channel[:options][:preserve] && !channel[:preserved] 128 | channel[:preserved] = true 129 | stat = channel[:stat] 130 | directive = "T%d %d %d %d\n" % [stat.mtime.to_i, stat.mtime.usec, stat.atime.to_i, stat.atime.usec] 131 | channel.send_data(directive) 132 | type = stat.directory? ? :directory : :file 133 | await_response(channel, "upload_#{type}") 134 | return false 135 | else 136 | channel[:preserved] = false 137 | return true 138 | end 139 | end 140 | end 141 | 142 | end; end -------------------------------------------------------------------------------- /lib/net/scp/version.rb: -------------------------------------------------------------------------------- 1 | module Net 2 | class SCP 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/scp/version' 11 | # 12 | # if Net::SCP::Version::CURRENT < Net::SCP::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::SSH library 49 | MAJOR = 4 50 | 51 | # The minor component of this version of the Net::SSH library 52 | MINOR = 1 53 | 54 | # The tiny component of this version of the Net::SSH library 55 | TINY = 0 56 | 57 | # The prerelease component of this version of the Net::SSH library 58 | # nil allowed 59 | PRE = nil 60 | 61 | # The current version of the Net::SSH library as a Version instance 62 | CURRENT = new(*[MAJOR, MINOR, TINY, PRE].compact) 63 | 64 | # The current version of the Net::SSH library as a String 65 | STRING = CURRENT.to_s 66 | end 67 | end 68 | end 69 | -------------------------------------------------------------------------------- /lib/uri/open-scp.rb: -------------------------------------------------------------------------------- 1 | require 'open-uri' 2 | require 'uri/scp' 3 | require 'net/scp' 4 | 5 | OpenURI::Options[:ssh] = nil 6 | 7 | module URI 8 | 9 | class SCP 10 | def buffer_open(buf, proxy, open_options) 11 | options = open_options.merge(:port => port, :password => password) 12 | progress = options.delete(:progress_proc) 13 | buf << Net::SCP.download!(host, user, path, nil, options, &progress) 14 | buf.io.rewind 15 | end 16 | 17 | include OpenURI::OpenRead 18 | end 19 | 20 | end 21 | -------------------------------------------------------------------------------- /lib/uri/scp.rb: -------------------------------------------------------------------------------- 1 | require 'uri/generic' 2 | 3 | module URI 4 | class SCP < Generic 5 | DEFAULT_PORT = 22 6 | 7 | COMPONENT = [ 8 | :scheme, 9 | :userinfo, 10 | :host, :port, :path, 11 | :query 12 | ].freeze 13 | 14 | attr_reader :options 15 | 16 | def self.new2(user, password, host, port, path, query) 17 | new('scp', [user, password], host, port, nil, path, nil, query) 18 | end 19 | 20 | def initialize(*args) 21 | super(*args) 22 | 23 | @options = Hash.new 24 | (query || "").split(/&/).each do |pair| 25 | name, value = pair.split(/=/, 2) 26 | opt_name = name.to_sym 27 | values = value.split(/,/).map { |v| v.to_i.to_s == v ? v.to_i : v } 28 | values = values.first if values.length == 1 29 | options[opt_name] = values 30 | end 31 | end 32 | end 33 | 34 | if respond_to? :register_scheme 35 | register_scheme "SCP", SCP 36 | else 37 | @@schemes["SCP"] = SCP 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /net-scp-public_cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDeDCCAmCgAwIBAgIBATANBgkqhkiG9w0BAQsFADBBMQ8wDQYDVQQDDAZuZXRz 3 | c2gxGTAXBgoJkiaJk/IsZAEZFglzb2x1dGlvdXMxEzARBgoJkiaJk/IsZAEZFgNj 4 | b20wHhcNMjQxMjI1MTEzNDQ3WhcNMjUxMjI1MTEzNDQ3WjBBMQ8wDQYDVQQDDAZu 5 | ZXRzc2gxGTAXBgoJkiaJk/IsZAEZFglzb2x1dGlvdXMxEzARBgoJkiaJk/IsZAEZ 6 | FgNjb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDGJ4TbZ9H+qZ08 7 | pQfJhPJTHaDCyQvCsKTFrL5O9z3tllQ7B/zksMMM+qFBpNYu9HCcg4yBATacE/PB 8 | qVVyUrpr6lbH/XwoN5ljXm+bdCfmnjZvTCL2FTE6o+bcnaF0IsJyC0Q2B1fbWdXN 9 | 6Off1ZWoUk6We2BIM1bn6QJLxBpGyYhvOPXsYoqSuzDf2SJDDsWFZ8kV5ON13Ohm 10 | JbBzn0oD8HF8FuYOewwsC0C1q4w7E5GtvHcQ5juweS7+RKsyDcVcVrLuNzoGRttS 11 | KP4yMn+TzaXijyjRg7gECfJr3TGASaA4bQsILFGG5dAWcwO4OMrZedR7SHj/o0Kf 12 | 3gL7P0axAgMBAAGjezB5MAkGA1UdEwQCMAAwCwYDVR0PBAQDAgSwMB0GA1UdDgQW 13 | BBQF8qLA7Z4zg0SJGtUbv3eoQ8tjIzAfBgNVHREEGDAWgRRuZXRzc2hAc29sdXRp 14 | b3VzLmNvbTAfBgNVHRIEGDAWgRRuZXRzc2hAc29sdXRpb3VzLmNvbTANBgkqhkiG 15 | 9w0BAQsFAAOCAQEAOEVGPubOS9dBQmiJYIZHOXe2Q50iQgxKa7hyEJcyA7q69Q5h 16 | Ha5r4WpZyW0Dkr0+jIkT8GS7hO0XnUZdOiuNFQrx30jfRSVT7680dF6wAHEQZJqC 17 | ZmYFthhR/mtzi7bA+Ubd0PyBNivqt3WhWP+Z19j1bVWIwzczUcFFao+FBjXptI0m 18 | VGRPnRIzATA2qQUuKGkwrNFSHD9tDIHXSvwJ62U9ahoMKfMoDP0WHdPIpFCB8bPg 19 | wxMvGTA/RH93o6dL09sq7rVtsS9NNFmBGJWLZWWPfcspNBUXS0HTWXsWS9XTm2bm 20 | bbXS+I4xE1yFIPs39ej57LGJDMgMhWTyJF2zVg== 21 | -----END CERTIFICATE----- 22 | -------------------------------------------------------------------------------- /net-scp.gemspec: -------------------------------------------------------------------------------- 1 | 2 | require_relative 'lib/net/scp/version' 3 | 4 | Gem::Specification.new do |spec| 5 | spec.name = "net-scp" 6 | spec.version = Net::SCP::Version::STRING 7 | spec.authors = ["Jamis Buck", "Delano Mandelbaum", "Mikl\u{f3}s Fazekas"] 8 | spec.email = ["net-ssh@solutious.com"] 9 | 10 | if ENV['NET_SSH_BUILDGEM_SIGNED'] 11 | spec.cert_chain = ["net-scp-public_cert.pem"] 12 | spec.signing_key = "/mnt/gem/net-ssh-private_key.pem" 13 | end 14 | 15 | spec.summary = %q{A pure Ruby implementation of the SCP client protocol.} 16 | spec.description = %q{A pure Ruby implementation of the SCP client protocol} 17 | spec.homepage = "https://github.com/net-ssh/net-scp" 18 | spec.license = "MIT" 19 | spec.required_rubygems_version = Gem::Requirement.new(">= 0") if spec.respond_to? :required_rubygems_version= 20 | spec.metadata = { 21 | "changelog_uri" => "https://github.com/net-ssh/net-scp/blob/master/CHANGES.txt" 22 | } 23 | 24 | spec.extra_rdoc_files = [ 25 | "LICENSE.txt", 26 | "README.md" 27 | ] 28 | 29 | spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) } 30 | spec.require_paths = ["lib"] 31 | 32 | if spec.respond_to? :specification_version then 33 | spec.specification_version = 3 34 | 35 | if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then 36 | spec.add_runtime_dependency(%q, [">= 2.6.5", "< 8.0.0"]) 37 | spec.add_development_dependency(%q, [">= 0"]) 38 | spec.add_development_dependency(%q, [">= 0"]) 39 | else 40 | spec.add_dependency(%q, [">= 2.6.5", "< 8.0.0"]) 41 | spec.add_dependency(%q, [">= 0"]) 42 | spec.add_dependency(%q, [">= 0"]) 43 | end 44 | else 45 | spec.add_dependency(%q, [">= 2.6.5", "< 8.0.0"]) 46 | spec.add_dependency(%q, [">= 0"]) 47 | spec.add_dependency(%q, [">= 0"]) 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /setup.rb: -------------------------------------------------------------------------------- 1 | # 2 | # setup.rb 3 | # 4 | # Copyright (c) 2000-2004 Minero Aoki 5 | # 6 | # This program is free software. 7 | # You can distribute/modify this program under the terms of 8 | # the GNU Lesser General Public License version 2.1. 9 | # 10 | 11 | # 12 | # For backward compatibility 13 | # 14 | 15 | unless Enumerable.method_defined?(:map) 16 | module Enumerable 17 | alias map collect 18 | end 19 | end 20 | 21 | unless Enumerable.method_defined?(:detect) 22 | module Enumerable 23 | alias detect find 24 | end 25 | end 26 | 27 | unless Enumerable.method_defined?(:select) 28 | module Enumerable 29 | alias select find_all 30 | end 31 | end 32 | 33 | unless Enumerable.method_defined?(:reject) 34 | module Enumerable 35 | def reject 36 | result = [] 37 | each do |i| 38 | result.push i unless yield(i) 39 | end 40 | result 41 | end 42 | end 43 | end 44 | 45 | unless Enumerable.method_defined?(:inject) 46 | module Enumerable 47 | def inject(result) 48 | each do |i| 49 | result = yield(result, i) 50 | end 51 | result 52 | end 53 | end 54 | end 55 | 56 | unless Enumerable.method_defined?(:any?) 57 | module Enumerable 58 | def any? 59 | each do |i| 60 | return true if yield(i) 61 | end 62 | false 63 | end 64 | end 65 | end 66 | 67 | unless File.respond_to?(:read) 68 | def File.read(fname) 69 | open(fname) {|f| 70 | return f.read 71 | } 72 | end 73 | end 74 | 75 | # 76 | # Application independent utilities 77 | # 78 | 79 | def File.binread(fname) 80 | open(fname, 'rb') {|f| 81 | return f.read 82 | } 83 | end 84 | 85 | # for corrupted windows stat(2) 86 | def File.dir?(path) 87 | File.directory?((path[-1,1] == '/') ? path : path + '/') 88 | end 89 | 90 | # 91 | # Config 92 | # 93 | 94 | if arg = ARGV.detect{|arg| /\A--rbconfig=/ =~ arg } 95 | ARGV.delete(arg) 96 | require arg.split(/=/, 2)[1] 97 | $".push 'rbconfig.rb' 98 | else 99 | require 'rbconfig' 100 | end 101 | 102 | def multipackage_install? 103 | FileTest.directory?(File.dirname($0) + '/packages') 104 | end 105 | 106 | 107 | class ConfigTable 108 | 109 | c = ::Config::CONFIG 110 | 111 | rubypath = c['bindir'] + '/' + c['ruby_install_name'] 112 | 113 | major = c['MAJOR'].to_i 114 | minor = c['MINOR'].to_i 115 | teeny = c['TEENY'].to_i 116 | version = "#{major}.#{minor}" 117 | 118 | # ruby ver. >= 1.4.4? 119 | newpath_p = ((major >= 2) or 120 | ((major == 1) and 121 | ((minor >= 5) or 122 | ((minor == 4) and (teeny >= 4))))) 123 | 124 | subprefix = lambda {|path| 125 | path.sub(/\A#{Regexp.quote(c['prefix'])}/o, '$prefix') 126 | } 127 | 128 | if c['rubylibdir'] 129 | # V < 1.6.3 130 | stdruby = subprefix.call(c['rubylibdir']) 131 | siteruby = subprefix.call(c['sitedir']) 132 | versite = subprefix.call(c['sitelibdir']) 133 | sodir = subprefix.call(c['sitearchdir']) 134 | elsif newpath_p 135 | # 1.4.4 <= V <= 1.6.3 136 | stdruby = "$prefix/lib/ruby/#{version}" 137 | siteruby = subprefix.call(c['sitedir']) 138 | versite = siteruby + '/' + version 139 | sodir = "$site-ruby/#{c['arch']}" 140 | else 141 | # V < 1.4.4 142 | stdruby = "$prefix/lib/ruby/#{version}" 143 | siteruby = "$prefix/lib/ruby/#{version}/site_ruby" 144 | versite = siteruby 145 | sodir = "$site-ruby/#{c['arch']}" 146 | end 147 | 148 | if arg = c['configure_args'].split.detect {|arg| /--with-make-prog=/ =~ arg } 149 | makeprog = arg.sub(/'/, '').split(/=/, 2)[1] 150 | else 151 | makeprog = 'make' 152 | end 153 | 154 | common_descripters = [ 155 | [ 'prefix', [ c['prefix'], 156 | 'path', 157 | 'path prefix of target environment' ] ], 158 | [ 'std-ruby', [ stdruby, 159 | 'path', 160 | 'the directory for standard ruby libraries' ] ], 161 | [ 'site-ruby-common', [ siteruby, 162 | 'path', 163 | 'the directory for version-independent non-standard ruby libraries' ] ], 164 | [ 'site-ruby', [ versite, 165 | 'path', 166 | 'the directory for non-standard ruby libraries' ] ], 167 | [ 'bin-dir', [ '$prefix/bin', 168 | 'path', 169 | 'the directory for commands' ] ], 170 | [ 'rb-dir', [ '$site-ruby', 171 | 'path', 172 | 'the directory for ruby scripts' ] ], 173 | [ 'so-dir', [ sodir, 174 | 'path', 175 | 'the directory for ruby extentions' ] ], 176 | [ 'data-dir', [ '$prefix/share', 177 | 'path', 178 | 'the directory for shared data' ] ], 179 | [ 'ruby-path', [ rubypath, 180 | 'path', 181 | 'path to set to #! line' ] ], 182 | [ 'ruby-prog', [ rubypath, 183 | 'name', 184 | 'the ruby program using for installation' ] ], 185 | [ 'make-prog', [ makeprog, 186 | 'name', 187 | 'the make program to compile ruby extentions' ] ], 188 | [ 'without-ext', [ 'no', 189 | 'yes/no', 190 | 'does not compile/install ruby extentions' ] ] 191 | ] 192 | multipackage_descripters = [ 193 | [ 'with', [ '', 194 | 'name,name...', 195 | 'package names that you want to install', 196 | 'ALL' ] ], 197 | [ 'without', [ '', 198 | 'name,name...', 199 | 'package names that you do not want to install', 200 | 'NONE' ] ] 201 | ] 202 | if multipackage_install? 203 | DESCRIPTER = common_descripters + multipackage_descripters 204 | else 205 | DESCRIPTER = common_descripters 206 | end 207 | 208 | SAVE_FILE = 'config.save' 209 | 210 | def ConfigTable.each_name(&block) 211 | keys().each(&block) 212 | end 213 | 214 | def ConfigTable.keys 215 | DESCRIPTER.map {|name, *dummy| name } 216 | end 217 | 218 | def ConfigTable.each_definition(&block) 219 | DESCRIPTER.each(&block) 220 | end 221 | 222 | def ConfigTable.get_entry(name) 223 | name, ent = DESCRIPTER.assoc(name) 224 | ent 225 | end 226 | 227 | def ConfigTable.get_entry!(name) 228 | get_entry(name) or raise ArgumentError, "no such config: #{name}" 229 | end 230 | 231 | def ConfigTable.add_entry(name, vals) 232 | ConfigTable::DESCRIPTER.push [name,vals] 233 | end 234 | 235 | def ConfigTable.remove_entry(name) 236 | get_entry(name) or raise ArgumentError, "no such config: #{name}" 237 | DESCRIPTER.delete_if {|n, arr| n == name } 238 | end 239 | 240 | def ConfigTable.config_key?(name) 241 | get_entry(name) ? true : false 242 | end 243 | 244 | def ConfigTable.bool_config?(name) 245 | ent = get_entry(name) or return false 246 | ent[1] == 'yes/no' 247 | end 248 | 249 | def ConfigTable.value_config?(name) 250 | ent = get_entry(name) or return false 251 | ent[1] != 'yes/no' 252 | end 253 | 254 | def ConfigTable.path_config?(name) 255 | ent = get_entry(name) or return false 256 | ent[1] == 'path' 257 | end 258 | 259 | 260 | class << self 261 | alias newobj new 262 | end 263 | 264 | def ConfigTable.new 265 | c = newobj() 266 | c.initialize_from_table 267 | c 268 | end 269 | 270 | def ConfigTable.load 271 | c = newobj() 272 | c.initialize_from_file 273 | c 274 | end 275 | 276 | def initialize_from_table 277 | @table = {} 278 | DESCRIPTER.each do |k, (default, vname, desc, default2)| 279 | @table[k] = default 280 | end 281 | end 282 | 283 | def initialize_from_file 284 | raise InstallError, "#{File.basename $0} config first"\ 285 | unless File.file?(SAVE_FILE) 286 | @table = {} 287 | File.foreach(SAVE_FILE) do |line| 288 | k, v = line.split(/=/, 2) 289 | @table[k] = v.strip 290 | end 291 | end 292 | 293 | def save 294 | File.open(SAVE_FILE, 'w') {|f| 295 | @table.each do |k, v| 296 | f.printf "%s=%s\n", k, v if v 297 | end 298 | } 299 | end 300 | 301 | def []=(k, v) 302 | raise InstallError, "unknown config option #{k}"\ 303 | unless ConfigTable.config_key?(k) 304 | @table[k] = v 305 | end 306 | 307 | def [](key) 308 | return nil unless @table[key] 309 | @table[key].gsub(%r<\$([^/]+)>) { self[$1] } 310 | end 311 | 312 | def set_raw(key, val) 313 | @table[key] = val 314 | end 315 | 316 | def get_raw(key) 317 | @table[key] 318 | end 319 | 320 | end 321 | 322 | 323 | module MetaConfigAPI 324 | 325 | def eval_file_ifexist(fname) 326 | instance_eval File.read(fname), fname, 1 if File.file?(fname) 327 | end 328 | 329 | def config_names 330 | ConfigTable.keys 331 | end 332 | 333 | def config?(name) 334 | ConfigTable.config_key?(name) 335 | end 336 | 337 | def bool_config?(name) 338 | ConfigTable.bool_config?(name) 339 | end 340 | 341 | def value_config?(name) 342 | ConfigTable.value_config?(name) 343 | end 344 | 345 | def path_config?(name) 346 | ConfigTable.path_config?(name) 347 | end 348 | 349 | def add_config(name, argname, default, desc) 350 | ConfigTable.add_entry name,[default,argname,desc] 351 | end 352 | 353 | def add_path_config(name, default, desc) 354 | add_config name, 'path', default, desc 355 | end 356 | 357 | def add_bool_config(name, default, desc) 358 | add_config name, 'yes/no', default ? 'yes' : 'no', desc 359 | end 360 | 361 | def set_config_default(name, default) 362 | if bool_config?(name) 363 | ConfigTable.get_entry!(name)[0] = (default ? 'yes' : 'no') 364 | else 365 | ConfigTable.get_entry!(name)[0] = default 366 | end 367 | end 368 | 369 | def remove_config(name) 370 | ent = ConfigTable.get_entry(name) 371 | ConfigTable.remove_entry name 372 | ent 373 | end 374 | 375 | end 376 | 377 | # 378 | # File Operations 379 | # 380 | 381 | module FileOperations 382 | 383 | def mkdir_p(dirname, prefix = nil) 384 | dirname = prefix + dirname if prefix 385 | $stderr.puts "mkdir -p #{dirname}" if verbose? 386 | return if no_harm? 387 | 388 | # does not check '/'... it's too abnormal case 389 | dirs = dirname.split(%r<(?=/)>) 390 | if /\A[a-z]:\z/i =~ dirs[0] 391 | disk = dirs.shift 392 | dirs[0] = disk + dirs[0] 393 | end 394 | dirs.each_index do |idx| 395 | path = dirs[0..idx].join('') 396 | Dir.mkdir path unless File.dir?(path) 397 | end 398 | end 399 | 400 | def rm_f(fname) 401 | $stderr.puts "rm -f #{fname}" if verbose? 402 | return if no_harm? 403 | 404 | if File.exist?(fname) or File.symlink?(fname) 405 | File.chmod 0777, fname 406 | File.unlink fname 407 | end 408 | end 409 | 410 | def rm_rf(dn) 411 | $stderr.puts "rm -rf #{dn}" if verbose? 412 | return if no_harm? 413 | 414 | Dir.chdir dn 415 | Dir.foreach('.') do |fn| 416 | next if fn == '.' 417 | next if fn == '..' 418 | if File.dir?(fn) 419 | verbose_off { 420 | rm_rf fn 421 | } 422 | else 423 | verbose_off { 424 | rm_f fn 425 | } 426 | end 427 | end 428 | Dir.chdir '..' 429 | Dir.rmdir dn 430 | end 431 | 432 | def move_file(src, dest) 433 | File.unlink dest if File.exist?(dest) 434 | begin 435 | File.rename src, dest 436 | rescue 437 | File.open(dest, 'wb') {|f| f.write File.binread(src) } 438 | File.chmod File.stat(src).mode, dest 439 | File.unlink src 440 | end 441 | end 442 | 443 | def install(from, dest, mode, prefix = nil) 444 | $stderr.puts "install #{from} #{dest}" if verbose? 445 | return if no_harm? 446 | 447 | realdest = prefix + dest if prefix 448 | realdest = File.join(realdest, File.basename(from)) if File.dir?(realdest) 449 | str = File.binread(from) 450 | if diff?(str, realdest) 451 | verbose_off { 452 | rm_f realdest if File.exist?(realdest) 453 | } 454 | File.open(realdest, 'wb') {|f| 455 | f.write str 456 | } 457 | File.chmod mode, realdest 458 | 459 | File.open("#{objdir_root()}/InstalledFiles", 'a') {|f| 460 | if prefix 461 | f.puts realdest.sub(prefix, '') 462 | else 463 | f.puts realdest 464 | end 465 | } 466 | end 467 | end 468 | 469 | def diff?(new_content, path) 470 | return true unless File.exist?(path) 471 | new_content != File.binread(path) 472 | end 473 | 474 | def command(str) 475 | $stderr.puts str if verbose? 476 | system str or raise RuntimeError, "'system #{str}' failed" 477 | end 478 | 479 | def ruby(str) 480 | command config('ruby-prog') + ' ' + str 481 | end 482 | 483 | def make(task = '') 484 | command config('make-prog') + ' ' + task 485 | end 486 | 487 | def extdir?(dir) 488 | File.exist?(dir + '/MANIFEST') 489 | end 490 | 491 | def all_files_in(dirname) 492 | Dir.open(dirname) {|d| 493 | return d.select {|ent| File.file?("#{dirname}/#{ent}") } 494 | } 495 | end 496 | 497 | REJECT_DIRS = %w( 498 | CVS SCCS RCS CVS.adm 499 | ) 500 | 501 | def all_dirs_in(dirname) 502 | Dir.open(dirname) {|d| 503 | return d.select {|n| File.dir?("#{dirname}/#{n}") } - %w(. ..) - REJECT_DIRS 504 | } 505 | end 506 | 507 | end 508 | 509 | # 510 | # Main Installer 511 | # 512 | 513 | class InstallError < StandardError; end 514 | 515 | 516 | module HookUtils 517 | 518 | def run_hook(name) 519 | try_run_hook "#{curr_srcdir()}/#{name}" or 520 | try_run_hook "#{curr_srcdir()}/#{name}.rb" 521 | end 522 | 523 | def try_run_hook(fname) 524 | return false unless File.file?(fname) 525 | begin 526 | instance_eval File.read(fname), fname, 1 527 | rescue 528 | raise InstallError, "hook #{fname} failed:\n" + $!.message 529 | end 530 | true 531 | end 532 | 533 | end 534 | 535 | 536 | module HookScriptAPI 537 | 538 | def get_config(key) 539 | @config[key] 540 | end 541 | 542 | alias config get_config 543 | 544 | def set_config(key, val) 545 | @config[key] = val 546 | end 547 | 548 | # 549 | # srcdir/objdir (works only in the package directory) 550 | # 551 | 552 | #abstract srcdir_root 553 | #abstract objdir_root 554 | #abstract relpath 555 | 556 | def curr_srcdir 557 | "#{srcdir_root()}/#{relpath()}" 558 | end 559 | 560 | def curr_objdir 561 | "#{objdir_root()}/#{relpath()}" 562 | end 563 | 564 | def srcfile(path) 565 | "#{curr_srcdir()}/#{path}" 566 | end 567 | 568 | def srcexist?(path) 569 | File.exist?(srcfile(path)) 570 | end 571 | 572 | def srcdirectory?(path) 573 | File.dir?(srcfile(path)) 574 | end 575 | 576 | def srcfile?(path) 577 | File.file? srcfile(path) 578 | end 579 | 580 | def srcentries(path = '.') 581 | Dir.open("#{curr_srcdir()}/#{path}") {|d| 582 | return d.to_a - %w(. ..) 583 | } 584 | end 585 | 586 | def srcfiles(path = '.') 587 | srcentries(path).select {|fname| 588 | File.file?(File.join(curr_srcdir(), path, fname)) 589 | } 590 | end 591 | 592 | def srcdirectories(path = '.') 593 | srcentries(path).select {|fname| 594 | File.dir?(File.join(curr_srcdir(), path, fname)) 595 | } 596 | end 597 | 598 | end 599 | 600 | 601 | class ToplevelInstaller 602 | 603 | Version = '3.2.4' 604 | Copyright = 'Copyright (c) 2000-2004 Minero Aoki' 605 | 606 | TASKS = [ 607 | [ 'config', 'saves your configurations' ], 608 | [ 'show', 'shows current configuration' ], 609 | [ 'setup', 'compiles ruby extentions and others' ], 610 | [ 'install', 'installs files' ], 611 | [ 'clean', "does `make clean' for each extention" ], 612 | [ 'distclean',"does `make distclean' for each extention" ] 613 | ] 614 | 615 | def ToplevelInstaller.invoke 616 | instance().invoke 617 | end 618 | 619 | @singleton = nil 620 | 621 | def ToplevelInstaller.instance 622 | @singleton ||= new(File.dirname($0)) 623 | @singleton 624 | end 625 | 626 | include MetaConfigAPI 627 | 628 | def initialize(ardir_root) 629 | @config = nil 630 | @options = { 'verbose' => true } 631 | @ardir = File.expand_path(ardir_root) 632 | end 633 | 634 | def inspect 635 | "#<#{self.class} #{__id__()}>" 636 | end 637 | 638 | def invoke 639 | run_metaconfigs 640 | task = parsearg_global() 641 | @config = load_config(task) 642 | __send__ "parsearg_#{task}" 643 | init_installers 644 | __send__ "exec_#{task}" 645 | end 646 | 647 | def run_metaconfigs 648 | eval_file_ifexist "#{@ardir}/metaconfig" 649 | end 650 | 651 | def load_config(task) 652 | case task 653 | when 'config' 654 | ConfigTable.new 655 | when 'clean', 'distclean' 656 | if File.exist?('config.save') 657 | then ConfigTable.load 658 | else ConfigTable.new 659 | end 660 | else 661 | ConfigTable.load 662 | end 663 | end 664 | 665 | def init_installers 666 | @installer = Installer.new(@config, @options, @ardir, File.expand_path('.')) 667 | end 668 | 669 | # 670 | # Hook Script API bases 671 | # 672 | 673 | def srcdir_root 674 | @ardir 675 | end 676 | 677 | def objdir_root 678 | '.' 679 | end 680 | 681 | def relpath 682 | '.' 683 | end 684 | 685 | # 686 | # Option Parsing 687 | # 688 | 689 | def parsearg_global 690 | valid_task = /\A(?:#{TASKS.map {|task,desc| task }.join '|'})\z/ 691 | 692 | while arg = ARGV.shift 693 | case arg 694 | when /\A\w+\z/ 695 | raise InstallError, "invalid task: #{arg}" unless valid_task =~ arg 696 | return arg 697 | 698 | when '-q', '--quiet' 699 | @options['verbose'] = false 700 | 701 | when '--verbose' 702 | @options['verbose'] = true 703 | 704 | when '-h', '--help' 705 | print_usage $stdout 706 | exit 0 707 | 708 | when '-v', '--version' 709 | puts "#{File.basename($0)} version #{Version}" 710 | exit 0 711 | 712 | when '--copyright' 713 | puts Copyright 714 | exit 0 715 | 716 | else 717 | raise InstallError, "unknown global option '#{arg}'" 718 | end 719 | end 720 | 721 | raise InstallError, <" 792 | out.puts " ruby #{File.basename $0} [] []" 793 | 794 | fmt = " %-20s %s\n" 795 | out.puts 796 | out.puts 'Global options:' 797 | out.printf fmt, '-q,--quiet', 'suppress message outputs' 798 | out.printf fmt, ' --verbose', 'output messages verbosely' 799 | out.printf fmt, '-h,--help', 'print this message' 800 | out.printf fmt, '-v,--version', 'print version and quit' 801 | out.printf fmt, ' --copyright', 'print copyright and quit' 802 | 803 | out.puts 804 | out.puts 'Tasks:' 805 | TASKS.each do |name, desc| 806 | out.printf " %-10s %s\n", name, desc 807 | end 808 | 809 | out.puts 810 | out.puts 'Options for config:' 811 | ConfigTable.each_definition do |name, (default, arg, desc, default2)| 812 | out.printf " %-20s %s [%s]\n", 813 | '--'+ name + (ConfigTable.bool_config?(name) ? '' : '='+arg), 814 | desc, 815 | default2 || default 816 | end 817 | out.printf " %-20s %s [%s]\n", 818 | '--rbconfig=path', 'your rbconfig.rb to load', "running ruby's" 819 | 820 | out.puts 821 | out.puts 'Options for install:' 822 | out.printf " %-20s %s [%s]\n", 823 | '--no-harm', 'only display what to do if given', 'off' 824 | out.printf " %-20s %s [%s]\n", 825 | '--prefix', 'install path prefix', '$prefix' 826 | 827 | out.puts 828 | end 829 | 830 | # 831 | # Task Handlers 832 | # 833 | 834 | def exec_config 835 | @installer.exec_config 836 | @config.save # must be final 837 | end 838 | 839 | def exec_setup 840 | @installer.exec_setup 841 | end 842 | 843 | def exec_install 844 | @installer.exec_install 845 | end 846 | 847 | def exec_show 848 | ConfigTable.each_name do |k| 849 | v = @config.get_raw(k) 850 | if not v or v.empty? 851 | v = '(not specified)' 852 | end 853 | printf "%-10s %s\n", k, v 854 | end 855 | end 856 | 857 | def exec_clean 858 | @installer.exec_clean 859 | end 860 | 861 | def exec_distclean 862 | @installer.exec_distclean 863 | end 864 | 865 | end 866 | 867 | 868 | class ToplevelInstallerMulti < ToplevelInstaller 869 | 870 | include HookUtils 871 | include HookScriptAPI 872 | include FileOperations 873 | 874 | def initialize(ardir) 875 | super 876 | @packages = all_dirs_in("#{@ardir}/packages") 877 | raise 'no package exists' if @packages.empty? 878 | end 879 | 880 | def run_metaconfigs 881 | eval_file_ifexist "#{@ardir}/metaconfig" 882 | @packages.each do |name| 883 | eval_file_ifexist "#{@ardir}/packages/#{name}/metaconfig" 884 | end 885 | end 886 | 887 | def init_installers 888 | @installers = {} 889 | @packages.each do |pack| 890 | @installers[pack] = Installer.new(@config, @options, 891 | "#{@ardir}/packages/#{pack}", 892 | "packages/#{pack}") 893 | end 894 | 895 | with = extract_selection(config('with')) 896 | without = extract_selection(config('without')) 897 | @selected = @installers.keys.select {|name| 898 | (with.empty? or with.include?(name)) \ 899 | and not without.include?(name) 900 | } 901 | end 902 | 903 | def extract_selection(list) 904 | a = list.split(/,/) 905 | a.each do |name| 906 | raise InstallError, "no such package: #{name}" \ 907 | unless @installers.key?(name) 908 | end 909 | a 910 | end 911 | 912 | def print_usage(f) 913 | super 914 | f.puts 'Inluded packages:' 915 | f.puts ' ' + @packages.sort.join(' ') 916 | f.puts 917 | end 918 | 919 | # 920 | # multi-package metaconfig API 921 | # 922 | 923 | attr_reader :packages 924 | 925 | def declare_packages(list) 926 | raise 'package list is empty' if list.empty? 927 | list.each do |name| 928 | raise "directory packages/#{name} does not exist"\ 929 | unless File.dir?("#{@ardir}/packages/#{name}") 930 | end 931 | @packages = list 932 | end 933 | 934 | # 935 | # Task Handlers 936 | # 937 | 938 | def exec_config 939 | run_hook 'pre-config' 940 | each_selected_installers {|inst| inst.exec_config } 941 | run_hook 'post-config' 942 | @config.save # must be final 943 | end 944 | 945 | def exec_setup 946 | run_hook 'pre-setup' 947 | each_selected_installers {|inst| inst.exec_setup } 948 | run_hook 'post-setup' 949 | end 950 | 951 | def exec_install 952 | run_hook 'pre-install' 953 | each_selected_installers {|inst| inst.exec_install } 954 | run_hook 'post-install' 955 | end 956 | 957 | def exec_clean 958 | rm_f 'config.save' 959 | run_hook 'pre-clean' 960 | each_selected_installers {|inst| inst.exec_clean } 961 | run_hook 'post-clean' 962 | end 963 | 964 | def exec_distclean 965 | rm_f 'config.save' 966 | run_hook 'pre-distclean' 967 | each_selected_installers {|inst| inst.exec_distclean } 968 | run_hook 'post-distclean' 969 | end 970 | 971 | # 972 | # lib 973 | # 974 | 975 | def each_selected_installers 976 | Dir.mkdir 'packages' unless File.dir?('packages') 977 | @selected.each do |pack| 978 | $stderr.puts "Processing the package `#{pack}' ..." if @options['verbose'] 979 | Dir.mkdir "packages/#{pack}" unless File.dir?("packages/#{pack}") 980 | Dir.chdir "packages/#{pack}" 981 | yield @installers[pack] 982 | Dir.chdir '../..' 983 | end 984 | end 985 | 986 | def verbose? 987 | @options['verbose'] 988 | end 989 | 990 | def no_harm? 991 | @options['no-harm'] 992 | end 993 | 994 | end 995 | 996 | 997 | class Installer 998 | 999 | FILETYPES = %w( bin lib ext data ) 1000 | 1001 | include HookScriptAPI 1002 | include HookUtils 1003 | include FileOperations 1004 | 1005 | def initialize(config, opt, srcroot, objroot) 1006 | @config = config 1007 | @options = opt 1008 | @srcdir = File.expand_path(srcroot) 1009 | @objdir = File.expand_path(objroot) 1010 | @currdir = '.' 1011 | end 1012 | 1013 | def inspect 1014 | "#<#{self.class} #{File.basename(@srcdir)}>" 1015 | end 1016 | 1017 | # 1018 | # Hook Script API bases 1019 | # 1020 | 1021 | def srcdir_root 1022 | @srcdir 1023 | end 1024 | 1025 | def objdir_root 1026 | @objdir 1027 | end 1028 | 1029 | def relpath 1030 | @currdir 1031 | end 1032 | 1033 | # 1034 | # configs/options 1035 | # 1036 | 1037 | def no_harm? 1038 | @options['no-harm'] 1039 | end 1040 | 1041 | def verbose? 1042 | @options['verbose'] 1043 | end 1044 | 1045 | def verbose_off 1046 | begin 1047 | save, @options['verbose'] = @options['verbose'], false 1048 | yield 1049 | ensure 1050 | @options['verbose'] = save 1051 | end 1052 | end 1053 | 1054 | # 1055 | # TASK config 1056 | # 1057 | 1058 | def exec_config 1059 | exec_task_traverse 'config' 1060 | end 1061 | 1062 | def config_dir_bin(rel) 1063 | end 1064 | 1065 | def config_dir_lib(rel) 1066 | end 1067 | 1068 | def config_dir_ext(rel) 1069 | extconf if extdir?(curr_srcdir()) 1070 | end 1071 | 1072 | def extconf 1073 | opt = @options['config-opt'].join(' ') 1074 | command "#{config('ruby-prog')} #{curr_srcdir()}/extconf.rb #{opt}" 1075 | end 1076 | 1077 | def config_dir_data(rel) 1078 | end 1079 | 1080 | # 1081 | # TASK setup 1082 | # 1083 | 1084 | def exec_setup 1085 | exec_task_traverse 'setup' 1086 | end 1087 | 1088 | def setup_dir_bin(rel) 1089 | all_files_in(curr_srcdir()).each do |fname| 1090 | adjust_shebang "#{curr_srcdir()}/#{fname}" 1091 | end 1092 | end 1093 | 1094 | # modify: #!/usr/bin/ruby 1095 | # modify: #! /usr/bin/ruby 1096 | # modify: #!ruby 1097 | # not modify: #!/usr/bin/env ruby 1098 | SHEBANG_RE = /\A\#!\s*\S*ruby\S*/ 1099 | 1100 | def adjust_shebang(path) 1101 | return if no_harm? 1102 | 1103 | tmpfile = File.basename(path) + '.tmp' 1104 | begin 1105 | File.open(path, 'rb') {|r| 1106 | File.open(tmpfile, 'wb') {|w| 1107 | first = r.gets 1108 | return unless SHEBANG_RE =~ first 1109 | 1110 | $stderr.puts "adjusting shebang: #{File.basename path}" if verbose? 1111 | w.print first.sub(SHEBANG_RE, '#!' + config('ruby-path')) 1112 | w.write r.read 1113 | } 1114 | } 1115 | move_file tmpfile, File.basename(path) 1116 | ensure 1117 | File.unlink tmpfile if File.exist?(tmpfile) 1118 | end 1119 | end 1120 | 1121 | def setup_dir_lib(rel) 1122 | end 1123 | 1124 | def setup_dir_ext(rel) 1125 | make if extdir?(curr_srcdir()) 1126 | end 1127 | 1128 | def setup_dir_data(rel) 1129 | end 1130 | 1131 | # 1132 | # TASK install 1133 | # 1134 | 1135 | def exec_install 1136 | exec_task_traverse 'install' 1137 | end 1138 | 1139 | def install_dir_bin(rel) 1140 | install_files collect_filenames_auto(), "#{config('bin-dir')}/#{rel}", 0755 1141 | end 1142 | 1143 | def install_dir_lib(rel) 1144 | install_files ruby_scripts(), "#{config('rb-dir')}/#{rel}", 0644 1145 | end 1146 | 1147 | def install_dir_ext(rel) 1148 | return unless extdir?(curr_srcdir()) 1149 | install_files ruby_extentions('.'), 1150 | "#{config('so-dir')}/#{File.dirname(rel)}", 1151 | 0555 1152 | end 1153 | 1154 | def install_dir_data(rel) 1155 | install_files collect_filenames_auto(), "#{config('data-dir')}/#{rel}", 0644 1156 | end 1157 | 1158 | def install_files(list, dest, mode) 1159 | mkdir_p dest, @options['install-prefix'] 1160 | list.each do |fname| 1161 | install fname, dest, mode, @options['install-prefix'] 1162 | end 1163 | end 1164 | 1165 | def ruby_scripts 1166 | collect_filenames_auto().select {|n| /\.rb\z/ =~ n || "module.yml" == n } 1167 | end 1168 | 1169 | # picked up many entries from cvs-1.11.1/src/ignore.c 1170 | reject_patterns = %w( 1171 | core RCSLOG tags TAGS .make.state 1172 | .nse_depinfo #* .#* cvslog.* ,* .del-* *.olb 1173 | *~ *.old *.bak *.BAK *.orig *.rej _$* *$ 1174 | 1175 | *.org *.in .* 1176 | ) 1177 | mapping = { 1178 | '.' => '\.', 1179 | '$' => '\$', 1180 | '#' => '\#', 1181 | '*' => '.*' 1182 | } 1183 | REJECT_PATTERNS = Regexp.new('\A(?:' + 1184 | reject_patterns.map {|pat| 1185 | pat.gsub(/[\.\$\#\*]/) {|ch| mapping[ch] } 1186 | }.join('|') + 1187 | ')\z') 1188 | 1189 | def collect_filenames_auto 1190 | mapdir((existfiles() - hookfiles()).reject {|fname| 1191 | REJECT_PATTERNS =~ fname 1192 | }) 1193 | end 1194 | 1195 | def existfiles 1196 | all_files_in(curr_srcdir()) | all_files_in('.') 1197 | end 1198 | 1199 | def hookfiles 1200 | %w( pre-%s post-%s pre-%s.rb post-%s.rb ).map {|fmt| 1201 | %w( config setup install clean ).map {|t| sprintf(fmt, t) } 1202 | }.flatten 1203 | end 1204 | 1205 | def mapdir(filelist) 1206 | filelist.map {|fname| 1207 | if File.exist?(fname) # objdir 1208 | fname 1209 | else # srcdir 1210 | File.join(curr_srcdir(), fname) 1211 | end 1212 | } 1213 | end 1214 | 1215 | def ruby_extentions(dir) 1216 | _ruby_extentions(dir) or 1217 | raise InstallError, "no ruby extention exists: 'ruby #{$0} setup' first" 1218 | end 1219 | 1220 | DLEXT = /\.#{ ::Config::CONFIG['DLEXT'] }\z/ 1221 | 1222 | def _ruby_extentions(dir) 1223 | Dir.open(dir) {|d| 1224 | return d.select {|fname| DLEXT =~ fname } 1225 | } 1226 | end 1227 | 1228 | # 1229 | # TASK clean 1230 | # 1231 | 1232 | def exec_clean 1233 | exec_task_traverse 'clean' 1234 | rm_f 'config.save' 1235 | rm_f 'InstalledFiles' 1236 | end 1237 | 1238 | def clean_dir_bin(rel) 1239 | end 1240 | 1241 | def clean_dir_lib(rel) 1242 | end 1243 | 1244 | def clean_dir_ext(rel) 1245 | return unless extdir?(curr_srcdir()) 1246 | make 'clean' if File.file?('Makefile') 1247 | end 1248 | 1249 | def clean_dir_data(rel) 1250 | end 1251 | 1252 | # 1253 | # TASK distclean 1254 | # 1255 | 1256 | def exec_distclean 1257 | exec_task_traverse 'distclean' 1258 | rm_f 'config.save' 1259 | rm_f 'InstalledFiles' 1260 | end 1261 | 1262 | def distclean_dir_bin(rel) 1263 | end 1264 | 1265 | def distclean_dir_lib(rel) 1266 | end 1267 | 1268 | def distclean_dir_ext(rel) 1269 | return unless extdir?(curr_srcdir()) 1270 | make 'distclean' if File.file?('Makefile') 1271 | end 1272 | 1273 | # 1274 | # lib 1275 | # 1276 | 1277 | def exec_task_traverse(task) 1278 | run_hook "pre-#{task}" 1279 | FILETYPES.each do |type| 1280 | if config('without-ext') == 'yes' and type == 'ext' 1281 | $stderr.puts 'skipping ext/* by user option' if verbose? 1282 | next 1283 | end 1284 | traverse task, type, "#{task}_dir_#{type}" 1285 | end 1286 | run_hook "post-#{task}" 1287 | end 1288 | 1289 | def traverse(task, rel, mid) 1290 | dive_into(rel) { 1291 | run_hook "pre-#{task}" 1292 | __send__ mid, rel.sub(%r[\A.*?(?:/|\z)], '') 1293 | all_dirs_in(curr_srcdir()).each do |d| 1294 | traverse task, "#{rel}/#{d}", mid 1295 | end 1296 | run_hook "post-#{task}" 1297 | } 1298 | end 1299 | 1300 | def dive_into(rel) 1301 | return unless File.dir?("#{@srcdir}/#{rel}") 1302 | 1303 | dir = File.basename(rel) 1304 | Dir.mkdir dir unless File.dir?(dir) 1305 | prevdir = Dir.pwd 1306 | Dir.chdir dir 1307 | $stderr.puts '---> ' + rel if verbose? 1308 | @currdir = rel 1309 | yield 1310 | Dir.chdir prevdir 1311 | $stderr.puts '<--- ' + rel if verbose? 1312 | @currdir = File.dirname(rel) 1313 | end 1314 | 1315 | end 1316 | 1317 | 1318 | if $0 == __FILE__ 1319 | begin 1320 | if multipackage_install? 1321 | ToplevelInstallerMulti.invoke 1322 | else 1323 | ToplevelInstaller.invoke 1324 | end 1325 | rescue 1326 | raise if $DEBUG 1327 | $stderr.puts $!.message 1328 | $stderr.puts "Try 'ruby #{$0} --help' for detailed usage." 1329 | exit 1 1330 | end 1331 | end 1332 | -------------------------------------------------------------------------------- /test/common.rb: -------------------------------------------------------------------------------- 1 | require 'test/unit' 2 | require 'mocha/test_unit' 3 | 4 | begin 5 | gem 'net-ssh', '>= 2.0.0' 6 | require 'net/ssh' 7 | rescue LoadError 8 | $LOAD_PATH.unshift "#{File.dirname(__FILE__)}/../../net-ssh/lib" 9 | 10 | begin 11 | require 'net/ssh' 12 | require 'net/ssh/version' 13 | raise LoadError, 'wrong version' unless Net::SSH::Version::STRING >= '1.99.0' 14 | rescue LoadError => e 15 | abort "could not load net/ssh v2 (#{e.inspect})" 16 | end 17 | end 18 | 19 | $LOAD_PATH.unshift "#{File.dirname(__FILE__)}/../lib" 20 | 21 | require 'net/scp' 22 | require 'net/ssh/test' 23 | 24 | class Net::SSH::Test::Channel 25 | def gets_ok 26 | gets_data "\0" 27 | end 28 | 29 | def sends_ok 30 | sends_data "\0" 31 | end 32 | end 33 | 34 | class Net::SCP::TestCase < Test::Unit::TestCase 35 | include Net::SSH::Test 36 | 37 | def default_test 38 | # do nothing, this is just a hacky-hack to work around Test::Unit's 39 | # insistence that all TestCase subclasses have at least one test 40 | # method defined. 41 | end 42 | 43 | protected 44 | 45 | def prepare_file(path, contents = '', mode = 0o0666, mtime = Time.now, atime = Time.now) 46 | entry = FileEntry.new(path, contents, mode, mtime, atime) 47 | entry.stub! 48 | entry 49 | end 50 | 51 | def prepare_directory(path, mode = 0o0777, mtime = Time.now, atime = Time.now) 52 | directory = DirectoryEntry.new(path, mode, mtime, atime) 53 | yield directory if block_given? 54 | directory.stub! 55 | end 56 | 57 | # The POSIX spec unfortunately allows all characters in file names except 58 | # ASCII 0x00(NUL) and 0x2F(/) 59 | # 60 | # Ideally, we should be testing filenames with newlines, but Mocha doesn't 61 | # like this at all, so we leave them out. However, the Shellwords module 62 | # handles newlines just fine, so we can be reasonably confident that they 63 | # will work in practice 64 | def awful_file_name 65 | (((0x00..0x7f).to_a - [0x00, 0x0a, 0x2b, 0x2f]).map { |n| n.chr }).join + '.txt' 66 | end 67 | 68 | def escaped_file_name 69 | "\\\001\\\002\\\003\\\004\\\005\\\006\\\a\\\b\\\t\\\v\\\f\\\r\\\016\\\017\\\020\\\021\\\022\\\023\\\024\\\025" \ 70 | "\\\026\\\027\\\030\\\031\\\032\\\e\\\034\\\035\\\036\\\037\\ \\!\\\"\\#\\$\\%\\&\\'\\(\\)\\*,-.0123456789:" \ 71 | "\\;\\<\\=\\>\\?@ABCDEFGHIJKLMNOPQRSTUVWXYZ\\[\\\\\\]\\^_\\`abcdefghijklmnopqrstuvwxyz\\{\\|\\}\\~\\\177.txt" 72 | end 73 | 74 | class FileEntry 75 | attr_reader :path, :contents, :mode, :mtime, :atime, :io 76 | 77 | def initialize(path, contents, mode = 0o0666, mtime = Time.now, atime = Time.now) 78 | @path = path 79 | @contents = contents 80 | @mode = mode 81 | @mtime = mtime 82 | @atime = atime 83 | end 84 | 85 | def name 86 | @name ||= File.basename(path) 87 | end 88 | 89 | def stub! 90 | # Stub for File::Stat 91 | stat = Object.new 92 | stat.stubs(size: contents.length, mode: mode, mtime: mtime, atime: atime, directory?: false) 93 | 94 | File.stubs(:stat).with(path).returns(stat) 95 | File.stubs(:directory?).with(path).returns(false) 96 | File.stubs(:file?).with(path).returns(true) 97 | File.stubs(:open).with(path, 'rb').returns(StringIO.new(contents)) 98 | 99 | @io = StringIO.new 100 | File.stubs(:new).with(path, 'wb', mode).returns(io) 101 | end 102 | end 103 | 104 | class DirectoryEntry 105 | attr_reader :path, :mode, :mtime, :atime, :entries 106 | 107 | def initialize(path, mode = 0o0777, mtime = Time.now, atime = Time.now) 108 | @path = path 109 | @mode = mode 110 | @mtime = mtime 111 | @atime = atime 112 | @entries = [] 113 | end 114 | 115 | def name 116 | @name ||= File.basename(path) 117 | end 118 | 119 | def file(name, *args) 120 | (entries << FileEntry.new(File.join(path, name), *args)).last 121 | end 122 | 123 | def directory(name, *args) 124 | entry = DirectoryEntry.new(File.join(path, name), *args) 125 | yield entry if block_given? 126 | (entries << entry).last 127 | end 128 | 129 | def stub! 130 | Dir.stubs(:mkdir).with { |*a| a.first == path } 131 | 132 | # Stub for File::Stat 133 | stat = Object.new 134 | stat.stubs(size: 1024, mode: mode, mtime: mtime, atime: atime, directory?: true) 135 | 136 | File.stubs(:stat).with(path).returns(stat) 137 | File.stubs(:directory?).with(path).returns(true) 138 | File.stubs(:file?).with(path).returns(false) 139 | Dir.stubs(:entries).with(path).returns(%w[. ..] + entries.map { |e| e.name }.sort) 140 | 141 | entries.each { |e| e.stub! } 142 | end 143 | end 144 | 145 | def expect_scp_session(arguments) 146 | story do |session| 147 | channel = session.opens_channel 148 | channel.sends_exec "scp #{arguments}" 149 | yield channel if block_given? 150 | channel.sends_eof 151 | channel.gets_exit_status 152 | channel.gets_eof 153 | channel.gets_close 154 | channel.sends_close 155 | end 156 | end 157 | 158 | def scp(options = {}) 159 | @scp ||= Net::SCP.new(connection(options)) 160 | end 161 | end 162 | -------------------------------------------------------------------------------- /test/test_all.rb: -------------------------------------------------------------------------------- 1 | Dir.chdir(File.dirname(__FILE__)) do 2 | (Dir['**/test_*.rb']-["test_all.rb"]).each { |file| require(file) } 3 | end 4 | -------------------------------------------------------------------------------- /test/test_download.rb: -------------------------------------------------------------------------------- 1 | require 'common' 2 | 3 | class TestDownload < Net::SCP::TestCase 4 | def test_download_file_should_transfer_file 5 | file = prepare_file("/path/to/local.txt", "a" * 1234) 6 | 7 | expect_scp_session "-f /path/to/remote.txt" do |channel| 8 | simple_download(channel) 9 | end 10 | 11 | assert_scripted { scp.download!("/path/to/remote.txt", "/path/to/local.txt") } 12 | assert_equal "a" * 1234, file.io.string 13 | end 14 | 15 | def test_download_file_with_spaces_in_name_should_escape_remote_file_name 16 | _file = prepare_file("/path/to/local file.txt", "") 17 | 18 | expect_scp_session "-f /path/to/remote\\ file.txt" do |channel| 19 | channel.sends_ok 20 | channel.gets_data "C0666 0 local file.txt\n" 21 | channel.sends_ok 22 | channel.gets_ok 23 | channel.sends_ok 24 | end 25 | 26 | assert_scripted { scp.download!("/path/to/remote file.txt", "/path/to/local file.txt") } 27 | end 28 | 29 | def test_download_file_with_metacharacters_in_name_should_escape_remote_file_name 30 | _file = prepare_file("/path/to/local/#{awful_file_name}", "") 31 | 32 | expect_scp_session "-f /path/to/remote/#{escaped_file_name}" do |channel| 33 | channel.sends_ok 34 | channel.gets_data "C0666 0 #{awful_file_name}\n" 35 | channel.sends_ok 36 | channel.gets_ok 37 | channel.sends_ok 38 | end 39 | 40 | assert_scripted { scp.download!("/path/to/remote/#{awful_file_name}", "/path/to/local/#{awful_file_name}") } 41 | end 42 | 43 | def test_download_with_preserve_should_send_times 44 | file = prepare_file("/path/to/local.txt", "a" * 1234, 0644, Time.at(1234567890, 123456), Time.at(12121212, 232323)) 45 | 46 | expect_scp_session "-f -p /path/to/remote.txt" do |channel| 47 | channel.sends_ok 48 | channel.gets_data "T1234567890 123456 12121212 232323\n" 49 | simple_download(channel, 0644) 50 | end 51 | 52 | File.expects(:utime).with(Time.at(12121212, 232323), Time.at(1234567890, 123456), "/path/to/local.txt") 53 | assert_scripted { scp.download!("/path/to/remote.txt", "/path/to/local.txt", :preserve => true) } 54 | assert_equal "a" * 1234, file.io.string 55 | end 56 | 57 | def test_download_with_error_should_respond_with_error_text 58 | story do |session| 59 | channel = session.opens_channel 60 | channel.sends_exec "scp -f /path/to/remote.txt" 61 | 62 | channel.sends_ok 63 | channel.gets_data "\x01File not found: /path/to/remote.txt\n" 64 | channel.sends_ok 65 | 66 | channel.gets_eof 67 | channel.gets_exit_status(1) 68 | channel.gets_close 69 | channel.sends_close 70 | end 71 | 72 | error = nil 73 | Net::SSH::Test::Extensions::IO.with_test_extension do 74 | begin 75 | scp.download!("/path/to/remote.txt") 76 | rescue 77 | error = $! 78 | end 79 | end 80 | assert_equal Net::SCP::Error, error.class 81 | assert_equal "SCP did not finish successfully (1): File not found: /path/to/remote.txt\n", error.message 82 | end 83 | 84 | def test_download_with_progress_callback_should_invoke_callback 85 | prepare_file("/path/to/local.txt", "a" * 3000 + "b" * 3000 + "c" * 3000 + "d" * 3000) 86 | 87 | expect_scp_session "-f /path/to/remote.txt" do |channel| 88 | channel.sends_ok 89 | channel.gets_data "C0666 12000 remote.txt\n" 90 | channel.sends_ok 91 | channel.gets_data "a" * 3000 92 | channel.inject_remote_delay! 93 | channel.gets_data "b" * 3000 94 | channel.inject_remote_delay! 95 | channel.gets_data "c" * 3000 96 | channel.inject_remote_delay! 97 | channel.gets_data "d" * 3000 98 | channel.gets_ok 99 | channel.sends_ok 100 | end 101 | 102 | calls = [] 103 | progress = Proc.new { |ch, *args| calls << args } 104 | 105 | assert_scripted do 106 | scp.download!("/path/to/remote.txt", "/path/to/local.txt", &progress) 107 | end 108 | 109 | assert_equal ["/path/to/local.txt", 0, 12000], calls.shift 110 | assert_equal ["/path/to/local.txt", 3000, 12000], calls.shift 111 | assert_equal ["/path/to/local.txt", 6000, 12000], calls.shift 112 | assert_equal ["/path/to/local.txt", 9000, 12000], calls.shift 113 | assert_equal ["/path/to/local.txt", 12000, 12000], calls.shift 114 | assert calls.empty? 115 | end 116 | 117 | def test_download_io_with_recursive_should_raise_error 118 | expect_scp_session "-f -r /path/to/remote.txt" 119 | Net::SSH::Test::Extensions::IO.with_test_extension do 120 | assert_raises(Net::SCP::Error) { scp.download!("/path/to/remote.txt", StringIO.new, :recursive => true) } 121 | end 122 | end 123 | 124 | def test_download_io_with_preserve_should_ignore_preserve 125 | expect_scp_session "-f -p /path/to/remote.txt" do |channel| 126 | simple_download(channel) 127 | end 128 | 129 | io = StringIO.new 130 | assert_scripted { scp.download!("/path/to/remote.txt", io, :preserve => true) } 131 | assert_equal "a" * 1234, io.string 132 | end 133 | 134 | def test_download_io_should_transfer_data 135 | expect_scp_session "-f /path/to/remote.txt" do |channel| 136 | simple_download(channel) 137 | end 138 | 139 | io = StringIO.new 140 | assert_scripted { scp.download!("/path/to/remote.txt", io) } 141 | assert_equal "a" * 1234, io.string 142 | end 143 | 144 | def test_download_bang_without_target_should_return_string 145 | expect_scp_session "-f /path/to/remote.txt" do |channel| 146 | simple_download(channel) 147 | end 148 | 149 | assert_scripted do 150 | assert_equal "a" * 1234, scp.download!("/path/to/remote.txt") 151 | end 152 | end 153 | 154 | def test_download_directory_without_recursive_should_raise_error 155 | expect_scp_session "-f /path/to/remote" do |channel| 156 | channel.sends_ok 157 | channel.gets_data "D0755 0 remote\n" 158 | end 159 | 160 | Net::SSH::Test::Extensions::IO.with_test_extension do 161 | assert_raises(Net::SCP::Error) { scp.download!("/path/to/remote") } 162 | end 163 | end 164 | 165 | def test_download_should_raise_error_if_gets_not_ok 166 | prepare_file("/path/to/local.txt", "") 167 | 168 | expect_scp_session "-f /path/to/remote.txt" do |channel| 169 | channel.sends_ok 170 | channel.gets_data "C0666 0 remote.txt\n" 171 | channel.sends_ok 172 | channel.gets_data "\1" 173 | end 174 | 175 | Net::SSH::Test::Extensions::IO.with_test_extension do 176 | e = assert_raises(Net::SCP::Error) { scp.download!("/path/to/remote.txt", "/path/to/local.txt") } 177 | assert_equal("\1", e.message) 178 | end 179 | end 180 | 181 | def test_download_directory_should_raise_error_if_local_exists_and_is_not_directory 182 | File.stubs(:exist?).with("/path/to/local").returns(true) 183 | File.stubs(:exist?).with("/path/to/local/remote").returns(true) 184 | File.stubs(:directory?).with("/path/to/local/remote").returns(false) 185 | 186 | expect_scp_session "-f -r /path/to/remote" do |channel| 187 | channel.sends_ok 188 | channel.gets_data "D0755 0 remote\n" 189 | channel.sends_ok 190 | channel.gets_data "E\n" 191 | channel.sends_ok 192 | end 193 | 194 | Net::SSH::Test::Extensions::IO.with_test_extension do 195 | e = assert_raises(Net::SCP::Error) { scp.download!("/path/to/remote", "/path/to/local", :recursive => true) } 196 | assert_match(/exists and is not a directory/, e.message) 197 | end 198 | end 199 | 200 | def test_download_directory_should_create_directory_and_files_locally 201 | file = nil 202 | prepare_directory "/path/to/local" do |dir| 203 | dir.directory "remote" do |dir2| 204 | dir2.directory "sub" do |dir3| 205 | file = dir3.file "remote.txt", "" 206 | end 207 | end 208 | end 209 | 210 | expect_scp_session "-f -r /path/to/remote" do |channel| 211 | channel.sends_ok 212 | channel.gets_data "D0755 0 remote\n" 213 | channel.sends_ok 214 | channel.gets_data "D0755 0 sub\n" 215 | simple_download(channel) 216 | channel.gets_data "E\n" 217 | channel.sends_ok 218 | channel.gets_data "E\n" 219 | channel.sends_ok 220 | end 221 | 222 | Net::SSH::Test::Extensions::IO.with_test_extension do 223 | scp.download!("/path/to/remote", "/path/to/local", :recursive => true, :ssh => { :verbose => :debug }) 224 | end 225 | assert_equal "a" * 1234, file.io.string 226 | end 227 | 228 | def test_download_should_work_when_remote_closes_channel_without_exit_status 229 | file = prepare_file('/path/to/local.txt', 'a' * 1234) 230 | 231 | story do |session| 232 | channel = session.opens_channel 233 | channel.sends_exec 'scp -f /path/to/remote.txt' 234 | simple_download(channel) 235 | # Remote closes without sending an exit-status 236 | channel.gets_close 237 | # We just send eof and close the channel 238 | channel.sends_eof 239 | channel.sends_close 240 | end 241 | 242 | assert_scripted { scp.download!('/path/to/remote.txt', '/path/to/local.txt') } 243 | assert_equal 'a' * 1234, file.io.string 244 | end 245 | 246 | def test_download_should_raise_error_when_remote_closes_channel_before_end 247 | prepare_file('/path/to/local.txt', 'a' * 1234) 248 | 249 | story do |session| 250 | channel = session.opens_channel 251 | channel.sends_exec 'scp -f /path/to/remote.txt' 252 | channel.sends_ok 253 | channel.gets_data "C0666 1234 remote.txt\n" 254 | channel.sends_ok 255 | channel.gets_data 'a' * 500 256 | # We should have received 1234 bytes and \0 but remote closed before the end 257 | channel.gets_close 258 | # We just send eof and close the channel 259 | channel.sends_eof 260 | channel.sends_close 261 | end 262 | 263 | error = nil 264 | begin 265 | assert_scripted { scp.download!('/path/to/remote.txt', '/path/to/local.txt') } 266 | rescue => e 267 | error = e 268 | end 269 | 270 | assert_equal Net::SCP::Error, error.class 271 | assert_equal 'SCP did not finish successfully (channel closed before end of transmission)', error.message 272 | end 273 | 274 | private 275 | 276 | def simple_download(channel, mode=0666) 277 | channel.sends_ok 278 | channel.gets_data "C%04o 1234 remote.txt\n" % mode 279 | channel.sends_ok 280 | channel.gets_data "a" * 1234 281 | channel.gets_ok 282 | channel.sends_ok 283 | end 284 | end 285 | -------------------------------------------------------------------------------- /test/test_scp.rb: -------------------------------------------------------------------------------- 1 | require 'common' 2 | 3 | class TestSCP < Net::SCP::TestCase 4 | def test_start_without_block_should_return_scp_instance 5 | ssh = stub('session', :logger => nil) 6 | Net::SSH.expects(:start). 7 | with("remote.host", "username", { :password => "foo" }). 8 | returns(ssh) 9 | 10 | ssh.expects(:close).never 11 | scp = Net::SCP.start("remote.host", "username", { :password => "foo" }) 12 | assert_instance_of Net::SCP, scp 13 | assert_equal ssh, scp.session 14 | end 15 | 16 | def test_start_with_block_should_yield_scp_and_close_ssh_session 17 | ssh = stub('session', :logger => nil) 18 | Net::SSH.expects(:start). 19 | with("remote.host", "username", { :password => "foo" }). 20 | returns(ssh) 21 | 22 | ssh.expects(:loop) 23 | ssh.expects(:close) 24 | 25 | yielded = false 26 | Net::SCP.start("remote.host", "username", { :password => "foo" }) do |scp| 27 | yielded = true 28 | assert_instance_of Net::SCP, scp 29 | assert_equal ssh, scp.session 30 | end 31 | 32 | assert yielded 33 | end 34 | 35 | def test_self_upload_should_instatiate_scp_and_invoke_synchronous_upload 36 | scp = stub('scp') 37 | scp.expects(:upload!).with("/path/to/local", "/path/to/remote", { :recursive => true }) 38 | 39 | Net::SCP.expects(:start). 40 | with("remote.host", "username", { :password => "foo" }). 41 | yields(scp) 42 | 43 | Net::SCP.upload!("remote.host", "username", "/path/to/local", "/path/to/remote", 44 | { :ssh => { :password => "foo" }, :recursive => true }) 45 | end 46 | 47 | def test_self_download_should_instatiate_scp_and_invoke_synchronous_download 48 | scp = stub('scp') 49 | scp.expects(:download!).with("/path/to/remote", "/path/to/local", { :recursive => true }).returns(:result) 50 | 51 | Net::SCP.expects(:start). 52 | with("remote.host", "username", { :password => "foo" }). 53 | yields(scp) 54 | 55 | result = Net::SCP.download!("remote.host", "username", "/path/to/remote", "/path/to/local", 56 | { :ssh => { :password => "foo" }, :recursive => true }) 57 | 58 | assert_equal :result, result 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /test/test_upload.rb: -------------------------------------------------------------------------------- 1 | require 'common' 2 | 3 | class TestUpload < Net::SCP::TestCase 4 | def test_upload_file_should_transfer_file 5 | prepare_file("/path/to/local.txt", "a" * 1234) 6 | 7 | expect_scp_session "-t /path/to/remote.txt" do |channel| 8 | channel.gets_ok 9 | channel.sends_data "C0666 1234 local.txt\n" 10 | channel.gets_ok 11 | channel.sends_data "a" * 1234 12 | channel.sends_ok 13 | channel.gets_ok 14 | end 15 | 16 | assert_scripted { scp.upload!("/path/to/local.txt", "/path/to/remote.txt") } 17 | end 18 | 19 | def test_upload_file_with_spaces_in_name_should_escape_remote_file_name 20 | prepare_file("/path/to/local file.txt", "") 21 | 22 | expect_scp_session "-t /path/to/remote\\ file.txt" do |channel| 23 | channel.gets_ok 24 | channel.sends_data "C0666 0 local file.txt\n" 25 | channel.gets_ok 26 | channel.sends_ok 27 | channel.gets_ok 28 | end 29 | 30 | assert_scripted { scp.upload!("/path/to/local file.txt", "/path/to/remote file.txt") } 31 | end 32 | 33 | def test_upload_file_with_metacharacters_in_name_should_escape_remote_file_name 34 | prepare_file("/path/to/local/#{awful_file_name}", "") 35 | 36 | expect_scp_session "-t /path/to/remote/#{escaped_file_name}" do |channel| 37 | channel.gets_ok 38 | channel.sends_data "C0666 0 #{awful_file_name}\n" 39 | channel.gets_ok 40 | channel.sends_ok 41 | channel.gets_ok 42 | end 43 | 44 | assert_scripted { scp.upload!("/path/to/local/#{awful_file_name}", "/path/to/remote/#{awful_file_name}") } 45 | end 46 | 47 | def test_upload_file_with_preserve_should_send_times 48 | prepare_file("/path/to/local.txt", "a" * 1234, 0666, Time.at(1234567890, 123456), Time.at(1234543210, 345678)) 49 | 50 | expect_scp_session "-t -p /path/to/remote.txt" do |channel| 51 | channel.gets_ok 52 | channel.sends_data "T1234567890 123456 1234543210 345678\n" 53 | channel.gets_ok 54 | channel.sends_data "C0666 1234 local.txt\n" 55 | channel.gets_ok 56 | channel.sends_data "a" * 1234 57 | channel.sends_ok 58 | channel.gets_ok 59 | end 60 | 61 | assert_scripted { scp.upload!("/path/to/local.txt", "/path/to/remote.txt", :preserve => true) } 62 | end 63 | 64 | def test_upload_file_with_progress_callback_should_invoke_callback 65 | prepare_file("/path/to/local.txt", "a" * 3000 + "b" * 3000 + "c" * 3000 + "d" * 3000) 66 | 67 | expect_scp_session "-t /path/to/remote.txt" do |channel| 68 | channel.gets_ok 69 | channel.sends_data "C0666 12000 local.txt\n" 70 | channel.gets_ok 71 | channel.sends_data "a" * 3000 72 | channel.sends_data "b" * 3000 73 | channel.sends_data "c" * 3000 74 | channel.sends_data "d" * 3000 75 | channel.sends_ok 76 | channel.gets_ok 77 | end 78 | 79 | calls = [] 80 | progress = Proc.new do |ch, name, sent, total| 81 | calls << [name, sent, total] 82 | end 83 | 84 | assert_scripted do 85 | scp.upload!("/path/to/local.txt", "/path/to/remote.txt", :chunk_size => 3000, &progress) 86 | end 87 | 88 | assert_equal ["/path/to/local.txt", 0, 12000], calls.shift 89 | assert_equal ["/path/to/local.txt", 3000, 12000], calls.shift 90 | assert_equal ["/path/to/local.txt", 6000, 12000], calls.shift 91 | assert_equal ["/path/to/local.txt", 9000, 12000], calls.shift 92 | assert_equal ["/path/to/local.txt", 12000, 12000], calls.shift 93 | assert calls.empty? 94 | end 95 | 96 | def test_upload_io_with_recursive_should_ignore_recursive 97 | expect_scp_session "-t -r /path/to/remote.txt" do |channel| 98 | channel.gets_ok 99 | channel.sends_data "C0640 1234 remote.txt\n" 100 | channel.gets_ok 101 | channel.sends_data "a" * 1234 102 | channel.sends_ok 103 | channel.gets_ok 104 | end 105 | 106 | io = StringIO.new("a" * 1234) 107 | assert_scripted { scp.upload!(io, "/path/to/remote.txt", :recursive => true) } 108 | end 109 | 110 | def test_upload_io_with_preserve_should_ignore_preserve 111 | expect_scp_session "-t -p /path/to/remote.txt" do |channel| 112 | channel.gets_ok 113 | channel.sends_data "C0640 1234 remote.txt\n" 114 | channel.gets_ok 115 | channel.sends_data "a" * 1234 116 | channel.sends_ok 117 | channel.gets_ok 118 | end 119 | 120 | io = StringIO.new("a" * 1234) 121 | assert_scripted { scp.upload!(io, "/path/to/remote.txt", :preserve => true) } 122 | end 123 | 124 | def test_upload_io_should_transfer_data 125 | expect_scp_session "-t /path/to/remote.txt" do |channel| 126 | channel.gets_ok 127 | channel.sends_data "C0640 1234 remote.txt\n" 128 | channel.gets_ok 129 | channel.sends_data "a" * 1234 130 | channel.sends_ok 131 | channel.gets_ok 132 | end 133 | 134 | io = StringIO.new("a" * 1234) 135 | assert_scripted { scp.upload!(io, "/path/to/remote.txt") } 136 | end 137 | 138 | def test_upload_io_with_mode_should_honor_mode_as_permissions 139 | expect_scp_session "-t /path/to/remote.txt" do |channel| 140 | channel.gets_ok 141 | channel.sends_data "C0666 1234 remote.txt\n" 142 | channel.gets_ok 143 | channel.sends_data "a" * 1234 144 | channel.sends_ok 145 | channel.gets_ok 146 | end 147 | 148 | io = StringIO.new("a" * 1234) 149 | assert_scripted { scp.upload!(io, "/path/to/remote.txt", :mode => 0666) } 150 | end 151 | 152 | def test_upload_directory_without_recursive_should_error 153 | prepare_directory("/path/to/local") 154 | 155 | expect_scp_session("-t /path/to/remote") do |channel| 156 | channel.gets_ok 157 | end 158 | 159 | Net::SSH::Test::Extensions::IO.with_test_extension do 160 | assert_raises(Net::SCP::Error) { scp.upload!("/path/to/local", "/path/to/remote") } 161 | end 162 | end 163 | 164 | def test_upload_empty_directory_should_create_directory_and_finish 165 | prepare_directory("/path/to/local") 166 | 167 | expect_scp_session("-t -r /path/to/remote") do |channel| 168 | channel.gets_ok 169 | channel.sends_data "D0777 0 local\n" 170 | channel.gets_ok 171 | channel.sends_data "E\n" 172 | channel.gets_ok 173 | end 174 | 175 | assert_scripted { scp.upload!("/path/to/local", "/path/to/remote", :recursive => true) } 176 | end 177 | 178 | def test_upload_directory_should_recursively_create_and_upload_items 179 | prepare_directory("/path/to/local") do |d| 180 | d.file "hello.txt", "hello world\n" 181 | d.directory "others" do |d2| 182 | d2.file "data.dat", "abcdefghijklmnopqrstuvwxyz" 183 | end 184 | d.file "zoo.doc", "going to the zoo\n" 185 | end 186 | 187 | expect_scp_session("-t -r /path/to/remote") do |channel| 188 | channel.gets_ok 189 | channel.sends_data "D0777 0 local\n" 190 | channel.gets_ok 191 | channel.sends_data "C0666 12 hello.txt\n" 192 | channel.gets_ok 193 | channel.sends_data "hello world\n" 194 | channel.sends_ok 195 | channel.gets_ok 196 | channel.sends_data "D0777 0 others\n" 197 | channel.gets_ok 198 | channel.sends_data "C0666 26 data.dat\n" 199 | channel.gets_ok 200 | channel.sends_data "abcdefghijklmnopqrstuvwxyz" 201 | channel.sends_ok 202 | channel.gets_ok 203 | channel.sends_data "E\n" 204 | channel.gets_ok 205 | channel.sends_data "C0666 17 zoo.doc\n" 206 | channel.gets_ok 207 | channel.sends_data "going to the zoo\n" 208 | channel.sends_ok 209 | channel.gets_ok 210 | channel.sends_data "E\n" 211 | channel.gets_ok 212 | end 213 | 214 | assert_scripted { scp.upload!("/path/to/local", "/path/to/remote", :recursive => true) } 215 | end 216 | 217 | def test_upload_directory_with_preserve_should_send_times_for_all_items 218 | prepare_directory("/path/to/local", 0755, Time.at(17171717, 191919), Time.at(18181818, 101010)) do |d| 219 | d.file "hello.txt", "hello world\n", 0640, Time.at(12345, 67890), Time.at(234567, 890) 220 | d.directory "others", 0770, Time.at(112233, 4455), Time.at(22334455, 667788) do |d2| 221 | d2.file "data.dat", "abcdefghijklmnopqrstuvwxyz", 0600, Time.at(13579135, 13131), Time.at(7654321, 654321) 222 | end 223 | d.file "zoo.doc", "going to the zoo\n", 0444, Time.at(12121212, 131313), Time.at(23232323, 242424) 224 | end 225 | 226 | expect_scp_session("-t -r -p /path/to/remote") do |channel| 227 | channel.gets_ok 228 | channel.sends_data "T17171717 191919 18181818 101010\n" 229 | channel.gets_ok 230 | channel.sends_data "D0755 0 local\n" 231 | channel.gets_ok 232 | channel.sends_data "T12345 67890 234567 890\n" 233 | channel.gets_ok 234 | channel.sends_data "C0640 12 hello.txt\n" 235 | channel.gets_ok 236 | channel.sends_data "hello world\n" 237 | channel.sends_ok 238 | channel.gets_ok 239 | channel.sends_data "T112233 4455 22334455 667788\n" 240 | channel.gets_ok 241 | channel.sends_data "D0770 0 others\n" 242 | channel.gets_ok 243 | channel.sends_data "T13579135 13131 7654321 654321\n" 244 | channel.gets_ok 245 | channel.sends_data "C0600 26 data.dat\n" 246 | channel.gets_ok 247 | channel.sends_data "abcdefghijklmnopqrstuvwxyz" 248 | channel.sends_ok 249 | channel.gets_ok 250 | channel.sends_data "E\n" 251 | channel.gets_ok 252 | channel.sends_data "T12121212 131313 23232323 242424\n" 253 | channel.gets_ok 254 | channel.sends_data "C0444 17 zoo.doc\n" 255 | channel.gets_ok 256 | channel.sends_data "going to the zoo\n" 257 | channel.sends_ok 258 | channel.gets_ok 259 | channel.sends_data "E\n" 260 | channel.gets_ok 261 | end 262 | 263 | assert_scripted { scp.upload!("/path/to/local", "/path/to/remote", :preserve => true, :recursive => true) } 264 | end 265 | 266 | def test_upload_should_not_block 267 | prepare_file("/path/to/local.txt", "data") 268 | story { |s| s.opens_channel(false) } 269 | assert_scripted { scp.upload("/path/to/local.txt", "/path/to/remote.txt") } 270 | end 271 | 272 | def test_upload_should_raise_error_if_gets_not_ok 273 | prepare_file("/path/to/local.txt", "") 274 | 275 | expect_scp_session "-t /path/to/remote.txt" do |channel| 276 | channel.gets_data "\1" 277 | end 278 | 279 | Net::SSH::Test::Extensions::IO.with_test_extension do 280 | e = assert_raises(Net::SCP::Error) { scp.upload!("/path/to/local.txt", "/path/to/remote.txt") } 281 | assert_equal("\1", e.message) 282 | end 283 | end 284 | end 285 | --------------------------------------------------------------------------------