P2: 清理与一致性
- CLI: --preset 实际生效;--directory-listing/--enable-stats/--enable-*/--ignore-hidden 支持 true/false(修复 type=bool 陷阱),未显式传入时不再覆盖 settings.json - 版本号统一 v2.3(settings.json/config.py/v1/v2/Dockerfile) - 移除硬编码 /tmp/pypi_debug.log 写文件;v2 裸 except 改记录错误返回 500 - webhook id 非法输入返回 400;下载趋势 timedelta 导入提升修复 NameError - 打包模式 auth_token 写入 exe 目录(不再写入 _MEIPASS) - README 修正: 线程池架构、pyinstaller 构建方式 - mirrors/__init__.py ' Quay.io' 笔误;chunked 响应去掉 Content-Length;docs/ui realpath 边界 - SQLite WAL+busy_timeout;下载计数原子 UPDATE - requirements 移除未使用的 cachetools/apscheduler
This commit is contained in:
@@ -1474,7 +1474,7 @@ class APIv1:
|
|||||||
"""API: 获取配置信息"""
|
"""API: 获取配置信息"""
|
||||||
handler.send_json_response({
|
handler.send_json_response({
|
||||||
"server_name": self.config.get("server_name", "Mirror Server"),
|
"server_name": self.config.get("server_name", "Mirror Server"),
|
||||||
"version": "2.2",
|
"version": "2.3",
|
||||||
"base_dir": self.config['base_dir'],
|
"base_dir": self.config['base_dir'],
|
||||||
"directory_listing": self.config.get('directory_listing', True),
|
"directory_listing": self.config.get('directory_listing', True),
|
||||||
"max_upload_size": self.config.get('max_upload_size'),
|
"max_upload_size": self.config.get('max_upload_size'),
|
||||||
@@ -2252,7 +2252,7 @@ class APIv1:
|
|||||||
"""API: 获取镜像站信息"""
|
"""API: 获取镜像站信息"""
|
||||||
info = {
|
info = {
|
||||||
"server_name": self.config.get("server_name", "Mirror Server"),
|
"server_name": self.config.get("server_name", "Mirror Server"),
|
||||||
"version": "2.1",
|
"version": "2.3",
|
||||||
"uptime": time.time() - self.config.get('start_time', time.time()),
|
"uptime": time.time() - self.config.get('start_time', time.time()),
|
||||||
"total_files": sum(1 for _, _, files in os.walk(self.config['base_dir']) for _ in files),
|
"total_files": sum(1 for _, _, files in os.walk(self.config['base_dir']) for _ in files),
|
||||||
"total_size": self._get_dir_size(self.config['base_dir']),
|
"total_size": self._get_dir_size(self.config['base_dir']),
|
||||||
|
|||||||
@@ -597,11 +597,6 @@ class APIv2(APIv1):
|
|||||||
# 获取镜像类型
|
# 获取镜像类型
|
||||||
mirror_type = mirror_config.get('type', 'http')
|
mirror_type = mirror_config.get('type', 'http')
|
||||||
|
|
||||||
# 调试
|
|
||||||
debug_file = '/tmp/pypi_debug.log'
|
|
||||||
with open(debug_file, 'a') as f:
|
|
||||||
f.write(f"[V2] mirror_type={mirror_type}, handler_class={get_mirror_handler(mirror_type)}\n")
|
|
||||||
|
|
||||||
# 获取处理器类
|
# 获取处理器类
|
||||||
handler_class = get_mirror_handler(mirror_type)
|
handler_class = get_mirror_handler(mirror_type)
|
||||||
if not handler_class:
|
if not handler_class:
|
||||||
@@ -887,8 +882,17 @@ class APIv2(APIv1):
|
|||||||
# v2 端点都没匹配到,尝试调用 APIv1
|
# v2 端点都没匹配到,尝试调用 APIv1
|
||||||
try:
|
try:
|
||||||
return super().handle_request(handler, method, path, query_params)
|
return super().handle_request(handler, method, path, query_params)
|
||||||
except:
|
except Exception as e:
|
||||||
pass
|
# 记录真实错误而非吞掉(避免 v1 内部异常被误报为 404)
|
||||||
|
if handler._is_debug_enabled('error'):
|
||||||
|
import traceback
|
||||||
|
handler._debug_log('error', f"APIv1 处理失败 ({path}): {e}\n{traceback.format_exc()}")
|
||||||
|
handler.send_json_response({
|
||||||
|
"error": f"API处理错误: {str(e)}",
|
||||||
|
"version": "v1",
|
||||||
|
"path": path
|
||||||
|
}, 500)
|
||||||
|
return
|
||||||
|
|
||||||
# 404 - 未找到端点
|
# 404 - 未找到端点
|
||||||
handler.send_error(404)
|
handler.send_error(404)
|
||||||
@@ -1283,6 +1287,8 @@ class APIv2(APIv1):
|
|||||||
def api_get_download_trend(self, handler, query_params):
|
def api_get_download_trend(self, handler, query_params):
|
||||||
"""获取下载趋势(按天统计)"""
|
"""获取下载趋势(按天统计)"""
|
||||||
days = int(query_params.get('days', [7])[0])
|
days = int(query_params.get('days', [7])[0])
|
||||||
|
from datetime import timedelta # 供数据库与回退分支共同使用
|
||||||
|
|
||||||
days = min(max(days, 1), 90) # 限制 1-90 天
|
days = min(max(days, 1), 90) # 限制 1-90 天
|
||||||
|
|
||||||
trend = []
|
trend = []
|
||||||
@@ -1291,7 +1297,6 @@ class APIv2(APIv1):
|
|||||||
db = self.get_db()
|
db = self.get_db()
|
||||||
if db:
|
if db:
|
||||||
try:
|
try:
|
||||||
from datetime import timedelta
|
|
||||||
from core.database import DownloadRecord
|
from core.database import DownloadRecord
|
||||||
|
|
||||||
now = datetime.now()
|
now = datetime.now()
|
||||||
@@ -1983,7 +1988,11 @@ class APIv2(APIv1):
|
|||||||
|
|
||||||
def api_get_webhook(self, handler, webhook_id):
|
def api_get_webhook(self, handler, webhook_id):
|
||||||
"""获取webhook详情"""
|
"""获取webhook详情"""
|
||||||
webhook_id = int(webhook_id)
|
try:
|
||||||
|
webhook_id = int(webhook_id)
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
handler.send_json_response({"error": "Invalid webhook id"}, 400)
|
||||||
|
return
|
||||||
|
|
||||||
if self.db_enabled and self.db:
|
if self.db_enabled and self.db:
|
||||||
try:
|
try:
|
||||||
@@ -2006,7 +2015,11 @@ class APIv2(APIv1):
|
|||||||
|
|
||||||
def api_delete_webhook(self, handler, webhook_id):
|
def api_delete_webhook(self, handler, webhook_id):
|
||||||
"""删除webhook"""
|
"""删除webhook"""
|
||||||
webhook_id = int(webhook_id)
|
try:
|
||||||
|
webhook_id = int(webhook_id)
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
handler.send_json_response({"error": "Invalid webhook id"}, 400)
|
||||||
|
return
|
||||||
|
|
||||||
if self.db_enabled and self.db:
|
if self.db_enabled and self.db:
|
||||||
try:
|
try:
|
||||||
@@ -2031,7 +2044,11 @@ class APIv2(APIv1):
|
|||||||
|
|
||||||
def api_test_webhook(self, handler, webhook_id):
|
def api_test_webhook(self, handler, webhook_id):
|
||||||
"""测试webhook"""
|
"""测试webhook"""
|
||||||
webhook_id = int(webhook_id)
|
try:
|
||||||
|
webhook_id = int(webhook_id)
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
handler.send_json_response({"error": "Invalid webhook id"}, 400)
|
||||||
|
return
|
||||||
|
|
||||||
if self.db_enabled and self.db:
|
if self.db_enabled and self.db:
|
||||||
try:
|
try:
|
||||||
@@ -2095,7 +2112,11 @@ class APIv2(APIv1):
|
|||||||
|
|
||||||
def api_get_webhook_deliveries(self, handler, webhook_id):
|
def api_get_webhook_deliveries(self, handler, webhook_id):
|
||||||
"""获取 webhook 交付历史"""
|
"""获取 webhook 交付历史"""
|
||||||
webhook_id = int(webhook_id)
|
try:
|
||||||
|
webhook_id = int(webhook_id)
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
handler.send_json_response({"error": "Invalid webhook id"}, 400)
|
||||||
|
return
|
||||||
|
|
||||||
# 验证 webhook 存在
|
# 验证 webhook 存在
|
||||||
if self.db_enabled and self.db:
|
if self.db_enabled and self.db:
|
||||||
@@ -2126,7 +2147,11 @@ class APIv2(APIv1):
|
|||||||
|
|
||||||
def api_get_webhook_stats(self, handler, webhook_id):
|
def api_get_webhook_stats(self, handler, webhook_id):
|
||||||
"""获取 webhook 交付统计"""
|
"""获取 webhook 交付统计"""
|
||||||
webhook_id = int(webhook_id)
|
try:
|
||||||
|
webhook_id = int(webhook_id)
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
handler.send_json_response({"error": "Invalid webhook id"}, 400)
|
||||||
|
return
|
||||||
|
|
||||||
if self.db_enabled and self.db:
|
if self.db_enabled and self.db:
|
||||||
try:
|
try:
|
||||||
@@ -2146,7 +2171,11 @@ class APIv2(APIv1):
|
|||||||
|
|
||||||
def api_update_webhook(self, handler, webhook_id):
|
def api_update_webhook(self, handler, webhook_id):
|
||||||
"""更新 webhook 配置"""
|
"""更新 webhook 配置"""
|
||||||
webhook_id = int(webhook_id)
|
try:
|
||||||
|
webhook_id = int(webhook_id)
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
handler.send_json_response({"error": "Invalid webhook id"}, 400)
|
||||||
|
return
|
||||||
|
|
||||||
content_length = int(handler.headers.get('Content-Length', 0))
|
content_length = int(handler.headers.get('Content-Length', 0))
|
||||||
if content_length == 0:
|
if content_length == 0:
|
||||||
@@ -3741,7 +3770,7 @@ class APIv2(APIv1):
|
|||||||
|
|
||||||
handler.send_json_response({
|
handler.send_json_response({
|
||||||
"name": self.config.get('server_name', 'HYC下载站'),
|
"name": self.config.get('server_name', 'HYC下载站'),
|
||||||
"version": "2.2.0",
|
"version": "2.3.0",
|
||||||
"uptime_seconds": round(uptime_seconds, 2),
|
"uptime_seconds": round(uptime_seconds, 2),
|
||||||
"uptime_formatted": uptime_str,
|
"uptime_formatted": uptime_str,
|
||||||
"api_version": "v2",
|
"api_version": "v2",
|
||||||
|
|||||||
+351
-351
@@ -1,351 +1,351 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
"""配置管理模块"""
|
"""配置管理模块"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import json
|
import json
|
||||||
import hashlib
|
import hashlib
|
||||||
import time
|
import time
|
||||||
from typing import Dict, Any, Optional
|
from typing import Dict, Any, Optional
|
||||||
|
|
||||||
from .utils import parse_size
|
from .utils import parse_size
|
||||||
|
|
||||||
|
|
||||||
def get_resource_path(relative_path: str) -> str:
|
def get_resource_path(relative_path: str) -> str:
|
||||||
"""获取打包后的资源路径"""
|
"""获取打包后的资源路径"""
|
||||||
if getattr(sys, 'frozen', False) and hasattr(sys, '_MEIPASS'):
|
if getattr(sys, 'frozen', False) and hasattr(sys, '_MEIPASS'):
|
||||||
# 打包后的路径
|
# 打包后的路径
|
||||||
return os.path.join(sys._MEIPASS, relative_path)
|
return os.path.join(sys._MEIPASS, relative_path)
|
||||||
return os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), relative_path)
|
return os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), relative_path)
|
||||||
|
|
||||||
# 打包模式
|
# 打包模式
|
||||||
if getattr(sys, 'frozen', False) and hasattr(sys, '_MEIPASS'):
|
if getattr(sys, 'frozen', False) and hasattr(sys, '_MEIPASS'):
|
||||||
# 外部目录:与 exe 同级
|
# 外部目录:与 exe 同级
|
||||||
external_path = os.path.join(os.path.dirname(sys.executable), relative_path)
|
external_path = os.path.join(os.path.dirname(sys.executable), relative_path)
|
||||||
if os.path.exists(external_path):
|
if os.path.exists(external_path):
|
||||||
return external_path
|
return external_path
|
||||||
|
|
||||||
# 打包后的资源路径(_MEIPASS)
|
# 打包后的资源路径(_MEIPASS)
|
||||||
bundled_path = os.path.join(sys._MEIPASS, relative_path)
|
bundled_path = os.path.join(sys._MEIPASS, relative_path)
|
||||||
if os.path.exists(bundled_path):
|
if os.path.exists(bundled_path):
|
||||||
return bundled_path
|
return bundled_path
|
||||||
|
|
||||||
return external_path
|
return external_path
|
||||||
|
|
||||||
# 开发模式
|
# 开发模式
|
||||||
return os.path.join(project_root, relative_path)
|
return os.path.join(project_root, relative_path)
|
||||||
|
|
||||||
|
|
||||||
def deep_merge(base: Dict[str, Any], override: Dict[str, Any]) -> Dict[str, Any]:
|
def deep_merge(base: Dict[str, Any], override: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
深度合并配置
|
深度合并配置
|
||||||
- 如果 override 中有的键,会完全替换 base 中的值(除非都是 dict)
|
- 如果 override 中有的键,会完全替换 base 中的值(除非都是 dict)
|
||||||
- 如果都是 dict,则递归合并
|
- 如果都是 dict,则递归合并
|
||||||
- 不会修改原参数
|
- 不会修改原参数
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
base: 默认配置(基础配置)
|
base: 默认配置(基础配置)
|
||||||
override: 要合并的配置(优先级更高)
|
override: 要合并的配置(优先级更高)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
合并后的配置
|
合并后的配置
|
||||||
"""
|
"""
|
||||||
result = base.copy()
|
result = base.copy()
|
||||||
|
|
||||||
for key, value in override.items():
|
for key, value in override.items():
|
||||||
if key in result and isinstance(result[key], dict) and isinstance(value, dict):
|
if key in result and isinstance(result[key], dict) and isinstance(value, dict):
|
||||||
# 两者都是字典,递归合并
|
# 两者都是字典,递归合并
|
||||||
result[key] = deep_merge(result[key], value)
|
result[key] = deep_merge(result[key], value)
|
||||||
else:
|
else:
|
||||||
# 直接覆盖
|
# 直接覆盖
|
||||||
result[key] = value
|
result[key] = value
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
def load_json_config(file_path: str) -> Optional[Dict[str, Any]]:
|
def load_json_config(file_path: str) -> Optional[Dict[str, Any]]:
|
||||||
"""加载 JSON 配置文件"""
|
"""加载 JSON 配置文件"""
|
||||||
if not os.path.exists(file_path):
|
if not os.path.exists(file_path):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with open(file_path, 'r', encoding='utf-8') as f:
|
with open(file_path, 'r', encoding='utf-8') as f:
|
||||||
return json.load(f)
|
return json.load(f)
|
||||||
except json.JSONDecodeError as e:
|
except json.JSONDecodeError as e:
|
||||||
print(f"警告: 配置文件 {file_path} JSON 格式错误: {e}")
|
print(f"警告: 配置文件 {file_path} JSON 格式错误: {e}")
|
||||||
return None
|
return None
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"警告: 无法读取配置文件 {file_path}: {e}")
|
print(f"警告: 无法读取配置文件 {file_path}: {e}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
class ConfigManager:
|
class ConfigManager:
|
||||||
"""配置管理器"""
|
"""配置管理器"""
|
||||||
|
|
||||||
def __init__(self, config: Dict[str, Any] = None, settings_path: str = None):
|
def __init__(self, config: Dict[str, Any] = None, settings_path: str = None):
|
||||||
"""
|
"""
|
||||||
初始化配置管理器
|
初始化配置管理器
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
config: 传入的配置(会覆盖默认配置)
|
config: 传入的配置(会覆盖默认配置)
|
||||||
settings_path: 默认配置文件路径(可以是位置参数或关键字参数)
|
settings_path: 默认配置文件路径(可以是位置参数或关键字参数)
|
||||||
"""
|
"""
|
||||||
# 支持 settings_path 作为位置参数
|
# 支持 settings_path 作为位置参数
|
||||||
if isinstance(config, str):
|
if isinstance(config, str):
|
||||||
settings_path = config
|
settings_path = config
|
||||||
config = None
|
config = None
|
||||||
elif settings_path is None:
|
elif settings_path is None:
|
||||||
# 使用打包后的资源路径
|
# 使用打包后的资源路径
|
||||||
settings_path = get_resource_path('settings.json')
|
settings_path = get_resource_path('settings.json')
|
||||||
elif settings_path:
|
elif settings_path:
|
||||||
settings_path = settings_path
|
settings_path = settings_path
|
||||||
|
|
||||||
self._settings_path = settings_path
|
self._settings_path = settings_path
|
||||||
|
|
||||||
# 加载默认配置
|
# 加载默认配置
|
||||||
self.default_config = self._load_default_config()
|
self.default_config = self._load_default_config()
|
||||||
|
|
||||||
# 合并传入的配置
|
# 合并传入的配置
|
||||||
if config:
|
if config:
|
||||||
self.config = self._validate_config(deep_merge(self.default_config, config))
|
self.config = self._validate_config(deep_merge(self.default_config, config))
|
||||||
else:
|
else:
|
||||||
self.config = self._validate_config(self.default_config.copy())
|
self.config = self._validate_config(self.default_config.copy())
|
||||||
|
|
||||||
def _load_default_config(self) -> Dict[str, Any]:
|
def _load_default_config(self) -> Dict[str, Any]:
|
||||||
"""加载默认配置文件"""
|
"""加载默认配置文件"""
|
||||||
default_config = load_json_config(self._settings_path)
|
default_config = load_json_config(self._settings_path)
|
||||||
if default_config is None:
|
if default_config is None:
|
||||||
# 如果找不到默认配置,使用内联最小配置
|
# 如果找不到默认配置,使用内联最小配置
|
||||||
default_config = {
|
default_config = {
|
||||||
'server_name': 'HYC下载站',
|
'server_name': 'HYC下载站',
|
||||||
'host': '0.0.0.0',
|
'host': '0.0.0.0',
|
||||||
'port': 8080,
|
'port': 8080,
|
||||||
'base_dir': './downloads',
|
'base_dir': './downloads',
|
||||||
'api_version': 'v2',
|
'api_version': 'v2',
|
||||||
'directory_listing': True,
|
'directory_listing': True,
|
||||||
'enable_stats': True,
|
'enable_stats': True,
|
||||||
'auth_type': 'none',
|
'auth_type': 'none',
|
||||||
'max_upload_size': 1024 * 1024 * 1024,
|
'max_upload_size': 1024 * 1024 * 1024,
|
||||||
'timeout': 30,
|
'timeout': 30,
|
||||||
'verbose': 0,
|
'verbose': 0,
|
||||||
'enable_range': True,
|
'enable_range': True,
|
||||||
'ignore_hidden': True,
|
'ignore_hidden': True,
|
||||||
'show_hash': False,
|
'show_hash': False,
|
||||||
'calculate_hash': False,
|
'calculate_hash': False,
|
||||||
'max_search_results': 100,
|
'max_search_results': 100,
|
||||||
'enable_ws': True,
|
'enable_ws': True,
|
||||||
'enable_sse': True,
|
'enable_sse': True,
|
||||||
'enable_monitor': True,
|
'enable_monitor': True,
|
||||||
'monitor_interval': 5,
|
'monitor_interval': 5,
|
||||||
'enable_sync': True,
|
'enable_sync': True,
|
||||||
'enable_mirrors': True,
|
'enable_mirrors': True,
|
||||||
'database': {
|
'database': {
|
||||||
'enabled': True,
|
'enabled': True,
|
||||||
'type': 'sqlite',
|
'type': 'sqlite',
|
||||||
'sqlite': {'path': './data/hyc.db'}
|
'sqlite': {'path': './data/hyc.db'}
|
||||||
},
|
},
|
||||||
'mirrors': {
|
'mirrors': {
|
||||||
'docker': {'enabled': True},
|
'docker': {'enabled': True},
|
||||||
'apt': {'enabled': True},
|
'apt': {'enabled': True},
|
||||||
'yum': {'enabled': True},
|
'yum': {'enabled': True},
|
||||||
'pypi': {'enabled': True},
|
'pypi': {'enabled': True},
|
||||||
'npm': {'enabled': True},
|
'npm': {'enabled': True},
|
||||||
'go': {'enabled': True}
|
'go': {'enabled': True}
|
||||||
},
|
},
|
||||||
'sync_sources': {},
|
'sync_sources': {},
|
||||||
'webhooks': {'enabled': False, 'storage': 'webhooks.json'},
|
'webhooks': {'enabled': False, 'storage': 'webhooks.json'},
|
||||||
'auth_sessions_file': 'auth_sessions.json',
|
'auth_sessions_file': 'auth_sessions.json',
|
||||||
'auth_session_timeout': 3600,
|
'auth_session_timeout': 3600,
|
||||||
'auth_cookie_max_age': 86400
|
'auth_cookie_max_age': 86400
|
||||||
}
|
}
|
||||||
print(f"警告: 未找到默认配置文件 ({self._settings_path}),使用内联默认配置")
|
print(f"警告: 未找到默认配置文件 ({self._settings_path}),使用内联默认配置")
|
||||||
return default_config
|
return default_config
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_settings(cls, custom_config: Dict[str, Any] = None, settings_path: str = None) -> 'ConfigManager':
|
def from_settings(cls, custom_config: Dict[str, Any] = None, settings_path: str = None) -> 'ConfigManager':
|
||||||
"""
|
"""
|
||||||
从默认配置创建配置管理器
|
从默认配置创建配置管理器
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
custom_config: 自定义配置,会覆盖默认配置
|
custom_config: 自定义配置,会覆盖默认配置
|
||||||
settings_path: 默认配置文件路径
|
settings_path: 默认配置文件路径
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
ConfigManager 实例
|
ConfigManager 实例
|
||||||
"""
|
"""
|
||||||
return cls(config=custom_config, settings_path=settings_path)
|
return cls(config=custom_config, settings_path=settings_path)
|
||||||
|
|
||||||
def _validate_config(self, config: Dict[str, Any]) -> Dict[str, Any]:
|
def _validate_config(self, config: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
"""验证和修复配置"""
|
"""验证和修复配置"""
|
||||||
# 确保必要配置存在
|
# 确保必要配置存在
|
||||||
required = ['base_dir', 'host', 'port']
|
required = ['base_dir', 'host', 'port']
|
||||||
for key in required:
|
for key in required:
|
||||||
if key not in config:
|
if key not in config:
|
||||||
raise ValueError(f"缺少必要配置: {key}")
|
raise ValueError(f"缺少必要配置: {key}")
|
||||||
|
|
||||||
# 修复路径配置
|
# 修复路径配置
|
||||||
config['base_dir'] = os.path.abspath(config['base_dir'])
|
config['base_dir'] = os.path.abspath(config['base_dir'])
|
||||||
|
|
||||||
# 设置默认值(不在 _validate_config 中处理,由默认配置提供)
|
# 设置默认值(不在 _validate_config 中处理,由默认配置提供)
|
||||||
|
|
||||||
# 验证认证配置
|
# 验证认证配置
|
||||||
auth_type = config.get('auth_type', 'none')
|
auth_type = config.get('auth_type', 'none')
|
||||||
if auth_type == 'basic':
|
if auth_type == 'basic':
|
||||||
if 'auth_user' not in config:
|
if 'auth_user' not in config:
|
||||||
config['auth_user'] = 'admin'
|
config['auth_user'] = 'admin'
|
||||||
if 'auth_pass' not in config:
|
if 'auth_pass' not in config:
|
||||||
config['auth_pass'] = 'admin123'
|
config['auth_pass'] = 'admin123'
|
||||||
elif auth_type == 'token':
|
elif auth_type == 'token':
|
||||||
# 只在缺失时生成 token,避免每次配置验证/更新都使已签发 token 失效
|
# 只在缺失时生成 token,避免每次配置验证/更新都使已签发 token 失效
|
||||||
if not config.get('auth_token'):
|
if not config.get('auth_token'):
|
||||||
import secrets
|
import secrets
|
||||||
config['auth_token'] = secrets.token_hex(32)
|
config['auth_token'] = secrets.token_hex(32)
|
||||||
|
|
||||||
# 验证上传大小配置
|
# 验证上传大小配置
|
||||||
if 'max_upload_size' in config:
|
if 'max_upload_size' in config:
|
||||||
try:
|
try:
|
||||||
if isinstance(config['max_upload_size'], str):
|
if isinstance(config['max_upload_size'], str):
|
||||||
config['max_upload_size'] = parse_size(config['max_upload_size'])
|
config['max_upload_size'] = parse_size(config['max_upload_size'])
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
print(f"警告: 无效的上传大小配置: {e}")
|
print(f"警告: 无效的上传大小配置: {e}")
|
||||||
config['max_upload_size'] = 1024 * 1024 * 1024
|
config['max_upload_size'] = 1024 * 1024 * 1024
|
||||||
|
|
||||||
# 验证端口范围
|
# 验证端口范围
|
||||||
if 'port' in config:
|
if 'port' in config:
|
||||||
port = config['port']
|
port = config['port']
|
||||||
if not (1 <= port <= 65535):
|
if not (1 <= port <= 65535):
|
||||||
raise ValueError(f"无效的端口号: {port}")
|
raise ValueError(f"无效的端口号: {port}")
|
||||||
|
|
||||||
# 验证并创建必要目录
|
# 验证并创建必要目录
|
||||||
base_dir = config['base_dir']
|
base_dir = config['base_dir']
|
||||||
try:
|
try:
|
||||||
if not os.path.exists(base_dir):
|
if not os.path.exists(base_dir):
|
||||||
os.makedirs(base_dir, exist_ok=True)
|
os.makedirs(base_dir, exist_ok=True)
|
||||||
|
|
||||||
# 测试写入权限
|
# 测试写入权限
|
||||||
test_file = os.path.join(base_dir, '.write_test')
|
test_file = os.path.join(base_dir, '.write_test')
|
||||||
with open(test_file, 'w') as f:
|
with open(test_file, 'w') as f:
|
||||||
f.write('test')
|
f.write('test')
|
||||||
os.remove(test_file)
|
os.remove(test_file)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise ValueError(f"基础目录无法访问: {e}")
|
raise ValueError(f"基础目录无法访问: {e}")
|
||||||
|
|
||||||
# 获取项目根目录(脚本所在目录)
|
# 获取项目根目录(脚本所在目录)
|
||||||
project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
|
||||||
# 在项目根目录创建必要的数据目录
|
# 在项目根目录创建必要的数据目录
|
||||||
necessary_dirs = [
|
necessary_dirs = [
|
||||||
os.path.join(project_root, 'data'),
|
os.path.join(project_root, 'data'),
|
||||||
os.path.join(project_root, 'logs'),
|
os.path.join(project_root, 'logs'),
|
||||||
]
|
]
|
||||||
|
|
||||||
for dir_path in necessary_dirs:
|
for dir_path in necessary_dirs:
|
||||||
if not os.path.exists(dir_path):
|
if not os.path.exists(dir_path):
|
||||||
try:
|
try:
|
||||||
os.makedirs(dir_path, exist_ok=True)
|
os.makedirs(dir_path, exist_ok=True)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"警告: 无法创建目录 {dir_path}: {e}")
|
print(f"警告: 无法创建目录 {dir_path}: {e}")
|
||||||
|
|
||||||
# 更新配置指向项目根目录
|
# 更新配置指向项目根目录
|
||||||
config['data_dir'] = os.path.join(project_root, 'data')
|
config['data_dir'] = os.path.join(project_root, 'data')
|
||||||
config['logs_dir'] = os.path.join(project_root, 'logs')
|
config['logs_dir'] = os.path.join(project_root, 'logs')
|
||||||
|
|
||||||
return config
|
return config
|
||||||
|
|
||||||
def get(self, key: str, default: Any = None) -> Any:
|
def get(self, key: str, default: Any = None) -> Any:
|
||||||
"""获取配置项"""
|
"""获取配置项"""
|
||||||
return self.config.get(key, default)
|
return self.config.get(key, default)
|
||||||
|
|
||||||
def update(self, updates: Dict[str, Any]):
|
def update(self, updates: Dict[str, Any]):
|
||||||
"""更新配置"""
|
"""更新配置"""
|
||||||
self.config.update(updates)
|
self.config.update(updates)
|
||||||
self.config = self._validate_config(self.config)
|
self.config = self._validate_config(self.config)
|
||||||
|
|
||||||
def get_full_config(self) -> Dict[str, Any]:
|
def get_full_config(self) -> Dict[str, Any]:
|
||||||
"""获取完整配置字典"""
|
"""获取完整配置字典"""
|
||||||
return self.config.copy()
|
return self.config.copy()
|
||||||
|
|
||||||
def to_dict(self) -> Dict[str, Any]:
|
def to_dict(self) -> Dict[str, Any]:
|
||||||
"""返回配置字典(不包含敏感信息)"""
|
"""返回配置字典(不包含敏感信息)"""
|
||||||
safe_config = {
|
safe_config = {
|
||||||
"server_name": self.config.get("server_name", "Mirror Server"),
|
"server_name": self.config.get("server_name", "Mirror Server"),
|
||||||
"version": "2.2",
|
"version": "2.3",
|
||||||
"base_dir": self.config['base_dir'],
|
"base_dir": self.config['base_dir'],
|
||||||
"directory_listing": self.config.get('directory_listing', True),
|
"directory_listing": self.config.get('directory_listing', True),
|
||||||
"max_upload_size": self.config.get('max_upload_size'),
|
"max_upload_size": self.config.get('max_upload_size'),
|
||||||
"enable_stats": self.config.get('enable_stats', True),
|
"enable_stats": self.config.get('enable_stats', True),
|
||||||
"auth_type": self.config.get('auth_type', 'none'),
|
"auth_type": self.config.get('auth_type', 'none'),
|
||||||
"sort_by": self.config.get('sort_by', 'name'),
|
"sort_by": self.config.get('sort_by', 'name'),
|
||||||
"sort_reverse": self.config.get('sort_reverse', False),
|
"sort_reverse": self.config.get('sort_reverse', False),
|
||||||
"ignore_hidden": self.config.get('ignore_hidden', True),
|
"ignore_hidden": self.config.get('ignore_hidden', True),
|
||||||
"enable_range": self.config.get('enable_range', True),
|
"enable_range": self.config.get('enable_range', True),
|
||||||
"show_hash": self.config.get('show_hash', False),
|
"show_hash": self.config.get('show_hash', False),
|
||||||
"calculate_hash": self.config.get('calculate_hash', False),
|
"calculate_hash": self.config.get('calculate_hash', False),
|
||||||
"max_search_results": self.config.get('max_search_results', 100),
|
"max_search_results": self.config.get('max_search_results', 100),
|
||||||
"api_version": self.config.get('api_version', 'v1'),
|
"api_version": self.config.get('api_version', 'v1'),
|
||||||
"verbose": self.config.get('verbose', 0)
|
"verbose": self.config.get('verbose', 0)
|
||||||
}
|
}
|
||||||
return safe_config
|
return safe_config
|
||||||
|
|
||||||
|
|
||||||
def load_config_file(config_path: str) -> Dict[str, Any]:
|
def load_config_file(config_path: str) -> Dict[str, Any]:
|
||||||
"""加载配置文件"""
|
"""加载配置文件"""
|
||||||
if not os.path.exists(config_path):
|
if not os.path.exists(config_path):
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with open(config_path, 'r', encoding='utf-8') as f:
|
with open(config_path, 'r', encoding='utf-8') as f:
|
||||||
return json.load(f)
|
return json.load(f)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"错误: 无法加载配置文件 {config_path}: {e}")
|
print(f"错误: 无法加载配置文件 {config_path}: {e}")
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
|
|
||||||
def load_settings_with_override(settings_path: str, override_path: str = None) -> Dict[str, Any]:
|
def load_settings_with_override(settings_path: str, override_path: str = None) -> Dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
加载默认配置并合并覆盖配置
|
加载默认配置并合并覆盖配置
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
settings_path: 默认配置文件路径
|
settings_path: 默认配置文件路径
|
||||||
override_path: 覆盖配置文件路径(可选)
|
override_path: 覆盖配置文件路径(可选)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
合并后的完整配置
|
合并后的完整配置
|
||||||
"""
|
"""
|
||||||
# 加载默认配置
|
# 加载默认配置
|
||||||
default_config = load_json_config(settings_path) or {}
|
default_config = load_json_config(settings_path) or {}
|
||||||
|
|
||||||
# 加载覆盖配置
|
# 加载覆盖配置
|
||||||
override_config = {}
|
override_config = {}
|
||||||
if override_path:
|
if override_path:
|
||||||
override_config = load_json_config(override_path) or {}
|
override_config = load_json_config(override_path) or {}
|
||||||
|
|
||||||
# 深度合并
|
# 深度合并
|
||||||
return deep_merge(default_config, override_config)
|
return deep_merge(default_config, override_config)
|
||||||
|
|
||||||
|
|
||||||
def save_config_file(config_path: str, config: Dict[str, Any]) -> bool:
|
def save_config_file(config_path: str, config: Dict[str, Any]) -> bool:
|
||||||
"""
|
"""
|
||||||
保存配置文件
|
保存配置文件
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
config_path: 保存路径
|
config_path: 保存路径
|
||||||
config: 配置字典
|
config: 配置字典
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
是否保存成功
|
是否保存成功
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
# 创建目录
|
# 创建目录
|
||||||
os.makedirs(os.path.dirname(config_path), exist_ok=True)
|
os.makedirs(os.path.dirname(config_path), exist_ok=True)
|
||||||
|
|
||||||
with open(config_path, 'w', encoding='utf-8') as f:
|
with open(config_path, 'w', encoding='utf-8') as f:
|
||||||
json.dump(config, f, ensure_ascii=False, indent=4)
|
json.dump(config, f, ensure_ascii=False, indent=4)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"错误: 无法保存配置文件 {config_path}: {e}")
|
print(f"错误: 无法保存配置文件 {config_path}: {e}")
|
||||||
return False
|
return False
|
||||||
|
|||||||
+1570
-1553
File diff suppressed because it is too large
Load Diff
+73
-73
@@ -1,73 +1,73 @@
|
|||||||
# ============================================
|
# ============================================
|
||||||
# HYC下载站 v2.2 - Docker 构建配置
|
# HYC下载站 v2.3 - Docker 构建配置
|
||||||
# ============================================
|
# ============================================
|
||||||
|
|
||||||
# 构建阶段
|
# 构建阶段
|
||||||
FROM python:3.11-slim AS builder
|
FROM python:3.11-slim AS builder
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# 安装构建依赖
|
# 安装构建依赖
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
gcc \
|
gcc \
|
||||||
musl-dev \
|
musl-dev \
|
||||||
libffi-dev \
|
libffi-dev \
|
||||||
libssl-dev \
|
libssl-dev \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
# 安装 Python 依赖
|
# 安装 Python 依赖
|
||||||
COPY requirements.txt .
|
COPY requirements.txt .
|
||||||
RUN pip install --no-cache-dir --prefix=/install -r requirements.txt
|
RUN pip install --no-cache-dir --prefix=/install -r requirements.txt
|
||||||
|
|
||||||
# ============================================
|
# ============================================
|
||||||
# 运行阶段
|
# 运行阶段
|
||||||
FROM python:3.11-slim
|
FROM python:3.11-slim
|
||||||
|
|
||||||
LABEL maintainer="HYC Download Station"
|
LABEL maintainer="HYC Download Station"
|
||||||
LABEL description="HYC下载站 v2.2 - 镜像文件服务器 + 下载加速源"
|
LABEL description="HYC下载站 v2.3 - 镜像文件服务器 + 下载加速源"
|
||||||
|
|
||||||
# 环境变量
|
# 环境变量
|
||||||
ENV PYTHONDONTWRITEBYTECODE=1
|
ENV PYTHONDONTWRITEBYTECODE=1
|
||||||
ENV PYTHONUNBUFFERED=1
|
ENV PYTHONUNBUFFERED=1
|
||||||
ENV HYC_HOME=/app
|
ENV HYC_HOME=/app
|
||||||
|
|
||||||
# 创建工作目录
|
# 创建工作目录
|
||||||
WORKDIR ${HYC_HOME}
|
WORKDIR ${HYC_HOME}
|
||||||
|
|
||||||
# 复制 Python 和依赖
|
# 复制 Python 和依赖
|
||||||
COPY --from=builder /install /usr/local
|
COPY --from=builder /install /usr/local
|
||||||
|
|
||||||
# 复制应用代码
|
# 复制应用代码
|
||||||
COPY main.py .
|
COPY main.py .
|
||||||
COPY settings.json .
|
COPY settings.json .
|
||||||
COPY requirements.txt .
|
COPY requirements.txt .
|
||||||
COPY core/ ./core/
|
COPY core/ ./core/
|
||||||
COPY api/ ./api/
|
COPY api/ ./api/
|
||||||
COPY handlers/ ./handlers/
|
COPY handlers/ ./handlers/
|
||||||
COPY mirrors/ ./mirrors/
|
COPY mirrors/ ./mirrors/
|
||||||
COPY scripts/ ./scripts/
|
COPY scripts/ ./scripts/
|
||||||
|
|
||||||
# 创建数据目录
|
# 创建数据目录
|
||||||
RUN mkdir -p \
|
RUN mkdir -p \
|
||||||
/data \
|
/data \
|
||||||
/downloads \
|
/downloads \
|
||||||
&& chown -R nobody:nogroup /data /downloads
|
&& chown -R nobody:nogroup /data /downloads
|
||||||
|
|
||||||
# 设置工作用户
|
# 设置工作用户
|
||||||
USER nobody
|
USER nobody
|
||||||
|
|
||||||
# 挂载卷
|
# 挂载卷
|
||||||
VOLUME ["/data", "/downloads"]
|
VOLUME ["/data", "/downloads"]
|
||||||
|
|
||||||
# 暴露端口
|
# 暴露端口
|
||||||
EXPOSE 8080
|
EXPOSE 8080
|
||||||
|
|
||||||
# 健康检查
|
# 健康检查
|
||||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
|
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
|
||||||
CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8080/api/v1/health')" || exit 1
|
CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8080/api/v1/health')" || exit 1
|
||||||
|
|
||||||
# 启动命令
|
# 启动命令
|
||||||
CMD ["python", "main.py", \
|
CMD ["python", "main.py", \
|
||||||
"--host", "0.0.0.0", \
|
"--host", "0.0.0.0", \
|
||||||
"--port", "8080", \
|
"--port", "8080", \
|
||||||
"--base-dir", "/downloads"]
|
"--base-dir", "/downloads"]
|
||||||
|
|||||||
@@ -668,7 +668,7 @@ class MirrorServerHandler(BaseHTTPRequestHandler):
|
|||||||
file_path = os.path.join(docs_dir, rel_path)
|
file_path = os.path.join(docs_dir, rel_path)
|
||||||
|
|
||||||
# 防止目录遍历
|
# 防止目录遍历
|
||||||
if not os.path.realpath(file_path).startswith(os.path.realpath(docs_dir)):
|
if not os.path.realpath(file_path).startswith(os.path.realpath(docs_dir) + os.sep):
|
||||||
self.send_error(403, "Access denied")
|
self.send_error(403, "Access denied")
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -765,7 +765,7 @@ class MirrorServerHandler(BaseHTTPRequestHandler):
|
|||||||
file_path = os.path.join(ui_dir, rel_path)
|
file_path = os.path.join(ui_dir, rel_path)
|
||||||
|
|
||||||
# 防止目录遍历
|
# 防止目录遍历
|
||||||
if not os.path.realpath(file_path).startswith(os.path.realpath(ui_dir)):
|
if not os.path.realpath(file_path).startswith(os.path.realpath(ui_dir) + os.sep):
|
||||||
self.send_error(403, "Access denied")
|
self.send_error(403, "Access denied")
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -1200,7 +1200,7 @@ class MirrorServerHandler(BaseHTTPRequestHandler):
|
|||||||
mime_type, _ = mimetypes.guess_type(file_path)
|
mime_type, _ = mimetypes.guess_type(file_path)
|
||||||
self.send_response(200)
|
self.send_response(200)
|
||||||
self.send_header("Content-Type", mime_type)
|
self.send_header("Content-Type", mime_type)
|
||||||
self.send_header("Content-Length", str(file_size))
|
# 使用 chunked 时不发送 Content-Length(协议不允许同时存在)
|
||||||
self.send_header("Content-Disposition",
|
self.send_header("Content-Disposition",
|
||||||
f'attachment; filename="{self._safe_disposition_filename(file_path)}"')
|
f'attachment; filename="{self._safe_disposition_filename(file_path)}"')
|
||||||
self.send_header("Accept-Ranges", "bytes")
|
self.send_header("Accept-Ranges", "bytes")
|
||||||
|
|||||||
+583
-583
File diff suppressed because it is too large
Load Diff
+8
-21
@@ -35,6 +35,14 @@ class PyPIMirror:
|
|||||||
os.makedirs(self.simple_dir, exist_ok=True)
|
os.makedirs(self.simple_dir, exist_ok=True)
|
||||||
os.makedirs(self.web_dir, exist_ok=True)
|
os.makedirs(self.web_dir, exist_ok=True)
|
||||||
|
|
||||||
|
def _debug_log(self, msg: str):
|
||||||
|
"""受 debug 配置控制的调试日志(原硬编码 /tmp/pypi_debug.log)"""
|
||||||
|
try:
|
||||||
|
if self.config and self.config.get('debug'):
|
||||||
|
print(f"[PyPI-Mirror] {msg}")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
def handle_request(self, handler, path: str) -> bool:
|
def handle_request(self, handler, path: str) -> bool:
|
||||||
"""
|
"""
|
||||||
处理PyPI请求
|
处理PyPI请求
|
||||||
@@ -155,29 +163,17 @@ class PyPIMirror:
|
|||||||
package = package.lower()
|
package = package.lower()
|
||||||
|
|
||||||
# 调试 - 确保函数被调用
|
# 调试 - 确保函数被调用
|
||||||
debug_file = '/tmp/pypi_debug.log'
|
|
||||||
with open(debug_file, 'a') as f:
|
|
||||||
f.write(f"[HANDLE_SIMPLE] START package={package}\n")
|
|
||||||
|
|
||||||
# 检查客户端Accept header
|
# 检查客户端Accept header
|
||||||
accept = handler.headers.get('Accept', '')
|
accept = handler.headers.get('Accept', '')
|
||||||
wants_json = 'application/vnd.pypi.simple.v1+json' in accept
|
wants_json = 'application/vnd.pypi.simple.v1+json' in accept
|
||||||
|
|
||||||
# 调试
|
# 调试
|
||||||
debug_file = '/tmp/pypi_debug.log'
|
|
||||||
with open(debug_file, 'a') as f:
|
|
||||||
f.write(f"[SIMPLE_INDEX] package={package}, wants_json={wants_json}, accept={accept[:50]}\n")
|
|
||||||
|
|
||||||
# 根据请求格式选择正确的缓存key,统一使用 simple/ 前缀
|
# 根据请求格式选择正确的缓存key,统一使用 simple/ 前缀
|
||||||
cache_key = f"simple/{package}"
|
cache_key = f"simple/{package}"
|
||||||
|
|
||||||
cached = self._get_cache(cache_key)
|
cached = self._get_cache(cache_key)
|
||||||
|
|
||||||
# 调试缓存
|
# 调试缓存
|
||||||
debug_file = '/tmp/pypi_debug.log'
|
|
||||||
with open(debug_file, 'a') as f:
|
|
||||||
f.write(f"[CACHE_CHECK] cache_key={cache_key}, cached={'YES' if cached else 'NO'}\n")
|
|
||||||
|
|
||||||
if cached:
|
if cached:
|
||||||
# 返回缓存,使用正确的Content-Type
|
# 返回缓存,使用正确的Content-Type
|
||||||
if wants_json:
|
if wants_json:
|
||||||
@@ -213,9 +209,6 @@ class PyPIMirror:
|
|||||||
cache_key = f"simple/{package}"
|
cache_key = f"simple/{package}"
|
||||||
else:
|
else:
|
||||||
# 转换为HTML格式
|
# 转换为HTML格式
|
||||||
debug_file = '/tmp/pypi_debug.log'
|
|
||||||
with open(debug_file, 'a') as f:
|
|
||||||
f.write(f"[CONVERT_CALL] Before conversion\n")
|
|
||||||
try:
|
try:
|
||||||
proxy_data = self._convert_simple_html(package, data)
|
proxy_data = self._convert_simple_html(package, data)
|
||||||
with open(debug_file, 'a') as f:
|
with open(debug_file, 'a') as f:
|
||||||
@@ -564,9 +557,6 @@ class PyPIMirror:
|
|||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
# 写入调试文件
|
# 写入调试文件
|
||||||
debug_file = '/tmp/pypi_debug.log'
|
|
||||||
with open(debug_file, 'a') as f:
|
|
||||||
f.write(f"[PyPI] Converting HTML for package: {package}\n")
|
|
||||||
# 打印前几个链接用于调试
|
# 打印前几个链接用于调试
|
||||||
import re
|
import re
|
||||||
test_matches = re.findall(r'href="([^"]+)"', html)[:3]
|
test_matches = re.findall(r'href="([^"]+)"', html)[:3]
|
||||||
@@ -621,9 +611,6 @@ class PyPIMirror:
|
|||||||
def convert_relative_match(match):
|
def convert_relative_match(match):
|
||||||
"""转换相对路径链接"""
|
"""转换相对路径链接"""
|
||||||
href = match.group(1)
|
href = match.group(1)
|
||||||
debug_file = '/tmp/pypi_debug.log'
|
|
||||||
with open(debug_file, 'a') as f:
|
|
||||||
f.write(f"[CONVERT] Input href: {href[:80]}...\n")
|
|
||||||
# 提取文件名
|
# 提取文件名
|
||||||
filename = href.split('/')[-1].split('#')[0]
|
filename = href.split('/')[-1].split('#')[0]
|
||||||
# 提取完整的hash路径(从 packages/ 后面的所有部分除了文件名)
|
# 提取完整的hash路径(从 packages/ 后面的所有部分除了文件名)
|
||||||
|
|||||||
+17
-19
@@ -1,19 +1,17 @@
|
|||||||
psutil>=5.9.0
|
psutil>=5.9.0
|
||||||
pyinstaller>=6.0.0
|
pyinstaller>=6.0.0
|
||||||
python-multipart>=0.0.6
|
python-multipart>=0.0.6
|
||||||
paramiko>=2.11.0
|
paramiko>=2.11.0
|
||||||
bcrypt>=4.0.0
|
bcrypt>=4.0.0
|
||||||
cachetools>=5.3.0
|
requests>=2.31.0
|
||||||
requests>=2.31.0
|
croniter>=2.0.0
|
||||||
croniter>=2.0.0
|
|
||||||
|
# Database (SQLite 内置, 以下为可选)
|
||||||
# Database (SQLite 内置, 以下为可选)
|
sqlalchemy>=2.0.0
|
||||||
sqlalchemy>=2.0.0
|
|
||||||
apscheduler>=3.10.0
|
# PostgreSQL 驱动
|
||||||
|
psycopg2-binary>=2.9.0
|
||||||
# PostgreSQL 驱动
|
|
||||||
psycopg2-binary>=2.9.0
|
# MySQL 驱动
|
||||||
|
pymysql>=1.1.0
|
||||||
# MySQL 驱动
|
cryptography>=41.0.0
|
||||||
pymysql>=1.1.0
|
|
||||||
cryptography>=41.0.0
|
|
||||||
|
|||||||
+1
-1
@@ -569,7 +569,7 @@
|
|||||||
"shutdown_timeout": 10,
|
"shutdown_timeout": 10,
|
||||||
"strategy": "graceful"
|
"strategy": "graceful"
|
||||||
},
|
},
|
||||||
"version": "2.2",
|
"version": "2.3",
|
||||||
"sort_by": "name",
|
"sort_by": "name",
|
||||||
"sort_reverse": false,
|
"sort_reverse": false,
|
||||||
"calculate_hash": false,
|
"calculate_hash": false,
|
||||||
|
|||||||
Reference in New Issue
Block a user