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:
275
src/config_man/utils/cloudflare.py
Normal file
275
src/config_man/utils/cloudflare.py
Normal 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()
|
||||
@@ -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', '')
|
||||
|
||||
|
||||
# 全局配置实例
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user