lib/backup/storage/rsync.rb in backup-3.1.3 vs lib/backup/storage/rsync.rb in backup-3.2.0

- old
+ new

@@ -1,149 +1,289 @@ # encoding: utf-8 -## -# Only load the Net::SSH library when the Backup::Storage::RSync class is loaded -Backup::Dependency.load('net-ssh') - module Backup module Storage class RSync < Base include Backup::Utilities::Helpers ## - # Server credentials - attr_accessor :username, :password + # Mode of operation + # + # [:ssh (default)] + # Connects to the remote via SSH. + # Does not use an rsync daemon on the remote. + # + # [:ssh_daemon] + # Connects to the remote via SSH. + # Spawns a single-use daemon on the remote, which allows certain + # daemon features (like modules) to be used. + # + # [:rsync_daemon] + # Connects directly to an rsync daemon via TCP. + # Data transferred is not encrypted. + # + attr_accessor :mode ## - # Server IP Address and SSH port - attr_accessor :ip, :port + # Server Address + # + # If not specified, the storage operation will be local. + attr_accessor :host ## - # Path to store backups to - attr_accessor :path + # SSH or RSync port + # + # For `:ssh` or `:ssh_daemon` mode, this specifies the SSH port to use + # and defaults to 22. + # + # For `:rsync_daemon` mode, this specifies the TCP port to use + # and defaults to 873. + attr_accessor :port ## - # Flag to use local backups - attr_accessor :local + # SSH User + # + # If the user running the backup is not the same user that needs to + # authenticate with the remote server, specify the user here. + # + # The user must have SSH keys setup for passphrase-less access to the + # remote. If the SSH User does not have passphrase-less keys, or no + # default keys in their `~/.ssh` directory, you will need to use the + # `-i` option in `:additional_ssh_options` to specify the + # passphrase-less key to use. + # + # Used only for `:ssh` and `:ssh_daemon` modes. + attr_accessor :ssh_user ## - # Creates a new instance of the storage object - def initialize(model, storage_id = nil, &block) - super(model, storage_id) + # Additional SSH Options + # + # Used to supply a String or Array of options to be passed to the SSH + # command in `:ssh` and `:ssh_daemon` modes. + # + # For example, if you need to supply a specific SSH key for the `ssh_user`, + # you would set this to: "-i '/path/to/id_rsa'". Which would produce: + # + # rsync -e "ssh -p 22 -i '/path/to/id_rsa'" + # + # Arguments may be single-quoted, but should not contain any double-quotes. + # + # Used only for `:ssh` and `:ssh_daemon` modes. + attr_accessor :additional_ssh_options - @port ||= 22 - @path ||= 'backups' - @local ||= false + ## + # RSync User + # + # If the user running the backup is not the same user that needs to + # authenticate with the rsync daemon, specify the user here. + # + # Used only for `:ssh_daemon` and `:rsync_daemon` modes. + attr_accessor :rsync_user - instance_eval(&block) if block_given? + ## + # RSync Password + # + # If specified, Backup will write the password to a temporary file and + # use it with rsync's `--password-file` option for daemon authentication. + # + # Note that setting this will override `rsync_password_file`. + # + # Used only for `:ssh_daemon` and `:rsync_daemon` modes. + attr_accessor :rsync_password - @path = path.sub(/^\~\//, '') - end + ## + # RSync Password File + # + # If specified, this path will be passed to rsync's `--password-file` + # option for daemon authentication. + # + # Used only for `:ssh_daemon` and `:rsync_daemon` modes. + attr_accessor :rsync_password_file - private + ## + # Additional String or Array of options for the rsync cli + attr_accessor :additional_rsync_options ## - # This is the remote path to where the backup files will be stored - # - # Note: This overrides the superclass' method - def remote_path_for(package) - File.join(path, package.trigger) - end + # Flag for compressing (only compresses for the transfer) + attr_accessor :compress ## - # Establishes a connection to the remote server - def connection - Net::SSH.start( - ip, username, :password => password, :port => port - ) {|ssh| yield ssh } + # Path to store the synced backup package file(s) to. + # + # If no +host+ is specified, then +path+ will be local, and the only + # other used option would be +additional_rsync_options+. + # +path+ will be expanded, so '~/my_path' will expand to '$HOME/my_path'. + # + # If a +host+ is specified, this will be a path on the host. + # If +mode+ is `:ssh` (default), then any relative path, or path starting + # with '~/' will be relative to the directory the ssh_user is logged + # into. For `:ssh_daemon` or `:rsync_daemon` modes, this would reference + # an rsync module/path. + # + # In :ssh_daemon and :rsync_daemon modes, the files will be stored + # directly to the +path+ given. The path (or path defined by your rsync + # module) must already exist. + # Note that no additional `<trigger>` directory will be added to this path. + # + # In :ssh mode or local operation (no +host+ specified), the actual + # destination path will be `<path>/<trigger>/`. This path will be created + # if needed - either locally, or on the remote for :ssh mode. + # This behavior will change in v4.0, when :ssh mode and local operations + # will also store the files directly in the +path+ given. + attr_accessor :path + + def initialize(model, storage_id = nil, &block) + super + instance_eval(&block) if block_given? + + @mode ||= :ssh + @port ||= mode == :rsync_daemon ? 873 : 22 + @compress ||= false + @path ||= '~/backups' end - ## - # Transfers the archived file to the specified remote server + private + def transfer! - write_password_file! unless local + Logger.info "#{ storage_name } Started..." - remote_path = remote_path_for(@package) + write_password_file! + create_dest_path! - create_remote_path!(remote_path) - files_to_transfer_for(@package) do |local_file, remote_file| - if local - Logger.info "#{storage_name} started transferring " + - "'#{ local_file }' to '#{ remote_path }'." - run( - "#{ utility(:rsync) } '#{ File.join(local_path, local_file) }' " + - "'#{ File.join(remote_path, remote_file) }'" - ) - else - Logger.info "#{storage_name} started transferring " + - "'#{ local_file }' to '#{ ip }'." - run( - "#{ utility(:rsync) } #{ rsync_options } #{ rsync_port } " + - "#{ rsync_password_file } '#{ File.join(local_path, local_file) }' " + - "'#{ username }@#{ ip }:#{ File.join(remote_path, remote_file) }'" - ) - end + src = "'#{ File.join(local_path, local_file) }'" + dest = "#{ host_options }'#{ File.join(dest_path, remote_file) }'" + Logger.info "Syncing to #{ dest }..." + run("#{ rsync_command } #{ src } #{ dest }") end + Logger.info "#{ storage_name } Finished!" ensure - remove_password_file! unless local + remove_password_file! end ## - # Note: Storage::RSync doesn't cycle - def remove!; end + # Storage::RSync doesn't cycle + def cycle!; end ## - # Creates (if they don't exist yet) all the directories on the remote - # server in order to upload the backup file. - def create_remote_path!(remote_path) - if @local - FileUtils.mkdir_p(remote_path) - else - connection do |ssh| - ssh.exec!("mkdir -p '#{ remote_path }'") + # Other storages use #remote_path_for to set the dest_path, + # which adds an additional timestamp directory to the path. + # This is not desired here, since we need to transfer the package files + # to the same location each time. + # + # Note: In v4.0, the additional trigger directory will to be dropped + # from dest_path for both local and :ssh mode, so the package files will + # be stored directly in #path. + def dest_path + @dest_path ||= begin + if host + if mode == :ssh + File.join(path.sub(/^~\//, ''), @package.trigger) + else + path.sub(/^~\//, '').sub(/\/$/, '') + end + else + File.join(File.expand_path(path), @package.trigger) end end end ## - # Writes the provided password to a temporary file so that - # the rsync utility can read the password from this file - def write_password_file! - unless password.nil? - @password_file = Tempfile.new('backup-rsync-password') - @password_file.write(password) - @password_file.close + # Runs a 'mkdir -p' command on the host (or locally) to ensure the + # dest_path exists. This is used because we're transferring a single + # file, and rsync won't attempt to create the intermediate directories. + # + # This is only applicable locally and in :ssh mode. + # In :ssh_daemon and :rsync_daemon modes the `path` would include a + # module name that must define a path on the remote that already exists. + def create_dest_path! + if host + run("#{ utility(:ssh) } #{ ssh_transport_args } #{ host } " + + %Q["mkdir -p '#{ dest_path }'"]) if mode == :ssh + else + FileUtils.mkdir_p dest_path end end - ## - # Removes the previously created @password_file - # (temporary file containing the password) - def remove_password_file! - @password_file.delete if @password_file - @password_file = nil + def host_options + @host_options ||= begin + if !host + '' + elsif mode == :ssh + "#{ host }:" + else + user = "#{ rsync_user }@" if rsync_user + "#{ user }#{ host }::" + end + end end - ## - # Returns Rsync syntax for using a password file - def rsync_password_file - "--password-file='#{@password_file.path}'" if @password_file + def rsync_command + @rsync_command ||= begin + cmd = utility(:rsync) << ' --archive' << + " #{ Array(additional_rsync_options).join(' ') }".rstrip + cmd << compress_option << password_option << transport_options if host + cmd + end end - ## - # Returns Rsync syntax for defining a port to connect to - def rsync_port - "-e 'ssh -p #{port}'" + def compress_option + compress ? ' --compress' : '' end - ## - # RSync options - # -z = Compresses the bytes that will be transferred to reduce bandwidth usage - def rsync_options - "-z" + def password_option + return '' if mode == :ssh + + path = @password_file ? @password_file.path : rsync_password_file + path ? " --password-file='#{ File.expand_path(path) }'" : '' end + def transport_options + if mode == :rsync_daemon + " --port #{ port }" + else + %Q[ -e "#{ utility(:ssh) } #{ ssh_transport_args }"] + end + end + + def ssh_transport_args + args = "-p #{ port } " + args << "-l #{ ssh_user } " if ssh_user + args << Array(additional_ssh_options).join(' ') + args.rstrip + end + + def write_password_file! + return unless host && rsync_password && mode != :ssh + + @password_file = Tempfile.new('backup-rsync-password') + @password_file.write(rsync_password) + @password_file.close + end + + def remove_password_file! + @password_file.delete if @password_file + end + + attr_deprecate :local, :version => '3.2.0', + :message => "If 'host' is not set, the operation will be local." + + attr_deprecate :username, :version => '3.2.0', + :message => 'Use #ssh_user instead.', + :action => lambda {|klass, val| + klass.ssh_user = val + } + attr_deprecate :password, :version => '3.2.0', + :message => 'Use #rsync_password instead.', + :action => lambda {|klass, val| + klass.rsync_password = val + } + attr_deprecate :ip, :version => '3.2.0', + :message => 'Use #host instead.', + :action => lambda {|klass, val| + klass.host = val + } end end end