Skip to content

Commit

Permalink
Refactor watch folder
Browse files Browse the repository at this point in the history
  • Loading branch information
drew2a committed Aug 16, 2022
1 parent 932f51a commit 9c68dc8
Show file tree
Hide file tree
Showing 9 changed files with 101 additions and 67 deletions.
1 change: 1 addition & 0 deletions .github/workflows/pytest.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ jobs:
uses: ./.github/actions/windows_dependencies

- name: Run Pytest
timeout-minutes: 10
run: |
pytest ./src/tribler/core ${{matrix.pytest-arguments}}
Expand Down
5 changes: 4 additions & 1 deletion .pylintrc
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,10 @@ confidence=
# --disable=W"
#disable=import-star-module-level,old-octal-literal,oct-method,print-statement,unpacking-in-except,parameter-unpacking,backtick,old-raise-syntax,old-ne-operator,long-suffix,dict-view-method,dict-iter-method,metaclass-assignment,next-method-called,raising-string,indexing-exception,raw_input-builtin,long-builtin,file-builtin,execfile-builtin,coerce-builtin,cmp-builtin,buffer-builtin,basestring-builtin,apply-builtin,filter-builtin-not-iterating,using-cmp-argument,useless-suppression,range-builtin-not-iterating,suppressed-message,no-absolute-import,old-division,cmp-method,reload-builtin,zip-builtin-not-iterating,intern-builtin,unichr-builtin,reduce-builtin,standarderror-builtin,unicode-builtin,xrange-builtin,coerce-method,delslice-method,getslice-method,setslice-method,input-builtin,round-builtin,hex-method,nonzero-method,map-builtin-not-iterating
disable=C0321,W0142,invalid-name,missing-docstring,no-member,no-name-in-module,
no-self-use,too-few-public-methods,C0330,W1203,too-many-ancestors,too-many-arguments,too-many-public-methods,too-many-statements,too-many-instance-attributes,too-many-locals,too-many-branches,too-many-return-statements
no-self-use,too-few-public-methods,C0330,W1203,too-many-ancestors,
too-many-arguments,too-many-public-methods,too-many-statements,
too-many-instance-attributes,too-many-locals,too-many-branches,
too-many-return-statements, E5110

#missing-type-doc

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -293,8 +293,12 @@ def on_save_resume_data_alert(self, alert: lt.save_resume_data_alert):
basename = hexlify(resume_data[b'info-hash']) + '.conf'
filename = self.dlmgr.get_checkpoint_dir() / basename
self.config.config['download_defaults']['name'] = self.tdef.get_name_as_unicode() # store name (for debugging)
self.config.write(str(filename))
self._logger.debug('Saving download config to file %s', filename)
try:
self.config.write(str(filename))
except OSError as e:
self._logger.warning(f'{e.__class__.__name__}: {e}')
else:
self._logger.debug(f'Resume data has been saved to: {filename}')

def on_tracker_reply_alert(self, alert: lt.tracker_reply_alert):
self._logger.info(f'On tracker reply alert: {alert}')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,8 @@ def load(config_path=None):
configspec=str(CONFIG_SPEC_PATH), default_encoding='utf-8'))

@staticmethod
def convert(settings: DownloadDefaultsSettings):
config = DownloadConfig()
def from_defaults(settings: DownloadDefaultsSettings, state_dir=None):
config = DownloadConfig(state_dir=state_dir)

config.set_hops(settings.number_hops)
config.set_safe_seeding(settings.safeseeding_enabled)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -539,7 +539,7 @@ def start_download(self, torrent_file=None, tdef=None, config: DownloadConfig =
hidden=False) -> Download:
self._logger.debug(f'Starting download: filename: {torrent_file}, torrent def: {tdef}')
if config is None:
config = DownloadConfig.convert(self.download_defaults)
config = DownloadConfig.from_defaults(self.download_defaults)
self._logger.debug('Use a default config.')

# the priority of the parameters is: (1) tdef, (2) torrent_file.
Expand Down
7 changes: 7 additions & 0 deletions src/tribler/core/components/reporter/exception_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@
}


class NoCrashException(Exception):
"""Raising exceptions of this type doesn't lead to forced Tribler stop"""


class CoreExceptionHandler:
"""
This class handles Python errors arising in the Core by catching them, adding necessary context,
Expand Down Expand Up @@ -84,6 +88,9 @@ def unhandled_error_observer(self, _, context):
if isinstance(exception, ComponentStartupException):
should_stop = exception.component.tribler_should_stop_on_component_error
exception = exception.__cause__
if isinstance(exception, NoCrashException):
should_stop = False
exception = exception.__cause__

if self._is_ignored(exception):
self.logger.warning(exception)
Expand Down
35 changes: 20 additions & 15 deletions src/tribler/core/components/watch_folder/tests/test_watch_folder.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import os
import shutil
from unittest.mock import MagicMock
from unittest.mock import MagicMock, Mock, patch

import pytest

from tribler.core.components.libtorrent.download_manager.download_manager import DownloadManager
from tribler.core.components.libtorrent.torrentdef import TorrentDef
from tribler.core.components.watch_folder.settings import WatchFolderSettings
from tribler.core.components.watch_folder.watch_folder import WatchFolder
from tribler.core.tests.tools.common import TESTS_DATA_DIR, TORRENT_UBUNTU_FILE
from tribler.core.utilities.path_util import Path


# pylint: disable=redefined-outer-name, protected-access
Expand Down Expand Up @@ -47,22 +50,24 @@ def test_watchfolder_utf8_dir(watch_folder, tmp_path):
watch_folder.check_watch_folder()


def test_watchfolder_torrent_file_one_corrupt(watch_folder: WatchFolder):
async def test_watchfolder_torrent_file_corrupt(watch_folder: WatchFolder):
directory = watch_folder.settings.get_path_as_absolute('directory', watch_folder.state_dir)
def mock_start_download(*_, **__):
mock_start_download.downloads_started += 1

mock_start_download.downloads_started = 0

shutil.copyfile(TORRENT_UBUNTU_FILE, directory / "test.torrent")
shutil.copyfile(TESTS_DATA_DIR / 'test_rss.xml', directory / "test2.torrent")
watch_folder.download_manager.start_download = mock_start_download
watch_folder.download_manager.download_exists = lambda *_: False
watch_folder.check_watch_folder()
assert mock_start_download.downloads_started == 1
assert (directory / "test2.torrent.corrupt").is_file()
corrupted_torrent = directory / "test2.torrent"
shutil.copyfile(TESTS_DATA_DIR / 'test_rss.xml', corrupted_torrent)

await watch_folder.check_watch_folder_handle_exceptions()

assert not corrupted_torrent.exists()
assert Path(f'{corrupted_torrent}.corrupt').exists()


def test_cleanup(watch_folder):
watch_folder.cleanup_torrent_file(TESTS_DATA_DIR, 'thisdoesnotexist123.bla')
assert not (TESTS_DATA_DIR / 'thisdoesnotexist123.bla.corrupt').exists()
@patch.object(TorrentDef, 'get_metainfo', Mock(return_value=None))
@patch.object(DownloadManager, 'start_download')
def test_watchfolder_torrent_file_no_metainfo(mocked_start_download: Mock, watch_folder: WatchFolder):
# Test that in the case of missing metainfo, the torrent file will be skipped
directory = watch_folder.settings.get_path_as_absolute('directory', watch_folder.state_dir)
shutil.copyfile(TORRENT_UBUNTU_FILE, directory / "test.torrent")
watch_folder.check_watch_folder()
assert not mocked_start_download.called
105 changes: 59 additions & 46 deletions src/tribler/core/components/watch_folder/watch_folder.py
Original file line number Diff line number Diff line change
@@ -1,86 +1,99 @@
import asyncio
import logging
import os
from pathlib import Path

from ipv8.taskmanager import TaskManager

from tribler.core import notifications
from tribler.core.components.libtorrent.download_manager.download_config import DownloadConfig
from tribler.core.components.libtorrent.download_manager.download_manager import DownloadManager
from tribler.core.components.libtorrent.torrentdef import TorrentDef
from tribler.core.components.reporter.exception_handler import NoCrashException
from tribler.core.components.watch_folder.settings import WatchFolderSettings
from tribler.core.utilities import path_util
from tribler.core.utilities.async_group import AsyncGroup
from tribler.core.utilities.notifier import Notifier
from tribler.core.utilities.path_util import Path

WATCH_FOLDER_CHECK_INTERVAL = 10


class WatchFolder(TaskManager):
class WatchFolder:
def __init__(self, state_dir: Path, settings: WatchFolderSettings, download_manager: DownloadManager,
notifier: Notifier):
super().__init__()
self.state_dir = state_dir
self.settings = settings
self.download_manager = download_manager
self.notifier = notifier

self.group = AsyncGroup()
self._logger = logging.getLogger(self.__class__.__name__)
self._logger.info(f'Initialised with {settings}')

def start(self):
self.register_task("check watch folder", self.check_watch_folder, interval=WATCH_FOLDER_CHECK_INTERVAL)
self.group.add(self.run())

async def stop(self):
await self.shutdown_task_manager()
await self.group.cancel()

def cleanup_torrent_file(self, root, name):
fullpath = root / name
if not fullpath.exists():
self._logger.warning("File with path %s does not exist (anymore)", root / name)
return
path = Path(str(fullpath) + ".corrupt")
try:
path.unlink(missing_ok=True)
fullpath.rename(path)
except (PermissionError, FileExistsError) as e:
self._logger.warning(f'Cant rename the file to {path}. Exception: {e}')
async def run(self):
while True:
await asyncio.sleep(WATCH_FOLDER_CHECK_INTERVAL)
self.group.add(self.check_watch_folder_handle_exceptions())

self._logger.warning("Watch folder - corrupt torrent file %s", name)
self.notifier[notifications.watch_folder_corrupt_file](name)
async def check_watch_folder_handle_exceptions(self):
try:
self.check_watch_folder()
except Exception as e:
self._logger.exception(f'Failed download attempt: {e}')
raise NoCrashException from e

def check_watch_folder(self):
self._logger.debug('Checking watch folder...')

if not self.settings.enabled or not self.state_dir:
self._logger.debug(f'Cancelled. Enabled: {self.settings.enabled}. State dir: {self.state_dir}.')
return

directory = self.settings.get_path_as_absolute('directory', self.state_dir)
self._logger.debug(f'Watch dir: {directory}')
if not directory.is_dir():
self._logger.debug(f'Cancelled. Is not directory: {directory}.')
return

# Make sure that we pass a str to os.walk
watch_dir = str(directory)
self._logger.debug(f'Watch dir: {watch_dir}')

for root, _, files in os.walk(watch_dir):
root = path_util.Path(root)
for root, _, files in os.walk(str(directory)):
for name in files:
if not name.endswith(".torrent"):
continue

try:
tdef = TorrentDef.load(root / name)
if not tdef.get_metainfo():
self.cleanup_torrent_file(root, name)
continue
except: # torrent appears to be corrupt
self.cleanup_torrent_file(root, name)
continue

infohash = tdef.get_infohash()

if not self.download_manager.download_exists(infohash):
self._logger.info("Starting download from torrent file %s", name)
self.download_manager.start_download(torrent_file=root / name)
path = Path(root) / name
self.process_torrent_file(path)

self._logger.debug('Checking watch folder completed.')

def process_torrent_file(self, path: Path):
if not path.name.endswith(".torrent"):
return

self._logger.info(f'Torrent file found: {path}')
exception = None
try:
self.start_download(path)
except Exception as e: # pylint: disable=broad-except
self._logger.error(f'{e.__class__.__name__}: {e}')
exception = e

if exception:
self._logger.info(f'Corrupted: {path}')
try:
path.replace(f'{path}.corrupt')
except OSError as e:
self._logger.warning(f'{e.__class__.__name__}: {e}')

def start_download(self, path: Path):
tdef = TorrentDef.load(path)
if not tdef.get_metainfo():
self._logger.warning(f'Missed metainfo: {path}')
return

infohash = tdef.get_infohash()

if not self.download_manager.download_exists(infohash):
self._logger.info("Starting download from torrent file %s", path.name)

download_config = DownloadConfig.from_defaults(self.download_manager.download_defaults,
state_dir=self.state_dir)

self.download_manager.start_download(torrent_file=path, config=download_config)
1 change: 1 addition & 0 deletions src/tribler/gui/error_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ def gui_error(self, *exc_info):
def core_error(self, reported_error: ReportedError):
if self._tribler_stopped or reported_error.type in self._handled_exceptions:
return
self._handled_exceptions.add(reported_error.type)

error_text = f'{reported_error.text}\n{reported_error.long_text}'
self._logger.error(error_text)
Expand Down

0 comments on commit 9c68dc8

Please sign in to comment.