feat: 实现功能3强制更新

- 新增 ForceUpdateCommand 类实现强制更新功能
- 添加 force-update CLI 命令支持
- 实现参数验证、配置对比、CDN缓存刷新
- 添加完整的测试用例和使用示例
- 更新文档和进度跟踪
- 支持模拟环境测试
- 添加警告提示和错误处理

主要文件:
- src/config_man/core/force_update_command.py (新增)
- src/config_man/cli/main.py (更新)
- examples/test_force_update.py (新增)
- examples/force_update_usage.py (新增)
- docs/progress-tracker.md (更新)
This commit is contained in:
2025-08-08 01:50:30 +08:00
parent 9eddb3133c
commit f534dc52cb
19 changed files with 2594 additions and 285 deletions

View File

@@ -0,0 +1,275 @@
"""
Cloudflare CDN刷新模块
提供Cloudflare CDN缓存刷新功能。
"""
import os
import requests
from typing import List, Optional, Dict
from .config import config
class CloudflareCDN:
"""Cloudflare CDN管理器"""
def __init__(self):
# 优先从配置文件读取,然后从环境变量读取
self.api_token = config.get_cloudflare_api_token() or os.getenv('CLOUDFLARE_API_TOKEN')
self.zone_id = config.get_cloudflare_zone_id() or os.getenv('CLOUDFLARE_ZONE_ID')
self.api_base_url = "https://api.cloudflare.com/client/v4"
# 从配置文件获取CDN配置
cdn_config = config.get_cdn_config()
self.timeout = cdn_config.get('timeout', 10)
self.retry_count = cdn_config.get('retry_count', 3)
def is_configured(self) -> bool:
"""检查Cloudflare配置是否完整"""
return bool(self.api_token and self.zone_id)
def purge_cache_by_urls(self, urls: List[str]) -> bool:
"""
通过URL列表清除缓存
Args:
urls: 需要清除缓存的URL列表
Returns:
是否成功
"""
if not self.is_configured():
return False
try:
headers = {
'Authorization': f'Bearer {self.api_token}',
'Content-Type': 'application/json'
}
data = {
'files': urls
}
url = f"{self.api_base_url}/zones/{self.zone_id}/purge_cache"
response = requests.post(
url,
headers=headers,
json=data,
timeout=self.timeout
)
if response.status_code == 200:
result = response.json()
if result.get('success'):
return True
else:
print(f"Cloudflare API错误: {result.get('errors', [])}")
return False
else:
print(f"Cloudflare API请求失败: {response.status_code} - {response.text}")
return False
except requests.RequestException as e:
print(f"Cloudflare API请求异常: {e}")
return False
except Exception as e:
print(f"Cloudflare缓存清除失败: {e}")
return False
def purge_cache_by_tags(self, tags: List[str]) -> bool:
"""
通过标签清除缓存
Args:
tags: 需要清除缓存的标签列表
Returns:
是否成功
"""
if not self.is_configured():
return False
try:
headers = {
'Authorization': f'Bearer {self.api_token}',
'Content-Type': 'application/json'
}
data = {
'tags': tags
}
url = f"{self.api_base_url}/zones/{self.zone_id}/purge_cache"
response = requests.post(
url,
headers=headers,
json=data,
timeout=self.timeout
)
if response.status_code == 200:
result = response.json()
if result.get('success'):
return True
else:
print(f"Cloudflare API错误: {result.get('errors', [])}")
return False
else:
print(f"Cloudflare API请求失败: {response.status_code} - {response.text}")
return False
except requests.RequestException as e:
print(f"Cloudflare API请求异常: {e}")
return False
except Exception as e:
print(f"Cloudflare缓存清除失败: {e}")
return False
def purge_entire_cache(self) -> bool:
"""
清除整个域名的缓存
Returns:
是否成功
"""
if not self.is_configured():
return False
try:
headers = {
'Authorization': f'Bearer {self.api_token}',
'Content-Type': 'application/json'
}
data = {
'purge_everything': True
}
url = f"{self.api_base_url}/zones/{self.zone_id}/purge_cache"
response = requests.post(
url,
headers=headers,
json=data,
timeout=self.timeout
)
if response.status_code == 200:
result = response.json()
if result.get('success'):
return True
else:
print(f"Cloudflare API错误: {result.get('errors', [])}")
return False
else:
print(f"Cloudflare API请求失败: {response.status_code} - {response.text}")
return False
except requests.RequestException as e:
print(f"Cloudflare API请求异常: {e}")
return False
except Exception as e:
print(f"Cloudflare缓存清除失败: {e}")
return False
def get_zone_info(self) -> Optional[Dict]:
"""
获取域名信息
Returns:
域名信息字典如果失败返回None
"""
if not self.is_configured():
return None
try:
headers = {
'Authorization': f'Bearer {self.api_token}',
'Content-Type': 'application/json'
}
url = f"{self.api_base_url}/zones/{self.zone_id}"
response = requests.get(
url,
headers=headers,
timeout=self.timeout
)
if response.status_code == 200:
result = response.json()
if result.get('success'):
return result.get('result', {})
else:
print(f"Cloudflare API错误: {result.get('errors', [])}")
return None
else:
print(f"Cloudflare API请求失败: {response.status_code} - {response.text}")
return None
except requests.RequestException as e:
print(f"Cloudflare API请求异常: {e}")
return None
except Exception as e:
print(f"获取Cloudflare域名信息失败: {e}")
return None
def refresh_cloudflare_cache(urls: List[str]) -> bool:
"""
刷新Cloudflare CDN缓存
Args:
urls: 需要刷新的URL列表
Returns:
是否成功
"""
cdn = CloudflareCDN()
if not cdn.is_configured():
print("警告: Cloudflare配置不完整跳过CDN缓存刷新")
print("请设置以下环境变量:")
print(" CLOUDFLARE_API_TOKEN: Cloudflare API Token")
print(" CLOUDFLARE_ZONE_ID: Cloudflare Zone ID")
return False
return cdn.purge_cache_by_urls(urls)
def refresh_cloudflare_cache_by_tags(tags: List[str]) -> bool:
"""
通过标签刷新Cloudflare CDN缓存
Args:
tags: 需要刷新的标签列表
Returns:
是否成功
"""
cdn = CloudflareCDN()
if not cdn.is_configured():
print("警告: Cloudflare配置不完整跳过CDN缓存刷新")
return False
return cdn.purge_cache_by_tags(tags)
def refresh_entire_cloudflare_cache() -> bool:
"""
刷新整个Cloudflare CDN缓存
Returns:
是否成功
"""
cdn = CloudflareCDN()
if not cdn.is_configured():
print("警告: Cloudflare配置不完整跳过CDN缓存刷新")
return False
return cdn.purge_entire_cache()

View File

@@ -49,7 +49,11 @@ class Config:
"cdn": {
"base_url": "",
"timeout": 10,
"retry_count": 3
"retry_count": 3,
"cloudflare": {
"api_token": "",
"zone_id": ""
}
},
"crypto": {
"algorithm": "DES",
@@ -98,6 +102,8 @@ class Config:
"CONFIG_MAN_CDN_BASE_URL": ("cdn", "base_url"),
"CONFIG_MAN_CDN_TIMEOUT": ("cdn", "timeout"),
"CONFIG_MAN_CDN_RETRY_COUNT": ("cdn", "retry_count"),
"CONFIG_MAN_CDN_CLOUDFLARE_API_TOKEN": ("cdn", "cloudflare", "api_token"),
"CONFIG_MAN_CDN_CLOUDFLARE_ZONE_ID": ("cdn", "cloudflare", "zone_id"),
"CONFIG_MAN_CRYPTO_KEY": ("crypto", "key"),
"CONFIG_MAN_CRYPTO_ALGORITHM": ("crypto", "algorithm"),
"CONFIG_MAN_LOG_LEVEL": ("logging", "level"),
@@ -216,6 +222,18 @@ class Config:
def get_logging_config(self) -> Dict[str, Any]:
"""获取日志配置"""
return self.get('logging', {})
def get_cloudflare_config(self) -> Dict[str, Any]:
"""获取Cloudflare配置"""
return self.get('cdn.cloudflare', {})
def get_cloudflare_api_token(self) -> str:
"""获取Cloudflare API Token"""
return self.get('cdn.cloudflare.api_token', '')
def get_cloudflare_zone_id(self) -> str:
"""获取Cloudflare Zone ID"""
return self.get('cdn.cloudflare.zone_id', '')
# 全局配置实例

View File

@@ -123,6 +123,28 @@ class MockRclone:
with open(local_path, 'r', encoding='utf-8') as f:
return f.read().strip()
def mock_rclone_rcat(self, path: str, content: str):
"""模拟rclone rcat命令上传文件"""
# 处理rclone路径格式remote:path 或 path
if ':' in path:
# 如果包含远程名称,提取路径部分
_, actual_path = path.split(':', 1)
else:
# 如果没有远程名称,直接使用路径
actual_path = path
# 将路径转换为本地文件路径
local_path = os.path.join(self.test_data_path, actual_path)
# 确保目录存在
os.makedirs(os.path.dirname(local_path), exist_ok=True)
# 保存内容到文件
with open(local_path, 'w', encoding='utf-8') as f:
f.write(content)
print(f"模拟上传文件: {path} -> {local_path}")
def patch_rclone():
@@ -132,26 +154,56 @@ def patch_rclone():
original_run = subprocess.run
def mock_run(cmd, *args, **kwargs):
if len(cmd) >= 2 and cmd[0] == 'rclone' and cmd[1] == 'cat':
# 模拟rclone cat命令
mock_rclone = MockRclone()
try:
content = mock_rclone.mock_rclone_cat(cmd[2])
# 返回模拟的subprocess结果
from types import SimpleNamespace
result = SimpleNamespace()
result.returncode = 0
result.stdout = content
result.stderr = ""
return result
except FileNotFoundError as e:
# 返回错误结果
from types import SimpleNamespace
result = SimpleNamespace()
result.returncode = 1
result.stdout = ""
result.stderr = str(e)
return result
if len(cmd) >= 2 and cmd[0] == 'rclone':
if cmd[1] == 'cat':
# 模拟rclone cat命令
mock_rclone = MockRclone()
try:
content = mock_rclone.mock_rclone_cat(cmd[2])
# 返回模拟的subprocess结果
from types import SimpleNamespace
result = SimpleNamespace()
result.returncode = 0
result.stdout = content
result.stderr = ""
return result
except FileNotFoundError as e:
# 返回错误结果
from types import SimpleNamespace
result = SimpleNamespace()
result.returncode = 1
result.stdout = ""
result.stderr = str(e)
return result
elif cmd[1] == 'rcat':
# 模拟rclone rcat命令上传文件
mock_rclone = MockRclone()
try:
# 获取文件路径
file_path = cmd[2]
# 从stdin读取内容
content = kwargs.get('input', '')
if not content and 'input' in kwargs:
content = kwargs['input']
# 保存到本地文件
mock_rclone.mock_rclone_rcat(file_path, content)
# 返回成功结果
from types import SimpleNamespace
result = SimpleNamespace()
result.returncode = 0
result.stdout = ""
result.stderr = ""
return result
except Exception as e:
# 返回错误结果
from types import SimpleNamespace
result = SimpleNamespace()
result.returncode = 1
result.stdout = ""
result.stderr = str(e)
return result
else:
# 对于其他命令,使用原始实现
return original_run(cmd, *args, **kwargs)