class MaterialSyncSuppressionMatcher def self.match source_kind:, source_uri: nil, drive_path: nil, drive_file_id: nil, source_path: nil, source_file_id: nil key = MaterialSyncSuppression.normalize_source_key(source_kind:, source_uri:, drive_path:, drive_file_id:, source_path:, source_file_id:) return nil if key.blank? MaterialSyncSuppression.find_by(normalized_source_key: key) end def self.match_google_drive_candidate drive_file_id:, relative_path: normalized_path = validate_relative_path!(relative_path) exact = [ normalize_key('google_drive_file', drive_file_id), normalize_key('google_drive_path', normalized_path), normalize_key('legacy_drive_path', normalized_path), ].compact suppression = MaterialSyncSuppression.find_by(normalized_source_key: exact) return suppression if suppression MaterialSyncSuppression .where(source_kind: ['google_drive_path_prefix', 'legacy_drive_path_prefix']) .where('drive_path = :path OR :path LIKE CONCAT(drive_path, "/%")', path: normalized_path) .order(:id) .first end def self.suppressed?(...) match(...).present? end def self.validate_relative_path! relative_path raw = relative_path.to_s.delete("\0").tr('\\', '/').strip raise ArgumentError, 'relative_path is required' if raw.blank? raise ArgumentError, 'relative_path must be relative' if raw.start_with?('/') raise ArgumentError, 'relative_path must be relative' if raw.match?(/\A[A-Za-z]:\//) if raw.start_with?('My Drive/', 'マイドライブ/') raise ArgumentError, 'relative_path must be relative to source folder' end raise ArgumentError, 'relative_path must not contain //' if raw.include?('//') raise ArgumentError, 'relative_path must not end with /' if raw.end_with?('/') parts = raw.split('/') if parts.any? { |part| part.in?(['.', '..']) } raise ArgumentError, 'relative_path must not contain dot segments' end raw end def self.normalize_key source_kind, value return nil if value.blank? MaterialSyncSuppression.normalize_source_key(source_kind:, source_path: value, source_file_id: value) end end