class PostImportsController < ApplicationController MAPPING_PROPERTIES = ['kind', 'value', 'columns'].freeze MAPPING_KINDS = ['columns', 'fixed'].freeze before_action :require_member! def parse source = if params[:format] == 'google_sheets' PostImportGoogleSheetsFetcher.fetch!(params[:source], rate_key: current_user.id) else params[:source] end format = params[:format] == 'google_sheets' ? 'csv' : params[:format] parsed = PostImportSourceParser.new( source:, format:, has_header: params[:has_header], json_path: params[:json_path]).parse render json: parsed rescue ArgumentError => e render_bad_request(e.message) end def preview parsed = parsed_params raw_url_column = params[:url_column].presence || (parsed[:columns].length == 1 ? 0 : nil) return render_bad_request('URL 列を選択してください.') if raw_url_column.nil? url_column = Integer(raw_url_column, exception: false) if url_column.nil? || url_column.negative? || url_column >= parsed[:columns].length return render_bad_request('URL 列が不正です.') end rows = PostImportPreviewer.new( parsed:, url_column:, mappings: mappings_params(parsed[:columns].length), existing: params[:existing]).preview render json: parsed.slice(:columns).merge(rows:) rescue ArgumentError => e render_bad_request(e.message) end def validate rows = normalised_import_rows(allow_warning_fields: true) changed_row = Integer(params[:changed_row], exception: false) result = PostImportPreviewer.new( parsed: { columns: [], rows: [] }, url_column: 0, mappings: {}, existing: params[:existing]) .preview_rows(rows:, fetch_metadata: changed_row, metadata_cache: {}) render json: { rows: result } rescue ArgumentError => e render_bad_request(e.message) end def create result = PostImportRunner.new( actor: current_user, rows: normalised_import_rows, existing: params[:existing]).run render json: result, status: result[:created].positive? ? :created : :ok rescue ArgumentError => e render_bad_request(e.message) end private def require_member! return head :unauthorized unless current_user return if current_user.gte_member? head :forbidden end def parsed_params value = params.require(:parsed).to_unsafe_h.deep_transform_keys { _1.to_s.underscore } columns = Array(value['columns']) rows = Array(value['rows']) unless columns.all?(Hash) && rows.all?(Hash) raise ArgumentError, '解析結果の形式が不正です.' end raise ArgumentError, '取込件数が多すぎます.' if rows.length > PostImportSourceParser::MAX_ROWS raise ArgumentError, '列数が多すぎます.' if columns.length > 50 raise ArgumentError, '解析結果のセルが不正です.' unless rows.all? { |row| values = Array(row['values']) values.length <= columns.length && values.all? { |value| value.is_a?(String) && value.bytesize <= PostImportSourceParser::MAX_CELL_BYTES } } if rows.sum { Array(_1['values']).sum(&:bytesize) } > PostImportSourceParser::MAX_BYTES raise ArgumentError, '解析結果が大きすぎます.' end parsed_rows = rows.map do |row| source_row = Integer(row['source_row'], exception: false) raise ArgumentError, '元行番号が不正です.' if source_row.nil? || source_row <= 0 { source_row:, values: Array(row['values']) } end if parsed_rows.map { _1[:source_row] }.uniq.length != parsed_rows.length raise ArgumentError, '元行番号が重複しています.' end { columns: columns.map { |column| column.slice('key', 'label') }, rows: parsed_rows } end def mappings_params column_count mappings = params[:mappings] return { } if mappings.blank? raise ArgumentError, '列割当ての形式が不正です.' unless mappings.is_a?(ActionController::Parameters) raw = mappings.to_unsafe_h unknown_fields = raw.keys - PostImportPreviewer::FIELDS raise ArgumentError, '列割当て先が不正です.' if unknown_fields.present? raise ArgumentError, '列割当てが大きすぎます.' if raw.to_json.bytesize > PostImportSourceParser::MAX_BYTES permitted = mappings.permit(*PostImportPreviewer::FIELDS.map { |field| { field => [:kind, :value, { columns: [] }] } }).to_h raw.each_with_object({ }) do |(field, value), result| raise ArgumentError, '列割当ての形式が不正です.' unless value.is_a?(Hash) unless (value.keys - MAPPING_PROPERTIES).empty? raise ArgumentError, '列割当ての項目が不正です.' end value = permitted.fetch(field) kind = value['kind'].presence || 'columns' raise ArgumentError, '列割当ての種別が不正です.' unless kind.in?(MAPPING_KINDS) columns = value['columns'] raise ArgumentError, '列番号が不正です.' unless ( columns.nil? || (columns.is_a?(Array) && columns.all? { |index| parsed = Integer(index, exception: false) parsed && parsed >= 0 && parsed < column_count }) ) parsed_columns = Array(columns).map { Integer(_1) } raise ArgumentError, '列番号が重複しています.' if parsed_columns.uniq.length != parsed_columns.length raise ArgumentError, '固定値が不正です.' if kind == 'fixed' && !value['value'].is_a?(String) if value['value'].to_s.bytesize > PostImportSourceParser::MAX_CELL_BYTES raise ArgumentError, '固定値が大きすぎます.' end result[field] = { 'kind' => kind, 'value' => value['value'].to_s, 'columns' => parsed_columns } end end def normalised_import_rows allow_warning_fields: false rows = params[:rows] PostImportRowNormaliser.normalise!(rows, allow_warning_fields:) end end