X-Git-Url: https://www.irasnyder.com/gitweb/?p=rarslave2.git;a=blobdiff_plain;f=rarslave.py;h=edd34ce87394bbebd590711beb8ad241af946242;hp=18bb540e48de820a5344aee9e8688445e0d0b4b3;hb=feeefeb8ea2f1e4724424d43c0eb872aee4743c2;hpb=3e0a5dd7c7549636eb70c6a641987da66742f1db diff --git a/rarslave.py b/rarslave.py index 18bb540..edd34ce 100755 --- a/rarslave.py +++ b/rarslave.py @@ -1,21 +1,20 @@ #!/usr/bin/env python -# vim: set ts=4 sts=4 sw=4 textwidth=92: +# vim: set ts=4 sts=4 sw=4 textwidth=80: """ -The main program of the rarslave project. +The main program of the rarslave project -This handles all of the commandline, configuration file, and option -work. It gets the environment set up for a run using the RarslaveDetector -class. +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,2007 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,2007 Ira W. Snyder (devel@irasnyder.com) +# 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 @@ -31,268 +30,334 @@ __license__ = "GNU GPL v2 (or, at your option, any later version)" # 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="rarslave" +VERSION = "2.1.0" +PROGRAM = "rarslave" -import os, sys, optparse, logging -import rsutil -import RarslaveDetector +import os, sys, optparse, logging, ConfigParser +from subprocess import CalledProcessError +import PAR2Set -# Global options from the rsutil.globals class -options = rsutil.globals.options -config = rsutil.globals.config +################################################################################ -# A tiny class to hold logging output until we're finished -class DelayedLogger (object): +# A simple-ish configuration class +class RarslaveConfig(object): - """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.""" + DEFAULT_CONFIG_FILE = PAR2Set.utils.absolutePath( + os.path.join('~', '.config', 'rarslave2', 'rarslave2.conf')) - def __init__ (self, output=sys.stdout.write): - self.__messages = [] - self.__output = output + def __init__(self, fileName=DEFAULT_CONFIG_FILE): - def write (self, msg): - self.__messages.append (msg) + # Make sure that the fileName is in absolute form + self.fileName = os.path.abspath(os.path.expanduser(fileName)) - def flush (self): - pass + # Open it with ConfigParser + self.config = ConfigParser.SafeConfigParser() + self.config.read(fileName) - def size (self): - """Returns the number of messages queued for printing""" - return len (self.__messages) + # Setup the default dictionary + self.defaults = dict() - def close (self): - """Print all messages, clear the queue""" - for m in self.__messages: - self.__output (m) + # 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) - self.__messages = [] + # Add a new default value + def add_default(self, section, key, value, typeConverter): -# A tiny class used to find unique PAR2 sets -class CompareSet (object): + self.defaults[(section, key)] = (value, typeConverter) - """A small class used to find unique PAR2 sets""" + # Get the default value + def get_default(self, section, key): - def __init__ (self, dir, p2file): - self.dir = dir - self.p2file = p2file + (value, typeConverter) = self.defaults[(section, key)] + return value - self.basename = rsutil.common.get_basename (self.p2file) - self.name_matches = rsutil.common.find_name_matches (self.dir, self.basename) + # Coerce the value from a string into the correct type + def coerceValue(self, section, key, value): - def __eq__ (self, rhs): - return (self.dir == rhs.dir) \ - and (self.basename == rhs.basename) \ - and rsutil.common.list_eq (self.name_matches, rhs.name_matches) + (defaultValue, typeConverter) = self.defaults[(section, key)] + # 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 find_all_par2_files (dir): - """Finds all par2 files in the given directory. + # Return the value + def get(self, section, key): - dir -- the directory in which to search for PAR2 files + 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)] - NOTE: does not return absolute paths""" + # Try to evaluate some safe things, for convenience + return self.coerceValue(section, key, value) - if not os.path.isdir (os.path.abspath (dir)): - raise ValueError # bad directory given + # Convert a string to an int (any base) + def toInt(s): + return int(s, 0) - dir = os.path.abspath (dir) - files = os.listdir (dir) + # Mark it static + toInt = staticmethod(toInt) - return rsutil.common.find_par2_files (files) + # Convert a string to a bool + def toBool(s): + if s in ['t', 'T', 'True', 'true', 'yes', '1']: + return True -def generate_all_parsets (dir): - """Generate all parsets in the given directory + if s in ['f', 'F', 'False', 'false', 'no', '0']: + return False - dir -- the directory in which to search""" + raise ValueError - assert os.path.isdir (dir) # Directory MUST be valid + # Mark it static + toBool = staticmethod(toBool) - parsets = [] - p2files = find_all_par2_files (dir) +################################################################################ - for f in p2files: - p = CompareSet (dir, f) - if p not in parsets: - parsets.append (p) +# Global configuration, read from default configuration file +config = RarslaveConfig() - return [(p.dir, p.p2file) for p in parsets] +################################################################################ -def check_required_progs(): - """Check if the required programs are installed""" +# A tiny class to hold logging output until we're finished +class DelayedLogger (object): - needed = [] + """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.""" - try: - rsutil.common.run_command(['par2repair', '--help']) - except OSError: - needed.append('par2repair') - except RuntimeError: - pass + def __init__ (self, output=sys.stdout.write): + self.__messages = [] + self.__output = output - try: - rsutil.common.run_command(['unrar', '--help']) - except OSError: - needed.append('unrar') - except RuntimeError: - pass + def write (self, msg): + self.__messages.append (msg) - try: - rsutil.common.run_command(['unzip', '--help']) - except OSError: - needed.append('unzip') - except RuntimeError: - pass + def flush (self): + pass - if needed: - for n in needed: - print 'Needed program "%s" not found in $PATH' % (n, ) + def size (self): + """Returns the number of messages queued for printing""" + return len (self.__messages) - sys.exit(1) + def close (self): + """Print all messages, clear the queue""" + map(self.__output, self.__messages) + self.__messages = [] -def run_options (options): - """Process all of the commandline options, doing thing such as printing the - version number, etc.""" +################################################################################ - # Fix directories - options.work_dir = rsutil.common.full_abspath (options.work_dir) +# 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): - # 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) + level = options.verbose - options.quiet - if options.extract_dir != None: - options.extract_dir = rsutil.common.full_abspath (options.extract_dir) + if level < -3: + level = -3 - 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 level > 1: + level = 1 - if options.check_progs: - check_required_progs () - sys.exit (0) + LEVELS = { + 1 : logging.DEBUG, + 0 : logging.INFO, + -1 : logging.WARNING, + -2 : logging.ERROR, + -3 : logging.CRITICAL + } - if options.write_def_config: - config.write_config (default=True) - sys.exit (0) + return LEVELS[level] - if options.write_config: - config.write_config () - sys.exit (0) +################################################################################ -def find_loglevel (options): - """Find the log level that should be printed by the logging class""" +def parseCommandLineOptions(): - loglevel = options.verbose - options.quiet + # 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") - if loglevel > 1: - loglevel = 1 + parser.add_option('-d', '--directory', dest='directory', type='string', + default=config.get('directories', 'start'), + help="Start working at DIR", metavar='DIR') - if loglevel < -3: - loglevel = -3 + parser.add_option('-i', '--interactive', dest='interactive', action='store_true', + default=config.get('options', 'interactive'), + help="Confirm before removing files") - LEVELS = { 1 : logging.DEBUG, - 0 : logging.INFO, - -1: logging.WARNING, - -2: logging.ERROR, - -3: logging.CRITICAL - } + parser.add_option('--no-delete', dest='delete', action='store_false', + default=config.get('options', 'delete'), + help="Do not delete files used to repair") - return LEVELS [loglevel] + parser.add_option('-q', '--quiet', dest='quiet', action='count', + default=0, help="Output fatal messages only") -def main (): + parser.add_option('-v', '--verbose', dest='verbose', action='count', + default=config.get('options', 'verbosity'), + 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 + (options, args) = parser.parse_args() + + # Postprocess the options, basically sanitizing them + options.directory = PAR2Set.utils.absolutePath(options.directory) - # Setup the logger - logger = DelayedLogger () - logging.basicConfig (stream=logger, level=logging.WARNING, \ - format='%(levelname)-8s %(message)s') + # 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) - # Build the OptionParser - parser = optparse.OptionParser() - parser.add_option('-n', '--not-recursive', action='store_false', dest='recursive', - default=rsutil.common.config_get_value('options', 'recursive'), - help="Don't run recursively") + 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) - parser.add_option('-d', '--work-dir', dest='work_dir', type='string', - default=rsutil.common.config_get_value('directories', 'working_directory'), - help="Start running at DIR", metavar='DIR') + return (options, args) - parser.add_option('-e', '--extract-dir', dest='extract_dir', type='string', - default=rsutil.common.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") +# Find each unique CompareSet in the given directory and set of files +def findUniqueSets(directory, files): - parser.add_option('-f', '--write-default-config', - action='store_true', dest='write_def_config', - default=False, help="Write out a new default config") + regex = r'^.*\.par2' + s = [] - parser.add_option('-c', '--write-new-config', - action='store_true', dest='write_config', - default=False, help="Write out the current config") + for f in PAR2Set.utils.findMatches(regex, files): - parser.add_option('-i', '--interactive', dest='interactive', action='store_true', - default=rsutil.common.config_get_value('options', 'interactive'), - help="Confirm before removing files") + try: + c = PAR2Set.CompareSet(directory, f) - parser.add_option('-q', '--quiet', dest='quiet', action='count', - default=0, help="Output fatal messages only") + if c not in s: + s.append(c) + except: + # We just ignore any errors that happen, such as + # parsing the PAR file + pass - parser.add_option('-v', '--verbose', dest='verbose', action='count', - default=0, help="Output extra information") + return s - parser.add_option('-V', '--version', dest='version', action='store_true', - default=False, help="Output version information") +################################################################################ - parser.version = VERSION +# Run each PAR2Set type on a CompareSet +def runEachType(cs, options): - # Parse the given options - global options - (rsutil.globals.options, args) = parser.parse_args() - options = rsutil.globals.options + types = ( + 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) + detected = True + logging.debug('%s detected for %s' % (t.__name__, cs.parityFile)) + except TypeError: + logging.debug('%s not detected for %s' % (t.__name__, cs.parityFile)) + continue + + # We detected something, try to run it + try: + instance.run() + logging.info('Success: %s' % instance) + + # Leave early, we're done + return + except (OSError, CalledProcessError): + logging.critical('Failure: %s' % instance) + + # 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: + logging.error('Unknown Exception: %s' % cs.parityFile) + +################################################################################ + +def main (): - # 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 - logging.getLogger().setLevel (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 (p2dir, p2file) in parsets: - detector = RarslaveDetector.RarslaveDetector (p2dir, p2file) - detector.runMatchingTypes () + # 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 (p2dir, p2file) in parsets: - detector = RarslaveDetector.RarslaveDetector (p2dir, p2file) - detector.runMatchingTypes () + # Non-recursive + else: + directory = options.directory + files = os.listdir(directory) - # Print the results - if logger.size () > 0: - print '\nLog\n' + '=' * 80 - logger.close () + 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 ()