127 lines
3.1 KiB
Ruby
127 lines
3.1 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
module ImportCsv
|
|
class BaseService
|
|
include Gitlab::Utils::StrongMemoize
|
|
|
|
def initialize(user, project, csv_io)
|
|
@user = user
|
|
@project = project
|
|
@csv_io = csv_io
|
|
@results = { success: 0, error_lines: [], parse_error: false }
|
|
end
|
|
|
|
PreprocessError = Class.new(StandardError)
|
|
|
|
def execute
|
|
process_csv
|
|
email_results_to_user
|
|
|
|
results
|
|
end
|
|
|
|
def email_results_to_user
|
|
raise NotImplementedError
|
|
end
|
|
|
|
private
|
|
|
|
attr_reader :user, :project, :csv_io, :results
|
|
|
|
def attributes_for(row)
|
|
raise NotImplementedError
|
|
end
|
|
|
|
def validate_headers_presence!(headers)
|
|
raise NotImplementedError
|
|
end
|
|
|
|
def create_object_class
|
|
raise NotImplementedError
|
|
end
|
|
|
|
def validate_structure!
|
|
header_line = csv_data.lines.first
|
|
raise CSV::MalformedCSVError.new('File is empty, no headers found', 1) if header_line.blank?
|
|
|
|
validate_headers_presence!(header_line)
|
|
detect_col_sep
|
|
end
|
|
|
|
def preprocess!
|
|
# any logic can be added in subclasses if needed
|
|
# hence just a no-op rather than NotImplementedError
|
|
end
|
|
|
|
def process_csv
|
|
validate_structure!
|
|
preprocess!
|
|
|
|
with_csv_lines.each do |row, line_no|
|
|
attributes = attributes_for(row)
|
|
|
|
if create_object(attributes)&.persisted?
|
|
results[:success] += 1
|
|
else
|
|
results[:error_lines].push(line_no)
|
|
end
|
|
end
|
|
rescue ArgumentError, CSV::MalformedCSVError => e
|
|
results[:parse_error] = true
|
|
results[:error_lines].push(e.line_number) if e.respond_to?(:line_number)
|
|
rescue PreprocessError
|
|
results[:parse_error] = false
|
|
end
|
|
|
|
def with_csv_lines
|
|
CSV.new(
|
|
csv_data,
|
|
col_sep: detect_col_sep,
|
|
headers: true,
|
|
header_converters: :symbol
|
|
).each.with_index(2)
|
|
end
|
|
|
|
def csv_data
|
|
@csv_io.open(&:read).force_encoding(Encoding::UTF_8)
|
|
end
|
|
strong_memoize_attr :csv_data
|
|
|
|
def detect_col_sep
|
|
header = csv_data.lines.first
|
|
|
|
if header.include?(",")
|
|
","
|
|
elsif header.include?(";")
|
|
";"
|
|
elsif header.include?("\t")
|
|
"\t"
|
|
else
|
|
raise CSV::MalformedCSVError.new('Invalid CSV format', 1)
|
|
end
|
|
end
|
|
strong_memoize_attr :detect_col_sep
|
|
|
|
def create_object(attributes)
|
|
# NOTE: CSV imports are performed by workers, so we do not have a request context in order
|
|
# to create a SpamParams object to pass to the issuable create service.
|
|
spam_params = nil
|
|
|
|
# default_params can be extracted into a method if we need
|
|
# to support creation of objects that belongs to groups.
|
|
default_params = { container: project,
|
|
current_user: user,
|
|
params: attributes,
|
|
spam_params: spam_params }
|
|
|
|
create_service = create_object_class.new(**default_params.merge(extra_create_service_params))
|
|
|
|
create_service.execute_without_rate_limiting
|
|
end
|
|
|
|
# Overidden in subclasses to support specific parameters
|
|
def extra_create_service_params
|
|
{}
|
|
end
|
|
end
|
|
end
|