feat(monitor): enhance file handling with transfer history checks

This commit is contained in:
jxxghp
2026-06-26 07:02:59 +08:00
parent 52c5f2900f
commit 1b6a548dee
2 changed files with 271 additions and 160 deletions

View File

@@ -1,5 +1,3 @@
import tempfile
import unittest
from pathlib import Path
from unittest.mock import MagicMock
@@ -30,160 +28,244 @@ class CallbackRecorder:
self.events.append((event, text, event_path, file_size))
class LocalDirectoryWatcherTest(unittest.TestCase):
def test_handle_changes_dispatches_added_and_modified_files(tmp_path):
"""
watchfiles 本地目录监控测试
新增和修改文件应转换成目录监控整理回调
"""
added_file = tmp_path / "a_added.mkv"
modified_file = tmp_path / "b_modified.mkv"
skipped_dir = tmp_path / "c_dir"
added_file.write_bytes(b"added")
modified_file.write_bytes(b"modified")
skipped_dir.mkdir()
def test_handle_changes_dispatches_added_and_modified_files(self):
"""
新增和修改文件应转换成目录监控整理回调。
"""
with tempfile.TemporaryDirectory() as temp_dir:
watch_dir = Path(temp_dir)
added_file = watch_dir / "a_added.mkv"
modified_file = watch_dir / "b_modified.mkv"
skipped_dir = watch_dir / "c_dir"
added_file.write_bytes(b"added")
modified_file.write_bytes(b"modified")
skipped_dir.mkdir()
callback = CallbackRecorder()
watcher = LocalDirectoryWatcher(tmp_path, callback=callback, force_polling=True)
watcher._handle_changes({
(Change.added, added_file.as_posix()),
(Change.modified, modified_file.as_posix()),
(Change.deleted, added_file.as_posix()),
(Change.added, skipped_dir.as_posix()),
})
callback = CallbackRecorder()
watcher = LocalDirectoryWatcher(watch_dir, callback=callback, force_polling=True)
watcher._handle_changes({
(Change.added, added_file.as_posix()),
(Change.modified, modified_file.as_posix()),
(Change.deleted, added_file.as_posix()),
(Change.added, skipped_dir.as_posix()),
})
self.assertEqual(2, len(callback.events))
self.assertEqual((Change.added, "新增", added_file.as_posix(), 5),
(callback.events[0][0].change_type,
callback.events[0][1],
callback.events[0][2],
callback.events[0][3]))
self.assertEqual((Change.modified, "修改", modified_file.as_posix(), 8),
(callback.events[1][0].change_type,
callback.events[1][1],
callback.events[1][2],
callback.events[1][3]))
def test_handle_changes_skips_missing_paths(self):
"""
事件到达时已经消失的路径不应触发整理。
"""
with tempfile.TemporaryDirectory() as temp_dir:
watch_dir = Path(temp_dir)
missing_file = watch_dir / "missing.mkv"
callback = CallbackRecorder()
watcher = LocalDirectoryWatcher(watch_dir, callback=callback, force_polling=True)
watcher._handle_changes({(Change.added, missing_file.as_posix())})
self.assertEqual([], callback.events)
assert len(callback.events) == 2
assert (
Change.added,
"新增",
added_file.as_posix(),
5,
) == (
callback.events[0][0].change_type,
callback.events[0][1],
callback.events[0][2],
callback.events[0][3],
)
assert (
Change.modified,
"修改",
modified_file.as_posix(),
8,
) == (
callback.events[1][0].change_type,
callback.events[1][1],
callback.events[1][2],
callback.events[1][3],
)
class MonitorWatchfilesEventTest(unittest.TestCase):
def test_handle_changes_skips_missing_paths(tmp_path):
"""
Monitor 对 watchfiles 事件的兼容处理测试
事件到达时已经消失的路径不应触发整理
"""
missing_file = tmp_path / "missing.mkv"
def test_event_handler_routes_file_events_to_transfer_handler(self):
callback = CallbackRecorder()
watcher = LocalDirectoryWatcher(tmp_path, callback=callback, force_polling=True)
watcher._handle_changes({(Change.added, missing_file.as_posix())})
assert callback.events == []
def test_event_handler_routes_file_events_to_transfer_handler():
"""
文件事件应继续按 local 存储交给整理流程。
"""
monitor = object.__new__(Monitor)
monitor.all_exts = [".mkv"]
handle_file = MagicMock()
setattr(monitor, "_Monitor__handle_file", handle_file)
event_path = Path("/downloads/movie.mkv")
event = DirectoryChangeEvent(
change_type=Change.added,
src_path=event_path.as_posix(),
is_directory=False
)
monitor.event_handler(
event=event,
text="新增",
event_path=event_path.as_posix(),
file_size=1024
)
handle_file.assert_called_once_with(
storage="local",
event_path=event_path,
file_size=1024
)
def test_event_handler_ignores_directory_events():
"""
目录事件不应进入文件整理流程。
"""
monitor = object.__new__(Monitor)
monitor.all_exts = [".mkv"]
handle_file = MagicMock()
setattr(monitor, "_Monitor__handle_file", handle_file)
event_path = Path("/downloads/folder")
event = DirectoryChangeEvent(
change_type=Change.added,
src_path=event_path.as_posix(),
is_directory=True
)
monitor.event_handler(
event=event,
text="新增",
event_path=event_path.as_posix()
)
handle_file.assert_not_called()
def test_event_handler_ignores_download_temp_files():
"""
下载器临时文件不应进入整理流程。
"""
monitor = object.__new__(Monitor)
monitor.all_exts = [".mkv"]
handle_file = MagicMock()
setattr(monitor, "_Monitor__handle_file", handle_file)
event_path = Path("/downloads/movie.mkv.!qB")
event = DirectoryChangeEvent(
change_type=Change.modified,
src_path=event_path.as_posix(),
is_directory=False
)
monitor.event_handler(
event=event,
text="修改",
event_path=event_path.as_posix(),
file_size=1024
)
handle_file.assert_not_called()
def test_event_handler_ignores_non_transferable_files():
"""
非可整理后缀文件不应进入整理流程。
"""
monitor = object.__new__(Monitor)
monitor.all_exts = [".mkv"]
handle_file = MagicMock()
setattr(monitor, "_Monitor__handle_file", handle_file)
event_path = Path("/downloads/movie.nfo")
event = DirectoryChangeEvent(
change_type=Change.added,
src_path=event_path.as_posix(),
is_directory=False
)
monitor.event_handler(
event=event,
text="新增",
event_path=event_path.as_posix(),
file_size=1024
)
handle_file.assert_not_called()
def test_handle_file_skips_transfer_when_history_exists(monkeypatch):
"""
已有整理记录的源文件不应再次进入整理链。
"""
monitor = object.__new__(Monitor)
monitor.all_exts = [".mkv"]
monitor._cache = {}
event_path = Path("/downloads/movie.mkv")
lookups = []
class FakeTransferHistoryOper:
"""
文件事件应继续按 local 存储交给整理流程
测试用整理历史查询
"""
monitor = object.__new__(Monitor)
monitor.all_exts = [".mkv"]
handle_file = MagicMock()
setattr(monitor, "_Monitor__handle_file", handle_file)
event_path = Path("/downloads/movie.mkv")
event = DirectoryChangeEvent(
change_type=Change.added,
src_path=event_path.as_posix(),
is_directory=False
)
monitor.event_handler(
event=event,
text="新增",
event_path=event_path.as_posix(),
file_size=1024
)
def get_by_src(self, src: str, storage: str = None):
"""
记录查询参数并返回已有记录。
"""
lookups.append((src, storage))
return object()
handle_file.assert_called_once_with(
storage="local",
event_path=event_path,
file_size=1024
)
transfer_chain = MagicMock()
logger_info = MagicMock()
logger_debug = MagicMock()
monkeypatch.setattr("app.monitor.TransferHistoryOper", FakeTransferHistoryOper)
monkeypatch.setattr("app.monitor.TransferChain", transfer_chain)
monkeypatch.setattr("app.monitor.logger.info", logger_info)
monkeypatch.setattr("app.monitor.logger.debug", logger_debug)
def test_event_handler_ignores_directory_events(self):
handled = monitor._Monitor__handle_file(
storage="local",
event_path=event_path,
file_size=1024,
)
assert not handled
assert lookups == [(event_path.as_posix(), "local")]
transfer_chain.assert_not_called()
logger_info.assert_not_called()
logger_debug.assert_not_called()
def test_handle_file_invokes_transfer_when_history_missing(monkeypatch):
"""
没有整理记录的源文件应继续进入整理链。
"""
monitor = object.__new__(Monitor)
monitor.all_exts = [".mkv"]
monitor._cache = {}
event_path = Path("/downloads/movie.mkv")
class FakeTransferHistoryOper:
"""
目录事件不应进入文件整理流程
测试用空整理历史查询
"""
monitor = object.__new__(Monitor)
monitor.all_exts = [".mkv"]
handle_file = MagicMock()
setattr(monitor, "_Monitor__handle_file", handle_file)
event_path = Path("/downloads/folder")
event = DirectoryChangeEvent(
change_type=Change.added,
src_path=event_path.as_posix(),
is_directory=True
)
monitor.event_handler(
event=event,
text="新增",
event_path=event_path.as_posix()
)
def get_by_src(self, src: str, storage: str = None):
"""
返回空整理记录。
"""
return None
handle_file.assert_not_called()
transfer_chain_instance = MagicMock()
transfer_chain = MagicMock(return_value=transfer_chain_instance)
monkeypatch.setattr("app.monitor.TransferHistoryOper", FakeTransferHistoryOper)
monkeypatch.setattr("app.monitor.TransferChain", transfer_chain)
def test_event_handler_ignores_download_temp_files(self):
"""
下载器临时文件不应进入整理流程。
"""
monitor = object.__new__(Monitor)
monitor.all_exts = [".mkv"]
handle_file = MagicMock()
setattr(monitor, "_Monitor__handle_file", handle_file)
event_path = Path("/downloads/movie.mkv.!qB")
event = DirectoryChangeEvent(
change_type=Change.modified,
src_path=event_path.as_posix(),
is_directory=False
)
handled = monitor._Monitor__handle_file(
storage="local",
event_path=event_path,
file_size=1024,
)
monitor.event_handler(
event=event,
text="修改",
event_path=event_path.as_posix(),
file_size=1024
)
handle_file.assert_not_called()
def test_event_handler_ignores_non_transferable_files(self):
"""
非可整理后缀文件不应进入整理流程。
"""
monitor = object.__new__(Monitor)
monitor.all_exts = [".mkv"]
handle_file = MagicMock()
setattr(monitor, "_Monitor__handle_file", handle_file)
event_path = Path("/downloads/movie.nfo")
event = DirectoryChangeEvent(
change_type=Change.added,
src_path=event_path.as_posix(),
is_directory=False
)
monitor.event_handler(
event=event,
text="新增",
event_path=event_path.as_posix(),
file_size=1024
)
handle_file.assert_not_called()
assert handled
transfer_chain_instance.do_transfer.assert_called_once()
fileitem = transfer_chain_instance.do_transfer.call_args.kwargs["fileitem"]
assert fileitem.storage == "local"
assert fileitem.path == event_path.as_posix()
assert fileitem.size == 1024