diff --git a/src/tribler/core/libtorrent/download_manager/download_config.py b/src/tribler/core/libtorrent/download_manager/download_config.py index b5dcceed23..23c8b1db4d 100644 --- a/src/tribler/core/libtorrent/download_manager/download_config.py +++ b/src/tribler/core/libtorrent/download_manager/download_config.py @@ -144,8 +144,9 @@ def write(self, filename: Path) -> None: """ Write the contents of this config to a file. """ - self.config["filename"] = str(filename) - self.config.write() + config_obj = cast(ConfigObj, self.config) + config_obj.filename = str(filename) + config_obj.write() def set_dest_dir(self, path: Path | str) -> None: """ diff --git a/src/tribler/core/libtorrent/download_manager/download_manager.py b/src/tribler/core/libtorrent/download_manager/download_manager.py index e7e2a7f880..3e997f3166 100644 --- a/src/tribler/core/libtorrent/download_manager/download_manager.py +++ b/src/tribler/core/libtorrent/download_manager/download_manager.py @@ -25,7 +25,6 @@ from validate import Validator from yarl import URL -from tribler.core.libtorrent import torrents from tribler.core.libtorrent.download_manager.download import Download from tribler.core.libtorrent.download_manager.download_config import DownloadConfig from tribler.core.libtorrent.download_manager.download_state import DownloadState, DownloadStatus @@ -35,7 +34,6 @@ if TYPE_CHECKING: from tribler.core.libtorrent.download_manager.dht_health_manager import DHTHealthManager - from tribler.core.libtorrent.torrents import TorrentFileResult from tribler.tribler_config import TriblerConfigManager SOCKS5_PROXY_DEF = 2 @@ -1061,17 +1059,6 @@ def get_checkpoint_dir(self) -> Path: """ return self.state_dir / "dlcheckpoints" - @staticmethod - async def create_torrent_file(file_path_list: list[str], params: dict | None = None) -> TorrentFileResult: - """ - Creates a torrent file. - - :param file_path_list: files to add in torrent file - :param params: optional parameters for torrent file - """ - return await asyncio.get_event_loop().run_in_executor(None, torrents.create_torrent_file, - file_path_list, params or {}) - def get_downloads_by_name(self, torrent_name: str) -> list[Download]: """ Get all downloads for which the UTF-8 name equals the given string. diff --git a/src/tribler/core/libtorrent/restapi/create_torrent_endpoint.py b/src/tribler/core/libtorrent/restapi/create_torrent_endpoint.py index 8ef5fa7593..825cee8e62 100644 --- a/src/tribler/core/libtorrent/restapi/create_torrent_endpoint.py +++ b/src/tribler/core/libtorrent/restapi/create_torrent_endpoint.py @@ -1,3 +1,4 @@ +import asyncio import base64 import json from pathlib import Path @@ -13,6 +14,7 @@ from tribler.core.libtorrent.download_manager.download_config import DownloadConfig from tribler.core.libtorrent.download_manager.download_manager import DownloadManager from tribler.core.libtorrent.torrentdef import TorrentDef +from tribler.core.libtorrent.torrents import create_torrent_file from tribler.core.restapi.rest_endpoint import ( HTTP_BAD_REQUEST, MAX_REQUEST_SIZE, @@ -91,7 +93,7 @@ async def create_torrent(self, request: Request) -> RESTResponse: params = {} if parameters.get("files"): - file_path_list = parameters["files"] + file_path_list = [Path(p) for p in parameters["files"]] else: return RESTResponse({"error": "files parameter missing"}, status=HTTP_BAD_REQUEST) @@ -118,24 +120,23 @@ async def create_torrent(self, request: Request) -> RESTResponse: params["encoding"] = False params["piece length"] = 0 # auto + save_path = export_dir / (f"{name}.torrent") if export_dir and export_dir.exists() else None + try: - result = await self.download_manager.create_torrent_file(file_path_list, recursive_bytes(params)) + result = await asyncio.get_event_loop().run_in_executor(None, create_torrent_file, + file_path_list, recursive_bytes(params), + save_path) except (OSError, UnicodeDecodeError, RuntimeError) as e: self._logger.exception(e) return return_handled_exception(e) metainfo_dict = lt.bdecode(result["metainfo"]) - if export_dir and export_dir.exists(): - save_path = export_dir / (f"{name}.torrent") - with open(save_path, "wb") as fd: # noqa: ASYNC101 - fd.write(result["metainfo"]) - # Download this torrent if specified if "download" in request.query and request.query["download"] and request.query["download"] == "1": download_config = DownloadConfig.from_defaults(self.download_manager.config) download_config.set_dest_dir(result["base_dir"]) download_config.set_hops(self.download_manager.config.get("libtorrent/download_defaults/number_hops")) - await self.download_manager.start_download(tdef=TorrentDef(metainfo_dict), config=download_config) + await self.download_manager.start_download(save_path, TorrentDef(metainfo_dict), download_config) return RESTResponse(json.dumps({"torrent": base64.b64encode(result["metainfo"]).decode()})) diff --git a/src/tribler/core/libtorrent/torrentdef.py b/src/tribler/core/libtorrent/torrentdef.py index f5f1423901..1d083cb215 100644 --- a/src/tribler/core/libtorrent/torrentdef.py +++ b/src/tribler/core/libtorrent/torrentdef.py @@ -16,7 +16,6 @@ import libtorrent as lt from tribler.core.libtorrent.torrent_file_tree import TorrentFileTree -from tribler.core.libtorrent.torrents import create_torrent_file from tribler.core.libtorrent.trackers import is_valid_url if TYPE_CHECKING: @@ -204,7 +203,6 @@ def __init__(self, metainfo: MetainfoDict | None = None, self._logger = logging.getLogger(self.__class__.__name__) self.torrent_parameters: TorrentParameters = cast(TorrentParameters, {}) self.metainfo: MetainfoDict | None = metainfo - self.files_list: list[Path] = [] self.infohash: bytes | None = None self._torrent_info: lt.torrent_info | None = None @@ -365,14 +363,6 @@ def filter_character(char: int) -> str: return "".join(map(filter_character, name)) - def add_content(self, file_path: Path | str) -> None: - """ - Add some content to the torrent file. - - :param file_path: The path of the file to add. - """ - self.files_list.append(Path(file_path).absolute()) - def set_encoding(self, enc: bytes) -> None: """ Set the character encoding for e.g. the 'name' field. @@ -515,20 +505,6 @@ def get_name_as_unicode(self) -> str: return "" - def save(self, torrent_filepath: str | None = None) -> None: - """ - Generate the metainfo and save the torrent file. - - :param torrent_filepath: An optional absolute path to where to save the generated .torrent file. - """ - torrent_dict = create_torrent_file(self.files_list, self.torrent_parameters, torrent_filepath=torrent_filepath) - self._torrent_info = None - with suppress(AttributeError): - del self.torrent_file_tree # Remove the cache without retrieving it or checking if it exists (Error) - self.metainfo = lt.bdecode(torrent_dict['metainfo']) - self.copy_metainfo_to_torrent_parameters() - self.infohash = torrent_dict['infohash'] - def _get_all_files_as_unicode_with_length(self) -> Generator[tuple[Path, int], None, None]: # noqa: C901, PLR0912 """ Get a generator for files in the torrent def. No filtering is possible and all tricks are allowed to obtain diff --git a/src/tribler/test_integration/test_anon_download.py b/src/tribler/test_integration/test_anon_download.py index 81f9eb1ac8..8bb6c1a9c9 100644 --- a/src/tribler/test_integration/test_anon_download.py +++ b/src/tribler/test_integration/test_anon_download.py @@ -4,6 +4,7 @@ from typing import TYPE_CHECKING, cast from unittest.mock import Mock +import libtorrent from ipv8.community import CommunitySettings from ipv8.keyvault.crypto import default_eccrypto from ipv8.messaging.anonymization.tunnel import PEER_FLAG_EXIT_BT @@ -16,6 +17,7 @@ from tribler.core.libtorrent.download_manager.download_manager import DownloadManager from tribler.core.libtorrent.download_manager.download_state import DownloadStatus from tribler.core.libtorrent.torrentdef import TorrentDef, TorrentDefNoMetainfo +from tribler.core.libtorrent.torrents import create_torrent_file from tribler.core.notifier import Notifier from tribler.core.socks5.server import Socks5Server from tribler.core.tunnel.community import TriblerTunnelCommunity, TriblerTunnelSettings @@ -176,9 +178,8 @@ async def start_seeding(self) -> bytes: with open(config.get_dest_dir() / "ubuntu-15.04-desktop-amd64.iso", "wb") as f: # noqa: ASYNC101 f.write(bytes([0] * 524288)) - tdef = TorrentDef() - tdef.add_content(config.get_dest_dir() / "ubuntu-15.04-desktop-amd64.iso") - tdef.save() + metainfo = create_torrent_file([config.get_dest_dir() / "ubuntu-15.04-desktop-amd64.iso"], {})["metainfo"] + tdef = TorrentDef(metainfo=libtorrent.bdecode(metainfo)) download = await self.download_manager_seeder.start_download(tdef=tdef, config=config) await download.wait_for_status(DownloadStatus.SEEDING) @@ -193,9 +194,9 @@ async def start_anon_download(self, infohash: bytes) -> Download: download = await self.download_manager_downloader.start_download(tdef=TorrentDefNoMetainfo(infohash, b"test"), config=config) - self.overlay(DOWNLOADER).bittorrent_peers[download] = [ + self.overlay(DOWNLOADER).bittorrent_peers[download] = { ("127.0.0.1", self.download_manager_seeder.listen_ports[0]["127.0.0.1"]) - ] + } return download diff --git a/src/tribler/test_integration/test_hidden_services.py b/src/tribler/test_integration/test_hidden_services.py index 1a3c69977c..fcba4e2519 100644 --- a/src/tribler/test_integration/test_hidden_services.py +++ b/src/tribler/test_integration/test_hidden_services.py @@ -4,6 +4,7 @@ from typing import TYPE_CHECKING, cast from unittest.mock import Mock +import libtorrent from ipv8.community import CommunitySettings from ipv8.keyvault.crypto import default_eccrypto from ipv8.messaging.anonymization.tunnel import PEER_FLAG_EXIT_BT @@ -17,6 +18,7 @@ from tribler.core.libtorrent.download_manager.download_manager import DownloadManager from tribler.core.libtorrent.download_manager.download_state import DownloadStatus from tribler.core.libtorrent.torrentdef import TorrentDef, TorrentDefNoMetainfo +from tribler.core.libtorrent.torrents import create_torrent_file from tribler.core.notifier import Notifier from tribler.core.socks5.server import Socks5Server from tribler.core.tunnel.community import TriblerTunnelCommunity, TriblerTunnelSettings @@ -207,9 +209,8 @@ async def start_seeding(self) -> bytes: with open(config.get_dest_dir() / "ubuntu-15.04-desktop-amd64.iso", "wb") as f: # noqa: ASYNC101 f.write(bytes([0] * 524288)) - tdef = TorrentDef() - tdef.add_content(config.get_dest_dir() / "ubuntu-15.04-desktop-amd64.iso") - tdef.save() + metainfo = create_torrent_file([config.get_dest_dir() / "ubuntu-15.04-desktop-amd64.iso"], {})["metainfo"] + tdef = TorrentDef(metainfo=libtorrent.bdecode(metainfo)) download = await self.download_manager_seeder.start_download(tdef=tdef, config=config) await download.wait_for_status(DownloadStatus.SEEDING) diff --git a/src/tribler/test_unit/core/libtorrent/restapi/test_create_torrent_endpoint.py b/src/tribler/test_unit/core/libtorrent/restapi/test_create_torrent_endpoint.py index bd43584ce1..78e7d16647 100644 --- a/src/tribler/test_unit/core/libtorrent/restapi/test_create_torrent_endpoint.py +++ b/src/tribler/test_unit/core/libtorrent/restapi/test_create_torrent_endpoint.py @@ -6,6 +6,7 @@ from configobj import ConfigObj from ipv8.test.base import TestBase +import tribler.core.libtorrent.restapi.create_torrent_endpoint as ep_module from tribler.core.libtorrent.download_manager.download_config import SPEC_CONTENT, DownloadConfig from tribler.core.libtorrent.restapi.create_torrent_endpoint import CreateTorrentEndpoint from tribler.core.restapi.rest_endpoint import HTTP_BAD_REQUEST, HTTP_INTERNAL_SERVER_ERROR @@ -71,9 +72,8 @@ async def test_failure_oserror(self) -> None: """ Test if processing a request that leads to an OSError is gracefully reported. """ - self.download_manager.create_torrent_file = AsyncMock(side_effect=OSError("test")) - - response = await self.endpoint.create_torrent(CreateTorrentRequest({"files": [str(Path(__file__))]})) + with patch.dict(ep_module.__dict__, {"create_torrent_file": Mock(side_effect=OSError("test"))}): + response = await self.endpoint.create_torrent(CreateTorrentRequest({"files": [str(Path(__file__))]})) response_body_json = await response_to_json(response) self.assertEqual(HTTP_INTERNAL_SERVER_ERROR, response.status) @@ -85,9 +85,9 @@ async def test_failure_unicodedecodeerror(self) -> None: """ Test if processing a request that leads to an OSError is gracefully reported. """ - self.download_manager.create_torrent_file = AsyncMock(side_effect=UnicodeDecodeError("utf-8", b"", 0, 1, "𓀬")) - - response = await self.endpoint.create_torrent(CreateTorrentRequest({"files": [str(Path(__file__))]})) + with patch.dict(ep_module.__dict__, {"create_torrent_file": + Mock(side_effect=UnicodeDecodeError("utf-8", b"", 0, 1, "𓀬"))}): + response = await self.endpoint.create_torrent(CreateTorrentRequest({"files": [str(Path(__file__))]})) response_body_json = await response_to_json(response) self.assertEqual(HTTP_INTERNAL_SERVER_ERROR, response.status) @@ -98,9 +98,8 @@ async def test_failure_runtimeerror(self) -> None: """ Test if processing a request that leads to an RuntimeError is gracefully reported. """ - self.download_manager.create_torrent_file = AsyncMock(side_effect=RuntimeError("test")) - - response = await self.endpoint.create_torrent(CreateTorrentRequest({"files": [str(Path(__file__))]})) + with patch.dict(ep_module.__dict__, {"create_torrent_file": Mock(side_effect=RuntimeError("test"))}): + response = await self.endpoint.create_torrent(CreateTorrentRequest({"files": [str(Path(__file__))]})) response_body_json = await response_to_json(response) self.assertEqual(HTTP_INTERNAL_SERVER_ERROR, response.status) @@ -112,12 +111,12 @@ async def test_create_default(self) -> None: """ Test if creating a torrent from defaults works. """ - self.download_manager.create_torrent_file = AsyncMock(return_value={"metainfo": TORRENT_WITH_DIRS_CONTENT}) - - response = await self.endpoint.create_torrent(CreateTorrentRequest({"files": [str(Path(__file__))]})) + mocked_create_torrent_file = Mock(return_value={"metainfo": TORRENT_WITH_DIRS_CONTENT}) + with patch.dict(ep_module.__dict__, {"create_torrent_file": mocked_create_torrent_file}): + response = await self.endpoint.create_torrent(CreateTorrentRequest({"files": [str(Path(__file__))]})) response_body_json = await response_to_json(response) - _, call_params = self.download_manager.create_torrent_file.call_args.args + _, call_params, __ = mocked_create_torrent_file.call_args.args self.assertEqual(200, response.status) self.assertEqual(TORRENT_WITH_DIRS_CONTENT, base64.b64decode(response_body_json["torrent"])) @@ -130,13 +129,13 @@ async def test_create_with_comment(self) -> None: """ Test if creating a torrent with a custom comment works. """ - self.download_manager.create_torrent_file = AsyncMock(return_value={"metainfo": TORRENT_WITH_DIRS_CONTENT}) - - response = await self.endpoint.create_torrent(CreateTorrentRequest({"files": [str(Path(__file__))], - "description": "test"})) + mocked_create_torrent_file = Mock(return_value={"metainfo": TORRENT_WITH_DIRS_CONTENT}) + with patch.dict(ep_module.__dict__, {"create_torrent_file": mocked_create_torrent_file}): + response = await self.endpoint.create_torrent(CreateTorrentRequest({"files": [str(Path(__file__))], + "description": "test"})) response_body_json = await response_to_json(response) - _, call_params = self.download_manager.create_torrent_file.call_args.args + _, call_params, __ = mocked_create_torrent_file.call_args.args self.assertEqual(200, response.status) self.assertEqual(TORRENT_WITH_DIRS_CONTENT, base64.b64decode(response_body_json["torrent"])) @@ -146,14 +145,15 @@ async def test_create_with_trackers(self) -> None: """ Test if creating a torrent with custom trackers works. """ - self.download_manager.create_torrent_file = AsyncMock(return_value={"metainfo": TORRENT_WITH_DIRS_CONTENT}) - - response = await self.endpoint.create_torrent(CreateTorrentRequest({"files": [str(Path(__file__))], - "trackers": ["http://127.0.0.1/announce", - "http://10.0.0.2/announce"]})) + mocked_create_torrent_file = Mock(return_value={"metainfo": TORRENT_WITH_DIRS_CONTENT}) + with patch.dict(ep_module.__dict__, {"create_torrent_file": mocked_create_torrent_file}): + response = await self.endpoint.create_torrent(CreateTorrentRequest({ + "files": [str(Path(__file__))], + "trackers": ["http://127.0.0.1/announce", "http://10.0.0.2/announce"] + })) response_body_json = await response_to_json(response) - _, call_params = self.download_manager.create_torrent_file.call_args.args + _, call_params, __ = mocked_create_torrent_file.call_args.args self.assertEqual(200, response.status) self.assertEqual(TORRENT_WITH_DIRS_CONTENT, base64.b64decode(response_body_json["torrent"])) @@ -164,13 +164,13 @@ async def test_create_with_name(self) -> None: """ Test if creating a torrent with a custom name works. """ - self.download_manager.create_torrent_file = AsyncMock(return_value={"metainfo": TORRENT_WITH_DIRS_CONTENT}) - - response = await self.endpoint.create_torrent(CreateTorrentRequest({"files": [str(Path(__file__))], - "name": "test"})) + mocked_create_torrent_file = Mock(return_value={"metainfo": TORRENT_WITH_DIRS_CONTENT}) + with patch.dict(ep_module.__dict__, {"create_torrent_file": mocked_create_torrent_file}): + response = await self.endpoint.create_torrent(CreateTorrentRequest({"files": [str(Path(__file__))], + "name": "test"})) response_body_json = await response_to_json(response) - _, call_params = self.download_manager.create_torrent_file.call_args.args + _, call_params, __ = mocked_create_torrent_file.call_args.args self.assertEqual(200, response.status) self.assertEqual(TORRENT_WITH_DIRS_CONTENT, base64.b64decode(response_body_json["torrent"])) @@ -181,19 +181,20 @@ async def test_create_and_start(self) -> None: Test if creating and starting a download works if download is set to 1. """ self.download_manager.config = MockTriblerConfigManager() - self.download_manager.create_torrent_file = AsyncMock(return_value={"metainfo": TORRENT_WITH_DIRS_CONTENT, - "base_dir": str(Path(__file__).parent)}) self.download_manager.start_download = AsyncMock() - + mocked_create = Mock(return_value={"metainfo": TORRENT_WITH_DIRS_CONTENT, + "base_dir": str(Path(__file__).parent)}) with patch("tribler.core.libtorrent.download_manager.download_config.DownloadConfig.from_defaults", - lambda _: DownloadConfig(ConfigObj(StringIO(SPEC_CONTENT)))): + lambda _: DownloadConfig(ConfigObj(StringIO(SPEC_CONTENT)))), patch.dict(ep_module.__dict__, + {"create_torrent_file": + mocked_create}): response = await self.endpoint.create_torrent(CreateTorrentRequest({"files": [str(Path(__file__))], "download": "1"})) response_body_json = await response_to_json(response) - call_kwargs = self.download_manager.start_download.call_args.kwargs + _, tdef, __ = self.download_manager.start_download.call_args.args self.assertEqual(200, response.status) self.assertEqual(TORRENT_WITH_DIRS_CONTENT, base64.b64decode(response_body_json["torrent"])) self.assertEqual(b"\xb3\xba\x19\xc93\xda\x95\x84k\xfd\xf7Z\xd0\x8a\x94\x9cl\xea\xc7\xbc", - call_kwargs["tdef"].infohash) + tdef.infohash) diff --git a/src/tribler/test_unit/core/libtorrent/test_torrentdef.py b/src/tribler/test_unit/core/libtorrent/test_torrentdef.py index ccb5dd9e88..606675c6a5 100644 --- a/src/tribler/test_unit/core/libtorrent/test_torrentdef.py +++ b/src/tribler/test_unit/core/libtorrent/test_torrentdef.py @@ -50,32 +50,6 @@ def test_create_invalid_tdef_empty_info_validate(self) -> None: with self.assertRaises(ValueError): TorrentDef(metainfo={b"info": {}}, ignore_validation=False) - def test_add_content_dir(self) -> None: - """ - Test if adding a single content directory with two files is working correctly. - """ - tdef = TorrentDef() - torrent_dir = Path(__file__).parent - tdef.add_content(torrent_dir / "__init__.py") - tdef.add_content(torrent_dir / "mocks.py") - - tdef.save() - metainfo = tdef.get_metainfo() - - self.assertEqual(2, len(metainfo[b"info"][b"files"])) - - def test_add_single_file(self) -> None: - """ - Test if adding a single file to a torrent is working correctly. - """ - tdef = TorrentDef() - tdef.add_content(Path(__file__)) - - tdef.save() - metainfo = tdef.get_metainfo() - - self.assertEqual(b"test_torrentdef.py", metainfo[b"info"][b"name"]) - def test_get_name_utf8_unknown(self) -> None: """ Test if we can succesfully get the UTF-8 name. @@ -97,19 +71,6 @@ def test_get_name_utf8(self) -> None: self.assertEqual("\xa1\xc0", tdef.get_name_utf8()) - def test_add_content_piece_length(self) -> None: - """ - Test if we can add single file with piece length to a TorrentDef. - """ - tdef = TorrentDef() - tdef.add_content(Path(__file__)) - - tdef.set_piece_length(2 ** 16) - tdef.save() - metainfo = tdef.get_metainfo() - - self.assertEqual(2 ** 16, metainfo[b"info"][b"piece length"]) - def test_is_private_non_private(self) -> None: """ Test if a torrent marked with private = 0 is not seen as private.