module Backup
  module Syncer
    module RSync
      class Push < Base
        ##
        # 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
        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

        ##
        # Flag for compressing (only compresses for the transfer)
        attr_accessor :compress

        def initialize(syncer_id = nil)
          super

          @mode ||= :ssh
          @port ||= mode == :rsync_daemon ? 873 : 22
          @compress ||= false
        end

        def perform!
          log!(:started)
          write_password_file!

          create_dest_path!
          run("#{rsync_command} #{paths_to_push} " \
              "#{host_options}'#{dest_path}'")

          log!(:finished)
        ensure
          remove_password_file!
        end

        private

        ##
        # Remove any preceeding '~/', since this is on the remote,
        # and remove any trailing `/`.
        def dest_path
          @dest_path ||= path.sub(/^~\//, "").sub(/\/$/, "")
        end

        ##
        # Runs a 'mkdir -p' command on the remote to ensure the dest_path exists.
        # This used because rsync will attempt to create the path, but will only
        # call 'mkdir' without the '-p' option. This is only applicable in :ssh
        # mode, and only used if the path would require this.
        def create_dest_path!
          return unless mode == :ssh && dest_path.index("/").to_i > 0

          run "#{utility(:ssh)} #{ssh_transport_args} #{host} " +
            %("mkdir -p '#{dest_path}'")
        end

        ##
        # For Push, this will prepend the #dest_path.
        # For Pull, this will prepend the first path in #paths_to_pull.
        def host_options
          if mode == :ssh
            "#{host}:"
          else
            user = "#{rsync_user}@" if rsync_user
            "#{user}#{host}::"
          end
        end

        ##
        # Common base command, plus options for Push/Pull
        def rsync_command
          super << compress_option << password_option << transport_options
        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 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
end
