From db0e1d594c548180ea26fd7d771917737a8bf011 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Wed, 13 Sep 2023 12:26:10 +0200 Subject: Merging upstream version 2.1.2~dev0+20230827. Signed-off-by: Daniel Baumann --- deluge/tests/data/v2_hybrid.torrent | Bin 0 -> 613 bytes deluge/tests/data/v2_test.torrent | Bin 0 -> 345 bytes deluge/tests/test_ui_common.py | 119 ++++++++++++++++++- deluge/ui/common.py | 220 ++++++++++++++++++++++++++++++++---- 4 files changed, 316 insertions(+), 23 deletions(-) create mode 100644 deluge/tests/data/v2_hybrid.torrent create mode 100644 deluge/tests/data/v2_test.torrent diff --git a/deluge/tests/data/v2_hybrid.torrent b/deluge/tests/data/v2_hybrid.torrent new file mode 100644 index 0000000..e58057c Binary files /dev/null and b/deluge/tests/data/v2_hybrid.torrent differ diff --git a/deluge/tests/data/v2_test.torrent b/deluge/tests/data/v2_test.torrent new file mode 100644 index 0000000..fe6cbd0 Binary files /dev/null and b/deluge/tests/data/v2_test.torrent differ diff --git a/deluge/tests/test_ui_common.py b/deluge/tests/test_ui_common.py index fc56ebc..87a4a2c 100644 --- a/deluge/tests/test_ui_common.py +++ b/deluge/tests/test_ui_common.py @@ -44,7 +44,7 @@ class TestUICommon: ti = TorrentInfo(filename, filetree=1) assert ti.files_tree == files_tree - filestree2 = { + files_tree2 = { 'contents': { 'torrent_filehash': { 'type': 'dir', @@ -71,7 +71,7 @@ class TestUICommon: 'type': 'dir', } ti = TorrentInfo(filename, filetree=2) - assert ti.files_tree == filestree2 + assert ti.files_tree == files_tree2 def test_hash_optional_md5sum(self): # Ensure `md5sum` key is not included in filetree output @@ -173,3 +173,118 @@ class TestUICommon: } ] assert ti.files == result_files + + def test_bittorrent_v2_path(self): + filename = common.get_test_data_file('v2_test.torrent') + files_tree = { + 'torrent_test': { + 'small.txt': (0, 22, True), + '還在一個人無聊嗎~還不趕緊上來聊天美.txt': (1, 32, True), + } + } + ti = TorrentInfo(filename, filetree=1) + assert ti.files_tree == files_tree + + files_tree2 = { + 'contents': { + 'torrent_test': { + 'type': 'dir', + 'contents': { + 'small.txt': { + 'type': 'file', + 'path': 'torrent_test/small.txt', + 'length': 22, + 'index': 0, + 'download': True, + }, + '還在一個人無聊嗎~還不趕緊上來聊天美.txt': { + 'type': 'file', + 'path': 'torrent_test/還在一個人無聊嗎~還不趕緊上來聊天美.txt', + 'length': 32, + 'index': 1, + 'download': True, + }, + }, + 'length': 54, + 'download': True, + } + }, + 'type': 'dir', + } + ti = TorrentInfo(filename, filetree=2) + assert ti.files_tree == files_tree2 + + def test_bittorrent_v2_hybrid_path(self): + filename = common.get_test_data_file('v2_hybrid.torrent') + files_tree = { + 'torrent_test': { + 'small.txt': (0, 22, True), + '還在一個人無聊嗎~還不趕緊上來聊天美.txt': (2, 32, True), + '.pad': { + '16362': (1, 16362, True), + '16352': (3, 16352, True), + }, + } + } + ti = TorrentInfo(filename, filetree=1, force_bt_version=1) + assert ti.files_tree == files_tree + del files_tree['torrent_test']['.pad'] + files_tree['torrent_test']['還在一個人無聊嗎~還不趕緊上來聊天美.txt'] = (1, 32, True) + ti = TorrentInfo(filename, filetree=1, force_bt_version=2) + assert ti.files_tree == files_tree + + files_tree2 = { + 'contents': { + 'torrent_test': { + 'type': 'dir', + 'contents': { + 'small.txt': { + 'type': 'file', + 'path': 'torrent_test/small.txt', + 'length': 22, + 'index': 0, + 'download': True, + }, + '還在一個人無聊嗎~還不趕緊上來聊天美.txt': { + 'type': 'file', + 'path': 'torrent_test/還在一個人無聊嗎~還不趕緊上來聊天美.txt', + 'length': 32, + 'index': 2, + 'download': True, + }, + '.pad': { + 'type': 'dir', + 'contents': { + '16362': { + 'type': 'file', + 'path': 'torrent_test/.pad/16362', + 'length': 16362, + 'index': 1, + 'download': True, + }, + '16352': { + 'type': 'file', + 'path': 'torrent_test/.pad/16352', + 'length': 16352, + 'index': 3, + 'download': True, + }, + }, + 'length': 32714, + 'download': True, + }, + }, + 'length': 32768, + 'download': True, + } + }, + 'type': 'dir', + } + ti = TorrentInfo(filename, filetree=2, force_bt_version=1) + assert ti.files_tree == files_tree2 + torrent_test = files_tree2['contents']['torrent_test'] + torrent_test['length'] -= torrent_test['contents']['.pad']['length'] + del torrent_test['contents']['.pad'] + torrent_test['contents']['還在一個人無聊嗎~還不趕緊上來聊天美.txt']['index'] = 1 + ti = TorrentInfo(filename, filetree=2, force_bt_version=2) + assert ti.files_tree == files_tree2 diff --git a/deluge/ui/common.py b/deluge/ui/common.py index e9b445d..64d5ca2 100644 --- a/deluge/ui/common.py +++ b/deluge/ui/common.py @@ -13,6 +13,7 @@ The ui common module contains methods and classes that are deemed useful for all import logging import os from hashlib import sha1 as sha +from typing import Tuple from deluge import bencode from deluge.common import decode_bytes @@ -171,10 +172,11 @@ class TorrentInfo: filename (str, optional): The path to the .torrent file. filetree (int, optional): The version of filetree to create (defaults to 1). torrent_file (dict, optional): A bdecoded .torrent file contents. + force_bt_version (int, optional): The BitTorrent spec to use for parsing (defaults to 1). """ - def __init__(self, filename='', filetree=1, torrent_file=None): + def __init__(self, filename='', filetree=1, torrent_file=None, force_bt_version=1): self._filedata = None if torrent_file: self._metainfo = torrent_file @@ -211,9 +213,24 @@ class TorrentInfo: else: self._name = decode_bytes(info_dict['name'], encoding) + meta_version = info_dict['meta version'] if 'meta version' in info_dict else -1 + is_hybrid = 'files' in info_dict and meta_version == 2 + + parse_v1 = False + parse_v2 = False + if is_hybrid: + if force_bt_version == 1: + parse_v1 = True + elif force_bt_version == 2: + parse_v2 = True + elif 'files' in info_dict: + parse_v1 = True + elif meta_version == 2 and 'file tree' in info_dict: + parse_v2 = True + # Get list of files from torrent info self._files = [] - if 'files' in info_dict: + if parse_v1: paths = {} dirs = {} prefix = self._name @@ -245,25 +262,67 @@ class TorrentInfo: if filetree == 2: - def walk(path, item): + def walk(full_path, item): if item['type'] == 'dir': - item.update(dirs[path]) + item.update(dirs[full_path]) else: - item.update(paths[path]) + item.update(paths[full_path]) item['download'] = True file_tree = FileTree2(list(paths)) file_tree.walk(walk) else: - def walk(path, item): + def walk(full_path, item): if isinstance(item, dict): return item - return [paths[path]['index'], paths[path]['length'], True] + return [paths[full_path]['index'], paths[full_path]['length'], True] file_tree = FileTree(paths) file_tree.walk(walk) self._files_tree = file_tree.get_tree() + elif parse_v2: + + def single_file_torrent(inner_info_dict): + if len(inner_info_dict['file tree']) > 1: + return False + + file_name = [key for key in inner_info_dict['file tree']][0] + return inner_info_dict['name'] == file_name + + if not single_file_torrent(info_dict): + info_dict['file tree'] = {info_dict['name']: info_dict['file tree']} + + if filetree == 2: + + def walk(full_path, item): + if item['type'] == 'file': + item['path'] = full_path + self._files.append( + { + 'path': full_path, + 'size': item['length'], + 'download': True, + } + ) + item['download'] = True + + file_tree = FileTree2BTv2(info_dict['file tree']) + file_tree.walk(walk) + else: + + def walk(full_path, item): + if isinstance(item, dict): + return item + self._files.append( + {'path': full_path, 'size': item[1], 'download': True} + ) + return [item[0], item[1], True] + + file_tree = FiletreeBTv2(info_dict['file tree']) + file_tree.walk(walk) + + self._files_tree = file_tree.get_tree() else: self._files.append( {'path': self._name, 'size': info_dict['length'], 'download': True} @@ -386,13 +445,31 @@ class TorrentInfo: class FileTree2: """ - Converts a list of paths in to a file tree. + Converts a list of paths, from a V1 torrent, into a file tree. + + Each file will have the dictionary structure of: + { file_name: {type, path, index, length, download} } + where: + type (str): will always be "file" + path (str): the absolute file path from the root the torrent + index (int): the index of file in the torrent + length (int): the size of the file, in bytes + download (bool): marks the file to download + + Folder will be dictionaries of files: + { dir1: type, contents: {file_name1: {...}, file_name2: {...}}, dir2: ... } + where: + type (str): will always be "dir" + contents (dict): a dictionary of inner files and folders + + The entire tree will start with a root dictionary: + { contents: {dirs...}, type: "dir" } - :param paths: The paths to be converted - :type paths: list + Args: + paths (list): The paths to be converted. """ - def __init__(self, paths): + def __init__(self, paths: list): self.tree = {'contents': {}, 'type': 'dir'} def get_parent(path): @@ -466,13 +543,23 @@ class FileTree2: class FileTree: """ - Convert a list of paths in a file tree. + Converts a dict of paths, from a V1 torrent, into a file tree. + + Each file will have the dictionary structure of: + { file_name: [index, length, download] } + Where: + index (int): the index of file in the torrent + length (int): the size of the file, in bytes + download (bool): marks the file to download + + Folder will be dictionaries of files: + { dir1: {file_name1: [...], file_name2: [...]}, dir2: ... } - :param paths: The paths to be converted. - :type paths: list + Args: + paths (dict): The paths to be converted. """ - def __init__(self, paths): + def __init__(self, paths: dict): self.tree = {} def get_parent(path): @@ -498,8 +585,8 @@ class FileTree: """ Return the tree, after first converting all file lists to a tuple. - :returns: the file tree. - :rtype: dictionary + Returns: + dict: the file tree. """ def to_tuple(path, item): @@ -515,10 +602,10 @@ class FileTree: Walk through the file tree calling the callback function on each item contained. - :param callback: The function to be used as a callback, it should have - the signature func(item, path) where item is a `tuple` for a file - and `dict` for a directory. - :type callback: function + Args: + callback (function): The function to be used as a callback, it should have + the signature func(item, path) where item is a `tuple` for a file + and `dict` for a directory. """ def walk(directory, parent_path): @@ -547,3 +634,94 @@ class FileTree: self.walk(write) return '\n'.join(lines) + + +class FiletreeBTv2(FileTree): + """ + Converts a dict of paths, from a V2 torrent, into a file tree. + + Each file will have the dictionary structure of: + { file_name: [index, length, download] } + Where: + index (int): the index of file in the torrent + length (int): the size of the file, in bytes + download (bool): marks the file to download + + Folder will be dictionaries of files: + { dir1: {file_name1: [...], file_name2: [...]}, dir2: ... } + + Args: + file_tree (dict): The paths to be converted. + """ + + def __init__(self, file_tree): + self.tree = {} + + def get_parent(curr_tree_dict, index, parent) -> int: + for key, item in curr_tree_dict.items(): + key = decode_bytes(key) + if b'' in item: + parent[key] = [index, item[b''][b'length']] + index += 1 + else: + parent[key] = {} + index = get_parent(item, index, parent[key]) + return index + + get_parent(file_tree, 0, self.tree) + + +class FileTree2BTv2(FileTree2): + """ + Converts a dict of paths, from a V2 torrent, into a file tree. + + Each file will have the dictionary structure of: + { file_name: {type, path, index, length, download} } + where: + type (str): will always be "file" + path (str): the absolute file path from the root the torrent + index (int): the index of file in the torrent + length (int): the size of the file, in bytes + download (bool): marks the file to download + + Folder will be dictionaries of files: + { dir1: type, contents: {file_name1: {...}, file_name2: {...}}, dir2: ... } + where: + type (str): will always be "dir" + contents (dict): a dictionary of inner files and folders + + The entire tree will start with a root dictionary: + { contents: {dirs...}, type: "dir" } + + Args: + file_tree (dict): The paths to be converted. + """ + + def __init__(self, file_tree): + self.tree = {'contents': {}, 'type': 'dir'} + + def get_parent(curr_tree_dict, index, parent) -> Tuple[int, int]: + total_length = 0 + for key, item in curr_tree_dict.items(): + key = decode_bytes(key) + if b'' in item: + length = item[b''][b'length'] + total_length += length + parent['contents'][key] = { + 'index': index, + 'length': length, + 'type': 'file', + } + index += 1 + else: + parent['contents'][key] = { + 'contents': {}, + 'type': 'dir', + 'length': 0, + } + index, length = get_parent(item, index, parent['contents'][key]) + parent['contents'][key]['length'] = length + total_length += length + return index, total_length + + get_parent(file_tree, 0, self.tree) -- cgit v1.2.3