class MaterialSyncSuppression < ApplicationRecord SOURCE_KINDS = [ 'uri', 'google_drive_path', 'google_drive_path_prefix', 'google_drive_file', 'legacy_drive_path', 'legacy_drive_path_prefix' ].freeze REASONS = [ 'copyright_high_risk', 'copyright_takedown', 'adult_or_sensitive', 'personal_information', 'malware_or_dangerous_file', 'duplicate_or_low_quality', 'source_owner_request', 'other' ].freeze belongs_to :created_by_user, class_name: 'User', optional: true before_validation :assign_normalized_source_key validates :source_kind, presence: true, inclusion: { in: SOURCE_KINDS } validates :reason, presence: true, inclusion: { in: REASONS } validates :normalized_source_key, presence: true, uniqueness: true validate :source_value_must_be_present validate :drive_path_must_be_safe_relative_path def self.normalize_source_key source_kind:, source_uri: nil, drive_path: nil, drive_file_id: nil, source_path: nil, source_file_id: nil kind = source_kind.to_s value = case kind when 'uri' source_uri.to_s.strip when 'google_drive_path', 'google_drive_path_prefix', 'legacy_drive_path', 'legacy_drive_path_prefix' (source_path || drive_path).to_s.strip.gsub(%r{/+}, '/') when 'google_drive_file' (source_file_id || drive_file_id).to_s.strip else '' end return nil if kind.blank? || value.blank? "#{ kind }:#{ value }" end private def assign_normalized_source_key normalize_path_fields! self.normalized_source_key = self.class.normalize_source_key(source_kind:, source_uri:, drive_path:, drive_file_id:) end def source_value_must_be_present return if normalized_source_key.present? errors.add(:base, '同期元を指定してください.') end def drive_path_must_be_safe_relative_path return unless source_kind.in?( ['google_drive_path', 'google_drive_path_prefix', 'legacy_drive_path', 'legacy_drive_path_prefix']) value = drive_path.to_s return if value.blank? if value.start_with?('/') || value.match?(/\A[A-Za-z]:\//) errors.add(:drive_path, '相対 path を指定してください.') end if value.start_with?('My Drive/', 'マイドライブ/') errors.add(:drive_path, '同期元フォルダからの相対 path を指定してください.') end errors.add(:drive_path, 'NUL は使へません.') if value.include?("\0") errors.add(:drive_path, '連続スラッシュは使へません.') if value.include?('//') errors.add(:drive_path, '末尾スラッシュは使へません.') if value.end_with?('/') parts = value.split('/') if parts.any? { |part| part.in?(['.', '..']) } errors.add(:drive_path, '. や .. は使へません.') end end def normalize_path_fields! return unless source_kind.in?( ['google_drive_path', 'google_drive_path_prefix', 'legacy_drive_path', 'legacy_drive_path_prefix']) self.drive_path = drive_path.to_s.tr('\\', '/').strip.presence end end