Files
HYC Fixer abdbec85a4 复查修复(四): 独立审查发现的问题
- S1/S2: 会话锁改 RLock(持锁可重入调 _save_sessions);cookie 时间戳改整数+解析兼容(会话创建/验证往返已实测)
- M1: api_login 接入 verify_user(账号锁定/失败计数生效),DB 无用户时才回退 config 凭据
- M3+L5: handler _do_auth 统一入口加 IP 白名单检查;未知 auth_type 返回 401
- M4+L10: metrics 与无版本 /api/admin/ 加入受保护端点
- M6: debug 日志敏感头脱敏;main.py 不再打印 token 前缀
- M7: auth_token.txt / auth_sessions.json chmod 600
- M9: verify_user 统一错误消息防用户枚举
- M10: AdminAPI 复用共享 APIAuthManager(修复会话状态分裂)
- L7: check_auth 大小写不敏感匹配(防 /API/.. 大写绕过)
- L8: token_expires_at 显式 is not None 判断
- L11: verify_password 对非 bcrypt 哈希回退 PBKDF2(重写,修复 ValueError 分支不落回退的问题)
2026-09-02 00:45:01 +08:00

650 lines
26 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
HYC下载站 v2.3 - 完整增强版
主入口文件
支持镜像同步、下载加速源、系统监控、实时通信等
"""
import os
import sys
import signal
import argparse
import time
# PyInstaller 资源路径处理
def get_resource_path(relative_path):
"""获取打包后的资源路径"""
if getattr(sys, 'frozen', False) and hasattr(sys, '_MEIPASS'):
# 打包后的路径
return os.path.join(sys._MEIPASS, relative_path)
return os.path.join(os.path.dirname(os.path.abspath(__file__)), relative_path)
# 添加项目根目录到Python路径
base_dir = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, base_dir)
from core.config import ConfigManager, load_config_file, deep_merge, load_settings_with_override
from core.server import MirrorServer
from core.utils import parse_size
from core.database import init_database, load_db_config_from_env, merge_config
from core.sync_scheduler import init_database_sync
from core.optimization import (
MemoryManager, LowMemoryConfig, ArchitectureDetector, check_compatibility
)
def signal_handler(signum, _frame):
"""处理退出信号(优雅退出:触发 KeyboardInterrupt 走正常关闭流程)"""
print(f"\n收到信号 {signum},正在优雅关闭服务器...")
raise KeyboardInterrupt
def _parse_bool_arg(value):
"""解析布尔命令行参数(--flag true/false/1/0/yes/no)"""
if isinstance(value, bool):
return value
return str(value).strip().lower() in ('1', 'true', 'yes', 'on')
def parse_arguments():
"""解析命令行参数"""
parser = argparse.ArgumentParser(
description='HYC下载站 v2.3 - 镜像文件服务器 + 下载加速源',
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog='''
示例:
# 基本启动
python main.py -p 8080 -d ./downloads
# 使用配置文件
python main.py --settings settings.json --config custom.json
# 认证配置
python main.py --auth-type token --auth-token your_token_here
# 功能开关
python main.py --enable-monitor --enable-sync --enable-mirrors
python main.py --disable-ws --disable-sse # 适合低端设备
# 镜像加速源 (通过 settings.json 配置各个镜像的启用/禁用)
python main.py --enable-mirrors
# 低端设备优化
python main.py --preset ultra_low
python main.py --memory-limit 256M --workers 2
# 调试
python main.py --debug
python main.py --debug-types http auth api v2 error
# 检查系统兼容性
python main.py --check-compat
'''
)
# 服务器配置
parser.add_argument('--host', default='0.0.0.0', help='监听地址 (默认: 0.0.0.0)')
parser.add_argument('-p', '--port', type=int, default=8080, help='监听端口 (默认: 8080)')
parser.add_argument('-d', '--base-dir', default='./downloads', help='文件存储目录 (默认: ./downloads)')
parser.add_argument('--server-name', default='HYC下载站', help='服务器名称')
# HTTPS配置
parser.add_argument('--ssl-cert', help='SSL证书文件路径')
parser.add_argument('--ssl-key', help='SSL私钥文件路径')
# 认证配置
auth_group = parser.add_argument_group('认证配置')
auth_group.add_argument('--auth-type', choices=['none', 'basic', 'token'], default=None,
help='认证类型: none(无), basic(基本认证), token(令牌认证)')
auth_group.add_argument('--auth-user', default='admin', help='基本认证用户名')
auth_group.add_argument('--auth-pass', help='基本认证密码')
auth_group.add_argument('--auth-token', help='令牌认证密钥')
# 功能配置
func_group = parser.add_argument_group('功能配置')
func_group.add_argument('--directory-listing', nargs='?', const=True, default=None,
type=_parse_bool_arg,
help='启用目录浏览 (默认: True; 传 false 可禁用)')
func_group.add_argument('--enable-stats', nargs='?', const=True, default=None,
type=_parse_bool_arg,
help='启用下载统计 (默认: True; 传 false 可禁用)')
func_group.add_argument('--show-hash', action='store_true', help='显示文件哈希值')
func_group.add_argument('--ignore-hidden', nargs='?', const=True, default=None,
type=_parse_bool_arg,
help='忽略隐藏文件 (默认: True; 传 false 显示隐藏文件)')
func_group.add_argument('--max-upload-size', default='1G', help='最大上传文件大小')
func_group.add_argument('--api-version', choices=['v1', 'v2'], default='v2',
help='API版本 (默认: v2)')
# 实时通信配置
realtime_group = parser.add_argument_group('实时通信配置')
realtime_group.add_argument('--enable-ws', nargs='?', const=True, default=None,
type=_parse_bool_arg,
help='启用WebSocket (默认: True; 传 false 可禁用)')
realtime_group.add_argument('--enable-sse', nargs='?', const=True, default=None,
type=_parse_bool_arg,
help='启用SSE (默认: True; 传 false 可禁用)')
# 系统监控配置
monitor_group = parser.add_argument_group('系统监控配置')
monitor_group.add_argument('--enable-monitor', nargs='?', const=True, default=None,
type=_parse_bool_arg,
help='启用系统监控 (默认: True; 传 false 可禁用)')
monitor_group.add_argument('--monitor-interval', type=int, default=5,
help='监控数据采集间隔(秒) (默认: 5)')
# 同步配置
sync_group = parser.add_argument_group('同步配置')
sync_group.add_argument('--enable-sync', nargs='?', const=True, default=None,
type=_parse_bool_arg,
help='启用镜像同步 (默认: True; 传 false 可禁用)')
sync_group.add_argument('--sync-config', help='同步配置文件路径')
# 镜像加速源配置
mirror_group = parser.add_argument_group('镜像加速源配置')
mirror_group.add_argument('--enable-mirrors', nargs='?', const=True, default=None,
type=_parse_bool_arg,
help='启用下载加速源 (默认: True; 传 false 可禁用)')
# 注意: 各个镜像的启用/禁用通过 settings.json 中的 mirrors.xxx.enabled 配置
# 下载限速
rate_group = parser.add_argument_group('下载限速配置')
rate_group.add_argument('--rate-limit', type=int, default=0,
help='全局下载限速(字节/秒) (默认: 0=不限速)')
# 低端设备优化配置
optimize_group = parser.add_argument_group('低端设备优化配置')
optimize_group.add_argument('--preset', choices=['ultra_low', 'low', 'medium', 'high', 'auto'],
default='auto', help='设备预设 (默认: auto)')
optimize_group.add_argument('--memory-limit', default='512M',
help='内存限制 (例如: 256M, 512M, 1G)')
optimize_group.add_argument('--workers', type=int, default=0,
help='工作进程数 (0=自动)')
optimize_group.add_argument('--chunk-size', default='128K',
help='文件传输块大小 (默认: 128K)')
optimize_group.add_argument('--buffer-size', default='256K',
help='缓冲区大小 (默认: 256K)')
optimize_group.add_argument('--disable-ws', action='store_true',
help='禁用WebSocket (低端设备)')
optimize_group.add_argument('--disable-sse', action='store_true',
help='禁用SSE (低端设备)')
optimize_group.add_argument('--disable-hash', action='store_true',
help='禁用文件哈希计算 (低端设备)')
optimize_group.add_argument('--check-compat', action='store_true',
help='检查系统兼容性后退出')
# 日志配置
log_group = parser.add_argument_group('日志配置')
log_group.add_argument('--access-log', help='访问日志文件路径')
log_group.add_argument('--verbose', '-v', action='count', default=0, help='详细输出')
log_group.add_argument('--quiet', '-q', action='store_true', help='静默模式')
# 细粒度调试开关
debug_group = parser.add_argument_group('调试选项')
debug_group.add_argument('--debug', '-D', action='store_true',
help='启用所有调试输出')
debug_group.add_argument('--debug-log', dest='debug_log_file',
help='调试日志文件路径 (debug 输出将写入此文件)')
debug_group.add_argument('--debug-http', action='store_true',
help='调试 HTTP 请求')
debug_group.add_argument('--debug-auth', action='store_true',
help='调试 认证检查')
debug_group.add_argument('--debug-api', action='store_true',
help='调试 API 路由')
debug_group.add_argument('--debug-v2', action='store_true',
help='调试 V2 API')
debug_group.add_argument('--debug-error', action='store_true',
help='调试 错误堆栈')
debug_group.add_argument('--debug-download', action='store_true',
help='调试 下载记录')
debug_group.add_argument('--debug-types', '--debug-list',
nargs='+', metavar='TYPE',
choices=['http', 'auth', 'api', 'v2', 'error', 'download'],
help='指定调试类型列表 (http auth api v2 error download)')
# 配置文件
parser.add_argument('--settings', '--default-config', dest='settings',
help='默认配置文件路径 (settings.json)')
parser.add_argument('--config', help='覆盖配置文件路径 (JSON格式,会覆盖默认配置)')
return parser.parse_args()
def build_config_from_args(args):
"""从命令行参数构建配置"""
cmd_config = {}
# 服务器基本配置
basic_args = ['host', 'port', 'server_name', 'ssl_cert', 'ssl_key',
'auth_type', 'auth_user', 'auth_pass', 'auth_token',
'access_log', 'verbose',
'api_version', 'directory_listing',
'enable_stats', 'show_hash', 'ignore_hidden', 'max_upload_size']
for arg_name in basic_args:
arg_value = getattr(args, arg_name, None)
if arg_value is not None:
cmd_config[arg_name] = arg_value
# Debug 配置处理
# 优先级: --debug > --debug-types > individual --debug-xxx
debug_types = []
if args.debug_types:
# 用户指定了类型列表
debug_types = list(args.debug_types)
cmd_config['debug'] = debug_types
elif args.debug_http or args.debug_auth or args.debug_api or args.debug_v2 or args.debug_error or args.debug_download:
# 用户指定了单个类型
if args.debug_http:
debug_types.append('http')
if args.debug_auth:
debug_types.append('auth')
if args.debug_api:
debug_types.append('api')
if args.debug_v2:
debug_types.append('v2')
if args.debug_error:
debug_types.append('error')
if args.debug_download:
debug_types.append('download')
cmd_config['debug'] = debug_types
elif args.debug:
# 开启所有
cmd_config['debug'] = True
else:
# 未开启任何 debug
cmd_config['debug'] = False
# Debug 日志文件
if args.debug_log_file:
cmd_config['debug_log_file'] = args.debug_log_file
# 路径处理
if args.base_dir:
cmd_config['base_dir'] = os.path.abspath(args.base_dir)
# 实时通信(仅在显式传入时覆盖 settings.json)
if args.enable_ws is not None:
cmd_config['enable_ws'] = args.enable_ws
if args.enable_sse is not None:
cmd_config['enable_sse'] = args.enable_sse
# 系统监控
if args.enable_monitor is not None:
cmd_config['enable_monitor'] = args.enable_monitor
cmd_config['monitor_interval'] = args.monitor_interval
# 同步
if args.enable_sync is not None:
cmd_config['enable_sync'] = args.enable_sync
# 镜像加速源
if args.enable_mirrors is not None:
cmd_config['enable_mirrors'] = args.enable_mirrors
# 设备预设(--preset 实际生效)
if args.preset:
cmd_config['preset'] = args.preset
# 各个镜像的启用/禁用通过 settings.json 中的 mirrors.xxx.enabled 配置
# 下载限速
cmd_config['rate_limit'] = args.rate_limit
# 静默模式
if args.quiet:
cmd_config['verbose'] = -1
return cmd_config
def main():
"""主入口函数"""
args = parse_arguments()
# 确定默认配置文件路径
project_root = os.path.dirname(os.path.abspath(__file__))
settings_path = args.settings or os.path.join(project_root, 'settings.json')
# 打印配置来源信息
print(f"[配置加载]")
print(f" 默认配置: {settings_path}")
# 加载配置(优先级从低到高:默认配置 -> 覆盖配置 -> 环境变量 -> 命令行参数)
# 使用深度合并,只替换覆盖配置中有的字段
# 1. 加载默认配置 (settings.json)
from core.config import load_json_config
default_config = load_json_config(settings_path) or {}
print(f" 默认配置加载: {'成功' if default_config else '使用内联默认'}")
# 2. 从覆盖配置文件加载
override_config = {}
if args.config:
override_config = load_json_config(args.config) or {}
print(f" 覆盖配置: {args.config} ({len(override_config)} 个顶层键)")
else:
print(f" 覆盖配置: 未指定")
# 3. 深度合并默认配置和覆盖配置
config = deep_merge(default_config, override_config)
# 4. 从环境变量加载数据库配置
env_db_config = load_db_config_from_env()
if env_db_config:
config = deep_merge(config, env_db_config)
# 5. 同步配置
if args.sync_config:
sync_config = load_json_config(args.sync_config)
if sync_config:
if 'sync_sources' in sync_config:
config['sync_sources'] = deep_merge(
config.get('sync_sources', {}),
sync_config['sync_sources']
)
if 'mirrors' in sync_config:
config['mirrors'] = deep_merge(
config.get('mirrors', {}),
sync_config['mirrors']
)
# 6. 命令行参数覆盖(最高优先级)
cmd_config = build_config_from_args(args)
config = deep_merge(config, cmd_config)
# 7. 处理 auth_token(优先从文件读取,否则生成新的)
import secrets
# 确定 token 文件路径
if getattr(sys, 'frozen', False):
# 打包环境:使用可执行文件所在目录(_MEIPASS 是临时解压目录,写入会丢失)
token_file = os.path.join(os.path.dirname(os.path.abspath(sys.executable)), 'auth_token.txt')
else:
# 非打包:使用脚本所在目录
token_file = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'auth_token.txt')
# 尝试从文件读取
existing_token = None
if os.path.exists(token_file):
try:
with open(token_file, 'r') as f:
existing_token = f.read().strip()
if existing_token:
config['auth_token'] = existing_token
print(" 已从文件加载 auth_token")
except Exception as e:
print(f" 警告: 读取 auth_token 失败: {e}")
# 如果没有现有 token,生成新的
if not existing_token:
config['auth_token'] = secrets.token_hex(32)
print(" 已生成新的 auth_token (保存在 auth_token.txt)")
# 保存新的 token 到文件(收紧权限)
try:
with open(token_file, 'w') as f:
f.write(config['auth_token'])
os.chmod(token_file, 0o600)
print(f" 已保存 auth_token 到: {token_file}")
except Exception as e:
print(f" 警告: 保存 auth_token 失败: {e}")
print(f" 最终配置: {len(config)} 个顶层配置项")
# ==================== 系统兼容性检查 ====================
if args.check_compat:
print("\n[系统兼容性检查]")
compat = check_compatibility()
print(f"\n兼容状态: {'✓ 通过' if compat['compatible'] else '✗ 存在问题'}")
arch = ArchitectureDetector.get_architecture()
print(f"\n架构信息:")
print(f" - 平台: {arch['platform']}")
print(f" - 机器: {arch['machine']}")
print(f" - 架构: {arch['architecture']}")
recommended = ArchitectureDetector.get_recommended_config()
print(f"\n推荐配置:")
for k, v in recommended.items():
print(f" - {k}: {v}")
if compat['warnings']:
print(f"\n警告:")
for w in compat['warnings']:
print(f" ⚠ {w}")
if compat['errors']:
print(f"\n错误:")
for e in compat['errors']:
print(f" ✗ {e}")
sys.exit(0 if compat['compatible'] else 1)
# ==================== 低端设备优化 ====================
print("\n[设备检测与优化]")
# 检测架构
arch = ArchitectureDetector.get_architecture()
# 应用低端设备配置
preset = config.get('preset', 'auto')
low_mem_config = LowMemoryConfig(preset)
# 获取设备信息
device_info = low_mem_config.get_device_info()
# 显示设备信息
if device_info['total_ram_mb'] > 0:
print(f" [硬件配置]")
print(f" - 总内存: {device_info['total_ram_mb']:.0f} MB")
print(f" - 可用内存: {device_info['available_ram_mb']:.0f} MB ({100-device_info['percent_used']:.1f}% 可用)")
print(f" - CPU核心: {device_info['cpu_count']} 核心")
print(f" - 系统架构: {arch['machine']} ({arch['architecture']})")
else:
print(f" - 系统架构: {arch['machine']} ({arch['architecture']})")
print(f"\n [性能优化]")
print(f" - 优化模式: {preset}")
# 应用配置
if preset != 'auto':
status = low_mem_config.get_status()
print(f" - 预设方案: {status.get('description', preset)}")
config = low_mem_config.apply_to_config(config)
# 覆盖命令行参数
if args.memory_limit:
config['memory_limit'] = parse_size(args.memory_limit)
if args.workers > 0:
config['workers'] = args.workers
if args.chunk_size:
config['chunk_size'] = parse_size(args.chunk_size)
if args.buffer_size:
config['buffer_size'] = parse_size(args.buffer_size)
# 禁用可选功能
if args.disable_ws:
config['enable_ws'] = False
if args.disable_sse:
config['enable_sse'] = False
if args.disable_hash:
config['calculate_hash'] = False
# 启动内存管理器
memory_manager = MemoryManager({
'enabled': True,
'memory_limit': config.get('memory_limit', 512 * 1024 * 1024),
'gc_interval': config.get('gc_interval', 300), # 定时GC间隔
'enable_scheduled_gc': config.get('enable_scheduled_gc', True)
})
memory_manager.start()
config['_memory_manager'] = memory_manager
mem_status = memory_manager.get_status()
print(f" - 内存限制: {mem_status['memory_limit_mb']} MB")
print(f" - 工作进程: {config.get('workers', 1)}")
print(f" - 传输块: {config.get('chunk_size', 128 * 1024) // 1024} KB")
print(f" - GC间隔: {config.get('gc_interval', 300)}秒")
print(f" - 缓存大小: {config.get('max_cache_size', 0) // (1024*1024)} MB")
# 设置信号处理
signal.signal(signal.SIGINT, signal_handler)
signal.signal(signal.SIGTERM, signal_handler)
# 打印启动信息
print()
print("╔" + "═" * 58 + "╗")
print("║" + " " * 22 + "HYC下载站 v2.3" + " " * 22 + "║")
print("╚" + "═" * 58 + "╝")
print()
print("【服务器配置】")
print(f" ▶ 监听地址: {config.get('host')}:{config.get('port')}")
print(f" ▶ 文件目录: {os.path.abspath(config.get('base_dir', './downloads'))}")
print(f" ▶ API版本: {config.get('api_version', 'v2')}")
print(f" ▶ 认证方式: {config.get('auth_type', 'none')}")
max_upload = config.get('max_upload_size', '1G')
if isinstance(max_upload, str):
max_upload = parse_size(max_upload)
else:
max_upload = int(max_upload)
print(f" ▶ 最大上传: {max_upload // (1024*1024)} MB")
# 数据库状态
db_config = config.get('database', {})
db_enabled = db_config.get('enabled', True)
print()
print("【功能模块】")
print(f" {'●' if config.get('enable_monitor') else '○'} 系统监控", end='')
print(f" {'●' if config.get('enable_sync') else '○'} 镜像同步", end='')
print(f" {'●' if config.get('enable_mirrors') else '○'} 加速源")
print(f" {'●' if config.get('enable_ws') else '○'} WebSocket", end='')
print(f" {'●' if config.get('enable_sse') else '○'} SSE", end='')
print(f" {'●' if db_enabled else '○'} 数据库")
if db_enabled:
print(f" └── 类型: {db_config.get('type', 'sqlite')} | 同步间隔: {db_config.get('sync_interval', 60)}s")
print()
print("【性能参数】")
print(f" ▶ 最大线程: {config.get('max_workers', 10)}")
print(f" ▶ 工作进程: {config.get('workers', 1)}")
print(f" ▶ 内存限制: {config.get('memory_limit', 512*1024*1024) // (1024*1024)} MB")
print(f" ▶ 连接超时: {config.get('timeout', 30)}s")
print()
print("=" * 60)
# 初始化数据库
db = None
scheduler = None
if db_enabled:
print("\n[初始化数据库...]")
try:
db = init_database(config)
# 健康检查
health = db.health_check()
if health.get('healthy'):
print(f" ✓ 数据库连接成功 ({db_config.get('type', 'sqlite')})")
print(f" ✓ 表数量: {health.get('table_count', 0)}")
else:
print(f" ✗ 数据库健康检查失败: {health.get('error')}")
# 检查并更新Schema
if db.check_schema_version():
print(f" ✓ 数据库结构版本检查通过")
# 获取统计
db_stats = db.get_stats()
print(f"\n 数据库统计:")
print(f" - 文件记录: {db_stats['total_files']}")
print(f" - 同步记录: {db_stats['total_sync_records']}")
print(f" - 缓存记录: {db_stats['total_cache_records']}")
print(f" - 下载记录: {db_stats['total_download_records']}")
# 将数据库实例添加到配置中
config['_db_instance'] = db
except Exception as e:
print(f"\n ✗ 数据库初始化失败: {e}")
print(" ⚠ 服务器将继续运行,但不使用数据库功能")
# 初始化同步调度器(不使用数据库)
if config.get('enable_sync'):
_, scheduler, _ = init_database_sync(config)
scheduler.start()
print(" - 同步调度器已启动")
# ==================== 优雅关闭处理 ====================
import atexit
def cleanup():
"""服务器关闭时清理资源"""
print("\n正在关闭服务器...")
# 关闭数据库连接池
if db:
try:
db.engine.dispose()
print("✓ 数据库连接已关闭")
except Exception as e:
print(f"✗ 关闭数据库连接时出错: {e}")
# 停止同步调度器
if scheduler:
try:
scheduler.stop()
print("✓ 同步调度器已停止")
except Exception as e:
print(f"✗ 停止同步调度器时出错: {e}")
print("服务器已关闭")
# 注册关闭处理函数
atexit.register(cleanup)
# 设置服务器启动时间(用于计算运行时间)
config['start_time'] = time.time()
# 默认凭据警告
try:
if config.get('auth_type') == 'basic' and config.get('auth_pass') in (None, '', 'admin123'):
print("\n" + "!" * 60)
print("! 安全警告: 正在使用默认/空认证密码 (admin/admin123)")
print("! 请立即修改 settings.json 中的 auth_pass 或使用 --auth-pass 指定")
print("!" * 60)
if config.get('auth_type') == 'token' and config.get('auth_token') in (None, '', 'admin_token_123456'):
print("\n" + "!" * 60)
print("! 安全警告: 正在使用默认/空认证 token")
print("! 请立即修改 settings.json 中的 auth_token 或使用 --auth-token 指定")
print("!" * 60)
except Exception:
pass
# 创建并启动服务器
try:
server = MirrorServer(config)
if server.start():
server.serve_forever()
else:
print("服务器启动失败")
sys.exit(1)
except KeyboardInterrupt:
# 信号处理已触发优雅关闭(server.stop + atexit cleanup 已执行)
print("\n服务器已正常退出")
sys.exit(0)
except Exception as e:
print(f"错误: {e}")
import traceback
traceback.print_exc()
sys.exit(1)
if __name__ == "__main__":
main()