Advertisement
Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- #!/usr/bin/python3
- # Torrentverify
- # Copyright (c) 2015-2016 Wintermute0110 <wintermute0110@gmail.com>
- #
- # Permission is hereby granted, free of charge, to any person obtaining a copy
- # of this software and associated documentation files (the "Software"), to deal
- # in the Software without restriction, including without limitation the rights
- # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- # copies of the Software, and to permit persons to whom the Software is
- # furnished to do so, subject to the following conditions:
- #
- # The above copyright notice and this permission notice shall be included in
- # all copies or substantial portions of the Software.
- #
- # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
- # THE SOFTWARE.
- #
- # Bencoder code based on Bencodepy by Eric Weast (c) 2014
- # Licensed under the GPL v2
- # https://github.com/eweast/BencodePy/commits/master
- import io
- import sys
- import os
- import hashlib
- import argparse
- import shutil
- from collections import OrderedDict
- # --- Global variables
- __software_version = '0.1.0';
- # --- Program options (from command line)
- __prog_options_override_torrent_dir = 0
- __prog_options_deleteWrongSizeFiles = 0
- __prog_options_truncateWrongSizeFiles = 0
- __prog_options_deleteUnneeded = 0
- # Unified torrent information object. Works for torrent files with 1 or several
- # files.
- class Torrent:
- torrent_file = None
- dir_name = None
- piece_length = 0
- num_pieces = 0
- num_files = 0
- file_name_list = []
- file_length_list = []
- pieces_hash_list = []
- pieces_file_list = []
- # --- Get size of terminal ---
- # shutil.get_terminal_size() only available in Python 3.3
- # https://docs.python.org/3/library/shutil.html#querying-the-size-of-the-output-terminal
- # print(sys.version_info)
- if sys.version_info < (3, 3, 0):
- # Disable long text-line chopping
- __cols = -1
- print('[NOTE] Your Python version is lower than 3.3.0. Terminal size cannot be determined.')
- print('[NOTE] Chopping of long text lines disabled.')
- else:
- __cols, __lines = shutil.get_terminal_size()
- # print('{0} cols and {1} lines'.format(__cols, __lines))
- # --- Bdecoder ----------------------------------------------------------------
- class DecodingError(Exception):
- def __init__(self, msg):
- self.msg = msg
- def __str__(self):
- return repr(self.msg)
- class Decoder:
- def __init__(self, data: bytes):
- self.data = data
- self.idx = 0
- def __read(self, i: int) -> bytes:
- """Returns a set number (i) of bytes from self.data."""
- b = self.data[self.idx: self.idx + i]
- self.idx += i
- if len(b) != i:
- raise DecodingError(
- "Incorrect byte length returned between indexes of {0} and {1}. Possible unexpected End of File."
- .format(str(self.idx), str(self.idx - i)))
- return b
- def __read_to(self, terminator: bytes) -> bytes:
- """Returns bytes from self.data starting at index (self.idx) until terminator character."""
- try:
- # noinspection PyTypeChecker
- i = self.data.index(terminator, self.idx)
- b = self.data[self.idx:i]
- self.idx = i + 1
- return b
- except ValueError:
- raise DecodingError(
- 'Unable to locate terminator character "{0}" after index {1}.'.format(str(terminator), str(self.idx)))
- def __parse(self) -> object:
- """Selects the appropriate method to decode next bencode element and returns the result."""
- char = self.data[self.idx: self.idx + 1]
- if char in [b'1', b'2', b'3', b'4', b'5', b'6', b'7', b'8', b'9', b'0']:
- str_len = int(self.__read_to(b':'))
- return self.__read(str_len)
- elif char == b'i':
- self.idx += 1
- return int(self.__read_to(b'e'))
- elif char == b'd':
- return self.__parse_dict()
- elif char == b'l':
- return self.__parse_list()
- elif char == b'':
- raise DecodingError('Unexpected End of File at index position of {0}.'.format(str(self.idx)))
- else:
- raise DecodingError('Invalid token character ({0}) at position {1}.'.format(str(char), str(self.idx)))
- def decode(self):
- """Start of decode process. Returns final results."""
- if self.data[0:1] not in (b'd', b'l'):
- return self.__wrap_with_tuple()
- return self.__parse()
- def __wrap_with_tuple(self) -> tuple:
- """Returns a tuple of all nested bencode elements."""
- l = list()
- length = len(self.data)
- while self.idx < length:
- l.append(self.__parse())
- return tuple(l)
- def __parse_dict(self) -> OrderedDict:
- """Returns an Ordered Dictionary of nested bencode elements."""
- self.idx += 1
- d = OrderedDict()
- key_name = None
- while self.data[self.idx: self.idx + 1] != b'e':
- if key_name is None:
- key_name = self.__parse()
- else:
- d[key_name] = self.__parse()
- key_name = None
- self.idx += 1
- return d
- def __parse_list(self) -> list:
- """Returns an list of nested bencode elements."""
- self.idx += 1
- l = []
- while self.data[self.idx: self.idx + 1] != b'e':
- l.append(self.__parse())
- self.idx += 1
- return l
- # --- Functions ---------------------------------------------------------------
- def query_yes_no_all(question, default="no"):
- """Ask a yes/no question via raw_input() and return their answer.
- "question" is a string that is presented to the user.
- "default" is the presumed answer if the user just hits <Enter>.
- It must be "yes" (the default), "no" or None (meaning
- an answer is required of the user).
- The "answer" return value is True for "yes" or False for "no".
- """
- valid = {"yes": 1, "y": 1, "ye": 1,
- "no": 0, "n": 0,
- "all": -1, "a": -1}
- if default is None:
- prompt = " [y/n/a] "
- elif default == "yes":
- prompt = " [Y/n/a] "
- elif default == "no":
- prompt = " [y/N/a] "
- elif default == "all":
- prompt = " [y/n/A] "
- else:
- raise ValueError("invalid default answer: '%s'" % default)
- while True:
- sys.stdout.write(question + prompt)
- choice = input().lower()
- if default is not None and choice == '':
- return valid[default]
- elif choice in valid:
- return valid[choice]
- else:
- sys.stdout.write("Please respond with 'yes', 'no' or 'all'"
- " (or 'y' or 'n' or 'a').\n")
- def confirm_file_action(action_str, result_str, force_delete):
- delete_file = 0
- if force_delete:
- delete_file = 1
- else:
- result = query_yes_no_all('{0} this file?'.format(action_str))
- if result == 1:
- delete_file = 1
- print('File {0}'.format(result_str))
- elif result == 0:
- delete_file = 0
- print('File not deleted')
- elif result == -1:
- delete_file = 1
- force_delete = True
- print('File {0}'.format(result_str))
- else:
- print('Logic error')
- return (delete_file, force_delete)
- # If max_length == -1 it means size of terminal could not be determined. Do
- # nothing witht the string.
- def limit_string_lentgh(string, max_length):
- if max_length > 1 and len(string) > max_length:
- string = (string[:max_length-1] + '*');
- return string
- # Convert a list of bytes into a path
- def join_file_byte_list(file_list_bytes):
- file_list_string = []
- for i in range(len(file_list_bytes)):
- file_list_string.append(file_list_bytes[i].decode("utf-8"))
- return '/'.join(file_list_string)
- # Returns a Torrent object with torrent metadata
- __debug_torrent_extract_metadata = 0
- def extract_torrent_metadata(filename):
- torrent = Torrent
- torrent.torrent_file = filename
- sys.stdout.write('Bdecoding torrent file {0}... '.format(torrentFileName))
- sys.stdout.flush()
- torrent_file = open(torrentFileName, "rb")
- # Use internal Bdecoder class
- decoder = Decoder(torrent_file.read())
- torr_ordered_dict = decoder.decode()
- info_ordered_dict = torr_ordered_dict[b'info']
- sys.stdout.write('done\n')
- if __debug_torrent_extract_metadata:
- print('=== Dumping torrent root ===')
- for key in torr_ordered_dict:
- print(' key {0} value {1}'.format(key, torr_ordered_dict[key]))
- print('=== Dumping torrent info ===')
- for key in info_ordered_dict:
- print(' key {0} value {1}'.format(key, info_ordered_dict[key]))
- # If torrent info has files field then torrent has several files
- if b'files' in info_ordered_dict:
- t_name = info_ordered_dict[b'name'] # Directory name to store torrent
- t_piece_length = info_ordered_dict[b'piece length']
- t_files_list = info_ordered_dict[b'files']
- # --- Converts the string into a file-like object
- t_pieces = info_ordered_dict[b'pieces']
- pieces = io.BytesIO(t_pieces)
- # --- Ensure num_pieces is integer
- num_pieces = len(t_pieces) / 20
- if not num_pieces.is_integer():
- print('num_pieces {0} is not integer!'.format(num_pieces))
- sys.exit(1)
- num_pieces = int(num_pieces)
- # --- Fill torrent object
- torrent.dir_name = t_name.decode("utf-8")
- torrent.piece_length = t_piece_length
- torrent.num_pieces = num_pieces
- torrent.num_files = len(t_files_list)
- for i in range(num_pieces):
- hash = pieces.read(20)
- torrent.pieces_hash_list.append(hash)
- torrent.total_bytes = 0
- for t_file in t_files_list:
- torrent.file_name_list.append(join_file_byte_list(t_file[b'path']))
- # print(type(t_file[b'length'])) # type is <class 'int'>
- torrent.file_length_list.append(t_file[b'length'])
- torrent.total_bytes += t_file[b'length']
- # DEBUG
- if __debug_torrent_extract_metadata:
- print(' Directory {0}'.format(t_name))
- print(' Piece length {0}'.format(t_piece_length))
- print(' Number of pieces {0}'.format(num_pieces))
- print(' Number of files {0}'.format(len(t_files_list)))
- print(' len(t_pieces) = {0}'.format(len(t_pieces)))
- print(' num_pieces * piece_length = {0}'.format(num_pieces * t_piece_length))
- print(' len(torrent.pieces_hash_list) = {0}'.format(len(torrent.pieces_hash_list)))
- # Single file torrent
- else:
- t_name = info_ordered_dict[b'name'] # File name11
- t_piece_length = info_ordered_dict[b'piece length']
- t_length = info_ordered_dict[b'length']
- # --- Converts the string into a file-like object
- t_pieces = info_ordered_dict[b'pieces']
- pieces = io.BytesIO(t_pieces)
- # --- Ensure num_pieces is integer
- num_pieces = len(t_pieces) / 20
- if not num_pieces.is_integer():
- print('num_pieces {0} is not integer!'.format(num_pieces))
- sys.exit(1)
- num_pieces = int(num_pieces)
- # --- Fill torrent object
- torrent.piece_length = t_piece_length
- torrent.num_pieces = num_pieces
- torrent.num_files = 1
- torrent.file_name_list.append(t_name)
- torrent.file_length_list.append(t_length)
- for i in range(num_pieces):
- hash = pieces.read(20)
- torrent.pieces_hash_list.append(hash)
- torrent.total_bytes = t_length
- # DEBUG
- if __debug_torrent_extract_metadata:
- print(' Filename {0}'.format(t_name))
- print(' Size {0}'.format(t_length))
- print(' Piece length {0}'.format(t_piece_length))
- print(' Number of pieces {0}'.format(num_pieces))
- print(' Number of files {0}'.format(1))
- print(' len(t_pieces) = {0}'.format(len(t_pieces)))
- print(' num_pieces * piece_length = {0}'.format(num_pieces * t_piece_length))
- print(' len(torrent.pieces_hash_list) = {0}'.format(len(torrent.pieces_hash_list)))
- # Make a list of files for each piece. Should include also the file offsets.
- # This is to find torrent that has padded files that must be trimmend.
- # Many Linux torrent clients have this bug in ext4 filesystems.
- # [ [{'file_idx': 0, 'start_offset': 1234, 'end_offset': 5678},
- # { ... } ],
- # [ ... ],
- # ...
- # ]
- piece_length = torrent.piece_length
- pieces_file_list = []
- piece_current_length = 0
- this_piece_files_list = []
- for i in range(torrent.num_files):
- file_dict = {}
- file_dict['file_idx'] = i
- file_dict['start_offset'] = 0
- file_size = file_current_size = torrent.file_length_list[i]
- while True:
- remaining_piece_bytes = piece_length - piece_current_length
- if file_current_size > remaining_piece_bytes:
- piece_current_length += remaining_piece_bytes
- file_current_size -= remaining_piece_bytes
- else:
- piece_current_length += file_current_size
- file_current_size = 0
- # Go for next file if no more bytes
- if file_current_size == 0:
- file_dict['end_offset'] = file_size
- this_piece_files_list.append(file_dict)
- break
- # Piece is ready, add to the list
- file_dict['end_offset'] = file_size - file_current_size
- this_piece_files_list.append(file_dict)
- pieces_file_list.append(this_piece_files_list)
- # Reset piece files list and size
- piece_current_length = 0
- this_piece_files_list = []
- # Add current file to piece files list
- file_dict = {}
- file_dict['file_idx'] = i
- file_dict['start_offset'] = file_size - file_current_size
- # Last piece
- if piece_current_length > 0:
- pieces_file_list.append(this_piece_files_list)
- # Put in torrent object
- torrent.pieces_file_list = pieces_file_list
- # DEBUG: print list of files per piece
- if __debug_torrent_extract_metadata:
- for piece_idx in range(len(pieces_file_list)):
- print('Piece {0:06d}'.format(piece_idx))
- this_piece_files_list = pieces_file_list[piece_idx]
- for file_idx in range(len(this_piece_files_list)):
- file_dict = this_piece_files_list[file_idx]
- print(' File {0:06d} start {1:8d} end {2:8d}'
- .format(file_dict['file_idx'], file_dict['start_offset'], file_dict['end_offset']))
- return torrent
- def list_torrent_contents(torrent):
- print('Printing torrent file contents...')
- # --- Print list of files
- text_size = 7 + 17 + 1
- for i in range(len(torrent.file_name_list)):
- print('{2}'.format(i+1, torrent.file_length_list[i], \
- limit_string_lentgh(torrent.file_name_list[i], __cols -text_size)))
- return 0
- # Checks that files listed in the torrent file exist, and that file size
- # is correct
- # Status can be: OK, MISSING, BAD_SIZE
- def check_torrent_files_only(torrent):
- print('Checking torrent files and sizes (NOT hash)')
- ret_value = 0
- num_files_OK = 0
- num_files_bigger_size = 0
- num_files_smaller_size = 0
- num_files_missing = 0
- force_delete = False
- force_truncate = False
- num_deleted_files = 0
- num_truncated_files = 0
- print(' F# Status Actual Bytes Torrent Bytes File name')
- print('------ -------- ---------------- ---------------- --------------')
- for i in range(len(torrent.file_name_list)):
- file_size = -1
- filename_path = os.path.join(torrent.dir_data, torrent.file_name_list[i])
- # print(filename_path)
- file_exists = os.path.isfile(filename_path)
- if file_exists:
- file_size = os.path.getsize(filename_path)
- if file_size == torrent.file_length_list[i]:
- status = 'OK'
- num_files_OK += 1
- else:
- ret_value = 1
- status = 'BAD_SIZE'
- if file_size > torrent.file_length_list[i]:
- num_files_bigger_size += 1
- else:
- num_files_smaller_size += 1
- else:
- ret_value = 1
- status = 'MISSING'
- num_files_missing += 1
- # --- Print file info
- text_size = 7+9+17+17+1
- print('{0:6} {1:>8} {2:16,} {3:16,} {4}'
- .format(i+1, status, file_size, torrent.file_length_list[i],
- limit_string_lentgh(torrent.file_name_list[i], __cols -text_size)))
- # --- Delete wrong size files (mutually exclusive with truncate)
- if status == 'BAD_SIZE' and file_size != torrent.file_length_list[i]:
- if __prog_options_deleteWrongSizeFiles:
- print('RM {0}'.format(filename_path))
- # This option is very dangerous if user writes the wrong directory
- # Always confirm with user
- delete_file, force_delete = confirm_file_action('Delete', 'deleted', force_delete)
- if delete_file:
- os.unlink(torrent.file_length_list[i])
- num_deleted_files += 1
- # --- Truncate bigger size files
- if status == 'BAD_SIZE' and file_size > torrent.file_length_list[i]:
- if __prog_options_truncateWrongSizeFiles:
- print('TRUNCATE {0}'.format(filename_path))
- # This option is very dangerous if user writes the wrong directory
- # Always confirm with user
- truncate_file, force_truncate = confirm_file_action('Truncate', 'truncated', force_truncate)
- if truncate_file:
- # w+ mode truncates the file, but the file if filled with zeros!
- # According to Python docs, r+ is for both read and writing, should work.
- fo = open(filename_path, "r+b")
- fo.truncate(torrent.file_length_list[i])
- fo.close()
- num_truncated_files += 1
- # --- Print torrent metadata
- print('')
- print('Torrent file : {0}'.format(torrent.torrent_file))
- print('Pieces info : {0:10,} pieces, {1:16,} bytes/piece'.format(torrent.num_pieces, torrent.piece_length))
- print('Files info : {0:10,} files, {1:16,} total bytes'.format(torrent.num_files, torrent.total_bytes))
- print('Torrent directory : {0}'.format(torrent.dir_name))
- print('Download directory : {0}'.format(torrent.dir_download))
- print('Data directory : {0}'.format(torrent.dir_data))
- print('Files OK : {0:,}'.format(num_files_OK))
- print('Files w big size : {0:,}'.format(num_files_bigger_size))
- print('Files w small size : {0:,}'.format(num_files_smaller_size))
- print('Files missing : {0:,}'.format(num_files_missing))
- if __prog_options_deleteWrongSizeFiles:
- print('Deleted files : {0:,}'.format(num_deleted_files))
- if __prog_options_truncateWrongSizeFiles:
- print('Truncated files : {0:,}'.format(num_truncated_files))
- if num_files_bigger_size > 0 and (num_files_smaller_size == 0 or num_files_missing == 0):
- print("""WARNING
- Found files with bigger size than it should be.
- Run torrentverify with --check and --truncateWrongSizeFiles parameters
- to correct the problems.""")
- elif num_files_smaller_size > 0 or num_files_missing > 0:
- print("""WARNING
- Found files with smaller size than it should be or there are missing files.
- It is likely there is some problem with this Torrent. Check with your Torrent
- client and download the torrent again.""")
- return ret_value
- # Lists torrent unneeded files
- def check_torrent_unneeded_files(torrent):
- print('Checking torrent unneeded files')
- ret_value = 0
- # --- Make a recursive list of files in torrent data directory
- torrent_directory = os.path.join(torrent.dir_data)
- file_list = []
- for root, dirs, files in os.walk(torrent_directory, topdown=False):
- for name in files:
- file_list.append(os.path.join(root, name))
- # --- Make a set of files in the list of torrent metadata files
- print(' Status File name')
- print('-------- ---------------------------------------')
- torrent_file_list = []
- for i in range(len(torrent.file_name_list)):
- filename_path = os.path.join(torrent.dir_data, torrent.file_name_list[i])
- torrent_file_list.append(filename_path);
- torrent_file_set = set(torrent_file_list)
- # Check number of elements in list and set are the same. This means there are no
- # duplicate files in the list
- if len(torrent_file_list) != len(torrent_file_set):
- print('len(torrent_file_list) != len(torrent_file_set)')
- exit(1)
- # --- Check if files in the torrent directory are on the metadata file set
- num_needed = 0
- num_redundant = 0
- num_deleted_files = 0
- force_delete = False
- text_size = 10
- for i in range(len(file_list)):
- if file_list[i] not in torrent_file_set:
- print('UNNEEDED {0}'.format(limit_string_lentgh(file_list[i], __cols -text_size)))
- ret_value = 1
- num_redundant += 1
- # --- Deleted unneeded file
- if __prog_options_deleteUnneeded:
- print(' RM {0}'.format(limit_string_lentgh(file_list[i], __cols -text_size)))
- # This option is very dangerous if user writes the wrong directory
- # Always confirm with user
- delete_file, force_delete = confirm_file_action('Delete', 'deleted', force_delete)
- if delete_file:
- os.unlink(file_list[i])
- num_deleted_files += 1
- else:
- print(' OK {0}'.format(limit_string_lentgh(file_list[i], __cols -text_size)))
- num_needed += 1
- # --- Print torrent metadata
- print('')
- print('Torrent file : {0}'.format(torrent.torrent_file))
- print('Pieces info : {0:10,} pieces, {1:16,} bytes/piece'.format(torrent.num_pieces, torrent.piece_length))
- print('Files info : {0:10,} files, {1:16,} total bytes'.format(torrent.num_files, torrent.total_bytes))
- print('Torrent directory : {0}'.format(torrent.dir_name))
- print('Download directory : {0}'.format(torrent.dir_download))
- print('Data directory : {0}'.format(torrent.dir_data))
- print('Files in data directory : {0:,}'.format(len(file_list)))
- print('Needed files : {0:,}'.format(num_needed))
- print('Unneeded files : {0:,}'.format(num_redundant))
- if __prog_options_deleteUnneeded:
- print('Deleted files : {0:,}'.format(num_deleted_files))
- if num_redundant > 0:
- print("""WARNING
- Found unneeded files in the torrent download directory.
- Run torrentverify with --checkUnneeded and --deleteUnneeded parameters
- to correct the problems.""")
- return ret_value
- # This naive piece generator only works if files have correct size and exist
- # on filesystem
- def pieces_generator_naive(torrent):
- """Yield pieces from download file(s)."""
- piece_length = torrent.piece_length
- # yield pieces from a multi-file torrent
- # Iterator finishes when function exits but not with the yield keyword
- if torrent.num_files > 1:
- piece = b''
- file_idx_list = []
- # --- Iterate through all files
- # print('{0:6d}'.format(pieces_counter))
- for i in range(len(torrent.file_name_list)):
- path = os.path.join(torrent.dir_data, torrent.file_name_list[i])
- # --- Read file
- sfile = open(path, "rb")
- file_idx_list.append(i)
- while True:
- piece += sfile.read(piece_length-len(piece))
- if len(piece) != piece_length:
- sfile.close()
- break
- yield (piece, file_idx_list)
- # --- Go for another piece
- piece = b''
- file_idx_list = []
- file_idx_list.append(i)
- if piece != b'':
- # print('yielding (last?) piece')
- yield (piece, file_idx_list)
- # yield pieces from a single file torrent
- else:
- path = info['name']
- print(path)
- sfile = open(path.decode('UTF-8'), "rb")
- while True:
- piece = sfile.read(piece_length)
- if not piece:
- sfile.close()
- return
- yield piece
- # This piece generator returns zeros if file does not exists. Also,
- # if files are padded at the end does not return that padding. This is to
- # mimic KTorrent behaviour: files will pass the SHA checksum of the torrent
- # but some files will have bigger sizes that need to be truncated.
- def pieces_generator(torrent, pieces_list=None):
- piece_length = torrent.piece_length
- pieces_range = range(torrent.num_pieces)
- if pieces_list != None:
- pieces_range = pieces_list
- for piece_idx in pieces_range:
- # Get list of files for this piece
- this_piece_files_list = torrent.pieces_file_list[piece_idx]
- # Iterate through files and make piece
- piece = b''
- file_idx_list = []
- for file_idx in range(len(this_piece_files_list)):
- # Get file info
- file_dict = this_piece_files_list[file_idx]
- file_name = torrent.file_name_list[file_dict['file_idx']]
- file_start = file_dict['start_offset']
- file_end = file_dict['end_offset']
- file_correct_size = torrent.file_length_list[file_dict['file_idx']]
- file_idx_list.append(file_dict['file_idx'])
- # Read file
- path = os.path.join(torrent.dir_data, file_name)
- file_exists = os.path.isfile(path)
- if file_exists:
- file_size = os.path.getsize(path)
- if file_size == file_correct_size:
- # If downloaded file has correct size then read whithin the file
- # limits. Maybe the whole file if file is smaller than the piece size
- sfile = open(path, "rb")
- sfile.seek(file_start)
- piece += sfile.read(file_end - file_start)
- sfile.close()
- elif file_size < file_correct_size:
- # If downloaded file has less size then pad with zeros.
- # To simplify things, treat file as if it doesn't exist.
- # Consequently, SHA1 check will fail.
- piece += bytearray(file_end - file_start)
- else:
- # If downloaded file has more size then truncate file read. Note that
- # SHA1 check may succed, but file will have an incorrect bigger size
- # that must be truncated later.
- sfile = open(path, "rb")
- sfile.seek(file_start)
- piece += sfile.read(file_end - file_start)
- sfile.close()
- else:
- # If file does not exists at all, just pad with zeros
- piece += bytearray(file_end - file_start)
- # Yield piece
- yield (piece, file_idx_list, piece_idx)
- # Checks torrent files against SHA1 hash for integrity
- def check_torrent_files_hash(torrent):
- ret_value = 0
- print('piece# file# HStatus FStatus Actual Bytes Torrent Bytes File name')
- print('------ ------ -------- -------- ---------------- ---------------- --------------')
- num_files_OK_list = []
- num_files_bigger_size_list = []
- num_files_smaller_size_list = []
- num_files_missing_list = []
- piece_counter = 0
- good_pieces = 0
- bad_pieces = 0
- for piece, file_idx_list, piece_index in pieces_generator(torrent):
- # --- Compare piece hash with expected hash
- piece_hash = hashlib.sha1(piece).digest()
- if piece_hash != torrent.pieces_hash_list[piece_index]:
- hash_status = 'BAD_SHA'
- bad_pieces += 1
- ret_value = 1
- else:
- hash_status = 'GOOD_SHA'
- good_pieces += 1
- # --- Print information
- for i in range(len(file_idx_list)):
- file_idx = file_idx_list[i]
- path = os.path.join(torrent.dir_data, torrent.file_name_list[file_idx])
- file_exists = os.path.isfile(path)
- if file_exists:
- file_size = os.path.getsize(path)
- if file_size == torrent.file_length_list[file_idx]:
- file_status = 'OK'
- num_files_OK_list.append(file_idx)
- else:
- file_status = 'BAD_SIZE'
- ret_value = 1
- if file_size > torrent.file_length_list[file_idx]:
- num_files_bigger_size_list.append(file_idx)
- else:
- num_files_smaller_size_list.append(file_idx)
- else:
- file_size = -1
- file_status = 'MISSING'
- ret_value = 1
- num_files_missing_list.append(file_idx)
- # --- Print odd/even pieces with different colors
- text_size = 7+7+9+9+17+17+1
- if piece_index % 2:
- print('{0:06d} {1:6} {2:>8} {3:>8} {4:16,} {5:16,} {6}'
- .format(piece_index+1, file_idx+1, hash_status, file_status,
- file_size, torrent.file_length_list[file_idx],
- limit_string_lentgh(torrent.file_name_list[file_idx], __cols -text_size)))
- else:
- print('\033[0;97m{0:06d} {1:6} {2:>8} {3:>8} {4:16,} {5:16,} {6}\033[0m'
- .format(piece_index+1, file_idx+1, hash_status, file_status,
- file_size, torrent.file_length_list[file_idx],
- limit_string_lentgh(torrent.file_name_list[file_idx], __cols -text_size)))
- # --- Increment piece counter
- piece_counter += 1
- # --- Make lists set to avoid duplicates
- num_files_OK_set = set(num_files_OK_list)
- num_files_bigger_size_set = set(num_files_bigger_size_list)
- num_files_smaller_size_set = set(num_files_smaller_size_list)
- num_files_missing_set = set(num_files_missing_list)
- # --- Print torrent metadata
- print('')
- print('Torrent file : {0}'.format(torrent.torrent_file))
- print('Pieces info : {0:10,} pieces, {1:16,} bytes/piece'.format(torrent.num_pieces, torrent.piece_length))
- print('Files info : {0:10,} files, {1:16,} total bytes'.format(torrent.num_files, torrent.total_bytes))
- print('Torrent directory : {0}'.format(torrent.dir_name))
- print('Download directory : {0}'.format(torrent.dir_download))
- print('Data directory : {0}'.format(torrent.dir_data))
- print('Files OK : {0:12,}'.format(len(num_files_OK_set)))
- print('Files w big size : {0:12,}'.format(len(num_files_bigger_size_set)))
- print('Files w small size : {0:12,}'.format(len(num_files_smaller_size_set)))
- print('Files missing : {0:12,}'.format(len(num_files_missing_set)))
- print('# of pieces checked : {0:12,}'.format(piece_counter))
- print('Good pieces : {0:12,}'.format(good_pieces))
- print('Bad pieces : {0:12,}'.format(bad_pieces))
- if bad_pieces == 0 and len(num_files_bigger_size_set):
- print("""WARNING
- Downloaded files pass SHA check but some files are bigger than they should be.
- Run torrentverify with --check and --truncateWrongSizeFiles parameters to correct the
- problems and then run torrentverify with --checkHash parameter only to make sure
- problems are solved.""")
- return ret_value
- # Checks single file against SHA1 hash for integrity
- __debug_file_location_in_torrent = 0
- def check_torrent_files_single_hash(torrent, fileName):
- ret_value = 0
- # Remove torrent download directory from path
- dir_data = torrent.dir_data
- fileName_search = fileName.replace(dir_data, '');
- fileName_search = fileName_search.strip('/')
- if __debug_file_location_in_torrent:
- print('dir_data {0}'.format(dir_data))
- print('fileName {0}'.format(fileName))
- print('fileName_search {0}'.format(fileName_search))
- # Locate which pieces of the torrent this file spans
- pieces_list = []
- for piece_idx in range(torrent.num_pieces):
- if __debug_file_location_in_torrent:
- print('Piece {0:6}'.format(piece_idx))
- this_piece_files_list = torrent.pieces_file_list[piece_idx]
- for file_idx in range(len(this_piece_files_list)):
- # Get file info
- cfile_dict = this_piece_files_list[file_idx]
- file_name = torrent.file_name_list[cfile_dict['file_idx']]
- if file_name == fileName_search:
- if __debug_file_location_in_torrent:
- print(' MATCHED {0}'.format(file_name))
- pieces_list.append(piece_idx)
- else:
- if __debug_file_location_in_torrent:
- print('UNMATCHED {0}'.format(file_name))
- # DEBUG info
- print('File {0}'.format(fileName))
- print('Internal name {0}'.format(fileName_search))
- print('File spans {0} pieces'.format(len(pieces_list)))
- if __debug_file_location_in_torrent:
- print('List of pieces')
- for i in range(len(pieces_list)):
- print(' #{0:6}'.format(pieces_list[i]))
- if len(pieces_list) < 1:
- print('ERROR File not found in torrent list of files. Exiting.')
- sys.exit(1)
- # --- Check pieces in list only
- print('piece# file# HStatus FStatus Actual Bytes Torrent Bytes File name')
- print('------ ------ -------- -------- ---------------- ---------------- --------------')
- piece_counter = 0
- good_pieces = 0
- bad_pieces = 0
- for piece, file_idx_list, piece_index in pieces_generator(torrent, pieces_list):
- # --- Compare piece hash with expected hash
- piece_hash = hashlib.sha1(piece).digest()
- if piece_hash != torrent.pieces_hash_list[piece_index]:
- hash_status = 'BAD_SHA'
- bad_pieces += 1
- ret_value = 1
- else:
- hash_status = 'GOOD_SHA'
- good_pieces += 1
- # --- Print information
- for i in range(len(file_idx_list)):
- file_idx = file_idx_list[i]
- path = os.path.join(torrent.dir_data, torrent.file_name_list[file_idx])
- file_exists = os.path.isfile(path)
- if file_exists:
- file_size = os.path.getsize(path)
- if file_size == torrent.file_length_list[file_idx]:
- file_status = 'OK'
- else:
- file_status = 'BAD_SIZE'
- ret_value = 1
- else:
- file_size = -1
- file_status = 'MISSING'
- ret_value = 1
- # --- Print odd/even pieces with different colors
- text_size = 7+7+9+9+17+17+1
- if piece_index % 2:
- print('{0:06d} {1:6} {2:>8} {3:>8} {4:16,} {5:16,} {6}'
- .format(piece_index+1, file_idx+1, hash_status, file_status,
- file_size, torrent.file_length_list[file_idx],
- limit_string_lentgh(torrent.file_name_list[file_idx], __cols -text_size)))
- else:
- print('\033[0;97m{0:06d} {1:6} {2:>8} {3:>8} {4:16,} {5:16,} {6}\033[0m'
- .format(piece_index+1, file_idx+1, hash_status, file_status,
- file_size, torrent.file_length_list[file_idx],
- limit_string_lentgh(torrent.file_name_list[file_idx], __cols -text_size)))
- # --- Increment piece counter
- piece_counter += 1
- # --- Print torrent metadata
- print('')
- print('Torrent file : {0}'.format(torrent.torrent_file))
- print('Pieces info : {0:10,} pieces, {1:16,} bytes/piece'.format(torrent.num_pieces, torrent.piece_length))
- print('Files info : {0:10,} files, {1:16,} total bytes'.format(torrent.num_files, torrent.total_bytes))
- print('Torrent directory : {0}'.format(torrent.dir_name))
- print('Download directory : {0}'.format(torrent.dir_download))
- print('Data directory : {0}'.format(torrent.dir_data))
- print('# of pieces checked : {0:12,}'.format(piece_counter))
- print('Good pieces : {0:12,}'.format(good_pieces))
- print('Bad pieces : {0:12,}'.format(bad_pieces))
- return ret_value
- def do_printHelp():
- print("""\033[32mUsage: torrentverify.py -t file.torrent [-d /download_dir/] [options]\033[0m
- If only the torrent file is input with -t file.torrent then torrent file contents
- are listed but no other action is performed.
- \033[32mOptions:
- \033[35m-t\033[0m \033[31mfile.torrent\033[0m Torrent filename.
- \033[35m-d\033[0m \033[31m/download_dir/\033[0m Directory where torrent is downloaded.
- \033[35m--otd\033[0m Override torrent data directory.
- \033[35m--check\033[0m Quick check of downloaded files.
- \033[35m--truncateWrongSizeFiles\033[0m Truncate files whose size is bigger than it should be.
- \033[35m--deleteWrongSizeFiles\033[0m Delete files whose size is incorrect.
- \033[35m--checkUnneeded\033[0m Finds unneeded files in data directory.
- \033[35m--deleteUnneeded\033[0m Deletes unneeded files in the data directory.
- \033[35m--checkHash\033[0m Checks Torrent data using SHA1 hash.
- \033[35m--checkFile\033[0m \033[31mfile\033[0m Checks a single downloaded file against the SHA1 checksum.""")
- # -----------------------------------------------------------------------------
- # main function
- #
- # Program returns
- # 0 everything OK
- # 1 error found with torrent files
- # 2 error in program arguments or no arguments given
- # 3 torrent file does not found
- # 4 data directory not found
- # -----------------------------------------------------------------------------
- print('\033[36mTorrentVerify\033[0m' + ' version ' + __software_version)
- # --- Command line parser
- p = argparse.ArgumentParser()
- p.add_argument('-t', help="Torrent file", nargs = 1)
- p.add_argument("-d", help="Data directory", nargs = 1)
- p.add_argument("--otd", help="Override torrent directory", action="store_true")
- g = p.add_mutually_exclusive_group()
- g.add_argument("--check", help="Do a basic torrent check: files there or not and size", action="store_true")
- g.add_argument("--checkUnneeded", help="Write me", action="store_true")
- g.add_argument("--checkHash", help="Full check with SHA1 hash", action="store_true")
- g.add_argument("--checkFile", help="Check single file with SHA1 hash", nargs = 1)
- d = p.add_mutually_exclusive_group()
- d.add_argument("--deleteWrongSizeFiles", help="Delete files having wrong size", action="store_true")
- d.add_argument("--truncateWrongSizeFiles", help="Chop files with incorrect size to right one", action="store_true")
- p.add_argument("--deleteUnneeded", help="Write me", action="store_true")
- args = p.parse_args();
- # --- Read arguments
- torrentFileName = data_directory = None
- check = checkUnneeded = checkHash = 0
- if args.t:
- torrentFileName = args.t[0];
- if args.d:
- data_directory = args.d[0];
- # Optional arguments
- if args.otd:
- __prog_options_override_torrent_dir = 1
- if args.deleteWrongSizeFiles:
- __prog_options_deleteWrongSizeFiles = 1
- if args.truncateWrongSizeFiles:
- __prog_options_truncateWrongSizeFiles = 1
- if args.deleteUnneeded:
- __prog_options_deleteUnneeded = 1
- # --- Extrant torrent metadata
- if not torrentFileName:
- do_printHelp()
- sys.exit(2)
- if (args.check or args.checkUnneeded or args.checkHash or args.checkFile) \
- and data_directory == None:
- do_printHelp()
- sys.exit(2)
- # --- Check for torrent file existence
- if not os.path.isfile(torrentFileName):
- print('Torrent file not found: {0}'.format(torrentFileName))
- sys.exit(3)
- # --- Read torrent file metadata
- torrent_obj = extract_torrent_metadata(torrentFileName)
- # --- Get torrent data directory and check it exists
- if data_directory != None:
- torrent_obj.dir_download = data_directory
- # User wants to override torrent data directory
- if __prog_options_override_torrent_dir:
- torrent_obj.dir_data = torrent_obj.dir_download
- # Normal mode of operation
- else:
- torrent_obj.dir_data = os.path.join(data_directory, torrent_obj.dir_name)
- # Check that data directory exists
- if not os.path.isdir(torrent_obj.dir_data):
- print('Data directory not found: {0}'.format(torrent_obj.dir_data))
- exit(4)
- # --- Decide what to do based on arguments
- ret_value = 0
- if args.check:
- ret_value = check_torrent_files_only(torrent_obj)
- elif args.checkUnneeded:
- ret_value = check_torrent_unneeded_files(torrent_obj)
- elif args.checkHash:
- ret_value = check_torrent_files_hash(torrent_obj)
- elif args.checkFile:
- ret_value = check_torrent_files_single_hash(torrent_obj, args.checkFile[0])
- else:
- ret_value = list_torrent_contents(torrent_obj)
- sys.exit(ret_value)
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement