复查修复(三): 调度与扫描
- scan: .tmp/.part 过滤 + list_files 分批遍历(消除 10 万条截断误判) - Scheduler: 持锁只做标记, 锁外异步执行任务(长任务不再冻结调度) - cron 范围/步进边界校验;FTP 下载后大小校验防静默截断
This commit is contained in:
+502
-493
@@ -1,493 +1,502 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
定时同步调度器
|
||||
负责本地数据和数据库之间的定时同步
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import json
|
||||
import hashlib
|
||||
import threading
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from typing import Dict, List, Optional, Callable
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
|
||||
# 添加项目根目录到路径
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
from core.database import DatabaseManager, get_db
|
||||
from core.scheduler import Scheduler, ScheduledTask
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SyncScheduler:
|
||||
"""同步调度器"""
|
||||
|
||||
def __init__(self, config: dict, db: DatabaseManager = None):
|
||||
self.config = config
|
||||
self.db = db
|
||||
self._running = False
|
||||
self._executor = ThreadPoolExecutor(max_workers=4)
|
||||
|
||||
# 同步配置
|
||||
self.sync_interval = config.get('database', {}).get('sync_interval', 60)
|
||||
self.auto_scan = config.get('auto_scan', True)
|
||||
self.scan_interval = config.get('scan_interval', 300) # 5分钟扫描一次
|
||||
|
||||
# 同步状态
|
||||
self.last_sync_time = 0
|
||||
self.last_scan_time = 0
|
||||
self.sync_in_progress = False
|
||||
self.scan_in_progress = False
|
||||
|
||||
# 回调函数
|
||||
self.on_file_added: Optional[Callable] = None
|
||||
self.on_file_deleted: Optional[Callable] = None
|
||||
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 = [] # 待更新的文件
|
||||
self._pending_delete = [] # 待删除的文件
|
||||
|
||||
# 定时任务调度器
|
||||
self.task_scheduler = None
|
||||
self.scheduled_syncs: Dict[str, dict] = {}
|
||||
|
||||
def _init_scheduled_syncs(self):
|
||||
"""初始化定时同步任务"""
|
||||
if not self.config.get('enable_sync', True):
|
||||
return
|
||||
|
||||
# 从配置加载定时同步设置
|
||||
sync_sources = self.config.get('sync_sources', {})
|
||||
scheduled_sources = {}
|
||||
|
||||
for name, source_config in sync_sources.items():
|
||||
schedule = source_config.get('schedule', {})
|
||||
if schedule.get('enabled', False):
|
||||
scheduled_sources[name] = {
|
||||
'type': schedule.get('type', 'interval'), # 'cron' 或 'interval'
|
||||
'config': {
|
||||
'cron': schedule.get('cron'),
|
||||
'interval': schedule.get('interval', {}),
|
||||
'enabled': True
|
||||
}
|
||||
}
|
||||
|
||||
if scheduled_sources:
|
||||
self.task_scheduler = Scheduler()
|
||||
for name, sched_config in scheduled_sources.items():
|
||||
self.task_scheduler.add_task(
|
||||
name=f"sync_{name}",
|
||||
task_type=sched_config['type'],
|
||||
config=sched_config['config'],
|
||||
callback=self._create_sync_callback(name)
|
||||
)
|
||||
self.scheduled_syncs = scheduled_sources
|
||||
|
||||
def _create_sync_callback(self, source_name: str):
|
||||
"""创建同步回调函数"""
|
||||
def sync_callback(task_name: str, config: dict):
|
||||
logger.info(f"定时同步任务触发: {source_name}")
|
||||
# 委托给注入的同步执行器(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:
|
||||
logger.warning("SyncScheduler 已经运行中")
|
||||
return
|
||||
|
||||
self._running = True
|
||||
self._executor.submit(self._sync_loop)
|
||||
self._executor.submit(self._scan_loop)
|
||||
|
||||
# 初始化并启动定时同步
|
||||
self._init_scheduled_syncs()
|
||||
if self.task_scheduler:
|
||||
self.task_scheduler.start()
|
||||
|
||||
logger.info(f"同步调度器已启动,间隔: {self.sync_interval}秒")
|
||||
|
||||
def stop(self):
|
||||
"""停止同步调度器"""
|
||||
self._running = False
|
||||
|
||||
# 停止定时任务调度器
|
||||
if self.task_scheduler:
|
||||
self.task_scheduler.stop()
|
||||
self.task_scheduler = None
|
||||
|
||||
self._executor.shutdown(wait=True)
|
||||
logger.info("同步调度器已停止")
|
||||
|
||||
def _sync_loop(self):
|
||||
"""同步循环"""
|
||||
while self._running:
|
||||
try:
|
||||
if time.time() - self.last_sync_time >= self.sync_interval:
|
||||
self.perform_sync()
|
||||
time.sleep(1)
|
||||
except Exception as e:
|
||||
logger.error(f"同步循环错误: {e}")
|
||||
time.sleep(5)
|
||||
|
||||
def _scan_loop(self):
|
||||
"""扫描循环 - 检测本地文件变化"""
|
||||
while self._running:
|
||||
try:
|
||||
if self.auto_scan and time.time() - self.last_scan_time >= self.scan_interval:
|
||||
self.scan_local_files()
|
||||
time.sleep(5)
|
||||
except Exception as e:
|
||||
logger.error(f"扫描循环错误: {e}")
|
||||
time.sleep(10)
|
||||
|
||||
def queue_add(self, file_info: dict):
|
||||
"""队列添加文件"""
|
||||
self._pending_add.append(file_info)
|
||||
|
||||
def queue_update(self, file_info: dict):
|
||||
"""队列更新文件"""
|
||||
self._pending_update.append(file_info)
|
||||
|
||||
def queue_delete(self, file_id: str):
|
||||
"""队列删除文件"""
|
||||
self._pending_delete.append(file_id)
|
||||
|
||||
def perform_sync(self):
|
||||
"""执行同步"""
|
||||
if self.sync_in_progress:
|
||||
logger.warning("同步已在进行中,跳过")
|
||||
return
|
||||
|
||||
self.sync_in_progress = True
|
||||
start_time = time.time()
|
||||
|
||||
try:
|
||||
logger.info("开始执行数据库同步...")
|
||||
|
||||
# 同步待添加的文件
|
||||
added = 0
|
||||
for file_info in self._pending_add[:]:
|
||||
try:
|
||||
self._sync_add_file(file_info)
|
||||
self._pending_add.remove(file_info)
|
||||
added += 1
|
||||
except Exception as e:
|
||||
logger.error(f"同步添加文件失败: {e}")
|
||||
|
||||
# 同步待更新的文件
|
||||
updated = 0
|
||||
for file_info in self._pending_update[:]:
|
||||
try:
|
||||
self._sync_update_file(file_info)
|
||||
self._pending_update.remove(file_info)
|
||||
updated += 1
|
||||
except Exception as e:
|
||||
logger.error(f"同步更新文件失败: {e}")
|
||||
|
||||
# 同步待删除的文件
|
||||
deleted = 0
|
||||
for file_id in self._pending_delete[:]:
|
||||
try:
|
||||
self._sync_delete_file(file_id)
|
||||
self._pending_delete.remove(file_id)
|
||||
deleted += 1
|
||||
except Exception as e:
|
||||
logger.error(f"同步删除文件失败: {e}")
|
||||
|
||||
# 同步统计
|
||||
self.db.reset_pending_count()
|
||||
|
||||
self.last_sync_time = time.time()
|
||||
duration = time.time() - start_time
|
||||
|
||||
logger.info(f"同步完成: 添加{added}, 更新{updated}, 删除{deleted}, 耗时{duration:.2f}秒")
|
||||
|
||||
# 回调
|
||||
if self.on_sync_complete:
|
||||
self.on_sync_complete({
|
||||
'added': added,
|
||||
'updated': updated,
|
||||
'deleted': deleted,
|
||||
'duration': duration
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"同步过程错误: {e}")
|
||||
finally:
|
||||
self.sync_in_progress = False
|
||||
|
||||
def scan_local_files(self):
|
||||
"""扫描本地文件"""
|
||||
if self.scan_in_progress:
|
||||
return
|
||||
|
||||
self.scan_in_progress = True
|
||||
|
||||
try:
|
||||
base_dir = self.config.get('base_dir', './downloads')
|
||||
if not os.path.exists(base_dir):
|
||||
self.last_scan_time = time.time()
|
||||
return
|
||||
|
||||
# 扫描文件
|
||||
scanned_files = []
|
||||
for root, dirs, files in os.walk(base_dir):
|
||||
for filename in files:
|
||||
full_path = os.path.join(root, filename)
|
||||
rel_path = os.path.relpath(full_path, base_dir).replace("\\", "/")
|
||||
|
||||
stat = os.stat(full_path)
|
||||
file_info = {
|
||||
'path': rel_path,
|
||||
'name': filename,
|
||||
'size': stat.st_size,
|
||||
'mtime': stat.st_mtime,
|
||||
'ctime': stat.st_ctime
|
||||
}
|
||||
scanned_files.append(file_info)
|
||||
|
||||
# 与数据库对比
|
||||
db_files = self.db.list_files(limit=100000)
|
||||
db_paths = {f.path for f in db_files if not f.is_dir}
|
||||
|
||||
# 检测新增
|
||||
local_paths = {f['path'] for f in scanned_files}
|
||||
new_paths = local_paths - db_paths
|
||||
|
||||
for path in new_paths:
|
||||
file_info = next((f for f in scanned_files if f['path'] == path), None)
|
||||
if file_info:
|
||||
file_id = hashlib.md5(path.encode()).hexdigest()
|
||||
self._sync_add_file({
|
||||
'file_id': file_id,
|
||||
'path': path,
|
||||
'name': file_info['name'],
|
||||
'size': file_info['size'],
|
||||
'updated_at': file_info['mtime']
|
||||
})
|
||||
|
||||
# 检测删除
|
||||
deleted_paths = db_paths - local_paths
|
||||
for path in deleted_paths:
|
||||
record = self.db.get_file_by_path(path)
|
||||
if record:
|
||||
self.db.delete_file(record.file_id)
|
||||
|
||||
self.last_scan_time = time.time()
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"扫描本地文件错误: {e}")
|
||||
finally:
|
||||
self.scan_in_progress = False
|
||||
|
||||
def _sync_add_file(self, file_info: dict):
|
||||
"""同步添加文件"""
|
||||
existing = self.db.get_file_by_path(file_info['path'])
|
||||
if existing:
|
||||
# 已存在,更新
|
||||
self.db.update_file(
|
||||
existing.file_id,
|
||||
size=file_info.get('size', 0),
|
||||
updated_at=file_info.get('updated_at', time.time()),
|
||||
hash=file_info.get('hash'),
|
||||
sync_status='synced'
|
||||
)
|
||||
else:
|
||||
# 新增
|
||||
file_id = file_info.get('file_id') or hashlib.md5(
|
||||
file_info['path'].encode()
|
||||
).hexdigest()
|
||||
|
||||
self.db.add_file(
|
||||
file_id=file_id,
|
||||
path=file_info['path'],
|
||||
name=file_info['name'],
|
||||
size=file_info.get('size', 0),
|
||||
hash=file_info.get('hash'),
|
||||
is_dir=False,
|
||||
created_at=file_info.get('created_at'),
|
||||
updated_at=file_info.get('updated_at', time.time())
|
||||
)
|
||||
|
||||
if self.on_file_added:
|
||||
self.on_file_added(file_info)
|
||||
|
||||
def _sync_update_file(self, file_info: dict):
|
||||
"""同步更新文件"""
|
||||
file_id = file_info.get('file_id')
|
||||
if file_id:
|
||||
self.db.update_file(
|
||||
file_id,
|
||||
size=file_info.get('size'),
|
||||
updated_at=file_info.get('updated_at', time.time()),
|
||||
hash=file_info.get('hash'),
|
||||
sync_status='synced'
|
||||
)
|
||||
|
||||
if self.on_file_updated:
|
||||
self.on_file_updated(file_info)
|
||||
|
||||
def _sync_delete_file(self, file_id: str):
|
||||
"""同步删除文件"""
|
||||
self.db.delete_file(file_id)
|
||||
|
||||
if self.on_file_deleted:
|
||||
self.on_file_deleted({'file_id': file_id})
|
||||
|
||||
def get_status(self) -> dict:
|
||||
"""获取同步状态"""
|
||||
return {
|
||||
'running': self._running,
|
||||
'last_sync_time': self.last_sync_time,
|
||||
'last_scan_time': self.last_scan_time,
|
||||
'sync_in_progress': self.sync_in_progress,
|
||||
'scan_in_progress': self.scan_in_progress,
|
||||
'pending_add': len(self._pending_add),
|
||||
'pending_update': len(self._pending_update),
|
||||
'pending_delete': len(self._pending_delete),
|
||||
'pending_operations': self.db.get_pending_operations() if self.db else 0
|
||||
}
|
||||
|
||||
def force_sync(self):
|
||||
"""强制立即同步"""
|
||||
self.last_sync_time = 0
|
||||
self.perform_sync()
|
||||
|
||||
|
||||
# ==================== 文件操作包装器 ====================
|
||||
|
||||
class DatabaseBackedFileOperations:
|
||||
"""数据库支持的文件操作"""
|
||||
|
||||
def __init__(self, config: dict, db: DatabaseManager, scheduler: SyncScheduler = None):
|
||||
self.config = config
|
||||
self.db = db
|
||||
self.scheduler = scheduler
|
||||
self.base_dir = config.get('base_dir', './downloads')
|
||||
|
||||
def add_file_record(self, path: str, name: str, size: int = 0,
|
||||
hash: str = None, is_dir: bool = False) -> dict:
|
||||
"""添加文件记录到数据库"""
|
||||
import hashlib
|
||||
|
||||
file_id = hashlib.md5(path.encode()).hexdigest()
|
||||
|
||||
file_info = {
|
||||
'file_id': file_id,
|
||||
'path': path,
|
||||
'name': name,
|
||||
'size': size,
|
||||
'hash': hash,
|
||||
'is_dir': is_dir,
|
||||
'created_at': time.time(),
|
||||
'updated_at': time.time()
|
||||
}
|
||||
|
||||
if self.scheduler:
|
||||
self.scheduler.queue_add(file_info)
|
||||
else:
|
||||
self.db.add_file(
|
||||
file_id=file_id,
|
||||
path=path,
|
||||
name=name,
|
||||
size=size,
|
||||
hash=hash,
|
||||
is_dir=is_dir,
|
||||
created_at=time.time(),
|
||||
updated_at=time.time()
|
||||
)
|
||||
|
||||
return file_info
|
||||
|
||||
def update_file_record(self, file_id: str, **kwargs):
|
||||
"""更新文件记录"""
|
||||
if self.scheduler:
|
||||
self.scheduler.queue_update({'file_id': file_id, **kwargs})
|
||||
else:
|
||||
self.db.update_file(file_id, **kwargs)
|
||||
|
||||
def delete_file_record(self, file_id: str, hard: bool = False):
|
||||
"""删除文件记录"""
|
||||
if self.scheduler:
|
||||
self.scheduler.queue_delete(file_id)
|
||||
else:
|
||||
self.db.delete_file(file_id, hard=hard)
|
||||
|
||||
def record_download(self, file_path: str, file_size: int = 0,
|
||||
client_ip: str = None, duration: float = 0,
|
||||
success: bool = True, error_message: str = None):
|
||||
"""记录下载"""
|
||||
self.db.add_download_record(
|
||||
file_path=file_path,
|
||||
file_size=file_size,
|
||||
client_ip=client_ip,
|
||||
duration=duration,
|
||||
success=success,
|
||||
error_message=error_message
|
||||
)
|
||||
|
||||
# 更新下载计数
|
||||
record = self.db.get_file_by_path(file_path)
|
||||
if record:
|
||||
self.db.increment_download_count(record.file_id)
|
||||
|
||||
def record_cache_hit(self, cache_key: str, cache_type: str):
|
||||
"""记录缓存命中"""
|
||||
record = self.db.get_cache_record(cache_key)
|
||||
if record:
|
||||
self.db.increment_cache_hits(cache_key)
|
||||
else:
|
||||
self.db.add_cache_record(
|
||||
cache_key=cache_key,
|
||||
cache_type=cache_type,
|
||||
hits=1,
|
||||
last_hit=time.time()
|
||||
)
|
||||
|
||||
|
||||
# ==================== 便捷函数 ====================
|
||||
|
||||
def get_sync_scheduler(config: dict) -> SyncScheduler:
|
||||
"""获取同步调度器"""
|
||||
db = get_db(config)
|
||||
return SyncScheduler(config, db)
|
||||
|
||||
|
||||
def init_database_sync(config: dict, db=None) -> tuple:
|
||||
"""初始化数据库和同步"""
|
||||
if db is None:
|
||||
db = get_db(config)
|
||||
scheduler = SyncScheduler(config, db)
|
||||
file_ops = DatabaseBackedFileOperations(config, db, scheduler)
|
||||
|
||||
return db, scheduler, file_ops
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
定时同步调度器
|
||||
负责本地数据和数据库之间的定时同步
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import json
|
||||
import hashlib
|
||||
import threading
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from typing import Dict, List, Optional, Callable
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
|
||||
# 添加项目根目录到路径
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
from core.database import DatabaseManager, get_db
|
||||
from core.scheduler import Scheduler, ScheduledTask
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SyncScheduler:
|
||||
"""同步调度器"""
|
||||
|
||||
def __init__(self, config: dict, db: DatabaseManager = None):
|
||||
self.config = config
|
||||
self.db = db
|
||||
self._running = False
|
||||
self._executor = ThreadPoolExecutor(max_workers=4)
|
||||
|
||||
# 同步配置
|
||||
self.sync_interval = config.get('database', {}).get('sync_interval', 60)
|
||||
self.auto_scan = config.get('auto_scan', True)
|
||||
self.scan_interval = config.get('scan_interval', 300) # 5分钟扫描一次
|
||||
|
||||
# 同步状态
|
||||
self.last_sync_time = 0
|
||||
self.last_scan_time = 0
|
||||
self.sync_in_progress = False
|
||||
self.scan_in_progress = False
|
||||
|
||||
# 回调函数
|
||||
self.on_file_added: Optional[Callable] = None
|
||||
self.on_file_deleted: Optional[Callable] = None
|
||||
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 = [] # 待更新的文件
|
||||
self._pending_delete = [] # 待删除的文件
|
||||
|
||||
# 定时任务调度器
|
||||
self.task_scheduler = None
|
||||
self.scheduled_syncs: Dict[str, dict] = {}
|
||||
|
||||
def _init_scheduled_syncs(self):
|
||||
"""初始化定时同步任务"""
|
||||
if not self.config.get('enable_sync', True):
|
||||
return
|
||||
|
||||
# 从配置加载定时同步设置
|
||||
sync_sources = self.config.get('sync_sources', {})
|
||||
scheduled_sources = {}
|
||||
|
||||
for name, source_config in sync_sources.items():
|
||||
schedule = source_config.get('schedule', {})
|
||||
if schedule.get('enabled', False):
|
||||
scheduled_sources[name] = {
|
||||
'type': schedule.get('type', 'interval'), # 'cron' 或 'interval'
|
||||
'config': {
|
||||
'cron': schedule.get('cron'),
|
||||
'interval': schedule.get('interval', {}),
|
||||
'enabled': True
|
||||
}
|
||||
}
|
||||
|
||||
if scheduled_sources:
|
||||
self.task_scheduler = Scheduler()
|
||||
for name, sched_config in scheduled_sources.items():
|
||||
self.task_scheduler.add_task(
|
||||
name=f"sync_{name}",
|
||||
task_type=sched_config['type'],
|
||||
config=sched_config['config'],
|
||||
callback=self._create_sync_callback(name)
|
||||
)
|
||||
self.scheduled_syncs = scheduled_sources
|
||||
|
||||
def _create_sync_callback(self, source_name: str):
|
||||
"""创建同步回调函数"""
|
||||
def sync_callback(task_name: str, config: dict):
|
||||
logger.info(f"定时同步任务触发: {source_name}")
|
||||
# 委托给注入的同步执行器(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:
|
||||
logger.warning("SyncScheduler 已经运行中")
|
||||
return
|
||||
|
||||
self._running = True
|
||||
self._executor.submit(self._sync_loop)
|
||||
self._executor.submit(self._scan_loop)
|
||||
|
||||
# 初始化并启动定时同步
|
||||
self._init_scheduled_syncs()
|
||||
if self.task_scheduler:
|
||||
self.task_scheduler.start()
|
||||
|
||||
logger.info(f"同步调度器已启动,间隔: {self.sync_interval}秒")
|
||||
|
||||
def stop(self):
|
||||
"""停止同步调度器"""
|
||||
self._running = False
|
||||
|
||||
# 停止定时任务调度器
|
||||
if self.task_scheduler:
|
||||
self.task_scheduler.stop()
|
||||
self.task_scheduler = None
|
||||
|
||||
self._executor.shutdown(wait=True)
|
||||
logger.info("同步调度器已停止")
|
||||
|
||||
def _sync_loop(self):
|
||||
"""同步循环"""
|
||||
while self._running:
|
||||
try:
|
||||
if time.time() - self.last_sync_time >= self.sync_interval:
|
||||
self.perform_sync()
|
||||
time.sleep(1)
|
||||
except Exception as e:
|
||||
logger.error(f"同步循环错误: {e}")
|
||||
time.sleep(5)
|
||||
|
||||
def _scan_loop(self):
|
||||
"""扫描循环 - 检测本地文件变化"""
|
||||
while self._running:
|
||||
try:
|
||||
if self.auto_scan and time.time() - self.last_scan_time >= self.scan_interval:
|
||||
self.scan_local_files()
|
||||
time.sleep(5)
|
||||
except Exception as e:
|
||||
logger.error(f"扫描循环错误: {e}")
|
||||
time.sleep(10)
|
||||
|
||||
def queue_add(self, file_info: dict):
|
||||
"""队列添加文件"""
|
||||
self._pending_add.append(file_info)
|
||||
|
||||
def queue_update(self, file_info: dict):
|
||||
"""队列更新文件"""
|
||||
self._pending_update.append(file_info)
|
||||
|
||||
def queue_delete(self, file_id: str):
|
||||
"""队列删除文件"""
|
||||
self._pending_delete.append(file_id)
|
||||
|
||||
def perform_sync(self):
|
||||
"""执行同步"""
|
||||
if self.sync_in_progress:
|
||||
logger.warning("同步已在进行中,跳过")
|
||||
return
|
||||
|
||||
self.sync_in_progress = True
|
||||
start_time = time.time()
|
||||
|
||||
try:
|
||||
logger.info("开始执行数据库同步...")
|
||||
|
||||
# 同步待添加的文件
|
||||
added = 0
|
||||
for file_info in self._pending_add[:]:
|
||||
try:
|
||||
self._sync_add_file(file_info)
|
||||
self._pending_add.remove(file_info)
|
||||
added += 1
|
||||
except Exception as e:
|
||||
logger.error(f"同步添加文件失败: {e}")
|
||||
|
||||
# 同步待更新的文件
|
||||
updated = 0
|
||||
for file_info in self._pending_update[:]:
|
||||
try:
|
||||
self._sync_update_file(file_info)
|
||||
self._pending_update.remove(file_info)
|
||||
updated += 1
|
||||
except Exception as e:
|
||||
logger.error(f"同步更新文件失败: {e}")
|
||||
|
||||
# 同步待删除的文件
|
||||
deleted = 0
|
||||
for file_id in self._pending_delete[:]:
|
||||
try:
|
||||
self._sync_delete_file(file_id)
|
||||
self._pending_delete.remove(file_id)
|
||||
deleted += 1
|
||||
except Exception as e:
|
||||
logger.error(f"同步删除文件失败: {e}")
|
||||
|
||||
# 同步统计
|
||||
self.db.reset_pending_count()
|
||||
|
||||
self.last_sync_time = time.time()
|
||||
duration = time.time() - start_time
|
||||
|
||||
logger.info(f"同步完成: 添加{added}, 更新{updated}, 删除{deleted}, 耗时{duration:.2f}秒")
|
||||
|
||||
# 回调
|
||||
if self.on_sync_complete:
|
||||
self.on_sync_complete({
|
||||
'added': added,
|
||||
'updated': updated,
|
||||
'deleted': deleted,
|
||||
'duration': duration
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"同步过程错误: {e}")
|
||||
finally:
|
||||
self.sync_in_progress = False
|
||||
|
||||
def scan_local_files(self):
|
||||
"""扫描本地文件"""
|
||||
if self.scan_in_progress:
|
||||
return
|
||||
|
||||
self.scan_in_progress = True
|
||||
|
||||
try:
|
||||
base_dir = self.config.get('base_dir', './downloads')
|
||||
if not os.path.exists(base_dir):
|
||||
self.last_scan_time = time.time()
|
||||
return
|
||||
|
||||
# 扫描文件(过滤传输中的临时文件)
|
||||
scanned_files = []
|
||||
for root, dirs, files in os.walk(base_dir):
|
||||
for filename in files:
|
||||
if filename.endswith(('.tmp', '.part')):
|
||||
continue
|
||||
full_path = os.path.join(root, filename)
|
||||
rel_path = os.path.relpath(full_path, base_dir).replace("\\", "/")
|
||||
|
||||
stat = os.stat(full_path)
|
||||
file_info = {
|
||||
'path': rel_path,
|
||||
'name': filename,
|
||||
'size': stat.st_size,
|
||||
'mtime': stat.st_mtime,
|
||||
'ctime': stat.st_ctime
|
||||
}
|
||||
scanned_files.append(file_info)
|
||||
|
||||
# 与数据库对比(分批遍历,避免 limit 截断导致误判新增/删除)
|
||||
db_paths = set()
|
||||
offset = 0
|
||||
BATCH = 5000
|
||||
while True:
|
||||
batch = self.db.list_files(limit=BATCH, offset=offset)
|
||||
if not batch:
|
||||
break
|
||||
db_paths.update(f.path for f in batch if not f.is_dir)
|
||||
offset += BATCH
|
||||
|
||||
# 检测新增
|
||||
local_paths = {f['path'] for f in scanned_files}
|
||||
new_paths = local_paths - db_paths
|
||||
|
||||
for path in new_paths:
|
||||
file_info = next((f for f in scanned_files if f['path'] == path), None)
|
||||
if file_info:
|
||||
file_id = hashlib.md5(path.encode()).hexdigest()
|
||||
self._sync_add_file({
|
||||
'file_id': file_id,
|
||||
'path': path,
|
||||
'name': file_info['name'],
|
||||
'size': file_info['size'],
|
||||
'updated_at': file_info['mtime']
|
||||
})
|
||||
|
||||
# 检测删除
|
||||
deleted_paths = db_paths - local_paths
|
||||
for path in deleted_paths:
|
||||
record = self.db.get_file_by_path(path)
|
||||
if record:
|
||||
self.db.delete_file(record.file_id)
|
||||
|
||||
self.last_scan_time = time.time()
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"扫描本地文件错误: {e}")
|
||||
finally:
|
||||
self.scan_in_progress = False
|
||||
|
||||
def _sync_add_file(self, file_info: dict):
|
||||
"""同步添加文件"""
|
||||
existing = self.db.get_file_by_path(file_info['path'])
|
||||
if existing:
|
||||
# 已存在,更新
|
||||
self.db.update_file(
|
||||
existing.file_id,
|
||||
size=file_info.get('size', 0),
|
||||
updated_at=file_info.get('updated_at', time.time()),
|
||||
hash=file_info.get('hash'),
|
||||
sync_status='synced'
|
||||
)
|
||||
else:
|
||||
# 新增
|
||||
file_id = file_info.get('file_id') or hashlib.md5(
|
||||
file_info['path'].encode()
|
||||
).hexdigest()
|
||||
|
||||
self.db.add_file(
|
||||
file_id=file_id,
|
||||
path=file_info['path'],
|
||||
name=file_info['name'],
|
||||
size=file_info.get('size', 0),
|
||||
hash=file_info.get('hash'),
|
||||
is_dir=False,
|
||||
created_at=file_info.get('created_at'),
|
||||
updated_at=file_info.get('updated_at', time.time())
|
||||
)
|
||||
|
||||
if self.on_file_added:
|
||||
self.on_file_added(file_info)
|
||||
|
||||
def _sync_update_file(self, file_info: dict):
|
||||
"""同步更新文件"""
|
||||
file_id = file_info.get('file_id')
|
||||
if file_id:
|
||||
self.db.update_file(
|
||||
file_id,
|
||||
size=file_info.get('size'),
|
||||
updated_at=file_info.get('updated_at', time.time()),
|
||||
hash=file_info.get('hash'),
|
||||
sync_status='synced'
|
||||
)
|
||||
|
||||
if self.on_file_updated:
|
||||
self.on_file_updated(file_info)
|
||||
|
||||
def _sync_delete_file(self, file_id: str):
|
||||
"""同步删除文件"""
|
||||
self.db.delete_file(file_id)
|
||||
|
||||
if self.on_file_deleted:
|
||||
self.on_file_deleted({'file_id': file_id})
|
||||
|
||||
def get_status(self) -> dict:
|
||||
"""获取同步状态"""
|
||||
return {
|
||||
'running': self._running,
|
||||
'last_sync_time': self.last_sync_time,
|
||||
'last_scan_time': self.last_scan_time,
|
||||
'sync_in_progress': self.sync_in_progress,
|
||||
'scan_in_progress': self.scan_in_progress,
|
||||
'pending_add': len(self._pending_add),
|
||||
'pending_update': len(self._pending_update),
|
||||
'pending_delete': len(self._pending_delete),
|
||||
'pending_operations': self.db.get_pending_operations() if self.db else 0
|
||||
}
|
||||
|
||||
def force_sync(self):
|
||||
"""强制立即同步"""
|
||||
self.last_sync_time = 0
|
||||
self.perform_sync()
|
||||
|
||||
|
||||
# ==================== 文件操作包装器 ====================
|
||||
|
||||
class DatabaseBackedFileOperations:
|
||||
"""数据库支持的文件操作"""
|
||||
|
||||
def __init__(self, config: dict, db: DatabaseManager, scheduler: SyncScheduler = None):
|
||||
self.config = config
|
||||
self.db = db
|
||||
self.scheduler = scheduler
|
||||
self.base_dir = config.get('base_dir', './downloads')
|
||||
|
||||
def add_file_record(self, path: str, name: str, size: int = 0,
|
||||
hash: str = None, is_dir: bool = False) -> dict:
|
||||
"""添加文件记录到数据库"""
|
||||
import hashlib
|
||||
|
||||
file_id = hashlib.md5(path.encode()).hexdigest()
|
||||
|
||||
file_info = {
|
||||
'file_id': file_id,
|
||||
'path': path,
|
||||
'name': name,
|
||||
'size': size,
|
||||
'hash': hash,
|
||||
'is_dir': is_dir,
|
||||
'created_at': time.time(),
|
||||
'updated_at': time.time()
|
||||
}
|
||||
|
||||
if self.scheduler:
|
||||
self.scheduler.queue_add(file_info)
|
||||
else:
|
||||
self.db.add_file(
|
||||
file_id=file_id,
|
||||
path=path,
|
||||
name=name,
|
||||
size=size,
|
||||
hash=hash,
|
||||
is_dir=is_dir,
|
||||
created_at=time.time(),
|
||||
updated_at=time.time()
|
||||
)
|
||||
|
||||
return file_info
|
||||
|
||||
def update_file_record(self, file_id: str, **kwargs):
|
||||
"""更新文件记录"""
|
||||
if self.scheduler:
|
||||
self.scheduler.queue_update({'file_id': file_id, **kwargs})
|
||||
else:
|
||||
self.db.update_file(file_id, **kwargs)
|
||||
|
||||
def delete_file_record(self, file_id: str, hard: bool = False):
|
||||
"""删除文件记录"""
|
||||
if self.scheduler:
|
||||
self.scheduler.queue_delete(file_id)
|
||||
else:
|
||||
self.db.delete_file(file_id, hard=hard)
|
||||
|
||||
def record_download(self, file_path: str, file_size: int = 0,
|
||||
client_ip: str = None, duration: float = 0,
|
||||
success: bool = True, error_message: str = None):
|
||||
"""记录下载"""
|
||||
self.db.add_download_record(
|
||||
file_path=file_path,
|
||||
file_size=file_size,
|
||||
client_ip=client_ip,
|
||||
duration=duration,
|
||||
success=success,
|
||||
error_message=error_message
|
||||
)
|
||||
|
||||
# 更新下载计数
|
||||
record = self.db.get_file_by_path(file_path)
|
||||
if record:
|
||||
self.db.increment_download_count(record.file_id)
|
||||
|
||||
def record_cache_hit(self, cache_key: str, cache_type: str):
|
||||
"""记录缓存命中"""
|
||||
record = self.db.get_cache_record(cache_key)
|
||||
if record:
|
||||
self.db.increment_cache_hits(cache_key)
|
||||
else:
|
||||
self.db.add_cache_record(
|
||||
cache_key=cache_key,
|
||||
cache_type=cache_type,
|
||||
hits=1,
|
||||
last_hit=time.time()
|
||||
)
|
||||
|
||||
|
||||
# ==================== 便捷函数 ====================
|
||||
|
||||
def get_sync_scheduler(config: dict) -> SyncScheduler:
|
||||
"""获取同步调度器"""
|
||||
db = get_db(config)
|
||||
return SyncScheduler(config, db)
|
||||
|
||||
|
||||
def init_database_sync(config: dict, db=None) -> tuple:
|
||||
"""初始化数据库和同步"""
|
||||
if db is None:
|
||||
db = get_db(config)
|
||||
scheduler = SyncScheduler(config, db)
|
||||
file_ops = DatabaseBackedFileOperations(config, db, scheduler)
|
||||
|
||||
return db, scheduler, file_ops
|
||||
|
||||
Reference in New Issue
Block a user