module GitlabCtl
  class Backup
    attr_reader :etc_backup_path, :etc_path, :backup_keep_time, :remove_timestamp

    def initialize(options = {})
      backup_path = options[:backup_path].nil? ? '/etc/gitlab/config_backup' : options[:backup_path]
      @etc_backup_path = File.expand_path(backup_path)
      @etc_path = '/etc/gitlab'
      # Earlier, the attribute was named `gitlab-rails`, but it in 15.11, was
      # changed to `gitlab_rails`. Hence we try both.
      @backup_keep_time = (node_attributes.dig('gitlab', 'gitlab_rails', 'backup_keep_time') || node_attributes.dig('gitlab', 'gitlab-rails', 'backup_keep_time')).to_i
      @remove_timestamp = Time.now - @backup_keep_time
      @delete_old_backups = options[:delete_old_backups]
      @removable_archives = []
    end

    # attribute methods
    def archive_path
      @archive_path = File.join(etc_backup_path, archive_name)
    end

    def archive_name
      @archive_name ||= "gitlab_config_#{Time.now.strftime('%s_%Y_%m_%d')}.tar"
    end

    def node_attributes
      @node_attributes ||= GitlabCtl::Util.get_node_attributes
    rescue GitlabCtl::Errors::NodeError => e
      warn(e.message)
      warn("Defaulting to keeping all backups")
      {}
    end

    def wants_pruned
      @delete_old_backups.nil? ? true : @delete_old_backups
    end

    def removable_archives
      return @removable_archives unless @removable_archives.empty?

      Dir.chdir(@etc_backup_path) do
        Dir.glob("gitlab_config_*.tar").map do |file_name|
          next unless file_name =~ %r{gitlab_config_(\d{10})_(\d{4}_\d{2}_\d{2}).tar}

          file_timestamp = Regexp.last_match(1).to_i

          next if @backup_keep_time.zero?

          next if Time.at(file_timestamp) >= @remove_timestamp

          file_path = File.expand_path(file_name, @etc_backup_path)

          @removable_archives.push(file_path)
        end
      end

      @removable_archives
    end

    # class methods
    def self.perform(options = {})
      backup = new(options)
      backup.perform
      backup.prune
    end

    def prune
      if wants_pruned && backup_keep_time.positive?
        remove_backups
      else
        puts "Keeping all older configuration backups"
      end
    end

    def perform(options = {})
      abort "Could not find '#{etc_path}' directory. Is your package installed correctly?" unless File.exist?(etc_path)
      unless File.exist?(etc_backup_path)
        puts "Could not find '#{etc_backup_path}' directory. Creating."
        FileUtils.mkdir(etc_backup_path, mode: 0700)
        begin
          FileUtils.chown('root', 'root', etc_backup_path)
        rescue Errno::EPERM
          warn("Warning: Could not change owner of #{etc_backup_path} to 'root:root'. As a result your " \
               'backups may be accessible to some non-root users.')
        end
      end

      warn("WARNING: #{etc_backup_path} may be read by non-root users") unless secure?(etc_backup_path)

      puts "Running configuration backup\nCreating configuration backup archive: #{archive_name}"

      command = %W(tar --absolute-names --dereference --verbose --create --file #{archive_path}
                   --exclude #{etc_backup_path} -- #{etc_path})
      status = system(*command)

      FileUtils.chmod(0600, archive_path) if File.exist?(archive_path)

      exit!(1) unless status

      puts "Configuration backup archive complete: #{archive_path}"
    end

    def remove_backups
      # delete old backups
      removed_count = 0
      puts "Removing configuration backups older than #{@remove_timestamp} ..."

      removable_archives.each do |archive_file|
        FileUtils.rm(archive_file)
        puts "  Removed #{archive_file}"
        removed_count += 1
      rescue StandardError => e
        warn("WARNING: Deleting file #{archive_file} failed: #{e.message}")
      end
      puts "done. Removed #{removed_count} older configuration backups."
    end

    def secure?(path)
      stat_data = File.stat(path)
      return false if stat_data.uid != 0
      return false unless stat_data.world_readable?.nil?

      true
    end
  end
end
