Files
LitePan/core/operation_wrapper.py
T
2026-05-26 09:10:51 +08:00

582 lines
24 KiB
Python
Executable File

"""操作包装器:统一缓存装饰器 + 写操作缓存清理 + 认证重试。"""
import time
from typing import List, Callable, Any, Optional
from functools import wraps
import asyncio
from contextvars import ContextVar
from core.base import OperationResult, FileItem
from core.log_manager import get_writer, LogModule
from cache.cache_keys import CacheKeyGenerator
# 通过 contextvar 透传当前账号 ID,让装饰器能在不改 API 签名的情况下识别出调用方账号
current_account_id: ContextVar[Optional[str]] = ContextVar('current_account_id', default=None)
from core.dependency_container import get_cache_manager, get_cache_cleaner, get_hit_tracker
def get_operation_logger():
try:
return get_writer(LogModule.FILE_OP)
except RuntimeError:
return None
def log_operation(level: str, message: str):
logger = get_operation_logger()
if logger:
getattr(logger, level)(message)
async def _maybe_increment_api_call(driver) -> None:
if hasattr(driver, '_increment_api_call_count'):
await driver._increment_api_call_count()
async def _maybe_record_response_time(driver, start_time: float) -> None:
if hasattr(driver, '_record_response_time'):
await driver._record_response_time((time.time() - start_time) * 1000)
async def _record_cache_hit() -> None:
hit_tracker = get_hit_tracker()
if hit_tracker:
await hit_tracker.record_hit()
async def _record_cache_miss() -> None:
hit_tracker = get_hit_tracker()
if hit_tracker:
await hit_tracker.record_miss()
def _get_driver_account_id(driver):
context_account_id = current_account_id.get()
if context_account_id not in (None, ''):
return context_account_id
for attr_name in ('_account_id', 'account_id'):
value = getattr(driver, attr_name, None)
if value not in (None, ''):
return value
config = getattr(driver, 'config', None)
value = getattr(config, 'account_id', None) if config is not None else None
return value if value not in (None, '') else None
async def ensure_driver_auth_request_allowed(driver) -> None:
account_id = _get_driver_account_id(driver)
if account_id in (None, '', 'default', 'temp_test'):
return
from core.auth_manager import ensure_auth_request_allowed
await ensure_auth_request_allowed(account_id)
async def _clear_directory_cache_prefix(cache_manager, account_id: str, parent_id: str) -> None:
from cache.cache_keys import CacheKeyGenerator
prefix = CacheKeyGenerator.directory_prefix(account_id, parent_id)
await cache_manager.clear_by_prefix(prefix)
def _get_driver_cache_manager(driver):
return getattr(driver, '_cache_manager', None)
async def _execute_driver_call(driver, func: Callable, *args, track_time: bool = True, **kwargs):
await ensure_driver_auth_request_allowed(driver)
await _maybe_increment_api_call(driver)
if not track_time:
return await func(driver, *args, **kwargs)
start_time = time.time()
result = await func(driver, *args, **kwargs)
await _maybe_record_response_time(driver, start_time)
return result
def _find_attr_value(args, kwargs, attr_name: str, default=None):
for arg in args:
if hasattr(arg, attr_name):
return getattr(arg, attr_name)
for value in kwargs.values():
if hasattr(value, attr_name):
return getattr(value, attr_name)
return default
def _normalize_parent_ids(parent_ids) -> List[str]:
return [str(parent_id) for parent_id in parent_ids if parent_id not in (None, '')]
class OperationWrapper:
def __init__(self, cache_manager=None, cache_cleaner=None):
self.cache_manager = cache_manager
self.cache_cleaner = cache_cleaner
def with_cache_cleanup(self, operation_type: str):
"""装饰器:写操作成功后按 operation_type 自动触发对应缓存清理。"""
def decorator(func: Callable):
@wraps(func)
async def wrapper(driver, *args, **kwargs):
try:
await ensure_driver_auth_request_allowed(driver)
result = await func(driver, *args, **kwargs)
if isinstance(result, OperationResult) and result.success:
await self._cleanup_cache_after_operation(
driver, operation_type, result, *args, **kwargs
)
return result
except Exception as e:
log_operation("error", f"[{operation_type}] 操作异常: {str(e)}")
raise
return wrapper
return decorator
async def _cleanup_cache_after_operation(self, driver, operation_type: str,
result: OperationResult, *args, **kwargs):
if not self.cache_cleaner:
log_operation("warning", "缓存清理器未初始化,跳过清理")
return
try:
# 优先用 contextvar 里的 account_id,其次再回退到驱动属性 / result.data
account_id = current_account_id.get()
if not account_id:
account_id = getattr(driver, '_account_id', None) or \
getattr(driver.config, 'account_id', None) or \
result.data.get('account_id') if result.data else None
if not account_id:
log_operation("warning", f"无法获取账号ID,跳过缓存清理: {operation_type}")
return
handler = self._get_cleanup_handler(operation_type)
if not handler:
log_operation("warning", f"未知操作类型: {operation_type}")
return
await handler(str(account_id), result, *args, **kwargs)
except Exception as e:
log_operation("error", f"缓存清理失败 [{operation_type}]: {str(e)}")
def _get_cleanup_handler(self, operation_type: str):
return {
'create_folder': self._cleanup_create_folder,
'delete_file': self._cleanup_delete_file,
'batch_delete_file': self._cleanup_batch_delete_file,
'move_file': self._cleanup_move_file,
'copy_file': self._cleanup_move_file,
'rename_file': self._cleanup_rename_file,
'upload_file': self._cleanup_upload_file,
}.get(operation_type)
async def _cleanup_create_folder(self, account_id: str, result: OperationResult, *args, **kwargs):
parent_path = args[0] if args else kwargs.get('parent_path', '0')
folder_name = args[1] if len(args) > 1 else kwargs.get('name', '')
folder_id = result.data.get('folder_id') if result.data else None
await self.cache_cleaner.on_file_created(
account_id=account_id,
parent_id=parent_path,
file_name=folder_name,
file_id=folder_id
)
async def _cleanup_delete_file(self, account_id: str, result: OperationResult, *args, **kwargs):
file_id = args[0] if args else kwargs.get('file_id', '')
parent_ids = result.data.get('parent_ids', []) if result.data else []
normalized_parent_ids = _normalize_parent_ids(parent_ids)
primary_parent_id = normalized_parent_ids[0] if normalized_parent_ids else None
await self.cache_cleaner.on_file_deleted(
account_id=account_id,
file_id=file_id,
parent_id=primary_parent_id
)
if len(normalized_parent_ids) > 1:
await self._clear_parent_directories(account_id, normalized_parent_ids[1:])
if not normalized_parent_ids:
await self._force_clear_account_cache(account_id)
async def _cleanup_batch_delete_file(self, account_id: str, result: OperationResult, *args, **kwargs):
file_ids = args[0] if args else kwargs.get('file_ids', [])
parent_ids = result.data.get('parent_ids', []) if result.data else []
normalized_parent_ids = _normalize_parent_ids(parent_ids)
shared_parent_id = normalized_parent_ids[0] if len(normalized_parent_ids) == 1 else None
for file_id in file_ids:
await self.cache_cleaner.on_file_deleted(
account_id=account_id,
file_id=file_id,
parent_id=shared_parent_id
)
if len(normalized_parent_ids) > 1:
await self._clear_parent_directories(account_id, normalized_parent_ids)
if not normalized_parent_ids:
await self._force_clear_account_cache(account_id)
async def _cleanup_move_file(self, account_id: str, result: OperationResult, *args, **kwargs):
file_ids = args[0] if args else kwargs.get('file_ids', [])
fallback_target_parent_id = args[1] if len(args) > 1 else kwargs.get('target_parent_id', '')
source_parent_ids = result.data.get('source_parent_ids', []) if result.data else []
target_parent_id = result.data.get('target_parent_id', fallback_target_parent_id) if result.data else fallback_target_parent_id
normalized_source_parent_ids = _normalize_parent_ids(source_parent_ids)
normalized_target_parent_id = str(target_parent_id) if target_parent_id not in (None, '') else ''
# 常见的移动场景源目录唯一可确定,直接复用统一的 cache_cleaner 入口,不在这里重写一套移动缓存逻辑
if normalized_target_parent_id and len(normalized_source_parent_ids) == 1 and file_ids:
old_parent_id = normalized_source_parent_ids[0]
for file_id in file_ids:
await self.cache_cleaner.on_file_moved(
account_id=account_id,
file_id=file_id,
old_parent_id=old_parent_id,
new_parent_id=normalized_target_parent_id
)
return
await self._clear_parent_directories(account_id, normalized_source_parent_ids)
if normalized_target_parent_id:
await self.cache_cleaner._clear_directory_cache(account_id, normalized_target_parent_id)
for file_id in file_ids:
try:
file_info_key = CacheKeyGenerator.file_info_key(account_id, file_id)
await self.cache_cleaner.cache_manager.delete(file_info_key)
except Exception as e:
log_operation("warning", f"清理文件信息缓存失败: {file_id}, 错误: {str(e)}")
if not normalized_source_parent_ids:
await self._clear_all_directory_cache(account_id)
async def _cleanup_rename_file(self, account_id: str, result: OperationResult, *args, **kwargs):
file_path = args[0] if args else kwargs.get('path', '')
new_name = args[1] if len(args) > 1 else kwargs.get('new_name', '')
parent_id = result.data.get('parent_id') if result.data else '0'
old_name = result.data.get('old_name', '') if result.data else ''
await self.cache_cleaner.on_file_renamed(
account_id=account_id,
parent_id=parent_id,
file_id=file_path,
old_name=old_name,
new_name=new_name
)
async def _cleanup_upload_file(self, account_id: str, result: OperationResult, *args, **kwargs):
parent_path = '0'
if result.data:
parent_path = result.data.get('parent_id') or result.data.get('parent_path') or parent_path
if parent_path == '0':
if len(args) > 2:
parent_path = args[2]
elif len(args) > 1 and isinstance(args[1], str):
parent_path = args[1]
else:
parent_path = kwargs.get('parent_path', '0')
file_name = result.data.get('file_name', '') if result.data else ''
file_id = result.data.get('file_id') if result.data else None
await self.cache_cleaner.on_file_created(
account_id=account_id,
parent_id=parent_path,
file_name=file_name,
file_id=file_id
)
async def _clear_parent_directories(self, account_id: str, parent_ids):
for parent_id in parent_ids:
await self.cache_cleaner._clear_directory_cache(account_id, parent_id)
async def _clear_all_directory_cache(self, account_id: str):
cache_manager = self._get_cleaner_cache_manager()
if not cache_manager:
log_operation("warning", "缓存管理器未初始化,跳过目录缓存清理")
return
try:
prefix = f"dir:{account_id}:"
await cache_manager.clear_by_prefix(prefix)
except Exception as e:
log_operation("error", f"清理目录缓存失败: 账号{account_id}, 错误: {str(e)}")
async def _force_clear_account_cache(self, account_id: str):
cache_manager = self._get_cleaner_cache_manager()
if not cache_manager:
log_operation("warning", "缓存管理器未初始化,跳过强制缓存清理")
return
try:
await cache_manager.clear_account_cache(account_id)
except Exception as e:
log_operation("error", f"强制清理账号缓存失败: 账号{account_id}, 错误: {str(e)}")
def _get_cleaner_cache_manager(self):
if not self.cache_cleaner:
return None
return getattr(self.cache_cleaner, "cache_manager", None)
operation_wrapper = OperationWrapper()
def init_operation_wrapper(cache_manager=None, cache_cleaner=None):
global operation_wrapper
if cache_manager is None:
cache_manager = get_cache_manager()
if cache_cleaner is None:
cache_cleaner = get_cache_cleaner()
operation_wrapper.cache_manager = cache_manager
operation_wrapper.cache_cleaner = cache_cleaner
def with_file_list_cache(func: Callable) -> Callable:
"""list_files 的缓存装饰器:命中走缓存,未命中回源后回写(空列表也要缓存,避免空目录反复穿透)。"""
@wraps(func)
async def wrapper(self, parent_id: str = "0", *args, **kwargs) -> List[FileItem]:
cache_manager = _get_driver_cache_manager(self)
if not cache_manager:
return await _execute_driver_call(self, func, parent_id, *args, **kwargs)
try:
account_id = str(getattr(self, 'account_id', 'default'))
cached_result = await cache_manager.get_directory_cache(
account_id=account_id,
parent_id=parent_id,
page=1
)
if cached_result is not None:
if isinstance(cached_result, list):
# 空列表(合法空目录)或元素是 FileItem 才算有效缓存
if not cached_result or isinstance(cached_result[0], FileItem):
await _record_cache_hit()
return cached_result
else:
# 元素类型不对,可能是历史遗留的旧格式,清掉重新回源
log_operation("warning", f"驱动层缓存元素类型不匹配,清理旧缓存: account={account_id}, parent={parent_id}")
try:
await _clear_directory_cache_prefix(cache_manager, account_id, parent_id)
except Exception:
pass
else:
# 不是 list(例如旧 dict 格式),直接清
log_operation("warning", f"驱动层缓存格式不匹配,清理旧缓存: account={account_id}, parent={parent_id}")
try:
await _clear_directory_cache_prefix(cache_manager, account_id, parent_id)
except Exception:
pass
except Exception as e:
log_operation("warning", f"缓存读取失败: {e}")
await _record_cache_miss()
result = await _execute_driver_call(self, func, parent_id, *args, **kwargs)
if result is not None:
try:
account_id = str(getattr(self, 'account_id', 'default'))
await cache_manager.set_directory_cache(
account_id=account_id,
parent_id=parent_id,
data=result,
page=1
)
except Exception as e:
log_operation("warning", f"缓存设置失败: {e}")
return result
return wrapper
def with_file_info_cache(func: Callable) -> Callable:
"""file_info 的缓存装饰器。"""
@wraps(func)
async def wrapper(self, file_id: str, *args, **kwargs) -> Optional[FileItem]:
cache_manager = _get_driver_cache_manager(self)
if not cache_manager:
return await _execute_driver_call(self, func, file_id, *args, track_time=False, **kwargs)
try:
account_id = str(getattr(self, 'account_id', 'default'))
cached_result = await cache_manager.get_file_info_cache(
account_id=account_id,
file_id=file_id
)
if cached_result and isinstance(cached_result, FileItem):
await _record_cache_hit()
return cached_result
except Exception as e:
log_operation("warning", f"缓存读取失败: {e}")
await _record_cache_miss()
result = await _execute_driver_call(self, func, file_id, *args, track_time=False, **kwargs)
if result:
try:
account_id = str(getattr(self, 'account_id', 'default'))
await cache_manager.set_file_info_cache(
account_id=account_id,
file_id=file_id,
data=result
)
except Exception as e:
log_operation("warning", f"缓存设置失败: {e}")
return result
return wrapper
def with_performance_tracking(func: Callable) -> Callable:
"""无缓存方法的性能统计装饰器。"""
@wraps(func)
async def wrapper(self, *args, **kwargs):
await ensure_driver_auth_request_allowed(self)
await _maybe_increment_api_call(self)
start_time = time.time()
result = await func(self, *args, **kwargs)
await _maybe_record_response_time(self, start_time)
return result
return wrapper
def auto_cleanup_cache(operation_type: str):
"""写操作缓存清理装饰器。operation_type 取值见 OperationWrapper._get_cleanup_handler。"""
return operation_wrapper.with_cache_cleanup(operation_type)
def auto_refresh_cache(account_id_func=None):
"""API 层强制刷新装饰器:调用原函数之前先清 (目录 / path_mapping / webdav_metadata) 缓存。"""
def decorator(func):
@wraps(func)
async def wrapper(*args, **kwargs):
if account_id_func:
account_id = account_id_func(*args, **kwargs)
else:
account_id = _find_attr_value(args, kwargs, 'account_id')
account_id = str(account_id) if account_id is not None else None
parent_id = _find_attr_value(args, kwargs, 'parent_id', '0') or '0'
if operation_wrapper.cache_cleaner and account_id:
try:
await operation_wrapper.cache_cleaner._clear_directory_cache(account_id, parent_id)
except Exception as e:
log_operation("warning", f"刷新前缓存清理失败: {str(e)}")
try:
await operation_wrapper.cache_cleaner.cache_manager.clear_by_prefix(
CacheKeyGenerator.path_mapping_prefix(account_id)
)
except Exception:
pass
try:
await operation_wrapper.cache_cleaner.cache_manager.clear_by_prefix(
CacheKeyGenerator.webdav_metadata_prefix(account_id)
)
except Exception:
pass
result = await func(*args, **kwargs)
return result
return wrapper
return decorator
async def clear_operation_cache(account_id: str, operation_type: str, **params):
"""业务代码用:按 operation_type 手动清理对应的缓存。"""
if not operation_wrapper.cache_cleaner:
log_operation("warning", "缓存清理器未初始化")
return
try:
if operation_type == 'directory_update':
parent_id = params.get('parent_id', '0')
await operation_wrapper.cache_cleaner._clear_directory_cache(account_id, parent_id)
elif operation_type == 'file_created':
parent_id = params.get('parent_id', '0')
file_name = params.get('file_name', '')
file_id = params.get('file_id')
await operation_wrapper.cache_cleaner.on_file_created(
account_id=account_id,
parent_id=parent_id,
file_name=file_name,
file_id=file_id,
)
elif operation_type == 'account_update':
config_changed = params.get('config_changed', True)
await operation_wrapper.cache_cleaner.on_account_updated(account_id, config_changed)
elif operation_type == 'account_delete':
await operation_wrapper.cache_cleaner.on_account_deleted(account_id)
log_operation("debug", f"手动缓存清理完成: {operation_type}")
except Exception as e:
log_operation("error", f"手动缓存清理失败: {str(e)}")
def with_auth_retry(max_retries: int = 1):
"""认证重试装饰器:401/403 触发一次 handle_auth_error(刷新 token 等),再重试原请求。"""
def decorator(func):
@wraps(func)
async def wrapper(self, *args, **kwargs):
last_exception = None
for attempt in range(max_retries + 1):
await ensure_driver_auth_request_allowed(self)
try:
return await func(self, *args, **kwargs)
except Exception as e:
last_exception = e
error_str = str(e).lower()
is_auth_error = (
"401" in str(e) or
"403" in str(e) or
"unauthorized" in error_str or
"token" in error_str and ("invalid" in error_str or "expired" in error_str) or
"认证失败" in str(e) or
"token认证失败" in str(e)
)
if attempt == 0 and is_auth_error:
log_operation("warning", f"检测到认证错误,尝试刷新: {e}")
try:
from core.auth_manager import handle_auth_error
account_id = getattr(self, 'account_id', None)
if account_id:
auth_handled = await handle_auth_error(account_id)
if auth_handled:
log_operation("info", "认证刷新成功,重试请求")
continue
else:
log_operation("error", "认证刷新失败")
else:
log_operation("error", "无法获取账号ID,跳过认证处理")
except Exception as auth_error:
log_operation("error", f"认证处理异常: {auth_error}")
break
raise last_exception
return wrapper
return decorator
def get_operation_wrapper():
return operation_wrapper