TorrentFile API Documentation
CLI
Module
torrentfile.
cli
Command Line Interface for TorrentFile project.
This module provides the primary command line argument parser for the torrentfile package. The main_script function is automatically invoked when called from command line, and parses accompanying arguments.
Functions: main_script: process command line arguments and run program.
HelpFormat
— Formatting class for help tips provided by the CLI.
create_magnet
(
metafile
)
(`str`) — Create a magnet URI from a Bittorrent meta file.main
(
)
— Initiate main function for CLI script.main_script
(
args
)
— Initialize Command Line Interface for torrentfile.
Something Clever
torrentfile.cli
HelpFormat (HelpFormatter)
Source code in torrentfile\cli.py
class HelpFormat(HelpFormatter):
"""Formatting class for help tips provided by the CLI.
Parameters
----------
prog : `str`
Name of the program.
width : `int`
Max width of help message output.
max_help_positions : `int`
max length until line wrap.
"""
def __init__(self, prog: str, width=75, max_help_pos=60):
"""Construct HelpFormat class."""
super().__init__(prog, width=width, max_help_position=max_help_pos)
def _split_lines(self, text, _):
"""Split multiline help messages and remove indentation."""
lines = text.split("\n")
return [line.strip() for line in lines if line]
def _format_text(self, text):
text = text % dict(prog=self._prog) if '%(prog)' in text else text
text = self._whitespace_matcher.sub(' ', text).strip()
return text + "\n\n"
__init__(self, prog, width=75, max_help_pos=60)
special
Source code in torrentfile\cli.py
def __init__(self, prog: str, width=75, max_help_pos=60):
"""Construct HelpFormat class."""
super().__init__(prog, width=width, max_help_position=max_help_pos)
create_magnet(metafile)
Source code in torrentfile\cli.py
def create_magnet(metafile):
"""Create a magnet URI from a Bittorrent meta file.
Parameters
----------
metafile : `str` | `os.PathLike`
path to bittorrent meta file.
Returns
-------
`str`
created magnet URI.
"""
import os
from hashlib import sha1 # nosec
from urllib.parse import quote_plus
import pyben
if not os.path.exists(metafile):
raise FileNotFoundError
meta = pyben.load(metafile)
info = meta["info"]
binfo = pyben.dumps(info)
infohash = sha1(binfo).hexdigest().upper() # nosec
scheme = "magnet:"
hasharg = "?xt=urn:btih:" + infohash
namearg = "&dn=" + quote_plus(info["name"])
if "announce-list" in meta:
announce_args = [
"&tr=" + quote_plus(url)
for urllist in meta["announce-list"]
for url in urllist
]
else:
announce_args = ["&tr=" + quote_plus(meta["announce"])]
full_uri = "".join([scheme, hasharg, namearg] + announce_args)
sys.stdout.write(full_uri)
return full_uri
main()
Source code in torrentfile\cli.py
def main():
"""Initiate main function for CLI script."""
main_script()
main_script(args=None)
Source code in torrentfile\cli.py
def main_script(args=None):
"""Initialize Command Line Interface for torrentfile.
Parameters
----------
args : `list`
Commandline arguments. default=None
"""
if not args:
if sys.argv[1:]:
args = sys.argv[1:]
else:
args = ["-h"]
parser = ArgumentParser(
"TorrentFile",
description="""
CLI Tool for creating, checking and editing Bittorrent meta files.
Supports all meta file versions including hybrid files.
""",
prefix_chars="-",
formatter_class=HelpFormat,
)
parser.add_argument(
"-i",
"--interactive",
action="store_true",
dest="interactive",
help="select program options interactively",
)
parser.add_argument(
"-V",
"--version",
action="version",
version=f"torrentfile v{torrentfile.__version__}",
help="show program version and exit",
)
parser.add_argument(
"-v",
"--verbose",
action="store_true",
dest="debug",
help="output debug information",
)
subparsers = parser.add_subparsers(
title="Actions",
description="Each sub-command triggers a specific action.",
dest="command",
)
create_parser = subparsers.add_parser(
"c",
help="""
Create a torrent meta file.
""",
prefix_chars="-",
aliases=["create", "new"],
formatter_class=HelpFormat,
)
create_parser.add_argument(
"-a",
"--announce",
action="store",
dest="announce",
metavar="<url>",
nargs="+",
default=[],
help="Alias for -t/--tracker",
)
create_parser.add_argument(
"-p",
"--private",
action="store_true",
dest="private",
help="Create a private torrent meta file",
)
create_parser.add_argument(
"-s",
"--source",
action="store",
dest="source",
metavar="<source>",
help="specify source tracker",
)
create_parser.add_argument(
"-m",
"--magnet",
action="store_true",
dest="magnet",
help="output Magnet Link after creation completes",
)
create_parser.add_argument(
"-c",
"--comment",
action="store",
dest="comment",
metavar="<comment>",
help="include a comment in file metadata",
)
create_parser.add_argument(
"-o",
"--out",
action="store",
dest="outfile",
metavar="<path>",
help="Output path for created .torrent file",
)
create_parser.add_argument(
"-t",
"--tracker",
action="store",
dest="tracker",
metavar="<url>",
nargs="+",
default=[],
help="""One or more Bittorrent tracker announce url(s).""",
)
create_parser.add_argument(
"--progress",
action="store_true",
dest="progress",
help="""
Enable showing the progress bar during torrent creation.
(Minimially impacts the duration of torrent file creation.)
"""
)
create_parser.add_argument(
"--meta-version",
default="1",
choices=["1", "2", "3"],
action="store",
dest="meta_version",
metavar="<int>",
help="""
Bittorrent metafile version.
Options = 1, 2 or 3.
(1) = Bittorrent v1 (Default)
(2) = Bittorrent v2
(3) = Bittorrent v1 & v2 hybrid
""",
)
create_parser.add_argument(
"--piece-length",
action="store",
dest="piece_length",
metavar="<int>",
help="""
Fixed amount of bytes for each chunk of data. (Default: None)
Acceptable input values include integers 14-24, which
will be interpreted as the exponent for 2^n, or any perfect
power of two integer between 16Kib and 16MiB (inclusive).
Examples:: [--piece-length 14] [-l 20] [-l 16777216]
""",
)
create_parser.add_argument(
"-w",
"--web-seed",
action="store",
dest="url_list",
metavar="<url>",
nargs="+",
help="""
One or more url(s) linking to a http server hosting
the torrent contents. This is useful if the torrent
tracker is ever unreachable. Example:: [-w url1 [url2 [url3]]]
""",
)
create_parser.add_argument(
"content",
action="store",
metavar="<content path>",
help="path to content file or directory",
)
edit_parser = subparsers.add_parser(
"e",
help="""
Edit existing torrent meta file.
""",
aliases=["edit"],
prefix_chars="-",
formatter_class=HelpFormat,
)
edit_parser.add_argument(
"metafile",
action="store",
help="path to *.torrent file",
metavar="<*.torrent>",
)
edit_parser.add_argument(
"--tracker",
action="store",
dest="announce",
metavar="<url>",
nargs="+",
help="""
replace current list of tracker/announce urls with one or more space
seperated Bittorrent tracker announce url(s).
""",
)
edit_parser.add_argument(
"--web-seed",
action="store",
dest="url_list",
metavar="<url>",
nargs="+",
help="""
replace current list of web-seed urls with one or more space seperated url(s)
""",
)
edit_parser.add_argument(
"--private",
action="store_true",
help="If currently private, will make it public, if public then private.",
dest="private",
)
edit_parser.add_argument(
"--comment",
help="replaces any existing comment with <comment>",
metavar="<comment>",
dest="comment",
action="store",
)
edit_parser.add_argument(
"--source",
action="store",
dest="source",
metavar="<source>",
help="replaces current source with <source>",
)
magnet_parser = subparsers.add_parser(
"m",
help="""
Create magnet url from an existing Bittorrent meta file.
""",
aliases=["magnet"],
prefix_chars="-",
formatter_class=HelpFormat,
)
magnet_parser.add_argument(
"metafile",
action="store",
help="path to Bittorrent meta file.",
metavar="<*.torrent>",
)
check_parser = subparsers.add_parser(
"r",
help="""
Calculate amount of torrent meta file's content is found on disk.
""",
aliases=["recheck", "check"],
prefix_chars="-",
formatter_class=HelpFormat,
)
check_parser.add_argument(
"metafile",
action="store",
metavar="<*.torrent>",
help="path to .torrent file.",
)
check_parser.add_argument(
"content",
action="store",
metavar="<content>",
help="path to content file or directory",
)
flags = parser.parse_args(args)
if flags.debug:
torrentfile.setLevel(logging.DEBUG)
logger.debug(str(flags))
if flags.interactive:
return select_action()
if flags.command in ["m", "magnet"]:
return create_magnet(flags.metafile)
if flags.command in ["recheck", "r", "check"]:
logger.debug("Program entering Recheck mode.")
metafile = flags.metafile
content = flags.content
logger.debug("Checking %s against %s contents", metafile, content)
checker = Checker(metafile, content)
logger.debug("Completed initialization of the Checker class")
result = checker.results()
logger.info("Final result for %s recheck: %s", metafile, result)
sys.stdout.write(str(result))
sys.stdout.flush()
return result
if flags.command in ["edit", "e"]:
metafile = flags.metafile
logger.info("Editing %s" % flags.metafile)
editargs = {
"url-list": flags.url_list,
"announce": flags.announce,
"source": flags.source,
"private": flags.private,
"comment": flags.comment,
}
return edit_torrent(metafile, editargs)
kwargs = {
"progress": flags.progress,
"url_list": flags.url_list,
"path": flags.content,
"announce": flags.announce + flags.tracker,
"piece_length": flags.piece_length,
"source": flags.source,
"private": flags.private,
"outfile": flags.outfile,
"comment": flags.comment,
}
logger.debug("Program has entered torrent creation mode.")
if flags.meta_version == "2":
torrent = TorrentFileV2(**kwargs)
elif flags.meta_version == "3":
torrent = TorrentFileHybrid(**kwargs)
else:
torrent = TorrentFile(**kwargs)
logger.debug("Completed torrent files meta info assembly.")
outfile, meta = torrent.write()
if flags.magnet:
create_magnet(outfile)
parser.kwargs = kwargs
parser.meta = meta
parser.outfile = outfile
logger.debug("New torrent file (%s) has been created.", str(outfile))
return parser
torrentfile.cli.HelpFormat (HelpFormatter)
Source code in torrentfile\cli.py
class HelpFormat(HelpFormatter):
"""Formatting class for help tips provided by the CLI.
Parameters
----------
prog : `str`
Name of the program.
width : `int`
Max width of help message output.
max_help_positions : `int`
max length until line wrap.
"""
def __init__(self, prog: str, width=75, max_help_pos=60):
"""Construct HelpFormat class."""
super().__init__(prog, width=width, max_help_position=max_help_pos)
def _split_lines(self, text, _):
"""Split multiline help messages and remove indentation."""
lines = text.split("\n")
return [line.strip() for line in lines if line]
def _format_text(self, text):
text = text % dict(prog=self._prog) if '%(prog)' in text else text
text = self._whitespace_matcher.sub(' ', text).strip()
return text + "\n\n"
__init__(self, prog, width=75, max_help_pos=60)
special
Source code in torrentfile\cli.py
def __init__(self, prog: str, width=75, max_help_pos=60):
"""Construct HelpFormat class."""
super().__init__(prog, width=width, max_help_position=max_help_pos)
Torrent
Module
torrentfile.
torrent
Classes and procedures pertaining to the creation of torrent meta files.
Classes
-
TorrentFile
construct .torrent file. -
TorrentFileV2
construct .torrent v2 files using provided data. -
MetaFile
base class for all MetaFile classes.
Constants
-
BLOCK_SIZE :
int
size of leaf hashes for merkle tree. -
HASH_SIZE :
int
Length of a sha256 hash.
Bittorrent V2
From Bittorrent.org Documentation pages. Implementation details for Bittorrent Protocol v2.
Attention
All strings in a .torrent file that contains text must be UTF-8 encoded.
Meta Version 2 Dictionary:
-
"announce": The URL of the tracker.
-
"info": This maps to a dictionary, with keys described below.
"name": A display name for the torrent. It is purely advisory.
"piece length": The number of bytes that each logical piece in the peer protocol refers to. I.e. it sets the granularity of piece, request, bitfield and have messages. It must be a power of two and at least 6KiB.
"meta version": An integer value, set to 2 to indicate compatibility with the current revision of this specification. Version 1 is not assigned to avoid confusion with BEP3. Future revisions will only increment this issue to indicate an incompatible change has been made, for example that hash algorithms were changed due to newly discovered vulnerabilities. Lementations must check this field first and indicate that a torrent is of a newer version than they can handle before performing other idations which may result in more general messages about invalid files. Files are mapped into this piece address space so that each non-empty
"file tree": A tree of dictionaries where dictionary keys represent UTF-8 encoded path elements. Entries with zero-length keys describe the properties of the composed path at that point. 'UTF-8 encoded' context only means that if the native encoding is known at creation time it must be converted to UTF-8. Keys may contain invalid UTF-8 sequences or characters and names that are reserved on specific filesystems. Implementations must be prepared to sanitize them. On platforms path components exactly matching '.' and '..' must be sanitized since they could lead to directory traversal attacks and conflicting path descriptions. On platforms that require UTF-8 path components this sanitizing step must happen after normalizing overlong UTF-8 encodings. File is aligned to a piece boundary and occurs in same order as the file tree. The last piece of each file may be shorter than the specified piece length, resulting in an alignment gap.
"length": Length of the file in bytes. Presence of this field indicates that the dictionary describes a file, not a directory. Which means it must not have any sibling entries.
"pieces root": For non-empty files this is the the root hash of a merkle tree with a branching factor of 2, constructed from 16KiB blocks of the file. The last block may be shorter than 16KiB. The remaining leaf hashes beyond the end of the file required to construct upper layers of the merkle tree are set to zero. As of meta version 2 SHA2-256 is used as digest function for the merkle tree. The hash is stored in its binary form, not as human-readable string.
-"piece layers": A dictionary of strings. For each file in the file tree that is larger than the piece size it contains one string value. The keys are the merkle roots while the values consist of concatenated hashes of one layer within that merkle tree. The layer is chosen so that one hash covers piece length bytes. For example if the piece size is 16KiB then the leaf hashes are used. If a piece size of 128KiB is used then 3rd layer up from the leaf hashes is used. Layer hashes which exclusively cover data beyond the end of file, i.e. are only needed to balance the tree, are omitted. All hashes are stored in their binary format. A torrent is not valid if this field is absent, the contained hashes do not match the merkle roots or are not from the correct layer.
Important
The file tree root dictionary itself must not be a file, i.e. it must not contain a zero-length key with a dictionary containing a length key.
Bittorrent V1
Version 1 meta-dictionary
-announce: The URL of the tracker.
- info: This maps to a dictionary, with keys described below.
Version 1 info-dictionary
-
name
: maps to a UTF-8 encoded string which is the suggested name to save the file (or directory) as. It is purely advisory. -
piece length
: maps to the number of bytes in each piece the file is split into. For the purposes of transfer, files are split into fixed-size pieces which are all the same length except for possibly the last one which may be truncated. -
piece length
: is almost always a power of two, most commonly 2^18 = 256 K -
pieces
: maps to a string whose length is a multiple of 20. It is to be subdivided into strings of length 20, each of which is the SHA1 hash of the piece at the corresponding index. -
length
: In the single file case, maps to the length of the file in bytes. -
files
: If present then the download represents a single file, otherwise it represents a set of files which go in a directory structure. For the purposes of the other keys, the multi-file case is treated as only having a single file by concatenating the files in the order they appear in the files list. The files list is the valuefiles
maps to, and is a list of dictionaries containing the following keys:path
: A list of UTF-8 encoded strings corresponding to subdirectory names, the last of which is the actual file namelength
: Maps to the length of the file in bytes.
Important
In the single file case, the name key is the name of a file, in the muliple file case, it's the name of a directory.
MetaFile
— Base Class for all TorrentFile classes.TorrentFile
— Class for creating Bittorrent meta files.TorrentFileV2
— Class for creating Bittorrent meta v2 files.TorrentFileHybrid
— Construct the Hybrid torrent meta file with provided parameters.
torrentfile.torrent
MetaFile
Source code in torrentfile\torrent.py
class MetaFile:
"""Base Class for all TorrentFile classes.
Parameters
----------
path : `str`
target path to torrent content. Default: None
announce : `str`
One or more tracker URL's. Default: None
comment : `str`
A comment. Default: None
piece_length : `int`
Size of torrent pieces. Default: None
private : `bool`
For private trackers. Default: None
outfile : `str`
target path to write .torrent file. Default: None
source : `str`
Private tracker source. Default: None
progress : `bool`
If True disable showing the progress bar.
"""
hasher = None
@classmethod
def set_callback(cls, func):
"""
Assign a callback function for the Hashing class to call for each hash.
Parameters
----------
func : function
The callback function which accepts a single paramter.
"""
if "hasher" in vars(cls) and vars(cls)["hasher"]:
cls.hasher.set_callback(func)
# fmt: off
def __init__(self, path=None, announce=None, private=False,
source=None, piece_length=None, comment=None,
outfile=None, url_list=None, progress=False):
"""Construct MetaFile superclass and assign local attributes."""
if not path:
raise utils.MissingPathError
# base path to torrent content.
self.path = path
# Format piece_length attribute.
if piece_length:
self.piece_length = utils.normalize_piece_length(piece_length)
else:
self.piece_length = utils.path_piece_length(self.path)
# Assign announce URL to empty string if none provided.
if not announce:
self.announce = ""
self.announce_list = [[""]]
# Most torrent clients have editting trackers as a feature.
elif isinstance(announce, str):
self.announce = announce
self.announce_list = [announce]
elif isinstance(announce, Sequence):
self.announce = announce[0]
self.announce_list = [announce]
if private:
self.private = 1
else:
self.private = None
self.outfile = outfile
self.progress = progress
self.comment = comment
self.url_list = url_list
self.source = source
self.meta = {
"announce": self.announce,
"announce-list": self.announce_list,
"created by": f"TorrentFile:v{version}",
"creation date": int(datetime.timestamp(datetime.now())),
"info": {},
}
logger.debug("Announce list = %s", str(self.announce_list))
if comment:
self.meta["info"]["comment"] = comment
if private:
self.meta["info"]["private"] = 1
if source:
self.meta["info"]["source"] = source
if url_list:
self.meta["url-list"] = url_list
self.meta["info"]["name"] = os.path.basename(self.path)
self.meta["info"]["piece length"] = self.piece_length
# fmt: on
def assemble(self):
"""Overload in subclasses.
Raises
------
`Exception`
NotImplementedError
"""
raise NotImplementedError
def sort_meta(self):
"""Sort the info and meta dictionaries."""
meta = self.meta
meta["info"] = dict(sorted(list(meta["info"].items())))
meta = dict(sorted(list(meta.items())))
return meta
def write(self, outfile=None):
"""Write meta information to .torrent file.
Parameters
----------
outfile : `str`
Destination path for .torrent file. default=None
Returns
-------
outfile : `str`
Where the .torrent file was writen.
meta : `dict`
.torrent meta information.
"""
if outfile is not None:
self.outfile = outfile
if self.outfile is None:
self.outfile = str(self.path) + ".torrent"
self.meta = self.sort_meta()
pyben.dump(self.meta, self.outfile)
return self.outfile, self.meta
__init__(self, path=None, announce=None, private=False, source=None, piece_length=None, comment=None, outfile=None, url_list=None, progress=False)
special
Source code in torrentfile\torrent.py
def __init__(self, path=None, announce=None, private=False,
source=None, piece_length=None, comment=None,
outfile=None, url_list=None, progress=False):
"""Construct MetaFile superclass and assign local attributes."""
if not path:
raise utils.MissingPathError
# base path to torrent content.
self.path = path
# Format piece_length attribute.
if piece_length:
self.piece_length = utils.normalize_piece_length(piece_length)
else:
self.piece_length = utils.path_piece_length(self.path)
# Assign announce URL to empty string if none provided.
if not announce:
self.announce = ""
self.announce_list = [[""]]
# Most torrent clients have editting trackers as a feature.
elif isinstance(announce, str):
self.announce = announce
self.announce_list = [announce]
elif isinstance(announce, Sequence):
self.announce = announce[0]
self.announce_list = [announce]
if private:
self.private = 1
else:
self.private = None
self.outfile = outfile
self.progress = progress
self.comment = comment
self.url_list = url_list
self.source = source
self.meta = {
"announce": self.announce,
"announce-list": self.announce_list,
"created by": f"TorrentFile:v{version}",
"creation date": int(datetime.timestamp(datetime.now())),
"info": {},
}
logger.debug("Announce list = %s", str(self.announce_list))
if comment:
self.meta["info"]["comment"] = comment
if private:
self.meta["info"]["private"] = 1
if source:
self.meta["info"]["source"] = source
if url_list:
self.meta["url-list"] = url_list
self.meta["info"]["name"] = os.path.basename(self.path)
self.meta["info"]["piece length"] = self.piece_length
assemble(self)
Source code in torrentfile\torrent.py
def assemble(self):
"""Overload in subclasses.
Raises
------
`Exception`
NotImplementedError
"""
raise NotImplementedError
set_callback(func)
classmethod
Source code in torrentfile\torrent.py
@classmethod
def set_callback(cls, func):
"""
Assign a callback function for the Hashing class to call for each hash.
Parameters
----------
func : function
The callback function which accepts a single paramter.
"""
if "hasher" in vars(cls) and vars(cls)["hasher"]:
cls.hasher.set_callback(func)
sort_meta(self)
Source code in torrentfile\torrent.py
def sort_meta(self):
"""Sort the info and meta dictionaries."""
meta = self.meta
meta["info"] = dict(sorted(list(meta["info"].items())))
meta = dict(sorted(list(meta.items())))
return meta
write(self, outfile=None)
Source code in torrentfile\torrent.py
def write(self, outfile=None):
"""Write meta information to .torrent file.
Parameters
----------
outfile : `str`
Destination path for .torrent file. default=None
Returns
-------
outfile : `str`
Where the .torrent file was writen.
meta : `dict`
.torrent meta information.
"""
if outfile is not None:
self.outfile = outfile
if self.outfile is None:
self.outfile = str(self.path) + ".torrent"
self.meta = self.sort_meta()
pyben.dump(self.meta, self.outfile)
return self.outfile, self.meta
TorrentFile (MetaFile)
Source code in torrentfile\torrent.py
class TorrentFile(MetaFile):
"""Class for creating Bittorrent meta files.
Construct *Torrentfile* class instance object.
Parameters
----------
path : `str`
Path to torrent file or directory.
piece_length : `int`
Size of each piece of torrent data.
announce : `str` or `list`
One or more tracker URL's.
private : `int`
1 if private torrent else 0.
source : `str`
Source tracker.
comment : `str`
Comment string.
outfile : `str`
Path to write metfile to.
"""
hasher = Hasher
def __init__(self, **kwargs):
"""Construct TorrentFile instance with given keyword args.
Parameters
----------
kwargs : `dict`
dictionary of keyword args passed to superclass.
"""
super().__init__(**kwargs)
logger.debug("Making Bittorrent V1 meta file.")
self.assemble()
def assemble(self):
"""Assemble components of torrent metafile.
Returns
-------
`dict`
metadata dictionary for torrent file
"""
info = self.meta["info"]
size, filelist = utils.filelist_total(self.path)
if os.path.isfile(self.path):
info["length"] = size
else:
info["files"] = [
{
"length": os.path.getsize(path),
"path": os.path.relpath(path, self.path).split(os.sep),
}
for path in filelist
]
pieces = bytearray()
feeder = Hasher(filelist, self.piece_length)
if self.progress:
from tqdm import tqdm
for piece in tqdm(
iterable=feeder,
desc="Hashing Content",
total=size // self.piece_length,
unit="bytes",
unit_scale=True,
unit_divisor=self.piece_length,
initial=0,
leave=True
):
pieces.extend(piece)
else:
for piece in feeder:
pieces.extend(piece)
info["pieces"] = pieces
hasher (CbMixin)
Source code in torrentfile\torrent.py
class Hasher(CbMixin):
"""Piece hasher for Bittorrent V1 files.
Takes a sorted list of all file paths, calculates sha1 hash
for fixed size pieces of file data from each file
seemlessly until the last piece which may be smaller than others.
Parameters
----------
paths : `list`
List of files.
piece_length : `int`
Size of chuncks to split the data into.
total : `int`
Sum of all files in file list.
"""
def __init__(self, paths, piece_length):
"""Generate hashes of piece length data from filelist contents."""
self.piece_length = piece_length
self.paths = paths
self.total = sum([os.path.getsize(i) for i in self.paths])
self.index = 0
self.current = open(self.paths[0], "rb")
logger.debug(
"Hashing v1 torrent file. Size: %s Piece Length: %s",
humanize_bytes(self.total),
humanize_bytes(self.piece_length),
)
def __iter__(self):
"""Iterate through feed pieces.
Returns
-------
self : `iterator`
Iterator for leaves/hash pieces.
"""
return self
def _handle_partial(self, arr):
"""Define the handling partial pieces that span 2 or more files.
Parameters
----------
arr : `bytearray`
Incomplete piece containing partial data
partial : `int`
Size of incomplete piece_length
Returns
-------
digest : `bytes`
SHA1 digest of the complete piece.
"""
while len(arr) < self.piece_length and self.next_file():
target = self.piece_length - len(arr)
temp = bytearray(target)
size = self.current.readinto(temp)
arr.extend(temp[:size])
if size == target:
break
return sha1(arr).digest() # nosec
def next_file(self):
"""Seemlessly transition to next file in file list."""
self.index += 1
if self.index < len(self.paths):
self.current.close()
self.current = open(self.paths[self.index], "rb")
return True
return False
def __next__(self):
"""Generate piece-length pieces of data from input file list."""
while True:
piece = bytearray(self.piece_length)
size = self.current.readinto(piece)
if size == 0:
if not self.next_file():
raise StopIteration
elif size < self.piece_length:
return self._handle_partial(piece[:size])
else:
return sha1(piece).digest() # nosec
__init__(self, paths, piece_length)
special
Source code in torrentfile\torrent.py
def __init__(self, paths, piece_length):
"""Generate hashes of piece length data from filelist contents."""
self.piece_length = piece_length
self.paths = paths
self.total = sum([os.path.getsize(i) for i in self.paths])
self.index = 0
self.current = open(self.paths[0], "rb")
logger.debug(
"Hashing v1 torrent file. Size: %s Piece Length: %s",
humanize_bytes(self.total),
humanize_bytes(self.piece_length),
)
__iter__(self)
special
Source code in torrentfile\torrent.py
def __iter__(self):
"""Iterate through feed pieces.
Returns
-------
self : `iterator`
Iterator for leaves/hash pieces.
"""
return self
__next__(self)
special
Source code in torrentfile\torrent.py
def __next__(self):
"""Generate piece-length pieces of data from input file list."""
while True:
piece = bytearray(self.piece_length)
size = self.current.readinto(piece)
if size == 0:
if not self.next_file():
raise StopIteration
elif size < self.piece_length:
return self._handle_partial(piece[:size])
else:
return sha1(piece).digest() # nosec
next_file(self)
Source code in torrentfile\torrent.py
def next_file(self):
"""Seemlessly transition to next file in file list."""
self.index += 1
if self.index < len(self.paths):
self.current.close()
self.current = open(self.paths[self.index], "rb")
return True
return False
__init__(self, **kwargs)
special
Source code in torrentfile\torrent.py
def __init__(self, **kwargs):
"""Construct TorrentFile instance with given keyword args.
Parameters
----------
kwargs : `dict`
dictionary of keyword args passed to superclass.
"""
super().__init__(**kwargs)
logger.debug("Making Bittorrent V1 meta file.")
self.assemble()
assemble(self)
Source code in torrentfile\torrent.py
def assemble(self):
"""Assemble components of torrent metafile.
Returns
-------
`dict`
metadata dictionary for torrent file
"""
info = self.meta["info"]
size, filelist = utils.filelist_total(self.path)
if os.path.isfile(self.path):
info["length"] = size
else:
info["files"] = [
{
"length": os.path.getsize(path),
"path": os.path.relpath(path, self.path).split(os.sep),
}
for path in filelist
]
pieces = bytearray()
feeder = Hasher(filelist, self.piece_length)
if self.progress:
from tqdm import tqdm
for piece in tqdm(
iterable=feeder,
desc="Hashing Content",
total=size // self.piece_length,
unit="bytes",
unit_scale=True,
unit_divisor=self.piece_length,
initial=0,
leave=True
):
pieces.extend(piece)
else:
for piece in feeder:
pieces.extend(piece)
info["pieces"] = pieces
TorrentFileHybrid (MetaFile)
Source code in torrentfile\torrent.py
class TorrentFileHybrid(MetaFile):
"""Construct the Hybrid torrent meta file with provided parameters.
Parameters
----------
path : `str`
path to torrentfile target.
announce : `str` or `list`
one or more tracker URL's.
comment : `str`
Some comment.
source : `str`
Used for private trackers.
outfile : `str`
target path to write output.
private : `bool`
Used for private trackers.
piece_length : `int`
torrentfile data piece length.
"""
hasher = HasherHybrid
def __init__(self, **kwargs):
"""Create Bittorrent v1 v2 hybrid metafiles."""
super().__init__(**kwargs)
logger.debug("Creating Hybrid torrent file.")
self.name = os.path.basename(self.path)
self.hashes = []
self.piece_layers = {}
self.bar = None
self.pieces = []
self.files = []
self.assemble()
def assemble(self):
"""Assemble the parts of the torrentfile into meta dictionary."""
info = self.meta["info"]
info["meta version"] = 2
if self.progress:
from tqdm import tqdm
lst = utils.get_file_list(self.path)
self.bar = tqdm(
desc="Hashing Files:",
total=len(lst),
leave=True,
unit="file",
)
if os.path.isfile(self.path):
info["file tree"] = {self.name: self._traverse(self.path)}
info["length"] = os.path.getsize(self.path)
if self.bar:
self.bar.update(n=1)
else:
info["file tree"] = self._traverse(self.path)
info["files"] = self.files
info["pieces"] = b"".join(self.pieces)
self.meta["piece layers"] = self.piece_layers
return info
def _traverse(self, path):
"""Build meta dictionary while walking directory.
Parameters
----------
path : `str`
Path to target file.
"""
if os.path.isfile(path):
fsize = os.path.getsize(path)
self.files.append(
{
"length": fsize,
"path": os.path.relpath(path, self.path).split(os.sep),
}
)
if fsize == 0:
if self.bar:
self.bar.update(n=1)
return {"": {"length": fsize}}
fhash = HasherHybrid(path, self.piece_length)
if fsize > self.piece_length:
self.piece_layers[fhash.root] = fhash.piece_layer
self.hashes.append(fhash)
self.pieces.extend(fhash.pieces)
if fhash.padding_file:
self.files.append(fhash.padding_file)
if self.bar:
self.bar.update(n=1)
return {"": {"length": fsize, "pieces root": fhash.root}}
tree = {}
if os.path.isdir(path):
for name in sorted(os.listdir(path)):
tree[name] = self._traverse(os.path.join(path, name))
return tree
hasher (CbMixin)
Source code in torrentfile\torrent.py
class HasherHybrid(CbMixin):
"""Calculate root and piece hashes for creating hybrid torrent file.
Create merkle tree layers from sha256 hashed 16KiB blocks of contents.
With a branching factor of 2, merge layer hashes until blocks equal
piece_length bytes for the piece layer, and then the root hash.
Parameters
----------
path : `str`
path to target file.
piece_length : `int`
piece length for data chunks.
"""
def __init__(self, path, piece_length):
"""Construct Hasher class instances for each file in torrent."""
self.path = path
self.piece_length = piece_length
self.pieces = []
self.layer_hashes = []
self.piece_layer = None
self.root = None
self.padding_piece = None
self.padding_file = None
self.amount = piece_length // BLOCK_SIZE
logger.debug(
"Hashing partial Hybrid torrent file. Piece Length: %s Path: %s",
humanize_bytes(self.piece_length),
str(self.path),
)
with open(path, "rb") as data:
self._process_file(data)
def _pad_remaining(self, total, blocklen):
"""Generate Hash sized, 0 filled bytes for padding.
Parameters
----------
total : `int`
length of bytes processed.
blocklen : `int`
number of blocks processed.
Returns
-------
padding : `bytes`
Padding to fill remaining portion of tree.
"""
if not self.layer_hashes:
next_pow_2 = 1 << int(math.log2(total) + 1)
remaining = ((next_pow_2 - total) // BLOCK_SIZE) + 1
return [bytes(HASH_SIZE) for _ in range(remaining)]
return [bytes(HASH_SIZE) for _ in range(self.amount - blocklen)]
def _process_file(self, data):
"""Calculate layer hashes for contents of file.
Parameters
----------
data : `BytesIO`
File opened in read mode.
"""
while True:
plength = self.piece_length
blocks = []
piece = sha1() # nosec
total = 0
block = bytearray(BLOCK_SIZE)
for _ in range(self.amount):
size = data.readinto(block)
if not size:
break
total += size
plength -= size
blocks.append(sha256(block[:size]).digest())
piece.update(block[:size])
if not blocks:
break
if len(blocks) != self.amount:
padding = self._pad_remaining(len(blocks), size)
blocks.extend(padding)
layer_hash = merkle_root(blocks)
if self._cb:
self._cb(layer_hash)
self.layer_hashes.append(layer_hash)
if plength > 0:
self.padding_file = {
"attr": "p",
"length": size,
"path": [".pad", str(plength)],
}
piece.update(bytes(plength))
self.pieces.append(piece.digest()) # nosec
self._calculate_root()
def _calculate_root(self):
"""Calculate the root hash for opened file."""
self.piece_layer = b"".join(self.layer_hashes)
if len(self.layer_hashes) > 1:
pad_piece = merkle_root([bytes(32) for _ in range(self.amount)])
next_pow_two = 1 << (len(self.layer_hashes) - 1).bit_length()
remainder = next_pow_two - len(self.layer_hashes)
self.layer_hashes += [pad_piece for _ in range(remainder)]
self.root = merkle_root(self.layer_hashes)
__init__(self, path, piece_length)
special
Source code in torrentfile\torrent.py
def __init__(self, path, piece_length):
"""Construct Hasher class instances for each file in torrent."""
self.path = path
self.piece_length = piece_length
self.pieces = []
self.layer_hashes = []
self.piece_layer = None
self.root = None
self.padding_piece = None
self.padding_file = None
self.amount = piece_length // BLOCK_SIZE
logger.debug(
"Hashing partial Hybrid torrent file. Piece Length: %s Path: %s",
humanize_bytes(self.piece_length),
str(self.path),
)
with open(path, "rb") as data:
self._process_file(data)
__init__(self, **kwargs)
special
Source code in torrentfile\torrent.py
def __init__(self, **kwargs):
"""Create Bittorrent v1 v2 hybrid metafiles."""
super().__init__(**kwargs)
logger.debug("Creating Hybrid torrent file.")
self.name = os.path.basename(self.path)
self.hashes = []
self.piece_layers = {}
self.bar = None
self.pieces = []
self.files = []
self.assemble()
assemble(self)
Source code in torrentfile\torrent.py
def assemble(self):
"""Assemble the parts of the torrentfile into meta dictionary."""
info = self.meta["info"]
info["meta version"] = 2
if self.progress:
from tqdm import tqdm
lst = utils.get_file_list(self.path)
self.bar = tqdm(
desc="Hashing Files:",
total=len(lst),
leave=True,
unit="file",
)
if os.path.isfile(self.path):
info["file tree"] = {self.name: self._traverse(self.path)}
info["length"] = os.path.getsize(self.path)
if self.bar:
self.bar.update(n=1)
else:
info["file tree"] = self._traverse(self.path)
info["files"] = self.files
info["pieces"] = b"".join(self.pieces)
self.meta["piece layers"] = self.piece_layers
return info
TorrentFileV2 (MetaFile)
Source code in torrentfile\torrent.py
class TorrentFileV2(MetaFile):
"""Class for creating Bittorrent meta v2 files.
Parameters
----------
path : `str`
Path to torrent file or directory.
piece_length : `int`
Size of each piece of torrent data.
announce : `str` or `list`
one or more tracker URL's.
private : `int`
1 if private torrent else 0.
source : `str`
Source tracker.
comment : `str`
Comment string.
outfile : `str`
Path to write metfile to.
"""
hasher = HasherV2
def __init__(self, **kwargs):
"""Construct `TorrentFileV2` Class instance from given parameters.
Parameters
----------
kwargs : `dict`
keywword arguments to pass to superclass.
"""
super().__init__(**kwargs)
logger.debug("Create .torrent v2 file.")
self.piece_layers = {}
self.hashes = []
self.bar = None
self.assemble()
@property
def update(self):
"""Update for the progress bar."""
if self.bar:
self.bar.update(n=1)
return
def assemble(self):
"""Assemble then return the meta dictionary for encoding.
Returns
-------
meta : `dict`
Metainformation about the torrent.
"""
info = self.meta["info"]
if self.progress:
from tqdm import tqdm
lst = utils.get_file_list(self.path)
self.bar = tqdm(
desc="Hashing Files:",
total=len(lst),
leave=True,
unit="file",
)
if os.path.isfile(self.path):
info["file tree"] = {info["name"]: self._traverse(self.path)}
info["length"] = os.path.getsize(self.path)
self.update
else:
info["file tree"] = self._traverse(self.path)
info["meta version"] = 2
self.meta["piece layers"] = self.piece_layers
def _traverse(self, path):
"""Walk directory tree.
Parameters
----------
path : `str`
Path to file or directory.
"""
if os.path.isfile(path):
# Calculate Size and hashes for each file.
size = os.path.getsize(path)
if size == 0:
self.update
return {"": {"length": size}}
fhash = HasherV2(path, self.piece_length)
if size > self.piece_length:
self.piece_layers[fhash.root] = fhash.piece_layer
self.update
return {"": {"length": size, "pieces root": fhash.root}}
file_tree = {}
if os.path.isdir(path):
for name in sorted(os.listdir(path)):
file_tree[name] = self._traverse(os.path.join(path, name))
return file_tree
update
property
readonly
hasher (CbMixin)
Source code in torrentfile\torrent.py
class HasherV2(CbMixin):
"""Calculate the root hash and piece layers for file contents.
Iterates over 16KiB blocks of data from given file, hashes the data,
then creates a hash tree from the individual block hashes until size of
hashed data equals the piece-length. Then continues the hash tree until
root hash is calculated.
Parameters
----------
path : `str`
Path to file.
piece_length : `int`
Size of layer hashes pieces.
"""
def __init__(self, path, piece_length):
"""Calculate and store hash information for specific file."""
self.path = path
self.root = None
self.piece_layer = None
self.layer_hashes = []
self.piece_length = piece_length
self.num_blocks = piece_length // BLOCK_SIZE
logger.debug(
"Hashing partial v2 torrent file. Piece Length: %s Path: %s",
humanize_bytes(self.piece_length),
str(self.path),
)
with open(self.path, "rb") as fd:
self.process_file(fd)
def process_file(self, fd):
"""Calculate hashes over 16KiB chuncks of file content.
Parameters
----------
fd : `str`
Opened file in read mode.
"""
while True:
total = 0
blocks = []
leaf = bytearray(BLOCK_SIZE)
# generate leaves of merkle tree
for _ in range(self.num_blocks):
size = fd.readinto(leaf)
total += size
if not size:
break
blocks.append(sha256(leaf[:size]).digest())
# blocks is empty mean eof
if not blocks:
break
if len(blocks) != self.num_blocks:
# when size of file doesn't fill the last block
if not self.layer_hashes:
# when the there is only one block for file
next_pow_2 = 1 << int(math.log2(total) + 1)
remaining = ((next_pow_2 - total) // BLOCK_SIZE) + 1
else:
# when the file contains multiple pieces
remaining = self.num_blocks - size
# pad the the rest with zeroes to fill remaining space.
padding = [bytes(32) for _ in range(remaining)]
blocks.extend(padding)
# calculate the root hash for the merkle tree up to piece-length
layer_hash = merkle_root(blocks)
if self._cb:
self._cb(layer_hash)
self.layer_hashes.append(layer_hash)
self._calculate_root()
def _calculate_root(self):
"""Calculate root hash for the target file."""
self.piece_layer = b"".join(self.layer_hashes)
if len(self.layer_hashes) > 1:
next_pow_2 = 1 << int(math.log2(len(self.layer_hashes)) + 1)
remainder = next_pow_2 - len(self.layer_hashes)
pad_piece = [bytes(HASH_SIZE) for _ in range(self.num_blocks)]
for _ in range(remainder):
self.layer_hashes.append(merkle_root(pad_piece))
self.root = merkle_root(self.layer_hashes)
__init__(self, path, piece_length)
special
Source code in torrentfile\torrent.py
def __init__(self, path, piece_length):
"""Calculate and store hash information for specific file."""
self.path = path
self.root = None
self.piece_layer = None
self.layer_hashes = []
self.piece_length = piece_length
self.num_blocks = piece_length // BLOCK_SIZE
logger.debug(
"Hashing partial v2 torrent file. Piece Length: %s Path: %s",
humanize_bytes(self.piece_length),
str(self.path),
)
with open(self.path, "rb") as fd:
self.process_file(fd)
process_file(self, fd)
Source code in torrentfile\torrent.py
def process_file(self, fd):
"""Calculate hashes over 16KiB chuncks of file content.
Parameters
----------
fd : `str`
Opened file in read mode.
"""
while True:
total = 0
blocks = []
leaf = bytearray(BLOCK_SIZE)
# generate leaves of merkle tree
for _ in range(self.num_blocks):
size = fd.readinto(leaf)
total += size
if not size:
break
blocks.append(sha256(leaf[:size]).digest())
# blocks is empty mean eof
if not blocks:
break
if len(blocks) != self.num_blocks:
# when size of file doesn't fill the last block
if not self.layer_hashes:
# when the there is only one block for file
next_pow_2 = 1 << int(math.log2(total) + 1)
remaining = ((next_pow_2 - total) // BLOCK_SIZE) + 1
else:
# when the file contains multiple pieces
remaining = self.num_blocks - size
# pad the the rest with zeroes to fill remaining space.
padding = [bytes(32) for _ in range(remaining)]
blocks.extend(padding)
# calculate the root hash for the merkle tree up to piece-length
layer_hash = merkle_root(blocks)
if self._cb:
self._cb(layer_hash)
self.layer_hashes.append(layer_hash)
self._calculate_root()
__init__(self, **kwargs)
special
Source code in torrentfile\torrent.py
def __init__(self, **kwargs):
"""Construct `TorrentFileV2` Class instance from given parameters.
Parameters
----------
kwargs : `dict`
keywword arguments to pass to superclass.
"""
super().__init__(**kwargs)
logger.debug("Create .torrent v2 file.")
self.piece_layers = {}
self.hashes = []
self.bar = None
self.assemble()
assemble(self)
Source code in torrentfile\torrent.py
def assemble(self):
"""Assemble then return the meta dictionary for encoding.
Returns
-------
meta : `dict`
Metainformation about the torrent.
"""
info = self.meta["info"]
if self.progress:
from tqdm import tqdm
lst = utils.get_file_list(self.path)
self.bar = tqdm(
desc="Hashing Files:",
total=len(lst),
leave=True,
unit="file",
)
if os.path.isfile(self.path):
info["file tree"] = {info["name"]: self._traverse(self.path)}
info["length"] = os.path.getsize(self.path)
self.update
else:
info["file tree"] = self._traverse(self.path)
info["meta version"] = 2
self.meta["piece layers"] = self.piece_layers
Hasher
Module
torrentfile.
hasher
Piece/File Hashers for Bittorrent meta file contents.
CbMixin
— Mixin class to set a callback during hashing procedure.Hasher
— Piece hasher for Bittorrent V1 files.HasherV2
— Calculate the root hash and piece layers for file contents.HasherHybrid
— Calculate root and piece hashes for creating hybrid torrent file.
merkle_root
(
blocks
)
— Calculate the merkle root for a seq of sha256 hash digests.
torrentfile.hasher.merkle_root(blocks)
Source code in torrentfile\hasher.py
def merkle_root(blocks):
"""Calculate the merkle root for a seq of sha256 hash digests."""
while len(blocks) > 1:
blocks = [sha256(x + y).digest() for x, y in zip(*[iter(blocks)] * 2)]
return blocks[0]
torrentfile.hasher.Hasher (CbMixin)
Source code in torrentfile\hasher.py
class Hasher(CbMixin):
"""Piece hasher for Bittorrent V1 files.
Takes a sorted list of all file paths, calculates sha1 hash
for fixed size pieces of file data from each file
seemlessly until the last piece which may be smaller than others.
Parameters
----------
paths : `list`
List of files.
piece_length : `int`
Size of chuncks to split the data into.
total : `int`
Sum of all files in file list.
"""
def __init__(self, paths, piece_length):
"""Generate hashes of piece length data from filelist contents."""
self.piece_length = piece_length
self.paths = paths
self.total = sum([os.path.getsize(i) for i in self.paths])
self.index = 0
self.current = open(self.paths[0], "rb")
logger.debug(
"Hashing v1 torrent file. Size: %s Piece Length: %s",
humanize_bytes(self.total),
humanize_bytes(self.piece_length),
)
def __iter__(self):
"""Iterate through feed pieces.
Returns
-------
self : `iterator`
Iterator for leaves/hash pieces.
"""
return self
def _handle_partial(self, arr):
"""Define the handling partial pieces that span 2 or more files.
Parameters
----------
arr : `bytearray`
Incomplete piece containing partial data
partial : `int`
Size of incomplete piece_length
Returns
-------
digest : `bytes`
SHA1 digest of the complete piece.
"""
while len(arr) < self.piece_length and self.next_file():
target = self.piece_length - len(arr)
temp = bytearray(target)
size = self.current.readinto(temp)
arr.extend(temp[:size])
if size == target:
break
return sha1(arr).digest() # nosec
def next_file(self):
"""Seemlessly transition to next file in file list."""
self.index += 1
if self.index < len(self.paths):
self.current.close()
self.current = open(self.paths[self.index], "rb")
return True
return False
def __next__(self):
"""Generate piece-length pieces of data from input file list."""
while True:
piece = bytearray(self.piece_length)
size = self.current.readinto(piece)
if size == 0:
if not self.next_file():
raise StopIteration
elif size < self.piece_length:
return self._handle_partial(piece[:size])
else:
return sha1(piece).digest() # nosec
__init__(self, paths, piece_length)
special
Source code in torrentfile\hasher.py
def __init__(self, paths, piece_length):
"""Generate hashes of piece length data from filelist contents."""
self.piece_length = piece_length
self.paths = paths
self.total = sum([os.path.getsize(i) for i in self.paths])
self.index = 0
self.current = open(self.paths[0], "rb")
logger.debug(
"Hashing v1 torrent file. Size: %s Piece Length: %s",
humanize_bytes(self.total),
humanize_bytes(self.piece_length),
)
__iter__(self)
special
Source code in torrentfile\hasher.py
def __iter__(self):
"""Iterate through feed pieces.
Returns
-------
self : `iterator`
Iterator for leaves/hash pieces.
"""
return self
__next__(self)
special
Source code in torrentfile\hasher.py
def __next__(self):
"""Generate piece-length pieces of data from input file list."""
while True:
piece = bytearray(self.piece_length)
size = self.current.readinto(piece)
if size == 0:
if not self.next_file():
raise StopIteration
elif size < self.piece_length:
return self._handle_partial(piece[:size])
else:
return sha1(piece).digest() # nosec
next_file(self)
Source code in torrentfile\hasher.py
def next_file(self):
"""Seemlessly transition to next file in file list."""
self.index += 1
if self.index < len(self.paths):
self.current.close()
self.current = open(self.paths[self.index], "rb")
return True
return False
torrentfile.hasher.HasherV2 (CbMixin)
Source code in torrentfile\hasher.py
class HasherV2(CbMixin):
"""Calculate the root hash and piece layers for file contents.
Iterates over 16KiB blocks of data from given file, hashes the data,
then creates a hash tree from the individual block hashes until size of
hashed data equals the piece-length. Then continues the hash tree until
root hash is calculated.
Parameters
----------
path : `str`
Path to file.
piece_length : `int`
Size of layer hashes pieces.
"""
def __init__(self, path, piece_length):
"""Calculate and store hash information for specific file."""
self.path = path
self.root = None
self.piece_layer = None
self.layer_hashes = []
self.piece_length = piece_length
self.num_blocks = piece_length // BLOCK_SIZE
logger.debug(
"Hashing partial v2 torrent file. Piece Length: %s Path: %s",
humanize_bytes(self.piece_length),
str(self.path),
)
with open(self.path, "rb") as fd:
self.process_file(fd)
def process_file(self, fd):
"""Calculate hashes over 16KiB chuncks of file content.
Parameters
----------
fd : `str`
Opened file in read mode.
"""
while True:
total = 0
blocks = []
leaf = bytearray(BLOCK_SIZE)
# generate leaves of merkle tree
for _ in range(self.num_blocks):
size = fd.readinto(leaf)
total += size
if not size:
break
blocks.append(sha256(leaf[:size]).digest())
# blocks is empty mean eof
if not blocks:
break
if len(blocks) != self.num_blocks:
# when size of file doesn't fill the last block
if not self.layer_hashes:
# when the there is only one block for file
next_pow_2 = 1 << int(math.log2(total) + 1)
remaining = ((next_pow_2 - total) // BLOCK_SIZE) + 1
else:
# when the file contains multiple pieces
remaining = self.num_blocks - size
# pad the the rest with zeroes to fill remaining space.
padding = [bytes(32) for _ in range(remaining)]
blocks.extend(padding)
# calculate the root hash for the merkle tree up to piece-length
layer_hash = merkle_root(blocks)
if self._cb:
self._cb(layer_hash)
self.layer_hashes.append(layer_hash)
self._calculate_root()
def _calculate_root(self):
"""Calculate root hash for the target file."""
self.piece_layer = b"".join(self.layer_hashes)
if len(self.layer_hashes) > 1:
next_pow_2 = 1 << int(math.log2(len(self.layer_hashes)) + 1)
remainder = next_pow_2 - len(self.layer_hashes)
pad_piece = [bytes(HASH_SIZE) for _ in range(self.num_blocks)]
for _ in range(remainder):
self.layer_hashes.append(merkle_root(pad_piece))
self.root = merkle_root(self.layer_hashes)
__init__(self, path, piece_length)
special
Source code in torrentfile\hasher.py
def __init__(self, path, piece_length):
"""Calculate and store hash information for specific file."""
self.path = path
self.root = None
self.piece_layer = None
self.layer_hashes = []
self.piece_length = piece_length
self.num_blocks = piece_length // BLOCK_SIZE
logger.debug(
"Hashing partial v2 torrent file. Piece Length: %s Path: %s",
humanize_bytes(self.piece_length),
str(self.path),
)
with open(self.path, "rb") as fd:
self.process_file(fd)
process_file(self, fd)
Source code in torrentfile\hasher.py
def process_file(self, fd):
"""Calculate hashes over 16KiB chuncks of file content.
Parameters
----------
fd : `str`
Opened file in read mode.
"""
while True:
total = 0
blocks = []
leaf = bytearray(BLOCK_SIZE)
# generate leaves of merkle tree
for _ in range(self.num_blocks):
size = fd.readinto(leaf)
total += size
if not size:
break
blocks.append(sha256(leaf[:size]).digest())
# blocks is empty mean eof
if not blocks:
break
if len(blocks) != self.num_blocks:
# when size of file doesn't fill the last block
if not self.layer_hashes:
# when the there is only one block for file
next_pow_2 = 1 << int(math.log2(total) + 1)
remaining = ((next_pow_2 - total) // BLOCK_SIZE) + 1
else:
# when the file contains multiple pieces
remaining = self.num_blocks - size
# pad the the rest with zeroes to fill remaining space.
padding = [bytes(32) for _ in range(remaining)]
blocks.extend(padding)
# calculate the root hash for the merkle tree up to piece-length
layer_hash = merkle_root(blocks)
if self._cb:
self._cb(layer_hash)
self.layer_hashes.append(layer_hash)
self._calculate_root()
torrentfile.hasher.HasherHybrid (CbMixin)
Source code in torrentfile\hasher.py
class HasherHybrid(CbMixin):
"""Calculate root and piece hashes for creating hybrid torrent file.
Create merkle tree layers from sha256 hashed 16KiB blocks of contents.
With a branching factor of 2, merge layer hashes until blocks equal
piece_length bytes for the piece layer, and then the root hash.
Parameters
----------
path : `str`
path to target file.
piece_length : `int`
piece length for data chunks.
"""
def __init__(self, path, piece_length):
"""Construct Hasher class instances for each file in torrent."""
self.path = path
self.piece_length = piece_length
self.pieces = []
self.layer_hashes = []
self.piece_layer = None
self.root = None
self.padding_piece = None
self.padding_file = None
self.amount = piece_length // BLOCK_SIZE
logger.debug(
"Hashing partial Hybrid torrent file. Piece Length: %s Path: %s",
humanize_bytes(self.piece_length),
str(self.path),
)
with open(path, "rb") as data:
self._process_file(data)
def _pad_remaining(self, total, blocklen):
"""Generate Hash sized, 0 filled bytes for padding.
Parameters
----------
total : `int`
length of bytes processed.
blocklen : `int`
number of blocks processed.
Returns
-------
padding : `bytes`
Padding to fill remaining portion of tree.
"""
if not self.layer_hashes:
next_pow_2 = 1 << int(math.log2(total) + 1)
remaining = ((next_pow_2 - total) // BLOCK_SIZE) + 1
return [bytes(HASH_SIZE) for _ in range(remaining)]
return [bytes(HASH_SIZE) for _ in range(self.amount - blocklen)]
def _process_file(self, data):
"""Calculate layer hashes for contents of file.
Parameters
----------
data : `BytesIO`
File opened in read mode.
"""
while True:
plength = self.piece_length
blocks = []
piece = sha1() # nosec
total = 0
block = bytearray(BLOCK_SIZE)
for _ in range(self.amount):
size = data.readinto(block)
if not size:
break
total += size
plength -= size
blocks.append(sha256(block[:size]).digest())
piece.update(block[:size])
if not blocks:
break
if len(blocks) != self.amount:
padding = self._pad_remaining(len(blocks), size)
blocks.extend(padding)
layer_hash = merkle_root(blocks)
if self._cb:
self._cb(layer_hash)
self.layer_hashes.append(layer_hash)
if plength > 0:
self.padding_file = {
"attr": "p",
"length": size,
"path": [".pad", str(plength)],
}
piece.update(bytes(plength))
self.pieces.append(piece.digest()) # nosec
self._calculate_root()
def _calculate_root(self):
"""Calculate the root hash for opened file."""
self.piece_layer = b"".join(self.layer_hashes)
if len(self.layer_hashes) > 1:
pad_piece = merkle_root([bytes(32) for _ in range(self.amount)])
next_pow_two = 1 << (len(self.layer_hashes) - 1).bit_length()
remainder = next_pow_two - len(self.layer_hashes)
self.layer_hashes += [pad_piece for _ in range(remainder)]
self.root = merkle_root(self.layer_hashes)
__init__(self, path, piece_length)
special
Source code in torrentfile\hasher.py
def __init__(self, path, piece_length):
"""Construct Hasher class instances for each file in torrent."""
self.path = path
self.piece_length = piece_length
self.pieces = []
self.layer_hashes = []
self.piece_layer = None
self.root = None
self.padding_piece = None
self.padding_file = None
self.amount = piece_length // BLOCK_SIZE
logger.debug(
"Hashing partial Hybrid torrent file. Piece Length: %s Path: %s",
humanize_bytes(self.piece_length),
str(self.path),
)
with open(path, "rb") as data:
self._process_file(data)
Edit
Module
torrentfile.
edit
Edit torrent meta file.
edit_torrent
(
metafile
,args
)
— Edit the properties and values in a torrent meta file.filter_empty
(
args
,meta
,info
)
— Remove dictionary keys with empty values.
torrentfile.edit
edit_torrent(metafile, args)
Source code in torrentfile\edit.py
def edit_torrent(metafile, args):
"""
Edit the properties and values in a torrent meta file.
Parameters
----------
metafile : `str`
path to the torrent meta file.
args : `dict`
key value pairs of the properties to be edited.
"""
meta = pyben.load(metafile)
info = meta["info"]
filter_empty(args, meta, info)
if "comment" in args:
info["comment"] = args["comment"]
if "source" in args:
info["source"] = args["source"]
if "private" in args:
info["private"] = 1
if "announce" in args:
val = args.get("announce", None)
if isinstance(val, str):
vallist = val.split()
meta["announce"] = vallist[0]
meta["announce-list"] = [vallist]
elif isinstance(val, list):
meta["announce"] = val[0]
meta["announce-list"] = [val]
if "url-list" in args:
val = args.get("url-list")
if isinstance(val, str):
meta["url-list"] = val.split()
elif isinstance(val, list):
meta["url-list"] = val
meta["info"] = info
os.remove(metafile)
pyben.dump(meta, metafile)
return meta
filter_empty(args, meta, info)
Source code in torrentfile\edit.py
def filter_empty(args, meta, info):
"""
Remove dictionary keys with empty values.
Parameters
----------
args : `dict`
Editable metafile properties from user.
meta : `dict`
Metafile data dictionary.
info : `dict`
Metafile info dictionary.
"""
for key, val in list(args.items()):
if val is None:
del args[key]
continue
if val == "":
if key in meta:
del meta[key]
elif key in info:
del info[key]
del args[key]
Recheck
Module
torrentfile.
recheck
Module container Checker Class.
The CheckerClass takes a torrentfile and tha path to it's contents. It will then iterate through every file and directory contained and compare their data to values contained within the torrent file. Completion percentages will be printed to screen for each file and at the end for the torrentfile as a whole.
Checker
— Check a given file or directory to see if it matches a torrentfile.FeedChecker
— Validates torrent content.HashChecker
— Verify that root hashes of content files match the .torrent files.
torrentfile.recheck
Checker
Source code in torrentfile\recheck.py
class Checker:
"""Check a given file or directory to see if it matches a torrentfile.
Public constructor for Checker class instance.
Parameters
----------
metafile (`str`): Path to ".torrent" file.
location (`str`): Path where the content is located in filesystem.
Example
-------
>> metafile = "/path/to/torrentfile/content_file_or_dir.torrent"
>> location = "/path/to/location"
>> os.path.exists("/path/to/location/content_file_or_dir")
Out: True
>> checker = Checker(metafile, location)
"""
_hook = None
def __init__(self, metafile, path):
"""Validate data against hashes contained in .torrent file.
Parameters
----------
metafile : `str`
path to .torrent file
path : `str`
path to content or contents parent directory.
"""
self.metafile = metafile
self.meta_version = None
self.total = 0
self.paths = []
self.fileinfo = {}
self.last_log = None
if not os.path.exists(metafile):
raise FileNotFoundError
self.meta = pyben.load(metafile)
self.info = self.meta["info"]
self.name = self.info["name"]
self.piece_length = self.info["piece length"]
if "meta version" in self.info:
if "pieces" in self.info:
self.meta_version = 3
else:
self.meta_version = 2
else:
self.meta_version = 1
self.root = self.find_root(path)
self.log_msg("Checking: %s, %s", metafile, path)
self.check_paths()
@classmethod
def register_callback(cls, hook):
"""Register hooks from 3rd party programs to access generated info.
Parameters
----------
hook : `function`
callback function for the logging feature.
"""
cls._hook = hook
def hasher(self):
"""Return the hasher class related to torrents meta version.
Returns
-------
`Class[Hasher]`
the hashing implementation for specific torrent meta version.
"""
if self.meta_version == 2:
return HasherV2
if self.meta_version == 3:
return HasherHybrid
return None
def piece_checker(self):
"""Check individual pieces of the torrent.
Returns
-------
`Obj`
Individual piece hasher.
""" ""
if self.meta_version == 1:
return FeedChecker
return HashChecker
def results(self):
"""Generate result percentage and store for future calls."""
if self.meta_version == 1:
iterations = len(self.info["pieces"]) // SHA1
else:
iterations = (self.total // self.piece_length) + 1
responses = []
for response in tqdm(
iterable=self.iter_hashes(),
desc="Calculating",
total=iterations,
unit="piece",
):
responses.append(response)
print(responses)
return self._result
def log_msg(self, *args, level=logging.INFO):
"""Log message `msg` to logger and send `msg` to callback hook.
Parameters
----------
*args : `Iterable`[`str`]
formatting args for log message
level : `int`
Log level for this message; default=`logging.INFO`
"""
message = args[0]
if len(args) >= 3:
message = message % tuple(args[1:])
elif len(args) == 2:
message = message % args[1]
# Repeat log messages should be ignored.
if message != self.last_log:
self.last_log = message
logger.log(level, message)
if self._hook and level == logging.INFO:
self._hook(message)
def find_root(self, path):
"""Check path for torrent content.
The path can be a relative or absolute filesystem path. In the case
where the content is a single file, the path may point directly to the
the file, or it may point to the parent directory. If content points
to a directory. The directory will be checked to see if it matches
the torrent's name, if not the directories contents will be searched.
The returned value will be the absolute path that matches the torrent's
name.
Parameters
----------
path : `str`
root path to torrent content
Returns
-------
`str`: root path to content
"""
if not os.path.exists(path):
self.log_msg("Could not locate torrent content %s.", path)
raise FileNotFoundError(path)
root = Path(path)
if root.name == self.name:
self.log_msg("Content found: %s.", str(root))
return root
if self.name in os.listdir(root):
return root / self.name
self.log_msg("Could not locate torrent content in: %s", str(root))
raise FileNotFoundError(root)
def check_paths(self):
"""Gather all file paths described in the torrent file."""
finfo = self.fileinfo
if "length" in self.info:
self.log_msg("%s points to a single file", self.root)
self.total = self.info["length"]
self.paths.append(str(self.root))
finfo[0] = {
"path": self.root,
"length": self.info["length"],
}
if self.meta_version > 1:
root = self.info["file tree"][self.name][""]["pieces root"]
finfo[0]["pieces root"] = root
return
# Otherwise Content is more than 1 file.
self.log_msg("%s points to a directory", self.root)
if self.meta_version == 1:
for i, item in enumerate(self.info["files"]):
self.total += item["length"]
base = os.path.join(*item["path"])
self.fileinfo[i] = {
"path": str(self.root / base),
"length": item["length"],
}
self.paths.append(str(self.root / base))
self.log_msg("Including file path: %s", str(self.root / base))
return
self.walk_file_tree(self.info["file tree"], [])
def walk_file_tree(self, tree: dict, partials: list):
"""Traverse File Tree dictionary to get file details.
Extract full pathnames, length, root hash, and layer hashes
for each file included in the .torrent's file tree.
Parameters
----------
tree : `dict`
File Tree dict extracted from torrent file.
partials : `list`
list of intermediate pathnames.
"""
for key, val in tree.items():
# Empty string means the tree's leaf is value
if "" in val:
base = os.path.join(*partials, key)
roothash = val[""]["pieces root"]
length = val[""]["length"]
full = str(self.root / base)
self.fileinfo[len(self.paths)] = {
"path": full,
"length": length,
"pieces root": roothash,
}
self.paths.append(full)
self.total += length
self.log_msg(
"Including: path - %s, length - %s",
full,
humanize_bytes(length),
)
else:
self.walk_file_tree(val, partials + [key])
def iter_hashes(self):
"""Produce results of comparing torrent contents piece by piece.
Yields
------
chunck : `bytes`
hash of data found on disk
piece : `bytes`
hash of data when complete and correct
path : `str`
path to file being hashed
size : `int`
length of bytes hashed for piece
"""
matched = consumed = 0
checker = self.piece_checker()
hasher = self.hasher()
for chunk, piece, path, size in checker(self, hasher):
consumed += size
msg = "Match %s: %s %s"
humansize = humanize_bytes(size)
matching = 0
if chunk == piece:
matching += size
matched += size
logger.debug(msg, "Success", path, humansize)
else:
logger.debug(msg, "Fail", path, humansize)
yield chunk, piece, path, size
total_consumed = str(int(consumed / self.total * 100))
percent_matched = str(int(matched / consumed * 100))
self.log_msg(
"Processed: %s%%, Matched: %s%%",
total_consumed,
percent_matched,
)
self._result = (matched / consumed) * 100 if consumed > 0 else 0
__init__(self, metafile, path)
special
Source code in torrentfile\recheck.py
def __init__(self, metafile, path):
"""Validate data against hashes contained in .torrent file.
Parameters
----------
metafile : `str`
path to .torrent file
path : `str`
path to content or contents parent directory.
"""
self.metafile = metafile
self.meta_version = None
self.total = 0
self.paths = []
self.fileinfo = {}
self.last_log = None
if not os.path.exists(metafile):
raise FileNotFoundError
self.meta = pyben.load(metafile)
self.info = self.meta["info"]
self.name = self.info["name"]
self.piece_length = self.info["piece length"]
if "meta version" in self.info:
if "pieces" in self.info:
self.meta_version = 3
else:
self.meta_version = 2
else:
self.meta_version = 1
self.root = self.find_root(path)
self.log_msg("Checking: %s, %s", metafile, path)
self.check_paths()
check_paths(self)
Source code in torrentfile\recheck.py
def check_paths(self):
"""Gather all file paths described in the torrent file."""
finfo = self.fileinfo
if "length" in self.info:
self.log_msg("%s points to a single file", self.root)
self.total = self.info["length"]
self.paths.append(str(self.root))
finfo[0] = {
"path": self.root,
"length": self.info["length"],
}
if self.meta_version > 1:
root = self.info["file tree"][self.name][""]["pieces root"]
finfo[0]["pieces root"] = root
return
# Otherwise Content is more than 1 file.
self.log_msg("%s points to a directory", self.root)
if self.meta_version == 1:
for i, item in enumerate(self.info["files"]):
self.total += item["length"]
base = os.path.join(*item["path"])
self.fileinfo[i] = {
"path": str(self.root / base),
"length": item["length"],
}
self.paths.append(str(self.root / base))
self.log_msg("Including file path: %s", str(self.root / base))
return
self.walk_file_tree(self.info["file tree"], [])
find_root(self, path)
Source code in torrentfile\recheck.py
def find_root(self, path):
"""Check path for torrent content.
The path can be a relative or absolute filesystem path. In the case
where the content is a single file, the path may point directly to the
the file, or it may point to the parent directory. If content points
to a directory. The directory will be checked to see if it matches
the torrent's name, if not the directories contents will be searched.
The returned value will be the absolute path that matches the torrent's
name.
Parameters
----------
path : `str`
root path to torrent content
Returns
-------
`str`: root path to content
"""
if not os.path.exists(path):
self.log_msg("Could not locate torrent content %s.", path)
raise FileNotFoundError(path)
root = Path(path)
if root.name == self.name:
self.log_msg("Content found: %s.", str(root))
return root
if self.name in os.listdir(root):
return root / self.name
self.log_msg("Could not locate torrent content in: %s", str(root))
raise FileNotFoundError(root)
hasher(self)
Source code in torrentfile\recheck.py
def hasher(self):
"""Return the hasher class related to torrents meta version.
Returns
-------
`Class[Hasher]`
the hashing implementation for specific torrent meta version.
"""
if self.meta_version == 2:
return HasherV2
if self.meta_version == 3:
return HasherHybrid
return None
iter_hashes(self)
Source code in torrentfile\recheck.py
def iter_hashes(self):
"""Produce results of comparing torrent contents piece by piece.
Yields
------
chunck : `bytes`
hash of data found on disk
piece : `bytes`
hash of data when complete and correct
path : `str`
path to file being hashed
size : `int`
length of bytes hashed for piece
"""
matched = consumed = 0
checker = self.piece_checker()
hasher = self.hasher()
for chunk, piece, path, size in checker(self, hasher):
consumed += size
msg = "Match %s: %s %s"
humansize = humanize_bytes(size)
matching = 0
if chunk == piece:
matching += size
matched += size
logger.debug(msg, "Success", path, humansize)
else:
logger.debug(msg, "Fail", path, humansize)
yield chunk, piece, path, size
total_consumed = str(int(consumed / self.total * 100))
percent_matched = str(int(matched / consumed * 100))
self.log_msg(
"Processed: %s%%, Matched: %s%%",
total_consumed,
percent_matched,
)
self._result = (matched / consumed) * 100 if consumed > 0 else 0
log_msg(self, *args, *, level=20)
Source code in torrentfile\recheck.py
def log_msg(self, *args, level=logging.INFO):
"""Log message `msg` to logger and send `msg` to callback hook.
Parameters
----------
*args : `Iterable`[`str`]
formatting args for log message
level : `int`
Log level for this message; default=`logging.INFO`
"""
message = args[0]
if len(args) >= 3:
message = message % tuple(args[1:])
elif len(args) == 2:
message = message % args[1]
# Repeat log messages should be ignored.
if message != self.last_log:
self.last_log = message
logger.log(level, message)
if self._hook and level == logging.INFO:
self._hook(message)
piece_checker(self)
Source code in torrentfile\recheck.py
def piece_checker(self):
"""Check individual pieces of the torrent.
Returns
-------
`Obj`
Individual piece hasher.
""" ""
if self.meta_version == 1:
return FeedChecker
return HashChecker
register_callback(hook)
classmethod
Source code in torrentfile\recheck.py
@classmethod
def register_callback(cls, hook):
"""Register hooks from 3rd party programs to access generated info.
Parameters
----------
hook : `function`
callback function for the logging feature.
"""
cls._hook = hook
results(self)
Source code in torrentfile\recheck.py
def results(self):
"""Generate result percentage and store for future calls."""
if self.meta_version == 1:
iterations = len(self.info["pieces"]) // SHA1
else:
iterations = (self.total // self.piece_length) + 1
responses = []
for response in tqdm(
iterable=self.iter_hashes(),
desc="Calculating",
total=iterations,
unit="piece",
):
responses.append(response)
print(responses)
return self._result
walk_file_tree(self, tree, partials)
Source code in torrentfile\recheck.py
def walk_file_tree(self, tree: dict, partials: list):
"""Traverse File Tree dictionary to get file details.
Extract full pathnames, length, root hash, and layer hashes
for each file included in the .torrent's file tree.
Parameters
----------
tree : `dict`
File Tree dict extracted from torrent file.
partials : `list`
list of intermediate pathnames.
"""
for key, val in tree.items():
# Empty string means the tree's leaf is value
if "" in val:
base = os.path.join(*partials, key)
roothash = val[""]["pieces root"]
length = val[""]["length"]
full = str(self.root / base)
self.fileinfo[len(self.paths)] = {
"path": full,
"length": length,
"pieces root": roothash,
}
self.paths.append(full)
self.total += length
self.log_msg(
"Including: path - %s, length - %s",
full,
humanize_bytes(length),
)
else:
self.walk_file_tree(val, partials + [key])
FeedChecker
Source code in torrentfile\recheck.py
class FeedChecker:
"""Validates torrent content.
Seemlesly validate torrent file contents by comparing hashes in
metafile against data on disk.
Parameters
----------
checker : `object`
the checker class instance.
hasher : `Any`
hashing class for calculating piece hashes. default=None
"""
def __init__(self, checker, hasher=None):
"""Generate hashes of piece length data from filelist contents."""
self.piece_length = checker.piece_length
self.paths = checker.paths
self.pieces = checker.info["pieces"]
self.fileinfo = checker.fileinfo
self.hasher = hasher
self.piece_map = {}
self.index = 0
self.piece_count = 0
self.it = None
def __iter__(self):
"""Assign iterator and return self."""
self.it = self.iter_pieces()
return self
def __next__(self):
"""Yield back result of comparison."""
try:
partial = next(self.it)
except StopIteration as itererror:
raise StopIteration from itererror
chunck = sha1(partial).digest() # nosec
start = self.piece_count * SHA1
end = start + SHA1
piece = self.pieces[start:end]
self.piece_count += 1
path = self.paths[self.index]
return chunck, piece, path, len(partial)
def iter_pieces(self):
"""Iterate through, and hash pieces of torrent contents.
Yields
------
piece : `bytes`
hash digest for block of torrent data.
"""
partial = bytearray()
for i, path in enumerate(self.paths):
self.index = i
if os.path.exists(path):
for piece in self.extract(path, partial):
if len(piece) == self.piece_length:
yield piece
elif i + 1 == len(self.paths):
yield piece
else:
partial = piece
else:
length = self.fileinfo[i]["length"]
for pad in self._gen_padding(partial, length):
if len(pad) == self.piece_length:
yield pad
else:
partial = pad
def extract(self, path: str, partial: bytearray) -> bytearray:
"""Split file paths contents into blocks of data for hash pieces.
Parameters
----------
path : `str`
path to content.
partial : `bytes`
any remaining content from last file.
Returns
-------
partial : `bytes`
Hash digest for block of .torrent contents.
"""
read = 0
length = self.fileinfo[self.index]["length"]
partial = bytearray() if len(partial) == self.piece_length else partial
with open(path, "rb") as current:
while True:
bitlength = self.piece_length - len(partial)
part = bytearray(bitlength)
amount = current.readinto(part)
read += amount
partial.extend(part[:amount])
if amount < bitlength:
if amount > 0 and read == length:
yield partial
break
yield partial
partial = bytearray(0)
if length != read:
for pad in self._gen_padding(partial, length, read):
yield pad
def _gen_padding(self, partial, length, read=0):
"""Create padded pieces where file sizes do not match.
Parameters
----------
partial : `bytes`
any remaining data from last file processed.
length : `int`
size of space that needs padding
read : `int`
portion of length already padded
Yields
------
`bytes`
A piece length sized block of zeros.
"""
while read < length:
left = self.piece_length - len(partial)
if length - read > left:
padding = bytearray(left)
partial.extend(padding)
yield partial
read += left
partial = bytearray(0)
else:
partial.extend(bytearray(length - read))
read = length
yield partial
__init__(self, checker, hasher=None)
special
Source code in torrentfile\recheck.py
def __init__(self, checker, hasher=None):
"""Generate hashes of piece length data from filelist contents."""
self.piece_length = checker.piece_length
self.paths = checker.paths
self.pieces = checker.info["pieces"]
self.fileinfo = checker.fileinfo
self.hasher = hasher
self.piece_map = {}
self.index = 0
self.piece_count = 0
self.it = None
__iter__(self)
special
Source code in torrentfile\recheck.py
def __iter__(self):
"""Assign iterator and return self."""
self.it = self.iter_pieces()
return self
__next__(self)
special
Source code in torrentfile\recheck.py
def __next__(self):
"""Yield back result of comparison."""
try:
partial = next(self.it)
except StopIteration as itererror:
raise StopIteration from itererror
chunck = sha1(partial).digest() # nosec
start = self.piece_count * SHA1
end = start + SHA1
piece = self.pieces[start:end]
self.piece_count += 1
path = self.paths[self.index]
return chunck, piece, path, len(partial)
extract(self, path, partial)
Source code in torrentfile\recheck.py
def extract(self, path: str, partial: bytearray) -> bytearray:
"""Split file paths contents into blocks of data for hash pieces.
Parameters
----------
path : `str`
path to content.
partial : `bytes`
any remaining content from last file.
Returns
-------
partial : `bytes`
Hash digest for block of .torrent contents.
"""
read = 0
length = self.fileinfo[self.index]["length"]
partial = bytearray() if len(partial) == self.piece_length else partial
with open(path, "rb") as current:
while True:
bitlength = self.piece_length - len(partial)
part = bytearray(bitlength)
amount = current.readinto(part)
read += amount
partial.extend(part[:amount])
if amount < bitlength:
if amount > 0 and read == length:
yield partial
break
yield partial
partial = bytearray(0)
if length != read:
for pad in self._gen_padding(partial, length, read):
yield pad
iter_pieces(self)
Source code in torrentfile\recheck.py
def iter_pieces(self):
"""Iterate through, and hash pieces of torrent contents.
Yields
------
piece : `bytes`
hash digest for block of torrent data.
"""
partial = bytearray()
for i, path in enumerate(self.paths):
self.index = i
if os.path.exists(path):
for piece in self.extract(path, partial):
if len(piece) == self.piece_length:
yield piece
elif i + 1 == len(self.paths):
yield piece
else:
partial = piece
else:
length = self.fileinfo[i]["length"]
for pad in self._gen_padding(partial, length):
if len(pad) == self.piece_length:
yield pad
else:
partial = pad
HashChecker
Source code in torrentfile\recheck.py
class HashChecker:
"""Verify that root hashes of content files match the .torrent files.
Parameters
----------
checker : `Object`
the checker instance that maintains variables.
hasher : `Object`
the version specific hashing class for torrent content.
"""
def __init__(self, checker, hasher=None):
"""Construct a HybridChecker instance."""
self.checker = checker
self.paths = checker.paths
self.hasher = hasher
self.piece_length = checker.piece_length
self.fileinfo = checker.fileinfo
self.piece_layers = checker.meta["piece layers"]
self.piece_count = 0
self.it = None
logger.debug(
"Starting Hash Checker. piece length: %s",
humanize_bytes(self.piece_length),
)
def __iter__(self):
"""Assign iterator and return self."""
self.it = self.iter_paths()
return self
def __next__(self):
"""Provide the result of comparison."""
try:
value = next(self.it)
return value
except StopIteration as stopiter:
raise StopIteration() from stopiter
def iter_paths(self):
"""Iterate through and compare root file hashes to .torrent file.
Yields
------
results : `tuple`
The size of the file and result of match.
"""
for i, path in enumerate(self.paths):
info = self.fileinfo[i]
length, plength = info["length"], self.piece_length
logger.debug("%s length: %s", path, str(length))
roothash = info["pieces root"]
logger.debug("%s root hash %s", path, str(roothash))
if roothash in self.piece_layers:
pieces = self.piece_layers[roothash]
else:
pieces = roothash
amount = len(pieces) // SHA256
if not os.path.exists(path):
for i in range(amount):
start = i * SHA256
end = start + SHA256
piece = pieces[start:end]
if length > plength:
size = plength
else:
size = length
length -= size
block = sha256(bytearray(size)).digest()
logging.debug(
"Yielding: %s %s %s %s",
str(block),
str(piece),
path,
str(size),
)
yield block, piece, path, size
else:
hashed = self.hasher(path, plength)
if len(hashed.layer_hashes) == 1:
block = hashed.root
piece = roothash
size = length
yield block, piece, path, size
else:
for i in range(amount):
start = i * SHA256
end = start + SHA256
piece = pieces[start:end]
try:
block = hashed.piece_layer[start:end]
except IndexError: # pragma: nocover
block = sha256(bytearray(size)).digest()
size = plength if plength < length else length
length -= size
logger.debug(
"Yielding: %s, %s, %s, %s",
str(block),
str(piece),
str(path),
str(size),
)
yield block, piece, path, size
__init__(self, checker, hasher=None)
special
Source code in torrentfile\recheck.py
def __init__(self, checker, hasher=None):
"""Construct a HybridChecker instance."""
self.checker = checker
self.paths = checker.paths
self.hasher = hasher
self.piece_length = checker.piece_length
self.fileinfo = checker.fileinfo
self.piece_layers = checker.meta["piece layers"]
self.piece_count = 0
self.it = None
logger.debug(
"Starting Hash Checker. piece length: %s",
humanize_bytes(self.piece_length),
)
__iter__(self)
special
Source code in torrentfile\recheck.py
def __iter__(self):
"""Assign iterator and return self."""
self.it = self.iter_paths()
return self
__next__(self)
special
Source code in torrentfile\recheck.py
def __next__(self):
"""Provide the result of comparison."""
try:
value = next(self.it)
return value
except StopIteration as stopiter:
raise StopIteration() from stopiter
iter_paths(self)
Source code in torrentfile\recheck.py
def iter_paths(self):
"""Iterate through and compare root file hashes to .torrent file.
Yields
------
results : `tuple`
The size of the file and result of match.
"""
for i, path in enumerate(self.paths):
info = self.fileinfo[i]
length, plength = info["length"], self.piece_length
logger.debug("%s length: %s", path, str(length))
roothash = info["pieces root"]
logger.debug("%s root hash %s", path, str(roothash))
if roothash in self.piece_layers:
pieces = self.piece_layers[roothash]
else:
pieces = roothash
amount = len(pieces) // SHA256
if not os.path.exists(path):
for i in range(amount):
start = i * SHA256
end = start + SHA256
piece = pieces[start:end]
if length > plength:
size = plength
else:
size = length
length -= size
block = sha256(bytearray(size)).digest()
logging.debug(
"Yielding: %s %s %s %s",
str(block),
str(piece),
path,
str(size),
)
yield block, piece, path, size
else:
hashed = self.hasher(path, plength)
if len(hashed.layer_hashes) == 1:
block = hashed.root
piece = roothash
size = length
yield block, piece, path, size
else:
for i in range(amount):
start = i * SHA256
end = start + SHA256
piece = pieces[start:end]
try:
block = hashed.piece_layer[start:end]
except IndexError: # pragma: nocover
block = sha256(bytearray(size)).digest()
size = plength if plength < length else length
length -= size
logger.debug(
"Yielding: %s, %s, %s, %s",
str(block),
str(piece),
str(path),
str(size),
)
yield block, piece, path, size
Interactive
Module
torrentfile.
interactive
Module contains the procedures used for Interactive Mode.
Functions
program_Options
gather program behaviour Options.
InteractiveEditor
— Interactive dialog class for torrent editing.InteractiveCreator
— Class namespace for interactive program options.
create_torrent
(
)
— Create new torrent file interactively.edit_action
(
)
— Edit the editable values of the torrent meta file.get_input
(
*args
)
(`str`) — Determine appropriate input function to call.recheck_torrent
(
)
— Check torrent download completed percentage.select_action
(
)
— Operate TorrentFile program interactively through terminal.showcenter
(
txt
)
— Prints text to screen in the center position of the terminal.showtext
(
txt
)
— Print contents of txt to screen.
torrentfile.interactive
InteractiveCreator
Source code in torrentfile\interactive.py
class InteractiveCreator:
"""Class namespace for interactive program options.
Attributes
----------
_piece_length : int
_comment : str
_source : str
_url_list : list
_path : str
_outfile : str
_announce : str
"""
def __init__(self):
"""Initialize interactive meta file creator dialog."""
self.kwargs = {
"announce": None,
"url_list": None,
"private": None,
"source": None,
"comment": None,
"piece_length": None,
"outfile": None,
"path": None,
}
self.outfile, self.meta = self.get_props()
def get_props(self):
"""Gather details for torrentfile from user."""
piece_length = get_input(
"Piece Length (empty=auto): ", lambda x: x.isdigit()
)
self.kwargs["piece_length"] = piece_length
announce = get_input(
"Tracker list (empty): ", lambda x: isinstance(x, str)
)
if announce:
self.kwargs["announce"] = announce.split()
url_list = get_input(
"Web Seed list (empty): ", lambda x: isinstance(x, str)
)
if url_list:
self.kwargs["url_list"] = url_list.split()
comment = get_input("Comment (empty): ", None)
if comment:
self.kwargs["comment"] = comment
source = get_input("Source (empty): ", None)
if source:
self.kwargs["source"] = source
private = get_input(
"Private Torrent? {Y/N}: (N)", lambda x: x in "yYnN"
)
if private and private.lower() == "y":
self.kwargs["private"] = 1
contents = get_input("Content Path: ", os.path.exists)
self.kwargs["path"] = contents
outfile = get_input(
f"Output Path ({contents}.torrent): ",
lambda x: os.path.exists(os.path.dirname(x)),
)
if outfile:
self.kwargs["outfile"] = outfile
meta_version = get_input(
"Meta Version {1,2,3}: (1)", lambda x: x in "123"
)
showcenter(f"creating {outfile}")
if meta_version == "3":
torrent = TorrentFileHybrid(**self.kwargs)
elif meta_version == "2":
torrent = TorrentFileV2(**self.kwargs)
else:
torrent = TorrentFile(**self.kwargs)
return torrent.write()
__init__(self)
special
Source code in torrentfile\interactive.py
def __init__(self):
"""Initialize interactive meta file creator dialog."""
self.kwargs = {
"announce": None,
"url_list": None,
"private": None,
"source": None,
"comment": None,
"piece_length": None,
"outfile": None,
"path": None,
}
self.outfile, self.meta = self.get_props()
get_props(self)
Source code in torrentfile\interactive.py
def get_props(self):
"""Gather details for torrentfile from user."""
piece_length = get_input(
"Piece Length (empty=auto): ", lambda x: x.isdigit()
)
self.kwargs["piece_length"] = piece_length
announce = get_input(
"Tracker list (empty): ", lambda x: isinstance(x, str)
)
if announce:
self.kwargs["announce"] = announce.split()
url_list = get_input(
"Web Seed list (empty): ", lambda x: isinstance(x, str)
)
if url_list:
self.kwargs["url_list"] = url_list.split()
comment = get_input("Comment (empty): ", None)
if comment:
self.kwargs["comment"] = comment
source = get_input("Source (empty): ", None)
if source:
self.kwargs["source"] = source
private = get_input(
"Private Torrent? {Y/N}: (N)", lambda x: x in "yYnN"
)
if private and private.lower() == "y":
self.kwargs["private"] = 1
contents = get_input("Content Path: ", os.path.exists)
self.kwargs["path"] = contents
outfile = get_input(
f"Output Path ({contents}.torrent): ",
lambda x: os.path.exists(os.path.dirname(x)),
)
if outfile:
self.kwargs["outfile"] = outfile
meta_version = get_input(
"Meta Version {1,2,3}: (1)", lambda x: x in "123"
)
showcenter(f"creating {outfile}")
if meta_version == "3":
torrent = TorrentFileHybrid(**self.kwargs)
elif meta_version == "2":
torrent = TorrentFileV2(**self.kwargs)
else:
torrent = TorrentFile(**self.kwargs)
return torrent.write()
InteractiveEditor
Source code in torrentfile\interactive.py
class InteractiveEditor:
"""Interactive dialog class for torrent editing."""
def __init__(self, metafile):
"""
Initialize the Interactive torrent editor guide.
Parameters
----------
metafile : `str`
user input string identifying the path to a torrent meta file.
"""
self.metafile = metafile
self.meta = pyben.load(metafile)
self.info = self.meta["info"]
self.args = {
"url-list": self.meta.get("url-list", None),
"announce": self.meta.get("announce-list", None),
"source": self.info.get("source", None),
"private": self.info.get("private", None),
"comment": self.info.get("comment", None),
}
def show_current(self):
"""Display the current met file information to screen."""
out = "Current properties and values:\n"
longest = max([len(label) for label in self.args]) + 3
for key, val in self.args.items():
txt = (key.title() + ":").ljust(longest) + str(val)
out += f"\t{txt}\n"
showtext(out)
def sanatize_response(self, key, response):
"""
Convert the input data into a form recognizable by the program.
Parameters
----------
key : `str`
name of the property and attribute being eddited.
response : `str`
User input value the property is being edited to.
"""
if key in ["announce", "url-list"]:
val = response.split()
else:
val = response
self.args[key] = val
def edit_props(self):
"""Loop continuosly for edits until user signals DONE."""
while True:
showcenter(
"Choose the number for a propert the needs editing."
"Enter DONE when all editing has been completed."
)
props = {
1: "comment",
2: "source",
3: "private",
4: "tracker",
5: "web-seed",
}
args = {
1: "comment",
2: "source",
3: "private",
4: "announce",
5: "url-list",
}
txt = ", ".join((str(k) + ": " + v) for k, v in props.items())
prop = get_input(txt)
if prop.lower() == "done":
break
if prop.isdigit() and 0 < int(prop) < 6:
key = props[int(prop)]
key2 = args[int(prop)]
val = self.args.get(key2)
showtext(
"Enter new property value or leave empty for no value."
)
response = get_input(f"{key.title()} ({val}): ")
self.sanatize_response(key2, response)
else:
showtext("Invalid input: Try again.")
edit_torrent(self.metafile, self.args)
__init__(self, metafile)
special
Source code in torrentfile\interactive.py
def __init__(self, metafile):
"""
Initialize the Interactive torrent editor guide.
Parameters
----------
metafile : `str`
user input string identifying the path to a torrent meta file.
"""
self.metafile = metafile
self.meta = pyben.load(metafile)
self.info = self.meta["info"]
self.args = {
"url-list": self.meta.get("url-list", None),
"announce": self.meta.get("announce-list", None),
"source": self.info.get("source", None),
"private": self.info.get("private", None),
"comment": self.info.get("comment", None),
}
edit_props(self)
Source code in torrentfile\interactive.py
def edit_props(self):
"""Loop continuosly for edits until user signals DONE."""
while True:
showcenter(
"Choose the number for a propert the needs editing."
"Enter DONE when all editing has been completed."
)
props = {
1: "comment",
2: "source",
3: "private",
4: "tracker",
5: "web-seed",
}
args = {
1: "comment",
2: "source",
3: "private",
4: "announce",
5: "url-list",
}
txt = ", ".join((str(k) + ": " + v) for k, v in props.items())
prop = get_input(txt)
if prop.lower() == "done":
break
if prop.isdigit() and 0 < int(prop) < 6:
key = props[int(prop)]
key2 = args[int(prop)]
val = self.args.get(key2)
showtext(
"Enter new property value or leave empty for no value."
)
response = get_input(f"{key.title()} ({val}): ")
self.sanatize_response(key2, response)
else:
showtext("Invalid input: Try again.")
edit_torrent(self.metafile, self.args)
sanatize_response(self, key, response)
Source code in torrentfile\interactive.py
def sanatize_response(self, key, response):
"""
Convert the input data into a form recognizable by the program.
Parameters
----------
key : `str`
name of the property and attribute being eddited.
response : `str`
User input value the property is being edited to.
"""
if key in ["announce", "url-list"]:
val = response.split()
else:
val = response
self.args[key] = val
show_current(self)
Source code in torrentfile\interactive.py
def show_current(self):
"""Display the current met file information to screen."""
out = "Current properties and values:\n"
longest = max([len(label) for label in self.args]) + 3
for key, val in self.args.items():
txt = (key.title() + ":").ljust(longest) + str(val)
out += f"\t{txt}\n"
showtext(out)
create_torrent()
Source code in torrentfile\interactive.py
def create_torrent():
"""Create new torrent file interactively."""
showcenter("Create Torrent")
showtext(
"\nEnter values for each of the options for the torrent creator, "
"or leave blank for program defaults.\nSpaces are considered item "
"seperators for options that accept a list of values.\nValues "
"enclosed in () indicate the default value, while {} holds all "
"valid choices available for the option.\n\n"
)
creator = InteractiveCreator()
return creator
edit_action()
Source code in torrentfile\interactive.py
def edit_action():
"""Edit the editable values of the torrent meta file."""
showcenter("Edit Torrent")
metafile = get_input("Metafile(.torrent): ", os.path.exists)
dialog = InteractiveEditor(metafile)
dialog.show_current()
dialog.edit_props()
get_input(*args)
Source code in torrentfile\interactive.py
def get_input(*args): # pragma: no cover
"""
Determine appropriate input function to call.
Parameters
----------
args : `tuple`
Arbitrary number of args to pass to next function
Returns
-------
`str`
The results of the function call.
"""
if len(args) == 2:
return _get_input_loop(*args)
return _get_input(*args)
recheck_torrent()
Source code in torrentfile\interactive.py
def recheck_torrent():
"""Check torrent download completed percentage."""
showcenter("Check Torrent")
msg = (
"Enter absolute or relative path to torrent file content, and the "
"corresponding torrent metafile."
)
showtext(msg)
metafile = get_input(
"Conent Path (downloads/complete/torrentname):", os.path.exists
)
contents = get_input("Metafile (*.torrent): ", os.path.exists)
checker = Checker(metafile, contents)
results = checker.results()
showtext(f"Completion for {metafile} is {results}%")
return results
select_action()
Source code in torrentfile\interactive.py
def select_action():
"""Operate TorrentFile program interactively through terminal."""
showcenter("TorrentFile: Starting Interactive Mode")
action = get_input(
"Enter the action you wish to perform.\n"
"Action (Create | Edit | Recheck): "
)
if action.lower() == "create":
return create_torrent()
if "check" in action.lower():
return recheck_torrent()
return edit_action()
showcenter(txt)
Source code in torrentfile\interactive.py
def showcenter(txt):
"""
Prints text to screen in the center position of the terminal.
Parameters
----------
txt : `str`
the preformated message to send to stdout.
"""
termlen = shutil.get_terminal_size().columns
padding = " " * int(((termlen - len(txt)) / 2))
string = "".join(["\n", padding, txt, "\n"])
showtext(string)
showtext(txt)
Source code in torrentfile\interactive.py
def showtext(txt):
"""
Print contents of txt to screen.
Parameters
----------
txt : `str`
text to print to terminal.
"""
sys.stdout.write(txt)
Utils
Module
torrentfile.
utils
Utility functions and classes used throughout package.
Functions: get_piece_length: calculate ideal piece length for torrent file. sortfiles: traverse directory in sorted order yielding paths encountered. path_size: Sum the sizes of each file in path. get_file_list: Return list of all files contained in directory. path_stat: Get ideal piece length, total size, and file list for directory. path_piece_length: Get ideal piece length based on size of directory.
MissingPathError
— Path parameter is required to specify target content.PieceLengthValueError
— Piece Length parameter must equal a perfect power of 2.
filelist_total
(
pathstring
)
(`os.PathLike`) — Perform error checking and format conversion to os.PathLike.get_file_list
(
path
)
(filelist : `list`) — Return a sorted list of file paths contained in directory.get_piece_length
(
size
)
(piece_length : `int`) — Calculate the ideal piece length for bittorrent data.humanize_bytes
(
amount
)
(`str` :) — Convert integer into human readable memory sized denomination.normalize_piece_length
(
piece_length
)
(piece_length : `int`) — Verify input piece_length is valid and convert accordingly.path_piece_length
(
path
)
(piece_length : `int`) — Calculate piece length for input path and contents.path_size
(
path
)
(size : `int`) — Return the total size of all files in path recursively.path_stat
(
path
)
(filelist : `list`) — Calculate directory statistics.
torrentfile.utils
MissingPathError (Exception)
Source code in torrentfile\utils.py
class MissingPathError(Exception):
"""Path parameter is required to specify target content.
Creating a .torrent file with no contents seems rather silly.
Parameters
----------
message : `any`
Message for user (optional).
"""
def __init__(self, message=None):
"""Raise when creating a meta file without specifying target content.
The `message` argument is a message to pass to Exception base class.
"""
self.message = f"Path arguement is missing and required {str(message)}"
super().__init__(message)
__init__(self, message=None)
special
Source code in torrentfile\utils.py
def __init__(self, message=None):
"""Raise when creating a meta file without specifying target content.
The `message` argument is a message to pass to Exception base class.
"""
self.message = f"Path arguement is missing and required {str(message)}"
super().__init__(message)
PieceLengthValueError (Exception)
Source code in torrentfile\utils.py
class PieceLengthValueError(Exception):
"""Piece Length parameter must equal a perfect power of 2.
Parameters
----------
message : `any`
Message for user (optional).
"""
def __init__(self, message=None):
"""Raise when creating a meta file with incorrect piece length value.
The `message` argument is a message to pass to Exception base class.
"""
self.message = f"Incorrect value for piece length: {str(message)}"
super().__init__(message)
__init__(self, message=None)
special
Source code in torrentfile\utils.py
def __init__(self, message=None):
"""Raise when creating a meta file with incorrect piece length value.
The `message` argument is a message to pass to Exception base class.
"""
self.message = f"Incorrect value for piece length: {str(message)}"
super().__init__(message)
filelist_total(pathstring)
Source code in torrentfile\utils.py
def filelist_total(pathstring):
"""Perform error checking and format conversion to os.PathLike.
Parameters
----------
pathstring : `str`
An existing filesystem path.
Returns
-------
`os.PathLike`
Input path converted to bytes format.
Raises
------
MissingPathError
File could not be found.
"""
if os.path.exists(pathstring):
path = Path(pathstring)
return _filelist_total(path)
raise MissingPathError
get_file_list(path)
Source code in torrentfile\utils.py
def get_file_list(path):
"""Return a sorted list of file paths contained in directory.
Parameters
----------
path : `str`
target file or directory.
Returns
-------
filelist : `list`
sorted list of file paths.
"""
_, filelist = filelist_total(path)
return filelist
get_piece_length(size)
Source code in torrentfile\utils.py
def get_piece_length(size: int) -> int:
"""Calculate the ideal piece length for bittorrent data.
Parameters
----------
size : `int`
Total bits of all files incluided in .torrent file.
Returns
-------
piece_length : `int`
Ideal peace length size arguement.
"""
exp = 14
while size / (2 ** exp) > 200 and exp < 25:
exp += 1
return 2 ** exp
humanize_bytes(amount)
Source code in torrentfile\utils.py
def humanize_bytes(amount):
"""Convert integer into human readable memory sized denomination.
Parameters
----------
amount : `int`
total number of bytes.
Returns
-------
`str` :
human readable representation of the given amount of bytes.
"""
if amount < 1024:
return str(amount)
if 1024 <= amount < 1_048_576:
return f"{amount // 1024} KiB"
if 1_048_576 <= amount < 1_073_741_824:
return f"{amount // 1_048_576} MiB"
return f"{amount // 1073741824} GiB"
normalize_piece_length(piece_length)
Source code in torrentfile\utils.py
def normalize_piece_length(piece_length) -> int:
"""Verify input piece_length is valid and convert accordingly.
Parameters
----------
piece_length : `int` | `str`
The piece length provided by user.
Returns
-------
piece_length : `int`
normalized piece length.
Raises
------
PieceLengthValueError :
If piece length is improper value.
"""
if isinstance(piece_length, str):
if piece_length.isnumeric():
piece_length = int(piece_length)
else:
raise PieceLengthValueError(piece_length)
if 13 < piece_length < 26:
return 2 ** piece_length
if piece_length <= 13:
raise PieceLengthValueError(piece_length)
log = int(math.log2(piece_length))
if 2 ** log == piece_length:
return piece_length
raise PieceLengthValueError
path_piece_length(path)
Source code in torrentfile\utils.py
def path_piece_length(path):
"""Calculate piece length for input path and contents.
Parameters
----------
path : `str`
The absolute path to directory and contents.
Returns
-------
piece_length : `int`
The size of pieces of torrent content.
"""
psize = path_size(path)
return get_piece_length(psize)
path_size(path)
Source code in torrentfile\utils.py
def path_size(path):
"""Return the total size of all files in path recursively.
Parameters
----------
path : `str`
path to target file or directory.
Returns
-------
size : `int`
total size of files.
"""
total_size, _ = filelist_total(path)
return total_size
path_stat(path)
Source code in torrentfile\utils.py
def path_stat(path):
"""Calculate directory statistics.
Parameters
----------
path : `str`
The path to start calculating from.
Returns
-------
filelist : `list`
List of all files contained in Directory
size : `int`
Total sum of bytes from all contents of dir
piece_length : `int`
The size of pieces of the torrent contents.
"""
total_size, filelist = filelist_total(path)
piece_length = get_piece_length(total_size)
return (filelist, total_size, piece_length)