TorrentFile API Documentation

CLI Module

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.

Classes
  • HelpFormat Formatting class for help tips provided by the CLI.
Functions
  • 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.

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

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 value files 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 name

    length: 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.

Classes
  • 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

module
torrentfile.hasher

Piece/File Hashers for Bittorrent meta file contents.

Classes
  • 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.
Functions
  • 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

module
torrentfile.edit

Edit torrent meta file.

Functions
  • 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

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.

Classes
  • 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

module
torrentfile.interactive

Module contains the procedures used for Interactive Mode.

Functions

program_Options gather program behaviour Options.

Classes
  • InteractiveEditor Interactive dialog class for torrent editing.
  • InteractiveCreator Class namespace for interactive program options.
Functions
  • 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

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.

Classes
  • MissingPathError Path parameter is required to specify target content.
  • PieceLengthValueError Piece Length parameter must equal a perfect power of 2.
Functions
  • 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)