修复CDN URL和路径构建逻辑
- 修复CDN URL构建,正确处理路径重复问题
- 修复rclone路径构建,使用正确的格式: {rclone_remote}:{storage_path}/{version_path}/{filename}
- 修复CDN路径构建,使用正确的格式: {cdn_base_url}/{version_path}/{filename}
- 修复CDN内容解密处理,支持加密和解密后的内容
- 添加路径构建测试示例
- 完善错误处理和空内容处理
This commit is contained in:
@@ -43,6 +43,7 @@ config-man/
|
||||
│ └── basic_usage.py # 基本使用示例
|
||||
├── main.py # 主入口文件
|
||||
├── pyproject.toml # 项目配置文件
|
||||
├── config-man.json # 配置文件
|
||||
├── README.md # 项目主文档
|
||||
├── .gitignore # Git忽略文件
|
||||
└── PROJECT_STRUCTURE.md # 项目结构说明(本文件)
|
||||
|
||||
25
config-man.json
Normal file
25
config-man.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"storage": {
|
||||
"path": "n3ft/ab",
|
||||
"rclone_remote": "ft",
|
||||
"timeout": 30
|
||||
},
|
||||
"cdn": {
|
||||
"base_url": "https://ab.bamboogames.cc/ab",
|
||||
"timeout": 10,
|
||||
"retry_count": 3
|
||||
},
|
||||
"crypto": {
|
||||
"algorithm": "DES",
|
||||
"key": "tbambooz"
|
||||
},
|
||||
"logging": {
|
||||
"level": "INFO",
|
||||
"format": "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
||||
},
|
||||
"display": {
|
||||
"table_format": "grid",
|
||||
"max_width": 100,
|
||||
"truncate_length": 25
|
||||
}
|
||||
}
|
||||
@@ -43,10 +43,72 @@ python main.py view --help
|
||||
|
||||
- `--version`: 版本号,格式为 0.29, 0.30, 0.31 等(必需)
|
||||
- `--json`: 以JSON格式输出(可选)
|
||||
- `--storage-path`: 对象存储路径前缀,默认为 'ab'(可选)
|
||||
- `--cdn-url`: CDN基础URL(可选)
|
||||
- `--storage-path`: 对象存储路径前缀(覆盖配置文件)
|
||||
- `--cdn-url`: CDN基础URL(覆盖配置文件)
|
||||
- `--mock`: 使用模拟环境进行测试(可选)
|
||||
|
||||
### 配置管理命令
|
||||
|
||||
- `show-config`: 显示当前配置
|
||||
- `set-config`: 设置配置值
|
||||
|
||||
## 配置文件
|
||||
|
||||
Config-Man 支持多种配置方式:
|
||||
|
||||
### 1. 配置文件
|
||||
|
||||
默认配置文件位置:
|
||||
- 用户配置: `~/.config/config-man/config-man.json`
|
||||
- 项目配置: `./config-man.json`
|
||||
|
||||
配置文件格式:
|
||||
```json
|
||||
{
|
||||
"storage": {
|
||||
"path": "ab",
|
||||
"rclone_remote": "remote",
|
||||
"timeout": 30
|
||||
},
|
||||
"cdn": {
|
||||
"base_url": "",
|
||||
"timeout": 10,
|
||||
"retry_count": 3
|
||||
},
|
||||
"crypto": {
|
||||
"algorithm": "DES",
|
||||
"key": "tbambooz"
|
||||
},
|
||||
"display": {
|
||||
"table_format": "grid",
|
||||
"max_width": 80,
|
||||
"truncate_length": 25
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 环境变量
|
||||
|
||||
支持以下环境变量:
|
||||
|
||||
| 环境变量 | 配置项 | 说明 |
|
||||
|---------|--------|------|
|
||||
| `CONFIG_MAN_STORAGE_PATH` | `storage.path` | 存储路径 |
|
||||
| `CONFIG_MAN_STORAGE_RCLONE_REMOTE` | `storage.rclone_remote` | rclone远程名称 |
|
||||
| `CONFIG_MAN_STORAGE_TIMEOUT` | `storage.timeout` | 存储操作超时时间 |
|
||||
| `CONFIG_MAN_CDN_BASE_URL` | `cdn.base_url` | CDN基础URL |
|
||||
| `CONFIG_MAN_CDN_TIMEOUT` | `cdn.timeout` | CDN请求超时时间 |
|
||||
| `CONFIG_MAN_CDN_RETRY_COUNT` | `cdn.retry_count` | CDN重试次数 |
|
||||
| `CONFIG_MAN_CRYPTO_KEY` | `crypto.key` | 加密密钥 |
|
||||
| `CONFIG_MAN_CRYPTO_ALGORITHM` | `crypto.algorithm` | 加密算法 |
|
||||
| `CONFIG_MAN_DISPLAY_TABLE_FORMAT` | `display.table_format` | 表格格式 |
|
||||
| `CONFIG_MAN_DISPLAY_MAX_WIDTH` | `display.max_width` | 最大宽度 |
|
||||
| `CONFIG_MAN_DISPLAY_TRUNCATE_LENGTH` | `display.truncate_length` | 截断长度 |
|
||||
|
||||
### 3. 命令行参数
|
||||
|
||||
命令行参数会覆盖配置文件和环境变量的设置。
|
||||
|
||||
## 输出格式
|
||||
|
||||
### 表格格式(默认)
|
||||
@@ -146,16 +208,35 @@ python main.py view --version 0.30 --mock
|
||||
|
||||
## 高级用法
|
||||
|
||||
### 自定义存储路径
|
||||
### 配置管理
|
||||
|
||||
```bash
|
||||
python main.py view --version 0.30 --storage-path custom_path
|
||||
# 显示当前配置
|
||||
python main.py show-config
|
||||
|
||||
# 设置配置值
|
||||
python main.py set-config --key storage.path --value custom_path
|
||||
python main.py set-config --key cdn.base_url --value https://cdn.example.com
|
||||
python main.py set-config --key display.max_width --value 100
|
||||
```
|
||||
|
||||
### 指定CDN URL
|
||||
### 环境变量配置
|
||||
|
||||
```bash
|
||||
python main.py view --version 0.30 --cdn-url https://cdn.example.com
|
||||
# 使用环境变量设置配置
|
||||
export CONFIG_MAN_STORAGE_PATH=custom_path
|
||||
export CONFIG_MAN_CDN_BASE_URL=https://cdn.example.com
|
||||
export CONFIG_MAN_DISPLAY_MAX_WIDTH=120
|
||||
|
||||
# 运行命令
|
||||
python main.py view --version 0.30
|
||||
```
|
||||
|
||||
### 命令行参数覆盖
|
||||
|
||||
```bash
|
||||
# 命令行参数会覆盖配置文件设置
|
||||
python main.py view --version 0.30 --storage-path custom_path --cdn-url https://cdn.example.com
|
||||
```
|
||||
|
||||
### 组合使用
|
||||
|
||||
58
examples/config_usage.py
Normal file
58
examples/config_usage.py
Normal file
@@ -0,0 +1,58 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Config-Man 配置使用示例
|
||||
|
||||
演示如何使用配置文件和环境变量。
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
|
||||
# 添加项目根目录到路径
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
|
||||
|
||||
from config_man.utils import config
|
||||
|
||||
|
||||
def main():
|
||||
"""配置使用示例"""
|
||||
print("Config-Man 配置使用示例")
|
||||
print("=" * 50)
|
||||
|
||||
# 1. 显示当前配置
|
||||
print("\n1. 当前配置:")
|
||||
print(f"存储路径: {config.get_storage_path()}")
|
||||
print(f"CDN基础URL: {config.get_cdn_base_url()}")
|
||||
print(f"加密密钥: {config.get_crypto_key()}")
|
||||
|
||||
# 2. 设置配置值
|
||||
print("\n2. 设置配置值:")
|
||||
config.set('storage.path', 'custom_storage')
|
||||
config.set('cdn.base_url', 'https://cdn.example.com')
|
||||
config.set('display.max_width', 100)
|
||||
|
||||
print("配置已更新")
|
||||
|
||||
# 3. 显示更新后的配置
|
||||
print("\n3. 更新后的配置:")
|
||||
print(f"存储路径: {config.get_storage_path()}")
|
||||
print(f"CDN基础URL: {config.get_cdn_base_url()}")
|
||||
print(f"显示最大宽度: {config.get('display.max_width')}")
|
||||
|
||||
# 4. 保存配置到文件
|
||||
print("\n4. 保存配置:")
|
||||
config.save()
|
||||
print(f"配置已保存到: {config.config_file}")
|
||||
print(f"配置文件名称: config-man.json")
|
||||
|
||||
# 5. 环境变量示例
|
||||
print("\n5. 环境变量使用示例:")
|
||||
print("可以通过以下环境变量覆盖配置:")
|
||||
print(" CONFIG_MAN_STORAGE_PATH=custom_path")
|
||||
print(" CONFIG_MAN_CDN_BASE_URL=https://cdn.example.com")
|
||||
print(" CONFIG_MAN_CRYPTO_KEY=custom_key")
|
||||
print(" CONFIG_MAN_DISPLAY_MAX_WIDTH=120")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
50
examples/test_cdn_url.py
Normal file
50
examples/test_cdn_url.py
Normal file
@@ -0,0 +1,50 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
测试CDN URL构建
|
||||
|
||||
验证CDN URL的正确构建。
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
|
||||
# 添加项目根目录到路径
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
|
||||
|
||||
from config_man.core import ConfigManager
|
||||
|
||||
|
||||
def main():
|
||||
"""测试CDN URL构建"""
|
||||
print("测试CDN URL构建")
|
||||
print("=" * 50)
|
||||
|
||||
# 创建配置管理器
|
||||
config_manager = ConfigManager()
|
||||
|
||||
# 测试不同版本的CDN URL构建
|
||||
versions = ["0.29", "0.30"]
|
||||
platforms = ["android", "ios"]
|
||||
|
||||
print(f"CDN基础URL: {config_manager.cdn_base_url}")
|
||||
print(f"存储路径: {config_manager.storage_path}")
|
||||
print()
|
||||
|
||||
for version in versions:
|
||||
for platform in platforms:
|
||||
# 获取CDN URL
|
||||
cdn_url = config_manager.get_cdn_url(version, platform)
|
||||
|
||||
print(f"版本: {version}, 平台: {platform}")
|
||||
print(f"CDN URL: {cdn_url}")
|
||||
print()
|
||||
|
||||
# 显示当前配置
|
||||
print(f"当前CDN配置:")
|
||||
print(f"CDN基础URL: {config_manager.cdn_base_url}")
|
||||
print(f"CDN超时: {config_manager.cdn_config.get('timeout', 10)}")
|
||||
print(f"CDN重试次数: {config_manager.cdn_config.get('retry_count', 3)}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
53
examples/test_paths.py
Normal file
53
examples/test_paths.py
Normal file
@@ -0,0 +1,53 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
测试路径构建
|
||||
|
||||
验证rclone路径和CDN路径的正确构建。
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
|
||||
# 添加项目根目录到路径
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
|
||||
|
||||
from config_man.core import ConfigManager
|
||||
|
||||
|
||||
def main():
|
||||
"""测试路径构建"""
|
||||
print("测试路径构建")
|
||||
print("=" * 50)
|
||||
|
||||
# 创建配置管理器
|
||||
config_manager = ConfigManager()
|
||||
|
||||
# 测试不同版本的路径构建
|
||||
versions = ["0.29", "0.30"]
|
||||
platforms = ["android", "ios"]
|
||||
|
||||
print(f"存储配置:")
|
||||
print(f" rclone_remote: {config_manager.storage_config.get('rclone_remote', 'remote')}")
|
||||
print(f" storage_path: {config_manager.storage_path}")
|
||||
print()
|
||||
|
||||
print(f"CDN配置:")
|
||||
print(f" base_url: {config_manager.cdn_base_url}")
|
||||
print()
|
||||
|
||||
for version in versions:
|
||||
for platform in platforms:
|
||||
# 获取rclone路径
|
||||
rclone_path = config_manager.get_rclone_path(version, platform)
|
||||
|
||||
# 获取CDN URL
|
||||
cdn_url = config_manager.get_cdn_url(version, platform)
|
||||
|
||||
print(f"版本: {version}, 平台: {platform}")
|
||||
print(f"Rclone路径: {rclone_path}")
|
||||
print(f"CDN URL: {cdn_url}")
|
||||
print()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
48
examples/test_rclone_path.py
Normal file
48
examples/test_rclone_path.py
Normal file
@@ -0,0 +1,48 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
测试rclone路径构建
|
||||
|
||||
验证rclone_remote配置项的正确使用。
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
|
||||
# 添加项目根目录到路径
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
|
||||
|
||||
from config_man.core import ConfigManager
|
||||
|
||||
|
||||
def main():
|
||||
"""测试rclone路径构建"""
|
||||
print("测试rclone路径构建")
|
||||
print("=" * 50)
|
||||
|
||||
# 创建配置管理器
|
||||
config_manager = ConfigManager()
|
||||
|
||||
# 测试不同版本的路径构建
|
||||
versions = ["0.29", "0.30"]
|
||||
platforms = ["android", "ios"]
|
||||
|
||||
for version in versions:
|
||||
for platform in platforms:
|
||||
# 获取配置路径
|
||||
config_path = config_manager.get_config_path(version, platform)
|
||||
|
||||
# 获取rclone路径
|
||||
rclone_path = config_manager.get_rclone_path(version, platform)
|
||||
|
||||
print(f"\n版本: {version}, 平台: {platform}")
|
||||
print(f"配置路径: {config_path}")
|
||||
print(f"rclone路径: {rclone_path}")
|
||||
|
||||
# 显示当前配置
|
||||
print(f"\n当前存储配置:")
|
||||
print(f"存储路径: {config_manager.storage_path}")
|
||||
print(f"rclone远程: {config_manager.storage_config.get('rclone_remote', 'remote')}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -2,6 +2,7 @@ import click
|
||||
import os
|
||||
from ..core.view_command import ViewCommand
|
||||
from ..utils.mock_rclone import patch_rclone
|
||||
from ..utils.config import config
|
||||
|
||||
|
||||
@click.group()
|
||||
@@ -18,8 +19,8 @@ def cli():
|
||||
@cli.command()
|
||||
@click.option('--version', required=True, help='版本号,格式为 0.29, 0.30, 0.31 等')
|
||||
@click.option('--json', 'json_format', is_flag=True, help='以JSON格式输出')
|
||||
@click.option('--storage-path', default='ab', help='对象存储路径前缀')
|
||||
@click.option('--cdn-url', default='', help='CDN基础URL')
|
||||
@click.option('--storage-path', help='对象存储路径前缀(覆盖配置文件)')
|
||||
@click.option('--cdn-url', help='CDN基础URL(覆盖配置文件)')
|
||||
@click.option('--mock', is_flag=True, help='使用模拟环境进行测试')
|
||||
def view(version, json_format, storage_path, cdn_url, mock):
|
||||
"""查看配置文件
|
||||
@@ -89,6 +90,67 @@ def force_update(platform, target_version, update_version):
|
||||
return 1
|
||||
|
||||
|
||||
@cli.command()
|
||||
def show_config():
|
||||
"""显示当前配置
|
||||
|
||||
显示当前加载的配置信息。
|
||||
"""
|
||||
import json
|
||||
from rich.console import Console
|
||||
from rich.panel import Panel
|
||||
|
||||
console = Console()
|
||||
|
||||
# 获取所有配置
|
||||
all_config = {
|
||||
"storage": config.get_storage_config(),
|
||||
"cdn": config.get_cdn_config(),
|
||||
"crypto": config.get_crypto_config(),
|
||||
"display": config.get_display_config(),
|
||||
"logging": config.get_logging_config()
|
||||
}
|
||||
|
||||
# 格式化输出
|
||||
config_json = json.dumps(all_config, indent=2, ensure_ascii=False)
|
||||
panel = Panel(
|
||||
config_json,
|
||||
title="当前配置",
|
||||
border_style="blue"
|
||||
)
|
||||
console.print(panel)
|
||||
|
||||
# 显示配置文件路径
|
||||
console.print(f"\n配置文件路径: {config.config_file}")
|
||||
|
||||
|
||||
@cli.command()
|
||||
@click.option('--key', required=True, help='配置键,如 storage.path')
|
||||
@click.option('--value', required=True, help='配置值')
|
||||
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
|
||||
"""
|
||||
try:
|
||||
# 尝试转换数值类型
|
||||
if value.isdigit():
|
||||
value = int(value)
|
||||
elif value.lower() in ('true', 'false'):
|
||||
value = value.lower() == 'true'
|
||||
|
||||
config.set(key, value)
|
||||
config.save()
|
||||
click.echo(f"✅ 配置已更新: {key} = {value}")
|
||||
except Exception as e:
|
||||
click.echo(f"❌ 设置配置失败: {e}")
|
||||
return 1
|
||||
|
||||
|
||||
def _check_rclone():
|
||||
"""检查rclone是否可用"""
|
||||
import subprocess
|
||||
|
||||
@@ -4,14 +4,18 @@ import subprocess
|
||||
import requests
|
||||
from typing import Dict, Optional, Tuple
|
||||
from ..utils.crypto import encrypt, decrypt
|
||||
from ..utils.config import config
|
||||
|
||||
|
||||
class ConfigManager:
|
||||
"""配置文件管理器"""
|
||||
|
||||
def __init__(self, storage_path: str = "ab", cdn_base_url: str = ""):
|
||||
self.storage_path = storage_path
|
||||
self.cdn_base_url = cdn_base_url
|
||||
def __init__(self, storage_path: str = None, cdn_base_url: str = None):
|
||||
# 使用配置文件中的值,如果参数为None
|
||||
self.storage_path = storage_path or config.get_storage_path()
|
||||
self.cdn_base_url = cdn_base_url or config.get_cdn_base_url()
|
||||
self.storage_config = config.get_storage_config()
|
||||
self.cdn_config = config.get_cdn_config()
|
||||
|
||||
def version_to_path(self, version: str) -> str:
|
||||
"""将版本号转换为存储路径格式"""
|
||||
@@ -24,45 +28,54 @@ class ConfigManager:
|
||||
filename = f"{platform}config.json"
|
||||
return f"{self.storage_path}/{version_path}/{filename}"
|
||||
|
||||
def get_rclone_path(self, version: str, platform: str) -> str:
|
||||
"""获取rclone完整路径(包含远程名称)"""
|
||||
version_path = self.version_to_path(version)
|
||||
filename = f"{platform}config.json"
|
||||
rclone_remote = self.storage_config.get('rclone_remote', 'remote')
|
||||
return f"{rclone_remote}:{self.storage_path}/{version_path}/{filename}"
|
||||
|
||||
def get_cdn_url(self, version: str, platform: str) -> str:
|
||||
"""获取配置文件在CDN中的URL"""
|
||||
if not self.cdn_base_url:
|
||||
return ""
|
||||
version_path = self.version_to_path(version)
|
||||
filename = f"{platform}config.json"
|
||||
return f"{self.cdn_base_url}/{self.storage_path}/{version_path}/{filename}"
|
||||
return f"{self.cdn_base_url}/{version_path}/{filename}"
|
||||
|
||||
def download_from_storage(self, version: str, platform: str) -> Optional[str]:
|
||||
"""从对象存储下载并解密配置文件"""
|
||||
try:
|
||||
config_path = self.get_config_path(version, platform)
|
||||
# 获取rclone完整路径
|
||||
rclone_path = self.get_rclone_path(version, platform)
|
||||
|
||||
# 使用rclone下载文件
|
||||
timeout = self.storage_config.get('timeout', 30)
|
||||
result = subprocess.run(
|
||||
["rclone", "cat", config_path],
|
||||
["rclone", "cat", rclone_path],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=30
|
||||
timeout=timeout
|
||||
)
|
||||
|
||||
if result.returncode != 0:
|
||||
print(f"警告: 无法从对象存储下载 {config_path}: {result.stderr}")
|
||||
print(f"警告: 无法从对象存储下载 {rclone_path}: {result.stderr}")
|
||||
return None
|
||||
|
||||
# 解密文件内容
|
||||
encrypted_content = result.stdout.strip()
|
||||
if not encrypted_content:
|
||||
print(f"警告: 文件 {config_path} 为空")
|
||||
print(f"警告: 文件 {rclone_path} 为空")
|
||||
return None
|
||||
|
||||
decrypted_content = decrypt(encrypted_content)
|
||||
return decrypted_content
|
||||
|
||||
except subprocess.TimeoutExpired:
|
||||
print(f"错误: 下载 {config_path} 超时")
|
||||
print(f"错误: 下载 {rclone_path} 超时")
|
||||
return None
|
||||
except Exception as e:
|
||||
print(f"错误: 下载 {config_path} 失败: {str(e)}")
|
||||
print(f"错误: 下载 {rclone_path} 失败: {str(e)}")
|
||||
return None
|
||||
|
||||
def get_from_cdn(self, version: str, platform: str) -> Optional[str]:
|
||||
@@ -73,12 +86,25 @@ class ConfigManager:
|
||||
print(f"警告: CDN URL未配置,跳过CDN内容获取")
|
||||
return None
|
||||
|
||||
response = requests.get(cdn_url, timeout=10)
|
||||
timeout = self.cdn_config.get('timeout', 10)
|
||||
response = requests.get(cdn_url, timeout=timeout)
|
||||
response.raise_for_status()
|
||||
|
||||
# CDN中的内容应该是解密后的JSON
|
||||
return response.text
|
||||
# CDN中的内容可能是加密的,需要解密
|
||||
encrypted_content = response.text.strip()
|
||||
if not encrypted_content:
|
||||
print(f"警告: CDN返回空内容: {cdn_url}")
|
||||
return None
|
||||
|
||||
# 尝试解密内容
|
||||
try:
|
||||
decrypted_content = decrypt(encrypted_content)
|
||||
return decrypted_content
|
||||
except Exception as e:
|
||||
# 如果解密失败,可能CDN中的内容已经是解密后的JSON
|
||||
print(f"警告: CDN内容解密失败,尝试直接解析: {str(e)}")
|
||||
return encrypted_content
|
||||
|
||||
except requests.RequestException as e:
|
||||
print(f"警告: 无法从CDN获取 {cdn_url}: {str(e)}")
|
||||
return None
|
||||
|
||||
@@ -4,6 +4,7 @@ from rich.table import Table
|
||||
from rich.panel import Panel
|
||||
from rich.text import Text
|
||||
import json
|
||||
from ..utils.config import config
|
||||
|
||||
|
||||
class DisplayManager:
|
||||
@@ -11,6 +12,7 @@ class DisplayManager:
|
||||
|
||||
def __init__(self):
|
||||
self.console = Console()
|
||||
self.display_config = config.get_display_config()
|
||||
|
||||
def display_config_comparison(self, version: str, platform: str,
|
||||
storage_config: Dict, cdn_config: Dict,
|
||||
@@ -29,16 +31,17 @@ class DisplayManager:
|
||||
table.add_column("状态", style="red", width=10)
|
||||
|
||||
# 添加数据行
|
||||
truncate_length = self.display_config.get('truncate_length', 25)
|
||||
for key, data in differences.items():
|
||||
storage_value = str(data["storage"])
|
||||
cdn_value = str(data["cdn"])
|
||||
status = "❌ 不同" if data["different"] else "✅ 一致"
|
||||
|
||||
# 截断过长的值
|
||||
if len(storage_value) > 25:
|
||||
storage_value = storage_value[:22] + "..."
|
||||
if len(cdn_value) > 25:
|
||||
cdn_value = cdn_value[:22] + "..."
|
||||
if len(storage_value) > truncate_length:
|
||||
storage_value = storage_value[:truncate_length-3] + "..."
|
||||
if len(cdn_value) > truncate_length:
|
||||
cdn_value = cdn_value[:truncate_length-3] + "..."
|
||||
|
||||
table.add_row(key, storage_value, cdn_value, status)
|
||||
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
"""
|
||||
工具模块
|
||||
|
||||
包含加密解密、模拟环境等工具功能。
|
||||
包含加密解密、模拟环境、配置管理等工具功能。
|
||||
"""
|
||||
|
||||
from .crypto import encrypt, decrypt
|
||||
from .mock_rclone import MockRclone, patch_rclone
|
||||
from .config import config, Config
|
||||
|
||||
__all__ = ["encrypt", "decrypt", "MockRclone", "patch_rclone"]
|
||||
__all__ = ["encrypt", "decrypt", "MockRclone", "patch_rclone", "config", "Config"]
|
||||
222
src/config_man/utils/config.py
Normal file
222
src/config_man/utils/config.py
Normal file
@@ -0,0 +1,222 @@
|
||||
"""
|
||||
配置管理模块
|
||||
|
||||
支持从配置文件和环境变量读取配置信息。
|
||||
"""
|
||||
|
||||
import os
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Dict, Any, Optional
|
||||
|
||||
|
||||
class Config:
|
||||
"""配置管理类"""
|
||||
|
||||
def __init__(self, config_file: Optional[str] = None):
|
||||
"""
|
||||
初始化配置管理器
|
||||
|
||||
Args:
|
||||
config_file: 配置文件路径,如果为None则使用默认路径
|
||||
"""
|
||||
self.config_file = config_file or self._get_default_config_path()
|
||||
self._config = self._load_config()
|
||||
|
||||
def _get_default_config_path(self) -> str:
|
||||
"""获取默认配置文件路径"""
|
||||
# 优先使用用户主目录下的配置文件
|
||||
home_config = Path.home() / ".config" / "config-man" / "config-man.json"
|
||||
if home_config.exists():
|
||||
return str(home_config)
|
||||
|
||||
# 其次使用项目根目录下的配置文件
|
||||
project_config = Path(__file__).parent.parent.parent.parent / "config-man.json"
|
||||
if project_config.exists():
|
||||
return str(project_config)
|
||||
|
||||
# 最后使用默认配置文件
|
||||
return str(Path(__file__).parent.parent.parent.parent / "config-man.json")
|
||||
|
||||
def _load_config(self) -> Dict[str, Any]:
|
||||
"""加载配置文件"""
|
||||
default_config = {
|
||||
"storage": {
|
||||
"path": "ab",
|
||||
"rclone_remote": "remote",
|
||||
"timeout": 30
|
||||
},
|
||||
"cdn": {
|
||||
"base_url": "",
|
||||
"timeout": 10,
|
||||
"retry_count": 3
|
||||
},
|
||||
"crypto": {
|
||||
"algorithm": "DES",
|
||||
"key": "tbambooz"
|
||||
},
|
||||
"logging": {
|
||||
"level": "INFO",
|
||||
"format": "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
||||
},
|
||||
"display": {
|
||||
"table_format": "grid",
|
||||
"max_width": 80,
|
||||
"truncate_length": 25
|
||||
}
|
||||
}
|
||||
|
||||
# 如果配置文件存在,则加载并合并
|
||||
if os.path.exists(self.config_file):
|
||||
try:
|
||||
with open(self.config_file, 'r', encoding='utf-8') as f:
|
||||
file_config = json.load(f)
|
||||
# 深度合并配置
|
||||
self._merge_config(default_config, file_config)
|
||||
except (json.JSONDecodeError, IOError) as e:
|
||||
print(f"警告: 无法加载配置文件 {self.config_file}: {e}")
|
||||
|
||||
# 从环境变量覆盖配置
|
||||
self._load_from_env(default_config)
|
||||
|
||||
return default_config
|
||||
|
||||
def _merge_config(self, base: Dict[str, Any], override: Dict[str, Any]) -> None:
|
||||
"""深度合并配置"""
|
||||
for key, value in override.items():
|
||||
if key in base and isinstance(base[key], dict) and isinstance(value, dict):
|
||||
self._merge_config(base[key], value)
|
||||
else:
|
||||
base[key] = value
|
||||
|
||||
def _load_from_env(self, config: Dict[str, Any]) -> None:
|
||||
"""从环境变量加载配置"""
|
||||
env_mappings = {
|
||||
"CONFIG_MAN_STORAGE_PATH": ("storage", "path"),
|
||||
"CONFIG_MAN_STORAGE_RCLONE_REMOTE": ("storage", "rclone_remote"),
|
||||
"CONFIG_MAN_STORAGE_TIMEOUT": ("storage", "timeout"),
|
||||
"CONFIG_MAN_CDN_BASE_URL": ("cdn", "base_url"),
|
||||
"CONFIG_MAN_CDN_TIMEOUT": ("cdn", "timeout"),
|
||||
"CONFIG_MAN_CDN_RETRY_COUNT": ("cdn", "retry_count"),
|
||||
"CONFIG_MAN_CRYPTO_KEY": ("crypto", "key"),
|
||||
"CONFIG_MAN_CRYPTO_ALGORITHM": ("crypto", "algorithm"),
|
||||
"CONFIG_MAN_LOG_LEVEL": ("logging", "level"),
|
||||
"CONFIG_MAN_DISPLAY_TABLE_FORMAT": ("display", "table_format"),
|
||||
"CONFIG_MAN_DISPLAY_MAX_WIDTH": ("display", "max_width"),
|
||||
"CONFIG_MAN_DISPLAY_TRUNCATE_LENGTH": ("display", "truncate_length"),
|
||||
}
|
||||
|
||||
for env_var, config_path in env_mappings.items():
|
||||
env_value = os.getenv(env_var)
|
||||
if env_value is not None:
|
||||
# 获取配置路径的父级
|
||||
current = config
|
||||
for key in config_path[:-1]:
|
||||
if key not in current:
|
||||
current[key] = {}
|
||||
current = current[key]
|
||||
|
||||
# 设置值,尝试转换为适当类型
|
||||
key = config_path[-1]
|
||||
try:
|
||||
# 尝试转换为整数
|
||||
if isinstance(current.get(key), int):
|
||||
current[key] = int(env_value)
|
||||
# 尝试转换为布尔值
|
||||
elif isinstance(current.get(key), bool):
|
||||
current[key] = env_value.lower() in ('true', '1', 'yes', 'on')
|
||||
else:
|
||||
current[key] = env_value
|
||||
except ValueError:
|
||||
current[key] = env_value
|
||||
|
||||
def get(self, key: str, default: Any = None) -> Any:
|
||||
"""
|
||||
获取配置值
|
||||
|
||||
Args:
|
||||
key: 配置键,支持点号分隔的嵌套键,如 'storage.path'
|
||||
default: 默认值
|
||||
|
||||
Returns:
|
||||
配置值
|
||||
"""
|
||||
keys = key.split('.')
|
||||
value = self._config
|
||||
|
||||
for k in keys:
|
||||
if isinstance(value, dict) and k in value:
|
||||
value = value[k]
|
||||
else:
|
||||
return default
|
||||
|
||||
return value
|
||||
|
||||
def set(self, key: str, value: Any) -> None:
|
||||
"""
|
||||
设置配置值
|
||||
|
||||
Args:
|
||||
key: 配置键,支持点号分隔的嵌套键
|
||||
value: 配置值
|
||||
"""
|
||||
keys = key.split('.')
|
||||
current = self._config
|
||||
|
||||
# 导航到父级
|
||||
for k in keys[:-1]:
|
||||
if k not in current:
|
||||
current[k] = {}
|
||||
current = current[k]
|
||||
|
||||
# 设置值
|
||||
current[keys[-1]] = value
|
||||
|
||||
def save(self) -> None:
|
||||
"""保存配置到文件"""
|
||||
try:
|
||||
# 确保配置目录存在
|
||||
config_dir = os.path.dirname(self.config_file)
|
||||
if config_dir:
|
||||
os.makedirs(config_dir, exist_ok=True)
|
||||
|
||||
with open(self.config_file, 'w', encoding='utf-8') as f:
|
||||
json.dump(self._config, f, indent=2, ensure_ascii=False)
|
||||
except IOError as e:
|
||||
print(f"警告: 无法保存配置文件 {self.config_file}: {e}")
|
||||
|
||||
def get_storage_path(self) -> str:
|
||||
"""获取存储路径"""
|
||||
return self.get('storage.path', 'ab')
|
||||
|
||||
def get_cdn_base_url(self) -> str:
|
||||
"""获取CDN基础URL"""
|
||||
return self.get('cdn.base_url', '')
|
||||
|
||||
def get_crypto_key(self) -> str:
|
||||
"""获取加密密钥"""
|
||||
return self.get('crypto.key', 'tbambooz')
|
||||
|
||||
def get_display_config(self) -> Dict[str, Any]:
|
||||
"""获取显示配置"""
|
||||
return self.get('display', {})
|
||||
|
||||
def get_storage_config(self) -> Dict[str, Any]:
|
||||
"""获取存储配置"""
|
||||
return self.get('storage', {})
|
||||
|
||||
def get_cdn_config(self) -> Dict[str, Any]:
|
||||
"""获取CDN配置"""
|
||||
return self.get('cdn', {})
|
||||
|
||||
def get_crypto_config(self) -> Dict[str, Any]:
|
||||
"""获取加密配置"""
|
||||
return self.get('crypto', {})
|
||||
|
||||
def get_logging_config(self) -> Dict[str, Any]:
|
||||
"""获取日志配置"""
|
||||
return self.get('logging', {})
|
||||
|
||||
|
||||
# 全局配置实例
|
||||
config = Config()
|
||||
@@ -21,6 +21,8 @@ class MockRclone:
|
||||
|
||||
def _create_test_configs(self):
|
||||
"""创建测试配置文件"""
|
||||
from ..utils.config import config
|
||||
|
||||
test_configs = {
|
||||
"0.29": {
|
||||
"android": {
|
||||
@@ -82,17 +84,20 @@ class MockRclone:
|
||||
}
|
||||
}
|
||||
|
||||
# 获取配置的存储路径
|
||||
storage_path = config.get_storage_path()
|
||||
|
||||
# 创建测试目录结构
|
||||
for version, platforms in test_configs.items():
|
||||
version_path = f"{self.test_data_path}/ab/{version.replace('.', '_')}"
|
||||
version_path = f"{self.test_data_path}/{storage_path}/{version.replace('.', '_')}"
|
||||
os.makedirs(version_path, exist_ok=True)
|
||||
|
||||
for platform, config in platforms.items():
|
||||
for platform, config_data in platforms.items():
|
||||
filename = f"{platform}config.json"
|
||||
filepath = os.path.join(version_path, filename)
|
||||
|
||||
# 加密配置并保存
|
||||
config_json = json.dumps(config, indent=2, ensure_ascii=False)
|
||||
config_json = json.dumps(config_data, indent=2, ensure_ascii=False)
|
||||
encrypted_config = encrypt(config_json)
|
||||
|
||||
with open(filepath, 'w', encoding='utf-8') as f:
|
||||
@@ -102,8 +107,16 @@ class MockRclone:
|
||||
|
||||
def mock_rclone_cat(self, path: str) -> str:
|
||||
"""模拟rclone cat命令"""
|
||||
# 处理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, path)
|
||||
local_path = os.path.join(self.test_data_path, actual_path)
|
||||
|
||||
if not os.path.exists(local_path):
|
||||
raise FileNotFoundError(f"文件不存在: {path}")
|
||||
|
||||
Reference in New Issue
Block a user