# frozen_string_literal: true 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(%r{^~/}, "").sub(%r{/$}, "") 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).dup << " --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} ".dup 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