复查修复(三): 调度与扫描

- scan: .tmp/.part 过滤 + list_files 分批遍历(消除 10 万条截断误判)
- Scheduler: 持锁只做标记, 锁外异步执行任务(长任务不再冻结调度)
- cron 范围/步进边界校验;FTP 下载后大小校验防静默截断
This commit is contained in:
HYC Fixer
2026-09-02 00:40:09 +08:00
parent 82875b710a
commit ccd66b806f
3 changed files with 533 additions and 502 deletions
+6
View File
@@ -1308,6 +1308,12 @@ class MirrorSyncManager:
else: else:
ftp.retrbinary(f'RETR {remote_filename}', f.write) ftp.retrbinary(f'RETR {remote_filename}', f.write)
# 校验下载大小与远端一致,防止静默截断
actual_size = os.path.getsize(local_path)
expected = file_info.get('size')
if expected is not None and expected > 0 and actual_size != expected:
raise Exception(f"文件大小不匹配: {actual_size} != {expected}")
return True return True
except Exception as e: except Exception as e:
print(f"下载FTP文件失败 {remote_filename}: {e}") print(f"下载FTP文件失败 {remote_filename}: {e}")
+23 -7
View File
@@ -103,16 +103,27 @@ class ScheduledTask:
result.extend(self._parse_cron_part(sub.strip(), min_val, max_val)) result.extend(self._parse_cron_part(sub.strip(), min_val, max_val))
return result return result
# 处理范围: "1-5" # 处理范围: "1-5"(校验边界,越界返回空)
if '-' in part: if '-' in part:
try:
start, end = part.split('-') start, end = part.split('-')
return list(range(int(start), int(end) + 1)) start, end = int(start), int(end)
if not (min_val <= start <= end <= max_val):
return []
return list(range(start, end + 1))
except ValueError:
return []
# 处理步进: "*/5" # 处理步进: "*/5"
if '/' in part: if '/' in part:
base, step = part.split('/') base, step = part.split('/')
base_list = self._parse_cron_part(base or '*', min_val, max_val) base_list = self._parse_cron_part(base or '*', min_val, max_val)
try:
step = int(step) step = int(step)
if step <= 0:
return []
except ValueError:
return []
return base_list[::step] return base_list[::step]
# 单个值 # 单个值
@@ -301,18 +312,23 @@ class Scheduler:
logger.info("定时任务调度器已停止") logger.info("定时任务调度器已停止")
def _run_loop(self): def _run_loop(self):
"""运行循环""" """运行循环(持锁只做标记,锁外异步执行任务,避免长任务冻结调度)"""
while self._running: while self._running:
try: try:
now = datetime.now() now = datetime.now()
due = []
with self._lock: with self._lock:
for name, task in self.tasks.items(): for name, task in self.tasks.items():
if task.should_run_now(): if task.should_run_now():
# 使用线程池执行任务 due.append(task)
from concurrent.futures import ThreadPoolExecutor
with ThreadPoolExecutor(max_workers=1) as executor: # 锁外异步执行,不阻塞后续任务的调度
executor.submit(task.run) for task in due:
try:
threading.Thread(target=task.run, daemon=True).start()
except Exception as e:
logger.error(f"调度任务 {task.name} 启动失败: {e}")
time.sleep(self.check_interval) time.sleep(self.check_interval)
+13 -4
View File
@@ -259,10 +259,12 @@ class SyncScheduler:
self.last_scan_time = time.time() self.last_scan_time = time.time()
return return
# 扫描文件 # 扫描文件(过滤传输中的临时文件)
scanned_files = [] scanned_files = []
for root, dirs, files in os.walk(base_dir): for root, dirs, files in os.walk(base_dir):
for filename in files: for filename in files:
if filename.endswith(('.tmp', '.part')):
continue
full_path = os.path.join(root, filename) full_path = os.path.join(root, filename)
rel_path = os.path.relpath(full_path, base_dir).replace("\\", "/") rel_path = os.path.relpath(full_path, base_dir).replace("\\", "/")
@@ -276,9 +278,16 @@ class SyncScheduler:
} }
scanned_files.append(file_info) scanned_files.append(file_info)
# 与数据库对比 # 与数据库对比(分批遍历,避免 limit 截断导致误判新增/删除)
db_files = self.db.list_files(limit=100000) db_paths = set()
db_paths = {f.path for f in db_files if not f.is_dir} 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} local_paths = {f['path'] for f in scanned_files}