Rev 132 | Go to most recent revision | Blame | Compare with Previous | Last modification | View Log | RSS feed
#!/usr/bin/env python# Copyright: Ira W. Snyder (devel@irasnyder.com)# Start Date: 2005-10-13# End Date:# License: GNU General Public License v2 (or at your option, any later version)## Changelog Follows:# - 2005-10-13# - Added get_par2_filenames() to parse par2 files# - Added the parset object to represent each parset.## - 2005-10-14# - Finished the parset object. It will now verify and extract parsets.# - Small changes to the parset object. This makes the parjoin part# much more reliable.# - Added the OptionParser to make this nice to run at the command line.# - Made recursiveness an option.# - Made start directory an option.# - Check for appropriate programs before starting.################################################################################## REQUIREMENTS:## This code requires the programs cfv, par2repair, lxsplit, and rar to be able# to function properly. I will attempt to check that these are in your path.################################################################################################################################################################# Global Variables################################################################################WORK_DIR = '~/downloads/usenet'################################################################################################################################################################# The PAR2 Parser## This was stolen from cfv (see http://cfv.sourceforge.net/ for a copy)################################################################################import struct, errno# We always want to do crc checksdocrcchecks = Truedef chompnulls(line):p = line.find('\0')if p < 0: return lineelse: return line[:p]def get_par2_filenames(filename):"""Get all of the filenames that are protected by the par2file given as the filename"""try:file = open(filename, 'rb')except:print 'Could not open %s' % (filename, )return []pkt_header_fmt = '< 8s Q 16s 16s 16s'pkt_header_size = struct.calcsize(pkt_header_fmt)file_pkt_fmt = '< 16s 16s 16s Q'file_pkt_size = struct.calcsize(file_pkt_fmt)main_pkt_fmt = '< Q I'main_pkt_size = struct.calcsize(main_pkt_fmt)seen_file_ids = {}expected_file_ids = Nonefilenames = []while 1:d = file.read(pkt_header_size)if not d:breakmagic, pkt_len, pkt_md5, set_id, pkt_type = struct.unpack(pkt_header_fmt, d)if docrcchecks:import md5control_md5 = md5.new()control_md5.update(d[0x20:])d = file.read(pkt_len - pkt_header_size)control_md5.update(d)if control_md5.digest() != pkt_md5:raise EnvironmentError, (errno.EINVAL, \"corrupt par2 file - bad packet hash")if pkt_type == 'PAR 2.0\0FileDesc':if not docrcchecks:d = file.read(pkt_len - pkt_header_size)file_id, file_md5, file_md5_16k, file_size = \struct.unpack(file_pkt_fmt, d[:file_pkt_size])if seen_file_ids.get(file_id) is None:seen_file_ids[file_id] = 1filename = chompnulls(d[file_pkt_size:])filenames.append(filename)elif pkt_type == "PAR 2.0\0Main\0\0\0\0":if not docrcchecks:d = file.read(pkt_len - pkt_header_size)if expected_file_ids is None:expected_file_ids = []slice_size, num_files = struct.unpack(main_pkt_fmt, d[:main_pkt_size])num_nonrecovery = (len(d)-main_pkt_size)/16 - num_filesfor i in range(main_pkt_size,main_pkt_size+(num_files+num_nonrecovery)*16,16):expected_file_ids.append(d[i:i+16])else:if not docrcchecks:file.seek(pkt_len - pkt_header_size, 1)if expected_file_ids is None:raise EnvironmentError, (errno.EINVAL, \"corrupt or unsupported par2 file - no main packet found")for id in expected_file_ids:if not seen_file_ids.has_key(id):raise EnvironmentError, (errno.EINVAL, \"corrupt or unsupported par2 file - " \"expected file description packet not found")return filenames################################################################################# The parset object## This is an object based representation of a parset, and will verify itself# and extract itself, if possible.################################################################################import os, globclass parset:def __init__(self, par_filename):self.parfile = par_filenameself.extra_pars = []self.files = Falseself.used_parjoin = Falseself.verified = Falseself.extracted = Falsedef get_filenames(self):return get_par2_filenames(parfile)def all_there(self):"""Check if all the files for the parset are present.This will help us decide which par2 checker to use first"""for f in self.files:if not os.path.isfile(f):return False# The files were all therereturn Truedef verify(self):"""This will verify the parset by the most efficient method first,and then move to a slower method if that one fails"""retval = False #not verified yet# if all the files are there, try verifying fastif self.all_there():retval = self.__fast_verify()if retval == False:# Failed to verify fast, so try it slow, maybe it needs repairretval = self.__slow_verify()# If we've got a video file, maybe we should try to parjoin itelif self.__has_video_file():retval = self.__parjoin()else: #not all there, maybe we can slow-repairretval = self.__slow_verify()self.verified = retvalreturn self.verifieddef __fast_verify(self):retval = os.system('cfv -v -f "%s"' % (self.parfile, ))if retval == 0:return True #successreturn False #failuredef __slow_verify(self):retval = os.system('par2repair "%s"' % (self.parfile, ))if retval == 0:return True #successreturn False #failuredef __parjoin(self):retval = os.system('lxsplit -j "%s.001"' % (self.files[0], ))retval = self.__fast_verify()if retval == False:# Failed to verify fast, so try it slow, maybe it needs repairretval = self.__slow_verify()if retval == False: # failed to verify, so remove the lxsplit created fileos.remove(self.files[0])self.used_parjoin = retvalself.verified = retvalreturn self.verifieddef __has_video_file(self):for f in self.files:if os.path.splitext(f)[1] in ('.avi', '.ogm', '.mkv'):return Truereturn Falsedef __remove_currentset(self):"""Remove all of the files that are extractable, as well as the pars.Leave everything else alone"""if not self.extracted:print 'Did not extract yet, not removing currentset'return# remove the main paros.remove(self.parfile)# remove all of the extra parsfor i in self.extra_pars:os.remove(i)# remove any rars that are associated (leave EVERYTHING else)for i in self.files:if i[-3:] == 'rar':os.remove(i)# remove any .0?? files (from parjoin)if self.used_parjoin:for i in os.listdir(os.getcwd()):if i != self.files[0] and self.files[0] in i:os.remove(i)# remove any temp repair filesfor i in glob.glob('*.1'):os.remove(i)def __get_extract_file(self):"""Find the first extractable file"""for i in self.files:if os.path.splitext(i)[1] == '.rar':return ireturn Nonedef extract(self):"""Attempt to extract all of the files related to this parset"""if not self.verified:self.extracted = Falseprint 'Not (successfully) verified, not extracting'return False #failed to extractextract_file = self.__get_extract_file()if extract_file != None:retval = os.system('rar e -o+ "%s"' % (extract_file, ))if retval != 0:print 'Failed to extract'self.extracted = Falsereturn self.extracted# we extracted ok, so remove the currentsetself.extracted = Trueself.__remove_currentset()return self.extracted################################################################################# The rarslave program itself################################################################################import os, sys, globfrom optparse import OptionParserdef check_required_progs():"""Check if the required programs are installed"""shell_not_found = 32512needed = []if os.system('cfv --help > /dev/null 2>&1') == shell_not_found:needed.append('cfv')if os.system('par2repair --help > /dev/null 2>&1') == shell_not_found:needed.append('par2repair')if os.system('lxsplit --help > /dev/null 2>&1') == shell_not_found:needed.append('lxpsplit')if os.system('rar --help > /dev/null 2>&1') == shell_not_found:needed.append('rar')if needed:for n in needed:print 'Needed program "%s" not found in $PATH' % (n, )sys.exit(1)def get_parsets():"""Get a representation of each parset in the current directory, andreturn them as a list of parset instances"""par2files = glob.glob('*.par2')par2files += glob.glob('*.PAR2')parsets = []for i in par2files:filenames = get_par2_filenames(i)create_new = True# if we already have an instance for this set, append# this par file to the extra_pars fieldfor j in parsets:if j.files == filenames:j.extra_pars.append(i)create_new = False# we haven't seen this set yet, so we'll create it nowif create_new == True:cur = parset(i)cur.files = filenamesparsets.append(cur)return parsetsdef directory_worker(dir):"""Attempts to find, verify, and extract every parset in the directorygiven as a parameter"""cwd = os.getcwd()os.chdir(dir)parsets = get_parsets()# Verify each parsetfor p in parsets:p.verify()# Attempt to extract each parsetfor p in parsets:p.extract()os.chdir(cwd)def main():# Build the OptionParserparser = OptionParser()parser.add_option('-n', '--not-recursive', action='store_false', dest='recursive',default=True, help="don't run recursively")parser.add_option('-d', '--start-dir', dest='work_dir', default=WORK_DIR,help='start running at DIR', metavar='DIR')# Parse the given options(options, args) = parser.parse_args()# Fix up the working directoryoptions.work_dir = os.path.abspath(os.path.expanduser(options.work_dir))# Check that we have the required programs installedcheck_required_progs()# Run rarslave!if options.recursive:for root, dirs, files in os.walk(options.work_dir):directory_worker(root)else:directory_worker(options.work_dir)if __name__ == '__main__':main()