X-Git-Url: https://www.irasnyder.com/gitweb/?p=rarslave2.git;a=blobdiff_plain;f=rarslave.py;h=20c028840d3f38f43fb57475301a3e3b9aaf760e;hp=0d258836b1007dbedea4dd2ed4815825de11779f;hb=HEAD;hpb=25c6aa2221fa7294e253ca540aa1eb58f447459d diff --git a/rarslave.py b/rarslave.py old mode 100644 new mode 100755 index 0d25883..20c0288 --- a/rarslave.py +++ b/rarslave.py @@ -1,685 +1,370 @@ #!/usr/bin/env python -# vim: set ts=4 sts=4 sw=4 textwidth=112 : +# vim: set ts=4 sts=4 sw=4 textwidth=80: + +""" +The main program of the rarslave project + +This handles all of the commandline and configuration file work, then tries to +repair, extract, and delete any PAR2Sets that it finds. +""" + +__author__ = "Ira W. Snyder (devel@irasnyder.com)" +__copyright__ = "Copyright (c) 2006-2008 Ira W. Snyder (devel@irasnyder.com)" +__license__ = "GNU GPL v2 (or, at your option, any later version)" + +# rarslave.py -- a usenet autorepair and autoextract utility +# +# Copyright (C) 2006-2008 Ira W. Snyder (devel@irasnyder.com) +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -VERSION="2.0.0" -PROGRAM="rarslave2" +VERSION = "2.1.0" +PROGRAM = "rarslave" -import re, os, sys, optparse -import Par2Parser -import RarslaveConfig -import RarslaveLogger +import os, sys, optparse, logging, ConfigParser +from subprocess import CalledProcessError +import PAR2Set -# Global Variables -(TYPE_OLDRAR, TYPE_NEWRAR, TYPE_ZIP, TYPE_NOEXTRACT, TYPE_UNKNOWN) = range (5) -(SUCCESS, ECHECK, EEXTRACT, EDELETE) = range(4) -config = RarslaveConfig.RarslaveConfig() -logger = RarslaveLogger.RarslaveLogger () +################################################################################ -# Global options to be set / used later. -options = None +# A simple-ish configuration class +class RarslaveConfig(object): -class RarslaveExtractor (object): + DEFAULT_CONFIG_FILE = PAR2Set.utils.absolutePath( + os.path.join('~', '.config', 'rarslave', 'rarslave.conf')) - # Instance Variables - # ========================================================================== - # dir -- The directory in which this set lives - # p2files -- All PAR2 files in this set - # name_matched_files -- Files in this set, matched by name only - # prot_matched_files -- Files in this set, matched by parsing PAR2 files only - # type -- This set's type - # heads -- The heads to be extracted + def __init__(self, fileName=DEFAULT_CONFIG_FILE): - def __init__ (self, dir, p2files, name_files, prot_files): + # Make sure that the fileName is in absolute form + self.fileName = os.path.abspath(os.path.expanduser(fileName)) - self.dir = dir - self.p2files = p2files - self.name_matched_files = name_files - self.prot_matched_files = prot_files + # Open it with ConfigParser + self.config = ConfigParser.SafeConfigParser() + self.config.read(fileName) - # Find the type - self.type = self.__find_type () + # Setup the default dictionary + self.defaults = dict() - logger.addMessage ('Detected set of type: %s' % self, RarslaveLogger.MessageType.Debug) + # Add all of the defaults + self.add_default('directories', 'start', + os.path.join('~', 'downloads'), + PAR2Set.utils.absolutePath) + self.add_default('options', 'recursive', True, self.toBool) + self.add_default('options', 'interactive', False, self.toBool) + self.add_default('options', 'verbosity', 0, self.toInt) + self.add_default('options', 'delete', True, self.toBool) - # Find the heads - self.heads = self.__find_heads () + # Add a new default value + def add_default(self, section, key, value, typeConverter): - for h in self.heads: - logger.addMessage ('Adding extraction head: %s' % h, RarslaveLogger.MessageType.Debug) + self.defaults[(section, key)] = (value, typeConverter) - def __repr__ (self): - return \ - { TYPE_OLDRAR : 'Old RAR', - TYPE_NEWRAR : 'New RAR', - TYPE_ZIP : 'Zip', - TYPE_NOEXTRACT : 'No Extract', - TYPE_UNKNOWN : 'Unknown' } [self.type] + # Get the default value + def get_default(self, section, key): - def __find_type (self): + (value, typeConverter) = self.defaults[(section, key)] + return value - all_files = no_duplicates (self.name_matched_files + self.prot_matched_files) + # Coerce the value from a string into the correct type + def coerceValue(self, section, key, value): - if self.is_oldrar (all_files): - return TYPE_OLDRAR - elif self.is_newrar (all_files): - return TYPE_NEWRAR - elif self.is_zip (all_files): - return TYPE_ZIP - elif self.is_noextract (all_files): - return TYPE_NOEXTRACT + (defaultValue, typeConverter) = self.defaults[(section, key)] - return TYPE_UNKNOWN + # Try the coercion, error and exit if there is a problem + try: + return typeConverter(value) + except: + sys.stderr.write('Unable to parse configuration file\n') + sys.stderr.write('-> at section: %s\n' % section) + sys.stderr.write('-> at key: %s\n' % key) + sys.exit(2) - def __generic_find_heads (self, regex, ignorecase=True): + # Return the value + def get(self, section, key): - heads = [] + try: + # Get the user-provided value + value = self.config.get(section, key) + except: + # Oops, they didn't provide it, use the default + # NOTE: if you get an exception here, check your code ;) + value = self.defaults[(section, key)] - if ignorecase: - cregex = re.compile (regex, re.IGNORECASE) - else: - cregex = re.compile (regex) + # Try to evaluate some safe things, for convenience + return self.coerceValue(section, key, value) - all_files = no_duplicates (self.name_matched_files + self.prot_matched_files) + # Convert a string to an int (any base) + def toInt(s): + return int(s, 0) - for f in all_files: - if cregex.match (f): - heads.append (f) + # Mark it static + toInt = staticmethod(toInt) - return heads + # Convert a string to a bool + def toBool(s): + if s in ['t', 'T', 'True', 'true', 'yes', '1']: + return True - def __find_heads (self): + if s in ['f', 'F', 'False', 'false', 'no', '0']: + return False - if self.type == TYPE_OLDRAR: - return self.__generic_find_heads ('^.*\.rar$') - elif self.type == TYPE_NEWRAR: - return self.__generic_find_heads ('^.*\.part0*1\.rar$') - elif self.type == TYPE_ZIP: - return self.__generic_find_heads ('^.*\.zip$') - elif self.type == TYPE_NOEXTRACT: - return self.prot_matched_files + raise ValueError - return [] + # Mark it static + toBool = staticmethod(toBool) - def __create_directory (self, dir): - if dir == None: - return SUCCESS +################################################################################ - if os.path.isdir (dir): - return SUCCESS +# Global configuration, read from default configuration file +config = RarslaveConfig() - try: - os.makedirs (dir) - logger.addMessage ('Created directory: %s' % dir, RarslaveLogger.MessageType.Verbose) - except OSError: - logger.addMessage ('FAILED to create directory: %s' % dir, RarslaveLogger.MessageType.Fatal) - return -EEXTRACT +################################################################################ - return SUCCESS +# A tiny class to hold logging output until we're finished +class DelayedLogger (object): - def runExtract (self, todir=None): - # Extract all heads of this set + """A small class to hold logging output until the program is finished running. + It emulates sys.stdout in the needed ways for the logging module.""" - # Extract to the head's dir if we don't care where to extract - if todir == None: - todir = self.dir + def __init__ (self, output=sys.stdout.write): + self.__messages = [] + self.__output = output - # Create the directory $todir if it doesn't exist - ret = self.__create_directory (todir) + def write (self, msg): + self.__messages.append (msg) - if ret != SUCCESS: - return -EEXTRACT + def flush (self): + pass - # Extract all heads - extraction_func = \ - { TYPE_OLDRAR : self.__extract_rar, - TYPE_NEWRAR : self.__extract_rar, - TYPE_ZIP : self.__extract_zip, - TYPE_NOEXTRACT : self.__extract_noextract, - TYPE_UNKNOWN : self.__extract_unknown }[self.type] + def size (self): + """Returns the number of messages queued for printing""" + return len (self.__messages) - # Call the extraction function on each head - for h in self.heads: - full_head = full_abspath (h) - ret = extraction_func (full_head, todir) - logger.addMessage ('Extraction Function returned: %d' % ret, RarslaveLogger.MessageType.Debug) + def close (self): + """Print all messages, clear the queue""" + map(self.__output, self.__messages) + self.__messages = [] - # Check error code - if ret != SUCCESS: - logger.addMessage ('Failed extracting: %s' % h, RarslaveLogger.MessageType.Fatal) - return -EEXTRACT +################################################################################ - return SUCCESS +# Convert from the verbose command line option to the logging level that +# will be used by the logging class to print messages +def findLogLevel(options): - def __extract_rar (self, file, todir): - assert os.path.isfile (file) - assert os.path.isdir (todir) + level = options.verbose - options.quiet - RAR_CMD = config.get_value ('commands', 'unrar') + if level < -3: + level = -3 - cmd = '%s \"%s\"' % (RAR_CMD, file) - ret = run_command (cmd, todir) + if level > 1: + level = 1 - # Check error code - if ret != 0: - return -EEXTRACT + LEVELS = { + 1 : logging.DEBUG, + 0 : logging.INFO, + -1 : logging.WARNING, + -2 : logging.ERROR, + -3 : logging.CRITICAL + } - return SUCCESS + return LEVELS[level] - def __extract_zip (self, file, todir): - ZIP_CMD = config.get_value ('commands', 'unzip') +################################################################################ - cmd = ZIP_CMD % (file, todir) - ret = run_command (cmd) +def parseCommandLineOptions(): - # Check error code - if ret != 0: - return -EEXTRACT + # Build the OptionParser + parser = optparse.OptionParser() + parser.add_option('-n', '--not-recursive', dest='recursive', action='store_false', + default=config.get('options', 'recursive'), + help="Don't run recursively") - return SUCCESS + parser.add_option('-d', '--directory', dest='directory', type='string', + default=config.get('directories', 'start'), + help="Start working at DIR", metavar='DIR') - def __extract_noextract (self, file, todir): - # Just move this file to the $todir, since no extraction is needed - # FIXME: NOTE: mv will fail by itself if you're moving to the same dir! - NOEXTRACT_CMD = config.get_value ('commands', 'noextract') + parser.add_option('-i', '--interactive', dest='interactive', action='store_true', + default=config.get('options', 'interactive'), + help="Confirm before removing files") - # Make sure that both files are not the same file. If they are, don't run at all. - if os.path.samefile (file, os.path.join (todir, file)): - return SUCCESS + parser.add_option('--no-delete', dest='delete', action='store_false', + default=config.get('options', 'delete'), + help="Do not delete files used to repair") - cmd = NOEXTRACT_CMD % (file, todir) - ret = run_command (cmd) + parser.add_option('-q', '--quiet', dest='quiet', action='count', + default=0, help="Output fatal messages only") - # Check error code - if ret != 0: - return -EEXTRACT + parser.add_option('-v', '--verbose', dest='verbose', action='count', + default=config.get('options', 'verbosity'), + help="Output extra information") - return SUCCESS + parser.add_option('-V', '--version', dest='version', action='store_true', + default=False, help="Output version information") + + parser.version = VERSION - def __extract_unknown (self, file, todir): - return SUCCESS + # Parse the given options + (options, args) = parser.parse_args() - def __generic_matcher (self, files, regex, nocase=False): - """Run the regex over the files, and see if one matches or not. - NOTE: this does not return the matches, just if a match occurred.""" + # Postprocess the options, basically sanitizing them + options.directory = PAR2Set.utils.absolutePath(options.directory) - if nocase: - cregex = re.compile (regex, re.IGNORECASE) - else: - cregex = re.compile (regex) - - for f in files: - if cregex.match (f): - return True - - return False - - def is_oldrar (self, files): - return self.__generic_matcher (files, '^.*\.r00$') - - def is_newrar (self, files): - return self.__generic_matcher (files, '^.*\.part0*1\.rar$') - - def is_zip (self, files): - return self.__generic_matcher (files, '^.*\.zip$') - - def is_noextract (self, files): - # Type that needs no extraction. - # TODO: Add others ??? - return self.__generic_matcher (files, '^.*\.001$') - -class PAR2Set (object): - - # Instance Variables - # ========================================================================== - # dir -- The directory this set lives in - # p2file -- The starting PAR2 file - # basename -- The basename of the set, guessed from the PAR2 file - # all_p2files -- All PAR2 files of the set, guessed from the PAR2 file name only - # name_matched_files -- Files in this set, guessed by name only - # prot_matched_files -- Files in this set, guessed by parsing the PAR2 only - - def __init__ (self, dir, p2file): - assert os.path.isdir (dir) - assert os.path.isfile (os.path.join (dir, p2file)) - - self.dir = dir - self.p2file = p2file - self.basename = self.__get_basename (p2file) - - # Find files that match by name only - self.name_matched_files = self.__find_name_matches (self.dir, self.basename) - - # Find all par2 files for this set using name matches - self.all_p2files = find_par2_files (self.name_matched_files) - - # Try to get the protected files for this set - self.prot_matched_files = self.__parse_all_par2 () - - def __list_eq (self, l1, l2): - - if len(l1) != len(l2): - return False - - for e in l1: - if e not in l2: - return False - - return True - - def __eq__ (self, rhs): - return (self.dir == rhs.dir) and (self.basename == rhs.basename) and \ - self.__list_eq (self.name_matched_files, rhs.name_matched_files) and \ - self.__list_eq (self.prot_matched_files, rhs.prot_matched_files) - - def __get_basename (self, name): - """Strips most kinds of endings from a filename""" - - regex = config.get_value ('regular expressions', 'basename_regex') - r = re.compile (regex, re.IGNORECASE) - done = False - - while not done: - done = True - - if r.match (name): - g = r.match (name).groups() - name = g[0] - done = False - - return name - - def __parse_all_par2 (self): - """Searches though self.all_p2files and tries to parse at least one of them""" - done = False - files = [] - - for f in self.all_p2files: - - # Exit early if we've found a good file - if done: - break - - try: - files = Par2Parser.get_protected_files (self.dir, f) - done = True - except (EnvironmentError, OSError, OverflowError): - logger.addMessage ('Corrupt PAR2 file: %s' % f, RarslaveLogger.MessageType.Fatal) - - # Now that we're out of the loop, check if we really finished - if not done: - logger.addMessage ('All PAR2 files corrupt for: %s' % self.p2file, RarslaveLogger.MessageType.Fatal) - - # Return whatever we've got, empty or not - return files - - def __find_name_matches (self, dir, basename): - """Finds files which are likely to be part of the set corresponding - to $name in the directory $dir""" - - assert os.path.isdir (dir) - - ename = re.escape (basename) - regex = re.compile ('^%s.*$' % (ename, )) - - return [f for f in os.listdir (dir) if regex.match (f)] - - def __update_name_matches (self): - """Updates the self.name_matched_files variable with the most current information. - This should be called after the directory contents are likely to change.""" - - self.name_matched_files = self.__find_name_matches (self.dir, self.basename) - - def __is_joinfile (self, filename): - regex = re.compile ('^.*\.\d\d\d$', re.IGNORECASE) - if regex.match (filename): - return True - - return False - - def __should_be_joined (self, files): - for f in files: - if self.__is_joinfile (f): - return True - - def runCheckAndRepair (self): - PAR2_CMD = config.get_value ('commands', 'par2repair') - - # Get set up - all_files = no_duplicates (self.name_matched_files + self.prot_matched_files) - join = self.__should_be_joined (all_files) - - # assemble the command - # par2repair -- PAR2 PAR2_EXTRA [JOIN_FILES] - command = "%s \"%s\" " % (PAR2_CMD, self.p2file) - - for f in self.all_p2files: - if f != self.p2file: - command += "\"%s\" " % os.path.split (f)[1] - - # Only needed when using par2 to join - if join: - for f in all_files: - if self.__is_joinfile (f): - command += "\"%s\" " % os.path.split (f)[1] - - # run the command - ret = run_command (command, self.dir) - - # check the result - if ret != 0: - logger.addMessage ('PAR2 Check / Repair failed: %s' % self.p2file, RarslaveLogger.MessageType.Fatal) - return -ECHECK - - return SUCCESS - - def __find_deleteable_files (self): - all_files = no_duplicates (self.name_matched_files + self.prot_matched_files) - DELETE_REGEX = config.get_value ('regular expressions', 'delete_regex') - dregex = re.compile (DELETE_REGEX, re.IGNORECASE) - - return [f for f in all_files if dregex.match (f)] - - def __delete_list_of_files (self, dir, files, interactive=False): - # Delete a list of files - - assert os.path.isdir (dir) - - done = False - valid_y = ['Y', 'YES'] - valid_n = ['N', 'NO', ''] - - if interactive: - while not done: - print 'Do you want to delete the following?:' - for f in files: - print f - s = raw_input ('Delete [y/N]: ').upper() - - if s in valid_y + valid_n: - done = True - - if s in valid_n: - return SUCCESS - - for f in files: - try: - os.remove (os.path.join (dir, f)) - logger.addMessage ('Deleteing: %s' % os.path.join (dir, f), RarslaveLogger.MessageType.Debug) - except: - logger.addMessage ('Failed to delete: %s' % os.path.join (dir, f), - RarslaveLogger.MessageType.Fatal) - return -EDELETE - - return SUCCESS - - def runDelete (self): - deleteable_files = self.__find_deleteable_files () - ret = self.__delete_list_of_files (self.dir, deleteable_files, options.interactive) - - return ret - - def run_all (self): - all_files = no_duplicates (self.name_matched_files + self.prot_matched_files) - - # Repair Stage - ret = self.runCheckAndRepair () - - if ret != SUCCESS: - logger.addMessage ('Repair stage failed for: %s' % self.p2file, RarslaveLogger.MessageType.Fatal) - return -ECHECK - - self.__update_name_matches () - all_files = no_duplicates (self.name_matched_files + self.prot_matched_files) - - # Extraction Stage - extractor = RarslaveExtractor (self.dir, self.all_p2files, \ - self.name_matched_files, self.prot_matched_files) - ret = extractor.runExtract (options.extract_dir) - - if ret != SUCCESS: - logger.addMessage ('Extraction stage failed for: %s' % self.p2file, RarslaveLogger.MessageType.Fatal) - return -EEXTRACT - - self.__update_name_matches () - all_files = no_duplicates (self.name_matched_files + self.prot_matched_files) - - # Deletion Stage - ret = self.runDelete () - - if ret != SUCCESS: - logger.addMessage ('Deletion stage failed for: %s' % self.p2file, RarslaveLogger.MessageType.Fatal) - return -EDELETE - - logger.addMessage ('Successfully completed: %s' % self.p2file) - return SUCCESS - -def run_command (cmd, indir=None): - # Runs the specified command-line in the directory given (or, in the current directory - # if none is given). It returns the status code given by the application. - - pwd = os.getcwd () - - if indir != None: - assert os.path.isdir (indir) # MUST be a directory! - os.chdir (indir) - - ret = os.system (cmd) - os.chdir (pwd) - return ret - -def full_abspath (p): - return os.path.abspath (os.path.expanduser (p)) - -def find_par2_files (files): - """Find all par2 files in the list $files""" - - PAR2_REGEX = config.get_value ('regular expressions', 'par2_regex') - regex = re.compile (PAR2_REGEX, re.IGNORECASE) - return [f for f in files if regex.match (f)] - -def find_all_par2_files (dir): - """Finds all par2 files in a directory""" - # NOTE: does NOT return absolute paths - - if not os.path.isdir (os.path.abspath (dir)): - raise ValueError # bad directory given - - dir = os.path.abspath (dir) - files = os.listdir (dir) - - return find_par2_files (files) - -def no_duplicates (li): - """Removes all duplicates from a list""" - return list(set(li)) - -def generate_all_parsets (dir): - # Generate all parsets in the given directory. - - assert os.path.isdir (dir) # Directory MUST be valid - - parsets = [] - p2files = find_all_par2_files (dir) - - for f in p2files: - p = PAR2Set (dir, f) - if p not in parsets: - parsets.append (p) - - return parsets - -def check_required_progs(): - """Check if the required programs are installed""" - - shell_not_found = 32512 - needed = [] - - if run_command ('par2repair --help > /dev/null 2>&1') == shell_not_found: - needed.append ('par2repair') - - if run_command ('unrar --help > /dev/null 2>&1') == shell_not_found: - needed.append ('unrar') - - if run_command ('unzip --help > /dev/null 2>&1') == shell_not_found: - needed.append ('unzip') - - if needed: - for n in needed: - print 'Needed program "%s" not found in $PATH' % (n, ) - - sys.exit(1) - -def run_options (options): - - # Fix directories - options.work_dir = full_abspath (options.work_dir) - - # Make sure that the directory is valid - if not os.path.isdir (options.work_dir): - sys.stderr.write ('\"%s\" is not a valid directory. Use the \"-d\"\n' % options.work_dir) - sys.stderr.write ('option to override the working directory temporarily, or edit the\n') - sys.stderr.write ('configuration file to override the working directory permanently.\n') - sys.exit (1) - - if options.extract_dir != None: - options.extract_dir = full_abspath (options.extract_dir) - - if options.version: - print PROGRAM + ' - ' + VERSION - print - print 'Copyright (c) 2005,2006 Ira W. Snyder (devel@irasnyder.com)' - print - print 'This program comes with ABSOLUTELY NO WARRANTY.' - print 'This is free software, and you are welcome to redistribute it' - print 'under certain conditions. See the file COPYING for details.' - sys.exit (0) - - if options.check_progs: - check_required_progs () - - if options.write_def_config: - config.write_config (default=True) - - if options.write_config: - config.write_config () - -def find_loglevel (options): - - loglevel = options.verbose - options.quiet - - if loglevel < RarslaveLogger.MessageType.Fatal: - loglevel = RarslaveLogger.MessageType.Fatal - - if loglevel > RarslaveLogger.MessageType.Debug: - loglevel = RarslaveLogger.MessageType.Debug - - return loglevel - -def printMessageTable (loglevel): - - if logger.hasFatalMessages (): - print '\nFatal Messages\n' + '=' * 80 - logger.printLoglevel (RarslaveLogger.MessageType.Fatal) - - if loglevel == RarslaveLogger.MessageType.Fatal: - return - - if logger.hasNormalMessages (): - print '\nNormal Messages\n' + '=' * 80 - logger.printLoglevel (RarslaveLogger.MessageType.Normal) - - if loglevel == RarslaveLogger.MessageType.Normal: - return - - if logger.hasVerboseMessages (): - print '\nVerbose Messages\n' + '=' * 80 - logger.printLoglevel (RarslaveLogger.MessageType.Verbose) - - if loglevel == RarslaveLogger.MessageType.Verbose: - return - - if logger.hasDebugMessages (): - print '\nDebug Messages\n' + '=' * 80 - logger.printLoglevel (RarslaveLogger.MessageType.Debug) - - return + # Make sure that the directory is valid + if not os.path.isdir (options.directory): + sys.stderr.write ('\"%s\" is not a valid directory. Use the \"-d\"\n' % options.directory) + sys.stderr.write ('option to override the working directory temporarily, or edit the\n') + sys.stderr.write ('configuration file to override the working directory permanently.\n') + sys.exit (1) + + if options.version: + print PROGRAM + ' - ' + VERSION + print + print 'Copyright (c) 2005-2008 Ira W. Snyder (devel@irasnyder.com)' + print + print 'This program comes with ABSOLUTELY NO WARRANTY.' + print 'This is free software, and you are welcome to redistribute it' + print 'under certain conditions. See the file COPYING for details.' + sys.exit (0) + + return (options, args) + +################################################################################ + +# Find each unique CompareSet in the given directory and set of files +def findUniqueSets(directory, files): + + regex = r'^.*\.par2' + s = [] + + for f in PAR2Set.utils.findMatches(regex, files): + + try: + c = PAR2Set.CompareSet(directory, f) + except: + # We just ignore any errors that happen, such as + # parsing the PAR file + pass + else: + # Ok, we got a valid set, add it to s + if c not in s: + s.append(c) + + return s + +################################################################################ + +# Run each PAR2Set type on a CompareSet +def runEachType(cs, options): + + types = ( + PAR2Set.JoinProtected, + PAR2Set.Join, + PAR2Set.ZIP, + PAR2Set.OldRAR, + PAR2Set.NewRAR, + PAR2Set.ExtractFirstOldRAR, + PAR2Set.ExtractFirstNewRAR, + PAR2Set.NoExtract, + ) + + detected = False + + # Try to detect each type in turn + for t in types: + try: + instance = t(cs, options) + except TypeError: + logging.debug('%s not detected for %s' % (t.__name__, cs.parityFile)) + continue + else: + detected = True + logging.debug('%s detected for %s' % (t.__name__, cs.parityFile)) + + # We detected something, try to run it + try: + instance.run() + except (OSError, CalledProcessError): + logging.critical('Failure: %s' % instance) + else: + # Leave early, we're done + logging.info('Success: %s' % instance) + return + + # Check that at least one detection worked + if not detected: + logging.critical('Detection failed: %s' % cs.parityFile) + logging.debug('The following information will help to create a detector') + logging.debug('===== BEGIN CompareSet RAW INFO =====') + logging.debug(str(cs)) + logging.debug('===== END CompareSet RAW INFO =====') + + # If we got here, either the detection didn't work or the run itself didn't + # work, so print out the message telling the user that we were unsuccessful + logging.critical('Unsuccessful: %s' % cs.parityFile) + +################################################################################ + +def runDirectory(directory, files, options): + + logging.debug('Running in directory: %s' % directory) + sets = findUniqueSets(directory, files) + + for cs in sets: + try: + runEachType(cs, options) + except Exception, e: + import traceback + logging.error('Unknown Exception: %s' % cs.parityFile) + logging.error('===== BEGIN Bactrace =====') + [logging.error(l) for l in traceback.format_exc(e).split('\n')] + logging.error('===== END Bactrace =====') + +################################################################################ def main (): - # Build the OptionParser - parser = optparse.OptionParser() - parser.add_option('-n', '--not-recursive', - action='store_false', dest='recursive', - default=config.get_value('options', 'recursive'), - help="Don't run recursively") - - parser.add_option('-d', '--work-dir', - dest='work_dir', type='string', - default=config.get_value('directories', 'working_directory'), - help="Start running at DIR", metavar='DIR') - - parser.add_option('-e', '--extract-dir', - dest='extract_dir', type='string', - default=config.get_value('directories', 'extract_directory'), - help="Extract to DIR", metavar='DIR') - - parser.add_option('-p', '--check-required-programs', - action='store_true', dest='check_progs', - default=False, - help="Check for required programs") - - parser.add_option('-f', '--write-default-config', - action='store_true', dest='write_def_config', - default=False, help="Write out a new default config") - - parser.add_option('-c', '--write-new-config', - action='store_true', dest='write_config', - default=False, help="Write out the current config") - - parser.add_option('-i', '--interactive', dest='interactive', action='store_true', - default=config.get_value('options', 'interactive'), - help="Confirm before removing files") - - parser.add_option('-q', '--quiet', dest='quiet', action='count', - default=0, help="Output fatal messages only") - - parser.add_option('-v', '--verbose', dest='verbose', action='count', - default=0, help="Output extra information") - - parser.add_option('-V', '--version', dest='version', action='store_true', - default=False, help="Output version information") - - parser.version = VERSION - - # Parse the given options - global options - (options, args) = parser.parse_args() - - # Run any special actions that are needed on these options - run_options (options) + # Parse all of the command line options + (options, args) = parseCommandLineOptions() - # Find the loglevel using the options given - loglevel = find_loglevel (options) + # Set up the logger + logger = DelayedLogger() + logging.basicConfig(stream=logger, level=logging.WARNING, \ + format='%(levelname)-8s %(message)s') + logging.getLogger().setLevel (findLogLevel(options)) - # Run recursively - if options.recursive: - for (dir, subdirs, files) in os.walk (options.work_dir): - parsets = generate_all_parsets (dir) - for p in parsets: - p.run_all () + # Run recursively + if options.recursive: + for (directory, subDirectories, files) in os.walk(options.directory): + runDirectory(directory, files, options) - # Non-recursive - else: - parsets = generate_all_parsets (options.work_dir) - for p in parsets: - p.run_all () + # Non-recursive + else: + directory = options.directory + files = os.listdir(directory) - # Print the results - printMessageTable (loglevel) + runDirectory(directory, files, options) - # Done! - return 0 + # Print out all of the messages that have been accumulating + # in the DelayedLogger() + if logger.size() > 0: + print + print 'Log' + print '=' * 80 + logger.close() +# Check if we were called directly if __name__ == '__main__': - main () + main ()