# frozen_string_literal: true class Imports::SourceDetector class UnknownSourceError < StandardError; end DETECTION_RULES = { google_semantic_history: { required_keys: ['timelineObjects'], nested_patterns: [ ['timelineObjects', 0, 'activitySegment'], ['timelineObjects', 0, 'placeVisit'] ] }, google_records: { required_keys: ['locations'], nested_patterns: [ ['locations', 0, 'latitudeE7'], ['locations', 0, 'longitudeE7'] ] }, google_phone_takeout: { alternative_patterns: [ # Pattern 1: Object with semanticSegments { required_keys: ['semanticSegments'], nested_patterns: [['semanticSegments', 0, 'startTime']] }, # Pattern 2: Object with rawSignals { required_keys: ['rawSignals'] }, # Pattern 3: Array format with visit/activity objects { structure: :array, nested_patterns: [ [0, 'visit', 'topCandidate', 'placeLocation'], [0, 'activity'] ] } ] }, geojson: { required_keys: %w[type features], required_values: { 'type' => 'FeatureCollection' }, nested_patterns: [ ['features', 0, 'type'], ['features', 0, 'geometry'], ['features', 0, 'properties'] ] }, owntracks: { structure: :rec_file_lines, line_pattern: /"_type":"location"/ } }.freeze def initialize(file_content, filename = nil, file_path = nil) @file_content = file_content @filename = filename @file_path = file_path end def self.new_from_file_header(file_path) filename = File.basename(file_path) # For detection, read only first 2KB to optimize performance header_content = File.open(file_path, 'rb') { |f| f.read(2048) } new(header_content, filename, file_path) end def detect_source return :gpx if gpx_file? return :owntracks if owntracks_file? json_data = parse_json return nil unless json_data DETECTION_RULES.each do |format, rules| next if format == :owntracks # Already handled above return format if matches_format?(json_data, rules) end nil end def detect_source! format = detect_source raise UnknownSourceError, 'Unable to detect file format' unless format format end private attr_reader :file_content, :filename, :file_path def gpx_file? return false unless filename # Must have .gpx extension AND contain GPX XML structure return false unless filename.downcase.end_with?('.gpx') # Check content for GPX structure content_to_check = if file_path && File.exist?(file_path) # Read first 1KB for GPX detection File.open(file_path, 'rb') { |f| f.read(1024) } else file_content end ( content_to_check.strip.start_with?('= current.length current = current[key] elsif current.is_a?(Hash) return false unless current.key?(key) current = current[key] else return false end end !current.nil? end end