lib/backup/storage/rsync.rb
module Backup
module Storage
class RSync < Base
include Utilities::Helpers
##
# 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 Address
#
# If not specified, the storage operation will be local.
attr_accessor :host
##
# 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
##
# 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
##
# 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
##
# 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
##
# 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
##
# 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
##
# Additional String or Array of options for the rsync cli
attr_accessor :additional_rsync_options
##
# Flag for compressing (only compresses for the transfer)
attr_accessor :compress
##
# 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, +path+ (or path defined by
# your rsync module) must already exist.
#
# In :ssh mode or local operation (no +host+ specified), +path+ will
# be created if needed - either locally, or on the remote for :ssh mode.
attr_accessor :path
def initialize(model, storage_id = nil)
super
@mode ||= :ssh
@port ||= mode == :rsync_daemon ? 873 : 22
@compress ||= false
@path ||= "~/backups"
end
private
def transfer!
write_password_file
create_remote_path
package.filenames.each do |filename|
src = "'#{File.join(Config.tmp_path, filename)}'"
dest = "#{host_options}'#{File.join(remote_path, filename)}'"
Logger.info "Syncing to #{dest}..."
run("#{rsync_command} #{src} #{dest}")
end
ensure
remove_password_file
end
##
# Other storages add an additional timestamp directory to this path.
# This is not desired here, since we need to transfer the package files
# to the same location each time.
def remote_path
@remote_path ||= begin
if host
path.sub(/^~\//, "").sub(/\/$/, "")
else
File.expand_path(path)
end
end
end
##
# 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_remote_path
if host
return unless mode == :ssh
run "#{utility(:ssh)} #{ssh_transport_args} #{host} " +
%("mkdir -p '#{remote_path}'")
else
FileUtils.mkdir_p(remote_path)
end
end
def host_options
@host_options ||= begin
if !host
""
elsif mode == :ssh
"#{host}:"
else
user = "#{rsync_user}@" if rsync_user
"#{user}#{host}::"
end
end
end
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
def compress_option
compress ? " --compress" : ""
end
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
%( -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
end
end
end