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