| 1 | | require 'net/sftp' |
|---|
| 2 | | require 'net/sftp/operations/errors' |
|---|
| 3 | | require 'capistrano/errors' |
|---|
| 4 | | |
|---|
| 5 | | module Capistrano |
|---|
| 6 | | unless ENV['SKIP_VERSION_CHECK'] |
|---|
| 7 | | require 'capistrano/version' |
|---|
| 8 | | require 'net/sftp/version' |
|---|
| 9 | | sftp_version = [Net::SFTP::Version::MAJOR, Net::SFTP::Version::MINOR, Net::SFTP::Version::TINY] |
|---|
| 10 | | required_version = [1,1,0] |
|---|
| 11 | | if !Capistrano::Version.check(required_version, sftp_version) |
|---|
| 12 | | raise "You have Net::SFTP #{sftp_version.join(".")}, but you need at least #{required_version.join(".")}. Net::SFTP will not be used." |
|---|
| 13 | | end |
|---|
| 14 | | end |
|---|
| 15 | | |
|---|
| 16 | | # This class encapsulates a single file upload to be performed in parallel |
|---|
| 17 | | # across multiple machines, using the SFTP protocol. Although it is intended |
|---|
| 18 | | # to be used primarily from within Capistrano, it may also be used standalone |
|---|
| 19 | | # if you need to simply upload a file to multiple servers. |
|---|
| 20 | | # |
|---|
| 21 | | # Basic Usage: |
|---|
| 22 | | # |
|---|
| 23 | | # begin |
|---|
| 24 | | # uploader = Capistrano::Upload.new(sessions, "remote-file.txt", |
|---|
| 25 | | # :data => "the contents of the file to upload") |
|---|
| 26 | | # uploader.process! |
|---|
| 27 | | # rescue Capistrano::UploadError => e |
|---|
| 28 | | # warn "Could not upload the file: #{e.message}" |
|---|
| 29 | | # end |
|---|
| 30 | | class Upload |
|---|
| 31 | | def self.process(sessions, filename, options) |
|---|
| 32 | | new(sessions, filename, options).process! |
|---|
| 33 | | end |
|---|
| 34 | | |
|---|
| 35 | | attr_reader :sessions, :filename, :options |
|---|
| 36 | | attr_reader :failed, :completed |
|---|
| 37 | | |
|---|
| 38 | | # Creates and prepares a new Upload instance. The +sessions+ parameter |
|---|
| 39 | | # must be an array of open Net::SSH sessions. The +filename+ is the name |
|---|
| 40 | | # (including path) of the destination file on the remote server. The |
|---|
| 41 | | # +options+ hash accepts the following keys (as symbols): |
|---|
| 42 | | # |
|---|
| 43 | | # * data: required. Should refer to a String containing the contents of |
|---|
| 44 | | # the file to upload. |
|---|
| 45 | | # * mode: optional. The "mode" of the destination file. Defaults to 0660. |
|---|
| 46 | | # * logger: optional. Should point to a Capistrano::Logger instance, if |
|---|
| 47 | | # given. |
|---|
| 48 | | def initialize(sessions, filename, options) |
|---|
| 49 | | raise ArgumentError, "you must specify the data to upload via the :data option" unless options[:data] |
|---|
| 50 | | |
|---|
| 51 | | @sessions = sessions |
|---|
| 52 | | @filename = filename |
|---|
| 53 | | @options = options |
|---|
| 54 | | |
|---|
| 55 | | @completed = @failed = 0 |
|---|
| 56 | | @sftps = setup_sftp |
|---|
| 57 | | end |
|---|
| 58 | | |
|---|
| 59 | | # Uploads to all specified servers in parallel. If any one of the servers |
|---|
| 60 | | # fails, an exception will be raised (UploadError). |
|---|
| 61 | | def process! |
|---|
| 62 | | logger.debug "uploading #{filename}" if logger |
|---|
| 63 | | while running? |
|---|
| 64 | | @sftps.each do |sftp| |
|---|
| 65 | | next if sftp.channel[:done] |
|---|
| 66 | | begin |
|---|
| 67 | | sftp.channel.connection.process(true) |
|---|
| 68 | | rescue Net::SFTP::Operations::StatusException => error |
|---|
| 69 | | logger.important "uploading failed: #{error.description}", sftp.channel[:server] if logger |
|---|
| 70 | | failed!(sftp) |
|---|
| 71 | | end |
|---|
| 72 | | end |
|---|
| 73 | | sleep 0.01 # a brief respite, to keep the CPU from going crazy |
|---|
| 74 | | end |
|---|
| 75 | | logger.trace "upload finished" if logger |
|---|
| 76 | | |
|---|
| 77 | | if (failed = @sftps.select { |sftp| sftp.channel[:failed] }).any? |
|---|
| 78 | | hosts = failed.map { |sftp| sftp.channel[:server] } |
|---|
| 79 | | error = UploadError.new("upload of #{filename} failed on #{hosts.join(',')}") |
|---|
| 80 | | error.hosts = hosts |
|---|
| 81 | | raise error |
|---|
| 82 | | end |
|---|
| 83 | | |
|---|
| 84 | | self |
|---|
| 85 | | end |
|---|
| 86 | | |
|---|
| 87 | | private |
|---|
| 88 | | |
|---|
| 89 | | def logger |
|---|
| 90 | | options[:logger] |
|---|
| 91 | | end |
|---|
| 92 | | |
|---|
| 93 | | def setup_sftp |
|---|
| 94 | | sessions.map do |session| |
|---|
| 95 | | server = session.xserver |
|---|
| 96 | | sftp = session.sftp |
|---|
| 97 | | sftp.connect unless sftp.state == :open |
|---|
| 98 | | |
|---|
| 99 | | sftp.channel[:server] = server |
|---|
| 100 | | sftp.channel[:done] = false |
|---|
| 101 | | sftp.channel[:failed] = false |
|---|
| 102 | | |
|---|
| 103 | | real_filename = filename.gsub(/\$CAPISTRANO:HOST\$/, server.host) |
|---|
| 104 | | sftp.open(real_filename, IO::WRONLY | IO::CREAT | IO::TRUNC, options[:mode] || 0660) do |status, handle| |
|---|
| 105 | | break unless check_status(sftp, "open #{real_filename}", server, status) |
|---|
| 106 | | |
|---|
| 107 | | logger.info "uploading data to #{server}:#{real_filename}" if logger |
|---|
| 108 | | sftp.write(handle, options[:data] || "") do |status| |
|---|
| 109 | | break unless check_status(sftp, "write to #{server}:#{real_filename}", server, status) |
|---|
| 110 | | sftp.close_handle(handle) do |
|---|
| 111 | | logger.debug "done uploading data to #{server}:#{real_filename}" if logger |
|---|
| 112 | | completed!(sftp) |
|---|
| 113 | | end |
|---|
| 114 | | end |
|---|
| 115 | | end |
|---|
| 116 | | |
|---|
| 117 | | sftp |
|---|
| 118 | | end |
|---|
| 119 | | end |
|---|
| 120 | | |
|---|
| 121 | | def check_status(sftp, action, server, status) |
|---|
| 122 | | return true if status.code == Net::SFTP::Session::FX_OK |
|---|
| 123 | | |
|---|
| 124 | | logger.error "could not #{action} on #{server} (#{status.message})" if logger |
|---|
| 125 | | failed!(sftp) |
|---|
| 126 | | |
|---|
| 127 | | return false |
|---|
| 128 | | end |
|---|
| 129 | | |
|---|
| 130 | | def running? |
|---|
| 131 | | completed < @sftps.length |
|---|
| 132 | | end |
|---|
| 133 | | |
|---|
| 134 | | def failed!(sftp) |
|---|
| 135 | | completed!(sftp) |
|---|
| 136 | | @failed += 1 |
|---|
| 137 | | sftp.channel[:failed] = true |
|---|
| 138 | | end |
|---|
| 139 | | |
|---|
| 140 | | def completed!(sftp) |
|---|
| 141 | | @completed += 1 |
|---|
| 142 | | sftp.channel[:done] = true |
|---|
| 143 | | end |
|---|
| 144 | | end |
|---|
| 145 | | |
|---|
| 146 | | end |
|---|
| | 1 | # require 'net/sftp' |
|---|
| | 2 | # require 'net/sftp/operations/errors' |
|---|
| | 3 | # require 'capistrano/errors' |
|---|
| | 4 | # |
|---|
| | 5 | # module Capistrano |
|---|
| | 6 | # unless ENV['SKIP_VERSION_CHECK'] |
|---|
| | 7 | # require 'capistrano/version' |
|---|
| | 8 | # require 'net/sftp/version' |
|---|
| | 9 | # sftp_version = [Net::SFTP::Version::MAJOR, Net::SFTP::Version::MINOR, Net::SFTP::Version::TINY] |
|---|
| | 10 | # required_version = [1,1,0] |
|---|
| | 11 | # if !Capistrano::Version.check(required_version, sftp_version) |
|---|
| | 12 | # raise "You have Net::SFTP #{sftp_version.join(".")}, but you need at least #{required_version.join(".")}. Net::SFTP will not be used." |
|---|
| | 13 | # end |
|---|
| | 14 | # end |
|---|
| | 15 | # |
|---|
| | 16 | # # This class encapsulates a single file upload to be performed in parallel |
|---|
| | 17 | # # across multiple machines, using the SFTP protocol. Although it is intended |
|---|
| | 18 | # # to be used primarily from within Capistrano, it may also be used standalone |
|---|
| | 19 | # # if you need to simply upload a file to multiple servers. |
|---|
| | 20 | # # |
|---|
| | 21 | # # Basic Usage: |
|---|
| | 22 | # # |
|---|
| | 23 | # # begin |
|---|
| | 24 | # # uploader = Capistrano::Upload.new(sessions, "remote-file.txt", |
|---|
| | 25 | # # :data => "the contents of the file to upload") |
|---|
| | 26 | # # uploader.process! |
|---|
| | 27 | # # rescue Capistrano::UploadError => e |
|---|
| | 28 | # # warn "Could not upload the file: #{e.message}" |
|---|
| | 29 | # # end |
|---|
| | 30 | # class Upload |
|---|
| | 31 | # def self.process(sessions, filename, options) |
|---|
| | 32 | # new(sessions, filename, options).process! |
|---|
| | 33 | # end |
|---|
| | 34 | # |
|---|
| | 35 | # attr_reader :sessions, :filename, :options |
|---|
| | 36 | # attr_reader :failed, :completed |
|---|
| | 37 | # |
|---|
| | 38 | # # Creates and prepares a new Upload instance. The +sessions+ parameter |
|---|
| | 39 | # # must be an array of open Net::SSH sessions. The +filename+ is the name |
|---|
| | 40 | # # (including path) of the destination file on the remote server. The |
|---|
| | 41 | # # +options+ hash accepts the following keys (as symbols): |
|---|
| | 42 | # # |
|---|
| | 43 | # # * data: required. Should refer to a String containing the contents of |
|---|
| | 44 | # # the file to upload. |
|---|
| | 45 | # # * mode: optional. The "mode" of the destination file. Defaults to 0660. |
|---|
| | 46 | # # * logger: optional. Should point to a Capistrano::Logger instance, if |
|---|
| | 47 | # # given. |
|---|
| | 48 | # def initialize(sessions, filename, options) |
|---|
| | 49 | # raise ArgumentError, "you must specify the data to upload via the :data option" unless options[:data] |
|---|
| | 50 | # |
|---|
| | 51 | # @sessions = sessions |
|---|
| | 52 | # @filename = filename |
|---|
| | 53 | # @options = options |
|---|
| | 54 | # |
|---|
| | 55 | # @completed = @failed = 0 |
|---|
| | 56 | # @sftps = setup_sftp |
|---|
| | 57 | # end |
|---|
| | 58 | # |
|---|
| | 59 | # # Uploads to all specified servers in parallel. If any one of the servers |
|---|
| | 60 | # # fails, an exception will be raised (UploadError). |
|---|
| | 61 | # def process! |
|---|
| | 62 | # logger.debug "uploading #{filename}" if logger |
|---|
| | 63 | # while running? |
|---|
| | 64 | # @sftps.each do |sftp| |
|---|
| | 65 | # next if sftp.channel[:done] |
|---|
| | 66 | # begin |
|---|
| | 67 | # sftp.channel.connection.process(true) |
|---|
| | 68 | # rescue Net::SFTP::Operations::StatusException => error |
|---|
| | 69 | # logger.important "uploading failed: #{error.description}", sftp.channel[:server] if logger |
|---|
| | 70 | # failed!(sftp) |
|---|
| | 71 | # end |
|---|
| | 72 | # end |
|---|
| | 73 | # sleep 0.01 # a brief respite, to keep the CPU from going crazy |
|---|
| | 74 | # end |
|---|
| | 75 | # logger.trace "upload finished" if logger |
|---|
| | 76 | # |
|---|
| | 77 | # if (failed = @sftps.select { |sftp| sftp.channel[:failed] }).any? |
|---|
| | 78 | # hosts = failed.map { |sftp| sftp.channel[:server] } |
|---|
| | 79 | # error = UploadError.new("upload of #{filename} failed on #{hosts.join(',')}") |
|---|
| | 80 | # error.hosts = hosts |
|---|
| | 81 | # raise error |
|---|
| | 82 | # end |
|---|
| | 83 | # |
|---|
| | 84 | # self |
|---|
| | 85 | # end |
|---|
| | 86 | # |
|---|
| | 87 | # private |
|---|
| | 88 | # |
|---|
| | 89 | # def logger |
|---|
| | 90 | # options[:logger] |
|---|
| | 91 | # end |
|---|
| | 92 | # |
|---|
| | 93 | # def setup_sftp |
|---|
| | 94 | # sessions.map do |session| |
|---|
| | 95 | # server = session.xserver |
|---|
| | 96 | # sftp = session.sftp |
|---|
| | 97 | # sftp.connect unless sftp.state == :open |
|---|
| | 98 | # |
|---|
| | 99 | # sftp.channel[:server] = server |
|---|
| | 100 | # sftp.channel[:done] = false |
|---|
| | 101 | # sftp.channel[:failed] = false |
|---|
| | 102 | # |
|---|
| | 103 | # real_filename = filename.gsub(/\$CAPISTRANO:HOST\$/, server.host) |
|---|
| | 104 | # sftp.open(real_filename, IO::WRONLY | IO::CREAT | IO::TRUNC, options[:mode] || 0660) do |status, handle| |
|---|
| | 105 | # break unless check_status(sftp, "open #{real_filename}", server, status) |
|---|
| | 106 | # |
|---|
| | 107 | # logger.info "uploading data to #{server}:#{real_filename}" if logger |
|---|
| | 108 | # sftp.write(handle, options[:data] || "") do |status| |
|---|
| | 109 | # break unless check_status(sftp, "write to #{server}:#{real_filename}", server, status) |
|---|
| | 110 | # sftp.close_handle(handle) do |
|---|
| | 111 | # logger.debug "done uploading data to #{server}:#{real_filename}" if logger |
|---|
| | 112 | # completed!(sftp) |
|---|
| | 113 | # end |
|---|
| | 114 | # end |
|---|
| | 115 | # end |
|---|
| | 116 | # |
|---|
| | 117 | # sftp |
|---|
| | 118 | # end |
|---|
| | 119 | # end |
|---|
| | 120 | # |
|---|
| | 121 | # def check_status(sftp, action, server, status) |
|---|
| | 122 | # return true if status.code == Net::SFTP::Session::FX_OK |
|---|
| | 123 | # |
|---|
| | 124 | # logger.error "could not #{action} on #{server} (#{status.message})" if logger |
|---|
| | 125 | # failed!(sftp) |
|---|
| | 126 | # |
|---|
| | 127 | # return false |
|---|
| | 128 | # end |
|---|
| | 129 | # |
|---|
| | 130 | # def running? |
|---|
| | 131 | # completed < @sftps.length |
|---|
| | 132 | # end |
|---|
| | 133 | # |
|---|
| | 134 | # def failed!(sftp) |
|---|
| | 135 | # completed!(sftp) |
|---|
| | 136 | # @failed += 1 |
|---|
| | 137 | # sftp.channel[:failed] = true |
|---|
| | 138 | # end |
|---|
| | 139 | # |
|---|
| | 140 | # def completed!(sftp) |
|---|
| | 141 | # @completed += 1 |
|---|
| | 142 | # sftp.channel[:done] = true |
|---|
| | 143 | # end |
|---|
| | 144 | # end |
|---|
| | 145 | # |
|---|
| | 146 | # end |
|---|