P1: 功能修复
- v2 三处 self.format_file_size -> format_file_size(3 个端点不再必崩) - requirements 加 bcrypt、移除未使用的 aiohttp - verify_user 改用 bcrypt.checkpw(DB basic 登录恢复);SHA256 回退改自描述 PBKDF2,去掉明文接受 - config.py token 模式只在缺失时生成(不再每次 update 失效) - docker/npm/go/apt/yum 镜像补 cache_enabled/cache_ttl 初始化(不再必 500) - docker 镜像兼容 upstream_url 配置键 - 定时同步链路: 回调委托 sync_starter 并注入 _sync_manager;cron 逗号列表修复;星期偏移修复 - v2 同步方法与 v1 签名兼容;MirrorSyncManager 补 get_source_status/stop_all_tasks_for_source
This commit is contained in:
+4
-3
@@ -198,9 +198,10 @@ class ConfigManager:
|
||||
if 'auth_pass' not in config:
|
||||
config['auth_pass'] = 'admin123'
|
||||
elif auth_type == 'token':
|
||||
# 每次运行都重新生成标准的 token
|
||||
import secrets
|
||||
config['auth_token'] = secrets.token_hex(32)
|
||||
# 只在缺失时生成 token,避免每次配置验证/更新都使已签发 token 失效
|
||||
if not config.get('auth_token'):
|
||||
import secrets
|
||||
config['auth_token'] = secrets.token_hex(32)
|
||||
|
||||
# 验证上传大小配置
|
||||
if 'max_upload_size' in config:
|
||||
|
||||
+28
-20
@@ -1220,8 +1220,8 @@ class DatabaseManager:
|
||||
return True
|
||||
return False
|
||||
|
||||
def verify_user(self, username: str, password_hash: str) -> dict:
|
||||
"""验证用户登录"""
|
||||
def verify_user(self, username: str, password: str) -> dict:
|
||||
"""验证用户登录(password 为明文密码,用 bcrypt 校验)"""
|
||||
with self.session() as session:
|
||||
user = session.query(UserRecord).filter_by(username=username).first()
|
||||
|
||||
@@ -1236,8 +1236,8 @@ class DatabaseManager:
|
||||
if not user.enabled:
|
||||
return {'valid': False, 'reason': '账号已被禁用'}
|
||||
|
||||
# 验证密码
|
||||
if user.password_hash == password_hash:
|
||||
# 验证密码(bcrypt.checkpw,恒定时间比较)
|
||||
if self.verify_password(password, user.password_hash):
|
||||
# 登录成功
|
||||
user.last_login = time.time()
|
||||
user.login_count = (user.login_count or 0) + 1
|
||||
@@ -1291,16 +1291,22 @@ class DatabaseManager:
|
||||
print(f"[数据库] 已创建默认用户: {username}")
|
||||
|
||||
def hash_password(self, password: str) -> str:
|
||||
"""使用 bcrypt 加密密码"""
|
||||
"""使用 bcrypt 加密密码(回退: 自描述格式 PBKDF2-HMAC-SHA256)"""
|
||||
try:
|
||||
import bcrypt
|
||||
salt = bcrypt.gensalt(rounds=12)
|
||||
return bcrypt.hashpw(password.encode('utf-8'), salt).decode('utf-8')
|
||||
except ImportError:
|
||||
# 如果 bcrypt 不可用,回退到 SHA256
|
||||
# 如果 bcrypt 不可用,回退到 PBKDF2-HMAC-SHA256
|
||||
# 格式: $pbkdf2-sha256$<iterations>$<salt_b64>$<hash_b64>
|
||||
import hashlib
|
||||
salt = hashlib.sha256(str(time.time()).encode()).hexdigest()[:16]
|
||||
return hashlib.sha256((password + salt).encode()).hexdigest()
|
||||
import os as _os
|
||||
import base64
|
||||
iterations = 100_000
|
||||
salt = _os.urandom(16)
|
||||
dk = hashlib.pbkdf2_hmac('sha256', password.encode('utf-8'), salt, iterations)
|
||||
return (f"$pbkdf2-sha256${iterations}${base64.b64encode(salt).decode()}"
|
||||
f"${base64.b64encode(dk).decode()}")
|
||||
|
||||
def verify_password(self, password: str, password_hash: str) -> bool:
|
||||
"""验证密码是否正确"""
|
||||
@@ -1308,19 +1314,21 @@ class DatabaseManager:
|
||||
import bcrypt
|
||||
return bcrypt.checkpw(password.encode('utf-8'), password_hash.encode('utf-8'))
|
||||
except ImportError:
|
||||
# 如果 bcrypt 不可用,回退到 SHA256 验证
|
||||
# 回退: 只接受自描述 PBKDF2 格式,绝不接受明文
|
||||
import hashlib
|
||||
# 尝试直接比较(可能是旧格式)
|
||||
if password_hash == password:
|
||||
return True
|
||||
# 尝试带 salt 验证
|
||||
for salt_len in range(1, 17):
|
||||
salt = password_hash[:salt_len] if len(password_hash) > salt_len else ""
|
||||
if len(salt) >= 4:
|
||||
test_hash = hashlib.sha256((password + salt).encode()).hexdigest()
|
||||
if test_hash == password_hash:
|
||||
return True
|
||||
return False
|
||||
import base64
|
||||
import hmac
|
||||
if not password_hash.startswith('$pbkdf2-sha256$'):
|
||||
return False
|
||||
try:
|
||||
_, algo, iters_s, salt_b64, hash_b64 = password_hash.split('$')
|
||||
iterations = int(iters_s)
|
||||
salt = base64.b64decode(salt_b64)
|
||||
expected = base64.b64decode(hash_b64)
|
||||
dk = hashlib.pbkdf2_hmac('sha256', password.encode('utf-8'), salt, iterations)
|
||||
return hmac.compare_digest(dk, expected)
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def get_user_by_token(self, token: str) -> dict:
|
||||
"""通过 token 获取用户信息"""
|
||||
|
||||
@@ -1278,6 +1278,15 @@ class MirrorSyncManager:
|
||||
"""获取同步状态"""
|
||||
with self.sync_lock:
|
||||
return self.sync_status.copy()
|
||||
|
||||
def get_source_status(self, name):
|
||||
"""获取指定同步源状态"""
|
||||
with self.sync_lock:
|
||||
return dict(self.sync_status.get(name, {}))
|
||||
|
||||
def stop_all_tasks_for_source(self, name):
|
||||
"""停止指定同步源的所有任务(v2 API 兼容)"""
|
||||
return self.stop_sync(name)
|
||||
|
||||
def start(self):
|
||||
"""启动同步管理器"""
|
||||
|
||||
+8
-2
@@ -98,7 +98,10 @@ class ScheduledTask:
|
||||
|
||||
# 处理列表: "1,2,3"
|
||||
if ',' in part:
|
||||
return self._parse_cron_part(part.replace(',', ' '), min_val, max_val)
|
||||
result = []
|
||||
for sub in part.split(','):
|
||||
result.extend(self._parse_cron_part(sub.strip(), min_val, max_val))
|
||||
return result
|
||||
|
||||
# 处理范围: "1-5"
|
||||
if '-' in part:
|
||||
@@ -144,12 +147,15 @@ class ScheduledTask:
|
||||
if not self.cron_parts:
|
||||
return False
|
||||
|
||||
# cron 约定: 0=周日...6=周六; datetime.weekday(): 0=周一...6=周日
|
||||
cron_weekday = (dt.weekday() + 1) % 7
|
||||
|
||||
return (
|
||||
dt.minute in self.cron_parts['minute'] and
|
||||
dt.hour in self.cron_parts['hour'] and
|
||||
dt.day in self.cron_parts['day'] and
|
||||
dt.month in self.cron_parts['month'] and
|
||||
dt.weekday() in self.cron_parts['weekday']
|
||||
cron_weekday in self.cron_parts['weekday']
|
||||
)
|
||||
|
||||
def get_next_run_time(self) -> Optional[datetime]:
|
||||
|
||||
@@ -92,6 +92,8 @@ class MirrorServer:
|
||||
if self.config.get('enable_sync', True):
|
||||
self.sync_manager = MirrorSyncManager(self.config)
|
||||
self.sync_manager.start()
|
||||
# 注入到配置,供 SyncScheduler 定时回调使用
|
||||
self.config['_sync_manager'] = self.sync_manager
|
||||
|
||||
# 创建系统监控器(仅当启用时)
|
||||
self.monitor = None
|
||||
|
||||
+20
-2
@@ -52,6 +52,9 @@ class SyncScheduler:
|
||||
self.on_file_updated: Optional[Callable] = None
|
||||
self.on_sync_complete: Optional[Callable] = None
|
||||
|
||||
# 定时同步执行器(注入 MirrorSyncManager.start_sync 或等效 callable)
|
||||
self.sync_starter: Optional[Callable] = None
|
||||
|
||||
# 待同步队列
|
||||
self._pending_add = [] # 待添加的文件
|
||||
self._pending_update = [] # 待更新的文件
|
||||
@@ -97,10 +100,25 @@ class SyncScheduler:
|
||||
"""创建同步回调函数"""
|
||||
def sync_callback(task_name: str, config: dict):
|
||||
logger.info(f"定时同步任务触发: {source_name}")
|
||||
self.start_sync(source_name)
|
||||
return True
|
||||
# 委托给注入的同步执行器(MirrorSyncManager.start_sync)
|
||||
starter = self.sync_starter or self._default_sync_starter()
|
||||
if starter:
|
||||
try:
|
||||
return bool(starter(source_name))
|
||||
except Exception as e:
|
||||
logger.error(f"触发同步 {source_name} 失败: {e}")
|
||||
return False
|
||||
logger.error(f"无法触发同步 {source_name}: 未配置同步执行器(sync_starter)")
|
||||
return False
|
||||
return sync_callback
|
||||
|
||||
def _default_sync_starter(self):
|
||||
"""尝试从配置获取 MirrorSyncManager 实例(server.py 启动时注入)"""
|
||||
mgr = self.config.get('_sync_manager')
|
||||
if mgr and hasattr(mgr, 'start_sync'):
|
||||
return mgr.start_sync
|
||||
return None
|
||||
|
||||
def start(self):
|
||||
"""启动同步调度器"""
|
||||
if self._running:
|
||||
|
||||
Reference in New Issue
Block a user