2025-05-16 15:18:02 +08:00

324 lines
13 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""File system adapter for rule storage using JSON files."""
import os
import json
import glob
from typing import List, Dict, Optional, Any, Tuple, Type, Union, cast
import logging
from datetime import datetime
from .base_adapter import BaseRuleStorageAdapter
from ...models.rule_models import (
AnyRule, RuleQuery, BaseRule, RuleCategory, TargetType
)
from .rule_adapter_utils import parse_rule_data
class FilesystemAdapter(BaseRuleStorageAdapter):
"""
基于文件系统的规则存储适配器使用JSON文件保存规则。
文件结构约定:
规则文件将按以下方式组织:
rules/
json_schemas/
rule_id1/
1.0.0.json
1.1.0.json
rule_id2/
1.0.0.json
business_logic/
rule_id3/
1.0.0.json
...
"""
def __init__(self, base_path: str = "./rules", file_pattern: str = "*.json"):
"""
初始化适配器。
Args:
base_path: 存储规则的基本目录路径。
file_pattern: 匹配规则文件的glob模式。
"""
self.base_path = os.path.abspath(base_path)
self.file_pattern = file_pattern
self.logger = logging.getLogger(__name__)
def initialize(self) -> None:
"""确保基本目录存在,并验证其可访问性。"""
if not os.path.exists(self.base_path):
try:
os.makedirs(self.base_path)
self.logger.info(f"Created rules directory at {self.base_path}")
except Exception as e:
self.logger.error(f"Failed to create rules directory at {self.base_path}: {e}")
raise ValueError(f"Rules directory {self.base_path} does not exist and could not be created")
if not os.access(self.base_path, os.R_OK | os.W_OK):
self.logger.error(f"Rules directory {self.base_path} is not readable and writable")
raise ValueError(f"Rules directory {self.base_path} 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.base_path, 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}.json")
def _get_rule_from_file(self, file_path: str) -> Optional[AnyRule]:
"""从文件加载规则。"""
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 = json.load(f)
# 使用工具函数解析规则数据
return parse_rule_data(raw_data)
except json.JSONDecodeError as e:
self.logger.error(f"Failed to parse JSON 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:
"""将规则保存到文件。"""
try:
# 确保目录存在
dir_path = os.path.dirname(file_path)
if not os.path.exists(dir_path):
os.makedirs(dir_path)
# 序列化规则为JSON并写入文件
with open(file_path, 'w', encoding='utf-8') as f:
json.dump(rule.model_dump(), f, indent=2, ensure_ascii=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"的语义化版本
# 如果需要更复杂的版本比较可以使用packaging.version
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 []
# 查找目录中所有匹配的JSON文件并提取版本号文件名
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] # 移除.json扩展名
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
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)