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:
@@ -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:
|
||||
# 尝试转换数值类型
|
||||
|
||||
@@ -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"]
|
||||
@@ -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]")
|
||||
268
src/config_man/core/force_update_command.py
Normal file
268
src/config_man/core/force_update_command.py
Normal 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(
|
||||
"⚠️ 注意: 强制更新会立即影响使用该版本的应用,"
|
||||
"请确保所有相关方已了解此变更。"
|
||||
)
|
||||
274
src/config_man/core/suggest_update_command.py
Normal file
274
src/config_man/core/suggest_update_command.py
Normal 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}"
|
||||
)
|
||||
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