"""YAML adapter for rule storage using YAML files.""" import os import yaml import glob from typing import List, Dict, Optional, Any, Tuple, Type, Union, cast import logging from datetime import datetime from .adapters.base_adapter import BaseRuleStorageAdapter from ..models.rule_models import ( AnyRule, RuleQuery, BaseRule, RuleCategory, TargetType, RuleLifecycle, RuleScope ) from .adapters.rule_adapter_utils import parse_rule_data class YAMLAdapter(BaseRuleStorageAdapter): """ 基于YAML文件的规则存储适配器,使用YAML文件保存规则。 文件结构约定: 规则文件将按以下方式组织: rules/ yaml_rules/ json_schemas/ rule_id1/ 1.0.0.yaml 1.1.0.yaml rule_id2/ 1.0.0.yaml business_logic/ rule_id3/ 1.0.0.yaml ... """ def __init__(self, base_path: str = "./rules", file_pattern: str = "*.yaml"): """ 初始化适配器。 Args: base_path: 存储规则的基本目录路径。 file_pattern: 匹配规则文件的glob模式。 """ self.base_path = os.path.abspath(base_path) self.file_pattern = file_pattern self.logger = logging.getLogger(__name__) self.yaml_dir = os.path.join(self.base_path, "yaml_rules") def initialize(self) -> None: """确保基本目录存在,并验证其可访问性。""" if not os.path.exists(self.yaml_dir): try: os.makedirs(self.yaml_dir) self.logger.info(f"Created YAML rules directory at {self.yaml_dir}") except Exception as e: self.logger.error(f"Failed to create YAML rules directory at {self.yaml_dir}: {e}") raise ValueError(f"YAML rules directory {self.yaml_dir} does not exist and could not be created") if not os.access(self.yaml_dir, os.R_OK | os.W_OK): self.logger.error(f"YAML rules directory {self.yaml_dir} is not readable and writable") raise ValueError(f"YAML rules directory {self.yaml_dir} is not readable and writable") # 确保每个规则类别的子目录存在 for category in RuleCategory: category_dir = self._get_category_dir(category) if not os.path.exists(category_dir): try: os.makedirs(category_dir) self.logger.debug(f"Created category directory at {category_dir}") except Exception as e: self.logger.warning(f"Failed to create category directory at {category_dir}: {e}") def _get_category_dir(self, category: RuleCategory) -> str: """获取给定规则类别的目录路径。""" return os.path.join(self.yaml_dir, category.value.lower()) def _get_rule_dir(self, rule_id: str, category: Optional[RuleCategory] = None) -> str: """ 获取给定规则ID的目录路径。 如果指定了类别,则直接使用该类别的目录;否则尝试在所有类别中查找。 """ if category: return os.path.join(self._get_category_dir(category), rule_id) # 如果未指定类别,检查所有类别目录 for cat in RuleCategory: rule_dir = os.path.join(self._get_category_dir(cat), rule_id) if os.path.exists(rule_dir): return rule_dir # 如果未找到任何匹配项,默认返回通用类别目录 return os.path.join(self._get_category_dir(RuleCategory.GENERIC), rule_id) def _get_rule_file_path(self, rule_id: str, version: str, category: Optional[RuleCategory] = None) -> str: """获取给定规则ID和版本的文件路径。""" rule_dir = self._get_rule_dir(rule_id, category) return os.path.join(rule_dir, f"{version}.yaml") def _get_rule_from_file(self, file_path: str) -> Optional[AnyRule]: """从YAML文件加载规则。""" if not os.path.exists(file_path): self.logger.debug(f"Rule file {file_path} does not exist") return None try: with open(file_path, 'r', encoding='utf-8') as f: raw_data = yaml.safe_load(f) # 使用工具函数解析规则数据 return parse_rule_data(raw_data) except yaml.YAMLError as e: self.logger.error(f"Failed to parse YAML from {file_path}: {e}") return None except Exception as e: self.logger.error(f"Error loading rule from {file_path}: {e}") return None def _save_rule_to_file(self, rule: BaseRule, file_path: str) -> bool: """将规则保存到YAML文件。""" try: # 确保目录存在 dir_path = os.path.dirname(file_path) if not os.path.exists(dir_path): os.makedirs(dir_path) # 将规则序列化为YAML并写入文件 with open(file_path, 'w', encoding='utf-8') as f: yaml.dump(rule.model_dump(), f, default_flow_style=False, sort_keys=False) return True except Exception as e: self.logger.error(f"Failed to save rule to {file_path}: {e}") return False def _get_latest_version(self, rule_id: str, category: Optional[RuleCategory] = None) -> Optional[str]: """获取给定规则ID的最新版本。""" versions = self.get_rule_versions(rule_id, category) if not versions: return None # 简单地按字符串排序,假设版本格式是类似于"1.0.0"的语义化版本 versions.sort() return versions[-1] def get_rule_versions(self, rule_id: str, category: Optional[RuleCategory] = None) -> List[str]: """获取给定规则ID的所有版本。""" rule_dir = self._get_rule_dir(rule_id, category) if not os.path.exists(rule_dir): return [] # 查找目录中所有匹配的YAML文件,并提取版本号(文件名) pattern = os.path.join(rule_dir, self.file_pattern) version_files = glob.glob(pattern) versions = [] for vf in version_files: version = os.path.splitext(os.path.basename(vf))[0] # 移除.yaml扩展名 versions.append(version) return versions def load_rule_by_id(self, rule_id: str, version: Optional[str] = None) -> Optional[AnyRule]: """ 按ID加载单个规则。 Args: rule_id: 规则的唯一标识符。 version: 可选的版本标识符。如果未提供,则加载最新版本。 Returns: 找到的规则对象,或者如果未找到则返回None。 """ if not version: version = self._get_latest_version(rule_id) if not version: self.logger.debug(f"No versions found for rule ID {rule_id}") return None file_path = self._get_rule_file_path(rule_id, version) return self._get_rule_from_file(file_path) def query_rules(self, query: RuleQuery) -> List[AnyRule]: """ 根据查询条件查询规则。 Args: query: 包含筛选条件的查询对象。 Returns: 匹配查询条件的规则列表。 """ results = [] # 如果指定了规则ID,直接加载该规则 if query.rule_id: rule = self.load_rule_by_id(query.rule_id, query.version if query.version != "latest" else None) if rule and self._rule_matches_query(rule, query): results.append(rule) return results # 否则,根据查询条件扫描规则文件 categories_to_search = [query.category] if query.category else list(RuleCategory) for category in categories_to_search: category_dir = self._get_category_dir(category) if not os.path.exists(category_dir): continue # 获取该类别下的所有规则ID(子目录) rule_dirs = [d for d in os.listdir(category_dir) if os.path.isdir(os.path.join(category_dir, d))] for rule_id in rule_dirs: # 对于每个规则ID,加载指定版本或最新版本 if query.version and query.version != "latest": file_path = self._get_rule_file_path(rule_id, query.version, category) rule = self._get_rule_from_file(file_path) if rule and self._rule_matches_query(rule, query): results.append(rule) else: latest_version = self._get_latest_version(rule_id, category) if latest_version: file_path = self._get_rule_file_path(rule_id, latest_version, category) rule = self._get_rule_from_file(file_path) if rule and self._rule_matches_query(rule, query): results.append(rule) return results def _rule_matches_query(self, rule: BaseRule, query: RuleQuery) -> bool: """检查规则是否匹配查询条件。""" # 检查是否启用 if query.is_enabled is not None and rule.is_enabled != query.is_enabled: return False # 检查目标类型 if query.target_type and rule.target_type != query.target_type: return False # 检查目标标识符 if query.target_identifier and rule.target_identifier != query.target_identifier: return False # 检查标签 if query.tags: if not rule.tags: return False # 检查是否所有查询标签都在规则标签中 if not all(tag in rule.tags for tag in query.tags): return False # 检查生命周期 if query.lifecycle and rule.lifecycle != query.lifecycle: return False # 检查作用域 if query.scope and rule.scope != query.scope: return False return True def save_rule(self, rule: BaseRule) -> bool: """ 保存规则到文件系统。 Args: rule: 要保存的规则对象。 Returns: 操作是否成功。 """ file_path = self._get_rule_file_path(rule.id, rule.version, rule.category) return self._save_rule_to_file(rule, file_path) def delete_rule(self, rule_id: str, version: Optional[str] = None) -> bool: """ 删除规则。 Args: rule_id: 规则的唯一标识符。 version: 如果提供,仅删除该版本;否则删除所有版本。 Returns: 操作是否成功。 """ try: if version: # 删除特定版本 file_path = self._get_rule_file_path(rule_id, version) if os.path.exists(file_path): os.remove(file_path) self.logger.info(f"Deleted rule file: {file_path}") else: self.logger.warning(f"Rule file not found for deletion: {file_path}") return False else: # 删除所有版本(整个规则目录) rule_dir = self._get_rule_dir(rule_id) if os.path.exists(rule_dir): # 递归删除目录及其内容 for root, dirs, files in os.walk(rule_dir, topdown=False): for file in files: os.remove(os.path.join(root, file)) for dir in dirs: os.rmdir(os.path.join(root, dir)) os.rmdir(rule_dir) self.logger.info(f"Deleted rule directory: {rule_dir}") else: self.logger.warning(f"Rule directory not found for deletion: {rule_dir}") return False return True except Exception as e: self.logger.error(f"Error deleting rule {rule_id} (version={version}): {e}") return False def list_all_rule_ids(self) -> List[str]: """列出所有规则ID。""" all_rule_ids = set() # 扫描所有类别目录 for category in RuleCategory: category_dir = self._get_category_dir(category) if not os.path.exists(category_dir): continue # 获取该类别下的所有规则ID(子目录) rule_dirs = [d for d in os.listdir(category_dir) if os.path.isdir(os.path.join(category_dir, d))] all_rule_ids.update(rule_dirs) return list(all_rule_ids)