X-Git-Url: https://www.irasnyder.com/gitweb/?a=blobdiff_plain;f=rarslave.py;h=0d6a47153db256aaa346a218d7b3b8e30e19bbdb;hb=fe27621c32fdc44621755206b25712844176543e;hp=47fbb03e35ebc4ea2c24369f089e28a867ca49a8;hpb=5ec7a887a528ecf80185ef26a2d5b754cb05ef18;p=rarslave2.git diff --git a/rarslave.py b/rarslave.py old mode 100644 new mode 100755 index 47fbb03..0d6a471 --- a/rarslave.py +++ b/rarslave.py @@ -1,153 +1,286 @@ #!/usr/bin/env python -# vim: set ts=4 sts=4 sw=4 textwidth=112 : - -import re, os, sys +# vim: set ts=4 sts=4 sw=4 textwidth=92: + +""" +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. +""" + +__author__ = "Ira W. Snyder (devel@irasnyder.com)" +__copyright__ = "Copyright (c) 2006,2007 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) +# +# 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" + +import os, sys, optparse, logging +import rsutil +import RarslaveDetector + +# 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 small class to hold logging output until the program is finished running. + It emulates sys.stdout in the needed ways for the logging module.""" + + def __init__ (self, output=sys.stdout.write): + self.__messages = [] + self.__output = output + + def write (self, msg): + self.__messages.append (msg) + + def flush (self): + pass -def get_basename (name): - """Strips most kinds of endings from a filename""" + def size (self): + """Returns the number of messages queued for printing""" + return len (self.__messages) - regex = '^(.+)\.(par2|vol\d+\+\d+|\d\d\d|part\d+|rar|zip|avi|mp4|mkv|ogm)$' - r = re.compile (regex, re.IGNORECASE) - done = False + def close (self): + """Print all messages, clear the queue""" + for m in self.__messages: + self.__output (m) - while not done: - done = True + self.__messages = [] - if r.match (name): - g = r.match (name).groups() - name = g[0] - done = False +# A tiny class used to find unique PAR2 sets +class CompareSet (object): - return name + """A small class used to find unique PAR2 sets""" -def find_likely_files (name, dir): - """Finds files which are likely to be part of the set corresponding - to $name in the directory $dir""" + def __init__ (self, dir, p2file): + self.dir = dir + self.p2file = p2file - if not os.path.isdir (os.path.abspath (dir)): - raise ValueError # bad directory given + self.basename = rsutil.common.get_basename (self.p2file) + self.name_matches = rsutil.common.find_name_matches (self.dir, self.basename) - dir = os.path.abspath (dir) - ename = re.escape (name) - regex = re.compile ('^%s.*$' % (ename, )) + 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) - return [f for f in os.listdir (dir) if regex.match (f)] def find_all_par2_files (dir): - """Finds all par2 files in a directory""" + """Finds all par2 files in the given directory. + + dir -- the directory in which to search for PAR2 files + + 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) - regex = re.compile ('^.*\.par2$', re.IGNORECASE) + files = os.listdir (dir) - # Find all files - return [f for f in os.listdir (dir) if regex.match (f)] + return rsutil.common.find_par2_files (files) -def has_extension (f, ext): - """Checks if f has the extension ext""" +def generate_all_parsets (dir): + """Generate all parsets in the given directory - if ext[0] != '.': - ext = '.' + ext + dir -- the directory in which to search""" - ext = re.escape (ext) - regex = re.compile ('^.*%s$' % (ext, ), re.IGNORECASE) - return regex.match (f) + assert os.path.isdir (dir) # Directory MUST be valid -def find_extraction_heads (files): - """Takes a list of possible files and finds likely heads of - extraction.""" + parsets = [] + p2files = find_all_par2_files (dir) - # NOTE: perhaps this should happen AFTER repair is - # NOTE: successful. That way all files would already exist + for f in p2files: + p = CompareSet (dir, f) + if p not in parsets: + parsets.append (p) - # According to various sources online: - # 1) pre rar-3.0: .rar .r00 .r01 ... - # 2) post rar-3.0: .part01.rar .part02.rar - # 3) zip all ver: .zip + return [(p.dir, p.p2file) for p in parsets] - heads = [] +def check_required_progs(): + """Check if the required programs are installed""" - # Old RAR type, find all files ending in .rar - if is_oldrar (files): - regex = re.compile ('^.*\.rar$', re.IGNORECASE) - for f in files: - if regex.match (f): - heads.append (f) + shell_not_found = 32512 + needed = [] - return heads + if rsutil.common.run_command ('par2repair --help > /dev/null 2>&1') == shell_not_found: + needed.append ('par2repair') - if is_newrar (files): - regex = re.compile ('^.*\.part01.rar$', re.IGNORECASE) - for f in files: - if regex.match (f): - heads.append (f) + if rsutil.common.run_command ('unrar --help > /dev/null 2>&1') == shell_not_found: + needed.append ('unrar') - return heads + if rsutil.common.run_command ('unzip --help > /dev/null 2>&1') == shell_not_found: + needed.append ('unzip') - if is_zip (files): - regex = re.compile ('^.*\.zip$', re.IGNORECASE) - for f in files: - if regex.match (f): - heads.append (f) + if needed: + for n in needed: + print 'Needed program "%s" not found in $PATH' % (n, ) - return heads + sys.exit(1) - # Not a type we know yet - raise ValueError +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) -def is_oldrar (files): - for f in files: - if has_extension (f, '.r00'): - return True + # 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) -def is_newrar (files): - for f in files: - if has_extension (f, '.part01.rar'): - return True + if options.extract_dir != None: + options.extract_dir = rsutil.common.full_abspath (options.extract_dir) -def is_zip (files): - for f in files: - if has_extension (f, '.zip'): - return True + 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) -def is_noextract (files): - # Type that needs no extraction. - # TODO: Add others ??? - for f in files: - if has_extension (f, '.001'): - return True + if options.check_progs: + check_required_progs () -def find_deleteable_files (files): - # Deleteable types regex should come from the config - dfiles = [] - dregex = re.compile ('^.*\.(par2|\d|\d\d\d|rar|r\d\d|zip)$', re.IGNORECASE) + if options.write_def_config: + config.write_config (default=True) + sys.exit (0) - return [f for f in files if dregex.match (f)] + if options.write_config: + config.write_config () + sys.exit (0) -def extract (heads, todir): - # Try to extract each head - # NOTE: REQUIRES full paths to heads +def find_loglevel (options): + """Find the log level that should be printed by the logging class""" - PWD = os.getcwd() + loglevel = options.verbose - options.quiet - # FIXME: Should come from the config - RCMD = 'unrar x -o+ -- ' - ZCMD = 'unzip -- ' + if loglevel > 1: + loglevel = 1 - for h in heads: - # find type - # extract it - - # NOTE: probably not able to clean up effectively... - pass + if loglevel < -3: + loglevel = -3 + LEVELS = { 1 : logging.DEBUG, + 0 : logging.INFO, + -1: logging.WARNING, + -2: logging.ERROR, + -3: logging.CRITICAL + } + return LEVELS [loglevel] def main (): - print find_all_par2_files ('/home/irasnyd/downloads/test_material/01/') + + # Setup the logger + logger = DelayedLogger () + logging.basicConfig (stream=logger, level=logging.WARNING, \ + format='%(levelname)-8s %(message)s') + + # 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") + + 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') + + 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") + + 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=rsutil.common.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 + (rsutil.globals.options, args) = parser.parse_args() + options = rsutil.globals.options + + # Run any special actions that are needed on these options + run_options (options) + + # Find the loglevel using the options given + logging.getLogger().setLevel (find_loglevel (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) + ret = detector.runMatchingTypes () + + # Non-recursive + else: + parsets = generate_all_parsets (options.work_dir) + for (p2dir, p2file) in parsets: + detector = RarslaveDetector.RarslaveDetector (p2dir, p2file) + ret = detector.runMatchingTypes () + + # Print the results + if logger.size () > 0: + print '\nLog\n' + '=' * 80 + logger.close () + + # Done! + return 0 if __name__ == '__main__': main () +