# frozen_string_literal: true

module Pages
  class ZipDirectoryService
    include BaseServiceUtility
    include Gitlab::Utils::StrongMemoize

    # used only to track exceptions in Sentry
    InvalidEntryError = Class.new(StandardError)

    PUBLIC_DIR = 'public'

    attr_reader :public_dir, :real_dir

    def initialize(input_dir, ignore_invalid_entries: false)
      @input_dir = input_dir
      @ignore_invalid_entries = ignore_invalid_entries
    end

    def execute
      return success unless resolve_public_dir

      output_file = File.join(real_dir, "@migrated.zip") # '@' to avoid any name collision with groups or projects

      FileUtils.rm_f(output_file)

      entries_count = 0
      # Since we're writing not reading here, we can safely silence the cop.
      # It currently cannot discern between opening for reading or writing.
      ::Zip::File.open(output_file, ::Zip::File::CREATE) do |zipfile| # rubocop:disable Performance/Rubyzip
        write_entry(zipfile, PUBLIC_DIR)
        entries_count = zipfile.entries.count
      end

      success(archive_path: output_file, entries_count: entries_count)
    rescue StandardError => e
      FileUtils.rm_f(output_file) if output_file
      raise e
    end

    private

    def resolve_public_dir
      @real_dir = File.realpath(@input_dir)
      @public_dir = File.join(real_dir, PUBLIC_DIR)

      valid_path?(public_dir)
    rescue Errno::ENOENT
      false
    end

    def write_entry(zipfile, zipfile_path)
      disk_file_path = File.join(real_dir, zipfile_path)

      unless valid_path?(disk_file_path)
        # archive with invalid entry will just have this entry missing
        raise InvalidEntryError, "#{disk_file_path} is invalid, input_dir: #{@input_dir}"
      end

      ftype = File.lstat(disk_file_path).ftype
      case ftype
      when 'directory'
        recursively_zip_directory(zipfile, disk_file_path, zipfile_path)
      when 'file', 'link'
        zipfile.add(zipfile_path, disk_file_path)
      else
        raise InvalidEntryError, "#{disk_file_path} has invalid ftype: #{ftype}, input_dir: #{@input_dir}"
      end
    rescue Errno::ENOENT, Errno::ELOOP, InvalidEntryError => e
      Gitlab::ErrorTracking.track_exception(e, input_dir: @input_dir, disk_file_path: disk_file_path)

      raise e unless @ignore_invalid_entries
    end

    def recursively_zip_directory(zipfile, disk_file_path, zipfile_path)
      zipfile.mkdir(zipfile_path)

      entries = Dir.entries(disk_file_path) - %w[. ..]
      entries = entries.map { |entry| File.join(zipfile_path, entry) }

      write_entries(zipfile, entries)
    end

    def write_entries(zipfile, entries)
      entries.each do |zipfile_path|
        write_entry(zipfile, zipfile_path)
      end
    end

    # SafeZip was introduced only recently,
    # so we have invalid entries on disk
    def valid_path?(disk_file_path)
      realpath = File.realpath(disk_file_path)
      realpath == public_dir || realpath.start_with?(public_dir + "/")
    end
  end
end
