2 # vim: set ts=4 sts=4 sw=4 textwidth=80:
5 The main program of the rarslave project
7 This handles all of the commandline and configuration file work, then tries to
8 repair, extract, and delete any PAR2Sets that it finds.
11 __author__ = "Ira W. Snyder (devel@irasnyder.com)"
12 __copyright__ = "Copyright (c) 2006-2008 Ira W. Snyder (devel@irasnyder.com)"
13 __license__ = "GNU GPL v2 (or, at your option, any later version)"
15 # rarslave.py -- a usenet autorepair and autoextract utility
17 # Copyright (C) 2006-2008 Ira W. Snyder (devel@irasnyder.com)
19 # This program is free software; you can redistribute it and/or modify
20 # it under the terms of the GNU General Public License as published by
21 # the Free Software Foundation; either version 2 of the License, or
22 # (at your option) any later version.
24 # This program is distributed in the hope that it will be useful,
25 # but WITHOUT ANY WARRANTY; without even the implied warranty of
26 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
27 # GNU General Public License for more details.
29 # You should have received a copy of the GNU General Public License
30 # along with this program; if not, write to the Free Software
31 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
36 import os, sys, optparse, logging, ConfigParser
37 from subprocess import CalledProcessError
40 ################################################################################
42 # A simple-ish configuration class
43 class RarslaveConfig(object):
45 DEFAULT_CONFIG_FILE = PAR2Set.utils.absolutePath(
46 os.path.join('~', '.config', 'rarslave', 'rarslave.conf'))
48 def __init__(self, fileName=DEFAULT_CONFIG_FILE):
50 # Make sure that the fileName is in absolute form
51 self.fileName = os.path.abspath(os.path.expanduser(fileName))
53 # Open it with ConfigParser
54 self.config = ConfigParser.SafeConfigParser()
55 self.config.read(fileName)
57 # Setup the default dictionary
58 self.defaults = dict()
60 # Add all of the defaults
61 self.add_default('directories', 'start',
62 os.path.join('~', 'downloads'),
63 PAR2Set.utils.absolutePath)
64 self.add_default('options', 'recursive', True, self.toBool)
65 self.add_default('options', 'interactive', False, self.toBool)
66 self.add_default('options', 'verbosity', 0, self.toInt)
67 self.add_default('options', 'delete', True, self.toBool)
69 # Add a new default value
70 def add_default(self, section, key, value, typeConverter):
72 self.defaults[(section, key)] = (value, typeConverter)
74 # Get the default value
75 def get_default(self, section, key):
77 (value, typeConverter) = self.defaults[(section, key)]
80 # Coerce the value from a string into the correct type
81 def coerceValue(self, section, key, value):
83 (defaultValue, typeConverter) = self.defaults[(section, key)]
85 # Try the coercion, error and exit if there is a problem
87 return typeConverter(value)
89 sys.stderr.write('Unable to parse configuration file\n')
90 sys.stderr.write('-> at section: %s\n' % section)
91 sys.stderr.write('-> at key: %s\n' % key)
95 def get(self, section, key):
98 # Get the user-provided value
99 value = self.config.get(section, key)
101 # Oops, they didn't provide it, use the default
102 # NOTE: if you get an exception here, check your code ;)
103 value = self.defaults[(section, key)]
105 # Try to evaluate some safe things, for convenience
106 return self.coerceValue(section, key, value)
108 # Convert a string to an int (any base)
113 toInt = staticmethod(toInt)
115 # Convert a string to a bool
117 if s in ['t', 'T', 'True', 'true', 'yes', '1']:
120 if s in ['f', 'F', 'False', 'false', 'no', '0']:
126 toBool = staticmethod(toBool)
128 ################################################################################
130 # Global configuration, read from default configuration file
131 config = RarslaveConfig()
133 ################################################################################
135 # A tiny class to hold logging output until we're finished
136 class DelayedLogger (object):
138 """A small class to hold logging output until the program is finished running.
139 It emulates sys.stdout in the needed ways for the logging module."""
141 def __init__ (self, output=sys.stdout.write):
143 self.__output = output
145 def write (self, msg):
146 self.__messages.append (msg)
152 """Returns the number of messages queued for printing"""
153 return len (self.__messages)
156 """Print all messages, clear the queue"""
157 map(self.__output, self.__messages)
160 ################################################################################
162 # Convert from the verbose command line option to the logging level that
163 # will be used by the logging class to print messages
164 def findLogLevel(options):
166 level = options.verbose - options.quiet
177 -1 : logging.WARNING,
179 -3 : logging.CRITICAL
184 ################################################################################
186 def parseCommandLineOptions():
188 # Build the OptionParser
189 parser = optparse.OptionParser()
190 parser.add_option('-n', '--not-recursive', dest='recursive', action='store_false',
191 default=config.get('options', 'recursive'),
192 help="Don't run recursively")
194 parser.add_option('-d', '--directory', dest='directory', type='string',
195 default=config.get('directories', 'start'),
196 help="Start working at DIR", metavar='DIR')
198 parser.add_option('-i', '--interactive', dest='interactive', action='store_true',
199 default=config.get('options', 'interactive'),
200 help="Confirm before removing files")
202 parser.add_option('--no-delete', dest='delete', action='store_false',
203 default=config.get('options', 'delete'),
204 help="Do not delete files used to repair")
206 parser.add_option('-q', '--quiet', dest='quiet', action='count',
207 default=0, help="Output fatal messages only")
209 parser.add_option('-v', '--verbose', dest='verbose', action='count',
210 default=config.get('options', 'verbosity'),
211 help="Output extra information")
213 parser.add_option('-V', '--version', dest='version', action='store_true',
214 default=False, help="Output version information")
216 parser.version = VERSION
218 # Parse the given options
219 (options, args) = parser.parse_args()
221 # Postprocess the options, basically sanitizing them
222 options.directory = PAR2Set.utils.absolutePath(options.directory)
224 # Make sure that the directory is valid
225 if not os.path.isdir (options.directory):
226 sys.stderr.write ('\"%s\" is not a valid directory. Use the \"-d\"\n' % options.directory)
227 sys.stderr.write ('option to override the working directory temporarily, or edit the\n')
228 sys.stderr.write ('configuration file to override the working directory permanently.\n')
232 print PROGRAM + ' - ' + VERSION
234 print 'Copyright (c) 2005-2008 Ira W. Snyder (devel@irasnyder.com)'
236 print 'This program comes with ABSOLUTELY NO WARRANTY.'
237 print 'This is free software, and you are welcome to redistribute it'
238 print 'under certain conditions. See the file COPYING for details.'
241 return (options, args)
243 ################################################################################
245 # Find each unique CompareSet in the given directory and set of files
246 def findUniqueSets(directory, files):
251 for f in PAR2Set.utils.findMatches(regex, files):
254 c = PAR2Set.CompareSet(directory, f)
256 # We just ignore any errors that happen, such as
257 # parsing the PAR file
260 # Ok, we got a valid set, add it to s
266 ################################################################################
268 # Run each PAR2Set type on a CompareSet
269 def runEachType(cs, options):
276 PAR2Set.ExtractFirstOldRAR,
277 PAR2Set.ExtractFirstNewRAR,
283 # Try to detect each type in turn
286 instance = t(cs, options)
288 logging.debug('%s not detected for %s' % (t.__name__, cs.parityFile))
292 logging.debug('%s detected for %s' % (t.__name__, cs.parityFile))
294 # We detected something, try to run it
297 except (OSError, CalledProcessError):
298 logging.critical('Failure: %s' % instance)
300 # Leave early, we're done
301 logging.info('Success: %s' % instance)
304 # Check that at least one detection worked
306 logging.critical('Detection failed: %s' % cs.parityFile)
307 logging.debug('The following information will help to create a detector')
308 logging.debug('===== BEGIN CompareSet RAW INFO =====')
309 logging.debug(str(cs))
310 logging.debug('===== END CompareSet RAW INFO =====')
312 # If we got here, either the detection didn't work or the run itself didn't
313 # work, so print out the message telling the user that we were unsuccessful
314 logging.critical('Unsuccessful: %s' % cs.parityFile)
316 ################################################################################
318 def runDirectory(directory, files, options):
320 logging.debug('Running in directory: %s' % directory)
321 sets = findUniqueSets(directory, files)
325 runEachType(cs, options)
328 logging.error('Unknown Exception: %s' % cs.parityFile)
329 logging.error('===== BEGIN Bactrace =====')
330 [logging.error(l) for l in traceback.format_exc(e).split('\n')]
331 logging.error('===== END Bactrace =====')
333 ################################################################################
337 # Parse all of the command line options
338 (options, args) = parseCommandLineOptions()
341 logger = DelayedLogger()
342 logging.basicConfig(stream=logger, level=logging.WARNING, \
343 format='%(levelname)-8s %(message)s')
344 logging.getLogger().setLevel (findLogLevel(options))
347 if options.recursive:
348 for (directory, subDirectories, files) in os.walk(options.directory):
349 runDirectory(directory, files, options)
353 directory = options.directory
354 files = os.listdir(directory)
356 runDirectory(directory, files, options)
358 # Print out all of the messages that have been accumulating
359 # in the DelayedLogger()
360 if logger.size() > 0:
366 # Check if we were called directly
367 if __name__ == '__main__':