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

@@ -1,6 +1,8 @@
import click
import os
from ..core.view_command import ViewCommand
from ..core.suggest_update_command import SuggestUpdateCommand
from ..core.force_update_command import ForceUpdateCommand
from ..utils.mock_rclone import patch_rclone
from ..utils.config import config
@@ -61,16 +63,40 @@ def view(version, json_format, storage_path, cdn_url, mock):
help='平台类型')
@click.option('--target-version', required=True, help='目标版本号')
@click.option('--update-version', required=True, help='更新版本号')
def suggest_update(platform, target_version, update_version):
@click.option('--storage-path', help='对象存储路径前缀(覆盖配置文件)')
@click.option('--cdn-url', help='CDN基础URL覆盖配置文件')
@click.option('--mock', is_flag=True, help='使用模拟环境进行测试')
def suggest_update(platform, target_version, update_version, storage_path, cdn_url, mock):
"""建议更新
在目标版本的配置文件中添加 Ver_New 属性,提示用户有新版本可用。
示例:
config-man suggest-update --platform android --target-version 0.29 --update-version 0.30
config-man suggest-update --platform ios --target-version 0.29 --update-version 0.30 --mock
"""
click.echo("建议更新功能尚未实现")
return 1
try:
# 如果使用模拟环境启用rclone模拟
if mock:
patch_rclone()
click.echo("使用模拟环境进行测试...")
else:
# 检查rclone是否可用
if not _check_rclone():
click.echo("错误: rclone 未安装或不可用。请先安装 rclone。")
return 1
# 创建建议更新命令处理器
suggest_cmd = SuggestUpdateCommand(storage_path, cdn_url)
# 执行建议更新命令
success = suggest_cmd.execute(platform, target_version, update_version)
return 0 if success else 1
except Exception as e:
click.echo(f"错误: {str(e)}")
return 1
@cli.command()
@@ -78,16 +104,40 @@ def suggest_update(platform, target_version, update_version):
help='平台类型')
@click.option('--target-version', required=True, help='目标版本号')
@click.option('--update-version', required=True, help='更新版本号')
def force_update(platform, target_version, update_version):
@click.option('--storage-path', help='对象存储路径前缀(覆盖配置文件)')
@click.option('--cdn-url', help='CDN基础URL覆盖配置文件')
@click.option('--mock', is_flag=True, help='使用模拟环境进行测试')
def force_update(platform, target_version, update_version, storage_path, cdn_url, mock):
"""强制更新
直接更新目标版本的 Ver 属性,强制用户升级到新版本。
示例:
config-man force-update --platform ios --target-version 0.29 --update-version 0.30
config-man force-update --platform android --target-version 0.29 --update-version 0.30 --mock
"""
click.echo("强制更新功能尚未实现")
return 1
try:
# 如果使用模拟环境启用rclone模拟
if mock:
patch_rclone()
click.echo("使用模拟环境进行测试...")
else:
# 检查rclone是否可用
if not _check_rclone():
click.echo("错误: rclone 未安装或不可用。请先安装 rclone。")
return 1
# 创建强制更新命令处理器
force_cmd = ForceUpdateCommand(storage_path, cdn_url)
# 执行强制更新命令
success = force_cmd.execute(platform, target_version, update_version)
return 0 if success else 1
except Exception as e:
click.echo(f"错误: {str(e)}")
return 1
@cli.command()
@@ -124,6 +174,33 @@ def show_config():
console.print(f"\n配置文件路径: {config.config_file}")
@cli.command()
@click.option('--api-token', required=True, help='Cloudflare API Token')
@click.option('--zone-id', required=True, help='Cloudflare Zone ID')
def setup_cloudflare(api_token, zone_id):
"""设置Cloudflare CDN配置
设置Cloudflare API Token和Zone ID。
示例:
config-man setup-cloudflare --api-token your_api_token --zone-id your_zone_id
"""
try:
# 设置Cloudflare配置
config.set('cdn.cloudflare.api_token', api_token)
config.set('cdn.cloudflare.zone_id', zone_id)
config.save()
click.echo("✅ Cloudflare配置已更新")
click.echo(f" API Token: {api_token[:10]}...")
click.echo(f" Zone ID: {zone_id}")
click.echo("\n💡 提示: 配置已保存到配置文件,无需重复设置环境变量")
except Exception as e:
click.echo(f"❌ 设置Cloudflare配置失败: {e}")
return 1
@cli.command()
@click.option('--key', required=True, help='配置键,如 storage.path')
@click.option('--value', required=True, help='配置值')
@@ -135,6 +212,8 @@ def set_config(key, value):
示例:
config-man set-config --key storage.path --value custom_path
config-man set-config --key cdn.base_url --value https://cdn.example.com
config-man set-config --key cdn.cloudflare.api_token --value your_api_token
config-man set-config --key cdn.cloudflare.zone_id --value your_zone_id
"""
try:
# 尝试转换数值类型

View File

@@ -7,5 +7,7 @@
from .config_manager import ConfigManager
from .display_manager import DisplayManager
from .view_command import ViewCommand
from .suggest_update_command import SuggestUpdateCommand
from .force_update_command import ForceUpdateCommand
__all__ = ["ConfigManager", "DisplayManager", "ViewCommand"]
__all__ = ["ConfigManager", "DisplayManager", "ViewCommand", "SuggestUpdateCommand", "ForceUpdateCommand"]

View File

@@ -109,4 +109,8 @@ class DisplayManager:
def display_info(self, message: str):
"""显示信息"""
self.console.print(f"[bold blue]信息: {message}[/bold blue]")
self.console.print(f"[bold blue]信息: {message}[/bold blue]")
def display_success(self, message: str):
"""显示成功信息"""
self.console.print(f"[bold green]✅ 成功: {message}[/bold green]")

View File

@@ -0,0 +1,268 @@
import json
import subprocess
import requests
from typing import Dict, Optional, List
from .config_manager import ConfigManager
from .display_manager import DisplayManager
from ..utils.crypto import encrypt
from ..utils.cloudflare import refresh_cloudflare_cache
class ForceUpdateCommand:
"""强制更新命令处理器"""
def __init__(self, storage_path: str = "ab", cdn_base_url: str = ""):
self.config_manager = ConfigManager(storage_path, cdn_base_url)
self.display_manager = DisplayManager()
def execute(self, platform: str, target_version: str, update_version: str) -> bool:
"""执行强制更新命令"""
# 验证参数
if not self._validate_parameters(platform, target_version, update_version):
return False
self.display_manager.display_info(
f"正在为 {platform} 平台执行强制更新: "
f"目标版本 {target_version} -> 更新版本 {update_version}"
)
try:
# 1. 获取更新版本的配置提取Ver值
update_ver_value = self._get_update_version_ver(platform, update_version)
if not update_ver_value:
self.display_manager.display_error(
f"无法获取更新版本 {update_version} 的Ver值"
)
return False
# 2. 获取目标版本的配置(保存原始配置用于对比)
original_config = self._get_target_config(platform, target_version)
if not original_config:
self.display_manager.display_error(
f"无法获取目标版本 {target_version} 的配置"
)
return False
# 3. 创建更新后的配置复制原始配置并更新Ver
updated_config = original_config.copy()
updated_config["Ver"] = update_ver_value
# 4. 加密并上传到对象存储
success = self._upload_config(platform, target_version, updated_config)
if not success:
return False
# 5. 刷新CDN缓存
success = self._refresh_cdn_cache(platform, target_version)
if not success:
self.display_manager.display_warning("配置已更新但CDN缓存刷新失败")
# 6. 显示结果和对比
self._display_success_result(platform, target_version, update_version, update_ver_value)
self._display_config_comparison(platform, target_version, original_config, updated_config)
return True
except Exception as e:
self.display_manager.display_error(f"强制更新失败: {str(e)}")
return False
def _validate_parameters(self, platform: str, target_version: str, update_version: str) -> bool:
"""验证输入参数"""
# 验证平台
if platform not in ["android", "ios"]:
self.display_manager.display_error(f"无效的平台: {platform},支持 android 或 ios")
return False
# 验证版本号格式
for version in [target_version, update_version]:
if not self._validate_version_format(version):
self.display_manager.display_error(f"无效的版本号格式: {version}")
return False
# 验证版本号不能相同
if target_version == update_version:
self.display_manager.display_error("目标版本和更新版本不能相同")
return False
return True
def _validate_version_format(self, version: str) -> bool:
"""验证版本号格式"""
try:
parts = version.split('.')
if len(parts) != 2:
return False
major, minor = parts
if not major.isdigit() or not minor.isdigit():
return False
return True
except:
return False
def _get_update_version_ver(self, platform: str, update_version: str) -> Optional[str]:
"""获取更新版本的Ver值"""
try:
# 从对象存储获取更新版本的配置
content = self.config_manager.download_from_storage(update_version, platform)
if not content:
return None
config = self.config_manager.parse_config(content)
if not config:
return None
# 提取Ver值
ver_value = config.get("Ver")
if not ver_value:
self.display_manager.display_error(f"更新版本 {update_version} 的配置中没有Ver属性")
return None
return ver_value
except Exception as e:
self.display_manager.display_error(f"获取更新版本Ver值失败: {str(e)}")
return None
def _get_target_config(self, platform: str, target_version: str) -> Optional[Dict]:
"""获取目标版本的配置"""
try:
# 从对象存储获取目标版本的配置
content = self.config_manager.download_from_storage(target_version, platform)
if not content:
self.display_manager.display_error(f"目标版本 {target_version} 的配置不存在")
return None
config = self.config_manager.parse_config(content)
if not config:
self.display_manager.display_error(f"目标版本 {target_version} 的配置解析失败")
return None
return config
except Exception as e:
self.display_manager.display_error(f"获取目标版本配置失败: {str(e)}")
return None
def _upload_config(self, platform: str, version: str, config: Dict) -> bool:
"""加密并上传配置到对象存储"""
try:
# 将配置转换为JSON字符串
config_json = json.dumps(config, indent=2, ensure_ascii=False)
# 加密配置
encrypted_content = encrypt(config_json)
# 获取rclone路径
rclone_path = self.config_manager.get_rclone_path(version, platform)
# 使用rclone上传文件
timeout = self.config_manager.storage_config.get('timeout', 30)
result = subprocess.run(
["rclone", "rcat", rclone_path],
input=encrypted_content,
text=True,
timeout=timeout
)
if result.returncode != 0:
self.display_manager.display_error(
f"上传配置到对象存储失败: {result.stderr}"
)
return False
self.display_manager.display_info(f"配置已成功上传到对象存储: {rclone_path}")
return True
except subprocess.TimeoutExpired:
self.display_manager.display_error("上传配置超时")
return False
except Exception as e:
self.display_manager.display_error(f"上传配置失败: {str(e)}")
return False
def _refresh_cdn_cache(self, platform: str, version: str) -> bool:
"""刷新CDN缓存"""
try:
cdn_url = self.config_manager.get_cdn_url(version, platform)
if not cdn_url:
self.display_manager.display_warning("CDN URL未配置跳过缓存刷新")
return True
# 使用Cloudflare CDN刷新功能
urls_to_refresh = [cdn_url]
self.display_manager.display_info(f"正在刷新Cloudflare CDN缓存: {cdn_url}")
success = refresh_cloudflare_cache(urls_to_refresh)
if success:
self.display_manager.display_success(f"Cloudflare CDN缓存刷新成功: {cdn_url}")
else:
self.display_manager.display_warning(f"Cloudflare CDN缓存刷新失败: {cdn_url}")
return success
except Exception as e:
self.display_manager.display_error(f"刷新CDN缓存失败: {str(e)}")
return False
def _display_success_result(self, platform: str, target_version: str,
update_version: str, update_ver_value: str):
"""显示成功结果"""
self.display_manager.display_success(
f"✅ 强制更新成功完成!\n"
f" 平台: {platform}\n"
f" 目标版本: {target_version}\n"
f" 更新版本: {update_version}\n"
f" 新的Ver值: {update_ver_value}"
)
def _display_config_comparison(self, platform: str, target_version: str,
original_config: Dict, updated_config: Dict):
"""显示配置对比"""
self.display_manager.display_info(f"\n📋 配置变更对比 (平台: {platform}, 版本: {target_version})")
# 创建对比表格
from rich.table import Table
from rich.console import Console
console = Console()
table = Table(title="配置变更详情")
table.add_column("配置项", style="cyan")
table.add_column("原始值", style="red")
table.add_column("新值", style="green")
# 只显示有变化的配置项
changed_items = []
for key in original_config:
original_value = original_config.get(key, "")
updated_value = updated_config.get(key, "")
if original_value != updated_value:
changed_items.append((key, str(original_value), str(updated_value)))
# 添加新配置项
for key in updated_config:
if key not in original_config:
changed_items.append((key, "", str(updated_config[key])))
# 按配置项名称排序
changed_items.sort(key=lambda x: x[0])
for key, original_value, updated_value in changed_items:
table.add_row(key, original_value, updated_value)
if changed_items:
console.print(table)
else:
console.print("没有配置项发生变化")
# 显示警告信息
self.display_manager.display_warning(
"⚠️ 注意: 强制更新会立即影响使用该版本的应用,"
"请确保所有相关方已了解此变更。"
)

View File

@@ -0,0 +1,274 @@
import json
import subprocess
import requests
from typing import Dict, Optional, List
from .config_manager import ConfigManager
from .display_manager import DisplayManager
from ..utils.crypto import encrypt
from ..utils.cloudflare import refresh_cloudflare_cache
class SuggestUpdateCommand:
"""建议更新命令处理器"""
def __init__(self, storage_path: str = "ab", cdn_base_url: str = ""):
self.config_manager = ConfigManager(storage_path, cdn_base_url)
self.display_manager = DisplayManager()
def execute(self, platform: str, target_version: str, update_version: str) -> bool:
"""执行建议更新命令"""
# 验证参数
if not self._validate_parameters(platform, target_version, update_version):
return False
self.display_manager.display_info(
f"正在为 {platform} 平台执行建议更新: "
f"目标版本 {target_version} -> 更新版本 {update_version}"
)
try:
# 1. 获取更新版本的配置提取Ver值
update_ver_value = self._get_update_version_ver(platform, update_version)
if not update_ver_value:
self.display_manager.display_error(
f"无法获取更新版本 {update_version} 的Ver值"
)
return False
# 2. 获取目标版本的配置(保存原始配置用于对比)
original_config = self._get_target_config(platform, target_version)
if not original_config:
self.display_manager.display_error(
f"无法获取目标版本 {target_version} 的配置"
)
return False
# 3. 创建更新后的配置复制原始配置并添加Ver_New
updated_config = original_config.copy()
updated_config["Ver_New"] = update_ver_value
# 4. 加密并上传到对象存储
success = self._upload_config(platform, target_version, updated_config)
if not success:
return False
# 5. 刷新CDN缓存
success = self._refresh_cdn_cache(platform, target_version)
if not success:
self.display_manager.display_warning("配置已更新但CDN缓存刷新失败")
# 6. 显示结果和对比
self._display_success_result(platform, target_version, update_version, update_ver_value)
self._display_config_comparison(platform, target_version, original_config, updated_config)
return True
except Exception as e:
self.display_manager.display_error(f"建议更新失败: {str(e)}")
return False
def _validate_parameters(self, platform: str, target_version: str, update_version: str) -> bool:
"""验证输入参数"""
# 验证平台
if platform not in ["android", "ios"]:
self.display_manager.display_error(f"无效的平台: {platform},支持 android 或 ios")
return False
# 验证版本号格式
for version in [target_version, update_version]:
if not self._validate_version_format(version):
self.display_manager.display_error(f"无效的版本号格式: {version}")
return False
# 验证版本号不能相同
if target_version == update_version:
self.display_manager.display_error("目标版本和更新版本不能相同")
return False
return True
def _validate_version_format(self, version: str) -> bool:
"""验证版本号格式"""
try:
parts = version.split('.')
if len(parts) != 2:
return False
major, minor = parts
if not major.isdigit() or not minor.isdigit():
return False
return True
except:
return False
def _get_update_version_ver(self, platform: str, update_version: str) -> Optional[str]:
"""获取更新版本的Ver值"""
try:
# 从对象存储获取更新版本的配置
content = self.config_manager.download_from_storage(update_version, platform)
if not content:
return None
config = self.config_manager.parse_config(content)
if not config:
return None
return config.get("Ver")
except Exception as e:
self.display_manager.display_error(f"获取更新版本Ver值失败: {str(e)}")
return None
def _get_target_config(self, platform: str, target_version: str) -> Optional[Dict]:
"""获取目标版本的配置"""
try:
# 从对象存储获取目标版本的配置
content = self.config_manager.download_from_storage(target_version, platform)
if not content:
return None
config = self.config_manager.parse_config(content)
if not config:
return None
return config
except Exception as e:
self.display_manager.display_error(f"获取目标版本配置失败: {str(e)}")
return None
def _upload_config(self, platform: str, version: str, config: Dict) -> bool:
"""加密并上传配置到对象存储"""
try:
# 将配置转换为JSON字符串
config_json = json.dumps(config, indent=2, ensure_ascii=False)
# 加密配置
encrypted_content = encrypt(config_json)
# 获取rclone路径
rclone_path = self.config_manager.get_rclone_path(version, platform)
# 使用rclone上传文件
timeout = self.config_manager.storage_config.get('timeout', 30)
result = subprocess.run(
["rclone", "rcat", rclone_path],
input=encrypted_content,
text=True,
timeout=timeout
)
if result.returncode != 0:
self.display_manager.display_error(
f"上传配置到对象存储失败: {result.stderr}"
)
return False
self.display_manager.display_info(f"配置已成功上传到对象存储: {rclone_path}")
return True
except subprocess.TimeoutExpired:
self.display_manager.display_error("上传配置超时")
return False
except Exception as e:
self.display_manager.display_error(f"上传配置失败: {str(e)}")
return False
def _refresh_cdn_cache(self, platform: str, version: str) -> bool:
"""刷新CDN缓存"""
try:
cdn_url = self.config_manager.get_cdn_url(version, platform)
if not cdn_url:
self.display_manager.display_warning("CDN URL未配置跳过缓存刷新")
return True
# 使用Cloudflare CDN刷新功能
urls_to_refresh = [cdn_url]
self.display_manager.display_info(f"正在刷新Cloudflare CDN缓存: {cdn_url}")
success = refresh_cloudflare_cache(urls_to_refresh)
if success:
self.display_manager.display_success(f"Cloudflare CDN缓存刷新成功: {cdn_url}")
else:
self.display_manager.display_warning(f"Cloudflare CDN缓存刷新失败: {cdn_url}")
return success
except Exception as e:
self.display_manager.display_error(f"刷新CDN缓存失败: {str(e)}")
return False
def _display_success_result(self, platform: str, target_version: str,
update_version: str, update_ver_value: str):
"""显示成功结果"""
self.display_manager.display_success(
f"建议更新成功完成!\n"
f"平台: {platform}\n"
f"目标版本: {target_version}\n"
f"更新版本: {update_version}\n"
f"已添加 Ver_New: {update_ver_value}"
)
def _display_config_comparison(self, platform: str, target_version: str,
original_config: Dict, updated_config: Dict):
"""显示配置对比"""
self.display_manager.display_info(f"\n📊 配置对比 - {platform.title()} 平台 (版本 {target_version})")
# 创建对比表格
from rich.table import Table
from rich.console import Console
console = Console()
table = Table(show_header=True, header_style="bold magenta")
table.add_column("配置项", style="cyan", width=20)
table.add_column("更新前", style="red", width=30)
table.add_column("更新后", style="green", width=30)
table.add_column("状态", style="yellow", width=10)
# 获取所有配置项
all_keys = set(original_config.keys()) | set(updated_config.keys())
# 添加数据行
for key in sorted(all_keys):
original_value = original_config.get(key, "N/A")
updated_value = updated_config.get(key, "N/A")
if key == "Ver_New":
status = "🆕 新增"
elif original_value != updated_value:
status = "🔄 修改"
else:
status = "✅ 不变"
# 格式化显示值
original_display = str(original_value)
updated_display = str(updated_value)
# 截断过长的值
max_length = 25
if len(original_display) > max_length:
original_display = original_display[:max_length-3] + "..."
if len(updated_display) > max_length:
updated_display = updated_display[:max_length-3] + "..."
table.add_row(key, original_display, updated_display, status)
console.print(table)
# 显示统计信息
total_items = len(all_keys)
new_items = len([k for k in all_keys if k not in original_config])
modified_items = len([k for k in all_keys if k in original_config and k in updated_config and original_config[k] != updated_config[k]])
unchanged_items = total_items - new_items - modified_items
self.display_manager.display_info(
f"\n📈 统计信息:\n"
f" 总配置项: {total_items}\n"
f" 新增项: {new_items}\n"
f" 修改项: {modified_items}\n"
f" 不变项: {unchanged_items}"
)

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)