fix cache

This commit is contained in:
jxxghp
2025-08-23 14:06:44 +08:00
parent fcadac2adb
commit 343109836f
5 changed files with 109 additions and 265 deletions
+70 -227
View File
@@ -28,6 +28,49 @@ class CacheBackend(ABC):
缓存后端基类,定义通用的缓存接口 缓存后端基类,定义通用的缓存接口
""" """
# Dict-like operations
def __getitem__(self, key: str) -> Any:
"""
获取缓存项,类似 dict[key]
"""
value = self.get(key)
if value is None:
raise KeyError(key)
return value
def __setitem__(self, key: str, value: Any) -> None:
"""
设置缓存项,类似 dict[key] = value
"""
self.set(key, value)
def __delitem__(self, key: str) -> None:
"""
删除缓存项,类似 del dict[key]
"""
if not self.exists(key):
raise KeyError(key)
self.delete(key)
def __contains__(self, key: str) -> bool:
"""
检查键是否存在,类似 key in dict
"""
return self.exists(key)
def __iter__(self):
"""
返回缓存的迭代器,类似 iter(dict)
"""
for key, _ in self.items():
yield key
def __len__(self) -> int:
"""
返回缓存项的数量,类似 len(dict)
"""
return sum(1 for _ in self.items())
@abstractmethod @abstractmethod
def set(self, key: str, value: Any, ttl: Optional[int] = None, def set(self, key: str, value: Any, ttl: Optional[int] = None,
region: Optional[str] = DEFAULT_CACHE_REGION, **kwargs) -> None: region: Optional[str] = DEFAULT_CACHE_REGION, **kwargs) -> None:
@@ -74,15 +117,6 @@ class CacheBackend(ABC):
""" """
pass pass
@abstractmethod
def clear(self, region: Optional[str] = DEFAULT_CACHE_REGION) -> None:
"""
清除指定区域的缓存或全部缓存
:param region: 缓存的区,为None时清空所有区缓存
"""
pass
@abstractmethod @abstractmethod
def items(self, region: Optional[str] = DEFAULT_CACHE_REGION) -> Generator[Tuple[str, Any], None, None]: def items(self, region: Optional[str] = DEFAULT_CACHE_REGION) -> Generator[Tuple[str, Any], None, None]:
""" """
@@ -93,56 +127,6 @@ class CacheBackend(ABC):
""" """
pass pass
@abstractmethod
def close(self) -> None:
"""
关闭缓存连接
"""
pass
# Dict-like operations
def __getitem__(self, key: str) -> Any:
"""
获取缓存项,类似 dict[key]
"""
value = self.get(key)
if value is None:
raise KeyError(key)
return value
def __setitem__(self, key: str, value: Any) -> None:
"""
设置缓存项,类似 dict[key] = value
"""
self.set(key, value)
def __delitem__(self, key: str) -> None:
"""
删除缓存项,类似 del dict[key]
"""
if not self.exists(key):
raise KeyError(key)
self.delete(key)
def __contains__(self, key: str) -> bool:
"""
检查键是否存在,类似 key in dict
"""
return self.exists(key)
def __iter__(self):
"""
返回缓存的迭代器,类似 iter(dict)
"""
for key, _ in self.items():
yield key
def __len__(self) -> int:
"""
返回缓存项的数量,类似 len(dict)
"""
return sum(1 for _ in self.items())
def keys(self, region: Optional[str] = DEFAULT_CACHE_REGION) -> Generator[str, None, None]: def keys(self, region: Optional[str] = DEFAULT_CACHE_REGION) -> Generator[str, None, None]:
""" """
获取所有缓存键,类似 dict.keys() 获取所有缓存键,类似 dict.keys()
@@ -199,13 +183,31 @@ class CacheBackend(ABC):
return default return default
return value return value
def get_region(self, region: Optional[str] = None) -> str: @abstractmethod
def clear(self, region: Optional[str] = DEFAULT_CACHE_REGION) -> None:
"""
清除指定区域的缓存或全部缓存
:param region: 缓存的区,为None时清空所有区缓存
"""
pass
@abstractmethod
def close(self) -> None:
"""
关闭缓存连接
"""
pass
@staticmethod
def get_region(region: Optional[str] = None) -> str:
""" """
获取缓存的区 获取缓存的区
""" """
return f"region:{region}" if region else "region:default" return f"region:{region}" if region else "region:default"
def get_cache_key(self, func, args, kwargs) -> str: @staticmethod
def get_cache_key(func, args, kwargs) -> str:
""" """
根据函数和参数生成缓存键 根据函数和参数生成缓存键
@@ -229,14 +231,15 @@ class CacheBackend(ABC):
# 使用有序参数生成缓存键 # 使用有序参数生成缓存键
return f"{func.__name__}_{hashkey(*keys)}" return f"{func.__name__}_{hashkey(*keys)}"
def is_redis(self) -> bool: @staticmethod
def is_redis() -> bool:
""" """
判断当前缓存后端是否为 Redis 判断当前缓存后端是否为 Redis
""" """
return isinstance(self, RedisBackend) or isinstance(self, AsyncRedisBackend) return settings.CACHE_BACKEND_TYPE == "redis"
class AsyncCacheBackend(ABC): class AsyncCacheBackend(CacheBackend):
""" """
缓存后端基类,定义通用的缓存接口(异步) 缓存后端基类,定义通用的缓存接口(异步)
""" """
@@ -313,64 +316,18 @@ class AsyncCacheBackend(ABC):
""" """
pass pass
# Async dict-like operations
async def __getitem__(self, key: str) -> Any:
"""
获取缓存项,类似 dict[key](异步)
"""
value = await self.get(key)
if value is None:
raise KeyError(key)
return value
async def __setitem__(self, key: str, value: Any) -> None:
"""
设置缓存项,类似 dict[key] = value(异步)
"""
await self.set(key, value)
async def __delitem__(self, key: str) -> None:
"""
删除缓存项,类似 del dict[key](异步)
"""
if not await self.exists(key):
raise KeyError(key)
await self.delete(key)
async def __contains__(self, key: str) -> bool:
"""
检查键是否存在,类似 key in dict(异步)
"""
return await self.exists(key)
async def __aiter__(self):
"""
返回缓存的异步迭代器,类似 aiter(dict)
"""
async for key, _ in self.items():
yield key
async def __len__(self) -> int:
"""
返回缓存项的数量,类似 len(dict)(异步)
"""
count = 0
async for _ in self.items():
count += 1
return count
async def keys(self, region: Optional[str] = DEFAULT_CACHE_REGION) -> AsyncGenerator[str, None]: async def keys(self, region: Optional[str] = DEFAULT_CACHE_REGION) -> AsyncGenerator[str, None]:
""" """
获取所有缓存键,类似 dict.keys()(异步) 获取所有缓存键,类似 dict.keys()(异步)
""" """
async for key, _ in self.items(region=region): async for key, _ in await self.items(region=region):
yield key yield key
async def values(self, region: Optional[str] = DEFAULT_CACHE_REGION) -> AsyncGenerator[Any, None]: async def values(self, region: Optional[str] = DEFAULT_CACHE_REGION) -> AsyncGenerator[Any, None]:
""" """
获取所有缓存值,类似 dict.values()(异步) 获取所有缓存值,类似 dict.values()(异步)
""" """
async for _, value in self.items(region=region): async for _, value in await self.items(region=region):
yield value yield value
async def update(self, other: Dict[str, Any], region: Optional[str] = DEFAULT_CACHE_REGION, async def update(self, other: Dict[str, Any], region: Optional[str] = DEFAULT_CACHE_REGION,
@@ -398,7 +355,7 @@ class AsyncCacheBackend(ABC):
弹出最后一个缓存项,类似 dict.popitem()(异步) 弹出最后一个缓存项,类似 dict.popitem()(异步)
""" """
items = [] items = []
async for item in self.items(region=region): async for item in await self.items(region=region):
items.append(item) items.append(item)
if not items: if not items:
raise KeyError("popitem(): cache is empty") raise KeyError("popitem(): cache is empty")
@@ -417,42 +374,6 @@ class AsyncCacheBackend(ABC):
return default return default
return value return value
def get_region(self, region: Optional[str] = None) -> str:
"""
获取缓存的区
"""
return f"region:{region}" if region else "region:default"
def get_cache_key(self, func, args, kwargs) -> str:
"""
根据函数和参数生成缓存键
:param func: 函数对象
:param args: 位置参数
:param kwargs: 关键字参数
:return: 缓存键
"""
signature = inspect.signature(func)
# 绑定传入的参数并应用默认值
bound = signature.bind(*args, **kwargs)
bound.apply_defaults()
# 忽略第一个参数,如果它是实例(self)或类(cls)
parameters = list(signature.parameters.keys())
if parameters and parameters[0] in ("self", "cls"):
bound.arguments.pop(parameters[0], None)
# 按照函数签名顺序提取参数值列表
keys = [
bound.arguments[param] for param in signature.parameters if param in bound.arguments
]
# 使用有序参数生成缓存键
return f"{func.__name__}_{hashkey(*keys)}"
def is_redis(self) -> bool:
"""
判断当前缓存后端是否为 Redis
"""
return isinstance(self, RedisBackend) or isinstance(self, AsyncRedisBackend)
class MemoryBackend(CacheBackend): class MemoryBackend(CacheBackend):
""" """
@@ -1005,84 +926,6 @@ def Cache(maxsize: Optional[int] = None, ttl: Optional[int] = None) -> CacheBack
return MemoryBackend(maxsize=maxsize, ttl=ttl) return MemoryBackend(maxsize=maxsize, ttl=ttl)
class TTLCache:
"""
TTL缓存类,现在只是一个简单的包装器,建议直接使用Cache类
注意:此类已过时,建议直接使用Cache类,它提供了完整的dict操作特性
"""
def __init__(self, region: Optional[str] = DEFAULT_CACHE_REGION,
maxsize: int = None, ttl: int = None):
"""
初始化TTL缓存
:param region: 缓存的区,默认为 DEFAULT_CACHE_REGION
:param maxsize: 缓存的最大条目数
:param ttl: 缓存的存活时间,单位秒
"""
self.region = region
self.maxsize = maxsize
self.ttl = ttl
self._backend = Cache(maxsize=maxsize, ttl=ttl)
def set(self, key: str, value: Any, ttl: Optional[int] = None):
"""
设置缓存项,支持自定义 TTL
"""
try:
ttl = ttl or self.ttl
self._backend.set(key, value, ttl=ttl, region=self.region)
except Exception as e:
logger.warning(f"缓存设置失败: {e}")
def get(self, key: str, default: Any = None):
"""
获取缓存项,如果不存在返回默认值
"""
try:
value = self._backend.get(key, region=self.region)
if value is not None:
return value
except Exception as e:
logger.warning(f"缓存获取失败: {e}")
return default
def delete(self, key: str):
"""
删除缓存项
"""
try:
self._backend.delete(key, region=self.region)
except Exception as e:
logger.warning(f"缓存删除失败: {e}")
def clear(self):
"""
清空缓存
"""
try:
self._backend.clear(region=self.region)
except Exception as e:
logger.warning(f"缓存清空失败: {e}")
def is_redis(self) -> bool:
"""
判断当前缓存后端是否为 Redis
"""
return self._backend.is_redis()
def close(self):
"""
关闭缓存连接
"""
try:
self._backend.close()
except Exception as e:
logger.warning(f"缓存关闭失败: {e}")
def cached(region: Optional[str] = None, maxsize: Optional[int] = 1024, ttl: Optional[int] = None, def cached(region: Optional[str] = None, maxsize: Optional[int] = 1024, ttl: Optional[int] = None,
skip_none: Optional[bool] = True, skip_empty: Optional[bool] = False): skip_none: Optional[bool] = True, skip_empty: Optional[bool] = False):
""" """
+3 -7
View File
@@ -16,11 +16,10 @@ from app.db.systemconfig_oper import SystemConfigOper
from app.log import logger from app.log import logger
from app.schemas.types import MediaType, SystemConfigKey from app.schemas.types import MediaType, SystemConfigKey
from app.utils.http import RequestUtils from app.utils.http import RequestUtils
from app.utils.singleton import WeakSingleton
from app.utils.string import StringUtils from app.utils.string import StringUtils
class TorrentHelper(metaclass=WeakSingleton): class TorrentHelper:
""" """
种子帮助类 种子帮助类
""" """
@@ -199,15 +198,12 @@ class TorrentHelper(metaclass=WeakSingleton):
:param torrent_content: 种子内容 :param torrent_content: 种子内容
:return: 文件夹名、文件清单,单文件种子返回空文件夹名 :return: 文件夹名、文件清单,单文件种子返回空文件夹名
""" """
if not torrent_content: if not torrent_content:
return "", [] return "", []
# 检查是否为磁力链接 # 检查是否为磁力链接
if isinstance(torrent_content, str) and torrent_content.startswith("magnet:"): if StringUtils.is_magnet_link(torrent_content):
# 磁力链接无法预先获取文件信息,返回空
return "", []
elif isinstance(torrent_content, bytes) and torrent_content.startswith(b"magnet:"):
# 磁力链接无法预先获取文件信息,返回空
return "", [] return "", []
try: try:
+9 -13
View File
@@ -118,23 +118,17 @@ class QbittorrentModule(_ModuleBase, _DownloaderBase[Qbittorrent]):
if content.exists(): if content.exists():
torrent_content = content.read_bytes() torrent_content = content.read_bytes()
else: else:
# 缓存处理器
cache_backend = FileCache()
# 读取缓存的种子文件 # 读取缓存的种子文件
torrent_content = cache_backend.get(content.as_posix(), region="torrents") torrent_content = FileCache().get(content.as_posix(), region="torrents")
else: else:
torrent_content = content torrent_content = content
# 检查是否为磁力链接
if isinstance(torrent_content, str) and torrent_content.startswith("magnet:"):
# 磁力链接不需要解析种子文件
return None, torrent_content
elif isinstance(torrent_content, bytes) and torrent_content.startswith(b"magnet:"):
# 磁力链接不需要解析种子文件
return None, torrent_content
if torrent_content: if torrent_content:
torrent_info = Torrent.from_string(torrent_content) # 检查是否为磁力链接
if StringUtils.is_magnet_link(torrent_content):
return None, torrent_content
else:
torrent_info = Torrent.from_string(torrent_content)
return torrent_info, torrent_content return torrent_info, torrent_content
except Exception as e: except Exception as e:
@@ -147,7 +141,9 @@ class QbittorrentModule(_ModuleBase, _DownloaderBase[Qbittorrent]):
# 读取种子的名称 # 读取种子的名称
torrent, content = __get_torrent_info() torrent, content = __get_torrent_info()
# 检查是否为磁力链接 # 检查是否为磁力链接
is_magnet = isinstance(content, str) and content.startswith("magnet:") or isinstance(content, bytes) and content.startswith(b"magnet:") is_magnet = isinstance(content, str) and content.startswith("magnet:") or isinstance(content,
bytes) and content.startswith(
b"magnet:")
if not torrent and not is_magnet: if not torrent and not is_magnet:
return None, None, None, f"添加种子任务失败:无法读取种子文件" return None, None, None, f"添加种子任务失败:无法读取种子文件"
+9 -13
View File
@@ -119,23 +119,17 @@ class TransmissionModule(_ModuleBase, _DownloaderBase[Transmission]):
if content.exists(): if content.exists():
torrent_content = content.read_bytes() torrent_content = content.read_bytes()
else: else:
# 缓存处理器
cache_backend = FileCache()
# 读取缓存的种子文件 # 读取缓存的种子文件
torrent_content = cache_backend.get(content.as_posix(), region="torrents") torrent_content = FileCache().get(content.as_posix(), region="torrents")
else: else:
torrent_content = content torrent_content = content
# 检查是否为磁力链接
if isinstance(torrent_content, str) and torrent_content.startswith("magnet:"):
# 磁力链接不需要解析种子文件
return None, torrent_content
elif isinstance(torrent_content, bytes) and torrent_content.startswith(b"magnet:"):
# 磁力链接不需要解析种子文件
return None, torrent_content
if torrent_content: if torrent_content:
torrent_info = Torrent.from_string(torrent_content) # 检查是否为磁力链接
if StringUtils.is_magnet_link(torrent_content):
return None, torrent_content
else:
torrent_info = Torrent.from_string(torrent_content)
return torrent_info, torrent_content return torrent_info, torrent_content
except Exception as e: except Exception as e:
@@ -148,7 +142,9 @@ class TransmissionModule(_ModuleBase, _DownloaderBase[Transmission]):
# 读取种子的名称 # 读取种子的名称
torrent, content = __get_torrent_info() torrent, content = __get_torrent_info()
# 检查是否为磁力链接 # 检查是否为磁力链接
is_magnet = isinstance(content, str) and content.startswith("magnet:") or isinstance(content, bytes) and content.startswith(b"magnet:") is_magnet = isinstance(content, str) and content.startswith("magnet:") or isinstance(content,
bytes) and content.startswith(
b"magnet:")
if not torrent and not is_magnet: if not torrent and not is_magnet:
return None, None, None, f"添加种子任务失败:无法读取种子文件" return None, None, None, f"添加种子任务失败:无法读取种子文件"
+14 -1
View File
@@ -229,7 +229,7 @@ class StringUtils:
size = float(size) size = float(size)
d = [(1024 - 1, 'K'), (1024 ** 2 - 1, 'M'), (1024 ** 3 - 1, 'G'), (1024 ** 4 - 1, 'T')] d = [(1024 - 1, 'K'), (1024 ** 2 - 1, 'M'), (1024 ** 3 - 1, 'G'), (1024 ** 4 - 1, 'T')]
s = [x[0] for x in d] s = [x[0] for x in d]
index = bisect.bisect_left(s, size) - 1 # noqa index = bisect.bisect_left(s, size) - 1 # noqa
if index == -1: if index == -1:
return str(size) + "B" return str(size) + "B"
else: else:
@@ -925,3 +925,16 @@ class StringUtils:
if re.match(r'^[a-zA-Z0-9.-]+(\.[a-zA-Z]{2,})?$', text): if re.match(r'^[a-zA-Z0-9.-]+(\.[a-zA-Z]{2,})?$', text):
return True return True
return False return False
@staticmethod
def is_magnet_link(content: Union[str, bytes]) -> bool:
"""
判断内容是否为磁力链接
"""
if not content:
return False
if isinstance(content, str) and content.startswith("magnet:"):
return True
if isinstance(content, bytes) and content.startswith(b"magnet:"):
return True
return False