324 lines
13 KiB
Python
324 lines
13 KiB
Python
"""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) |