2 # vim: set ts=4 sts=4 sw=4 textwidth=112 :
7 import re, os, sys, optparse
13 (TYPE_OLDRAR, TYPE_NEWRAR, TYPE_ZIP, TYPE_NOEXTRACT) = range (4)
14 (SUCCESS, ECHECK, EEXTRACT, EDELETE) = range(4)
15 config = RarslaveConfig.RarslaveConfig()
16 logger = RarslaveLogger.RarslaveLogger ()
18 # Global options to be set / used later.
21 class RarslaveExtractor (object):
23 def __init__ (self, type):
27 def addHead (self, dir, head):
28 assert os.path.isdir (dir)
29 assert os.path.isfile (os.path.join (dir, head))
31 full_head = os.path.join (dir, head)
32 logger.addMessage ('Adding extraction head: %s' % full_head, RarslaveLogger.MessageType.Debug)
33 self.heads.append (full_head)
35 def extract (self, todir=None):
36 # Extract all heads of this set
38 # Create the directory $todir if it doesn't exist
39 if todir != None and not os.path.isdir (todir):
40 logger.addMessage ('Creating directory: %s' % todir, RarslaveLogger.MessageType.Verbose)
44 logger.addMessage ('FAILED to create directory: %s' % todir, RarslaveLogger.MessageType.Fatal)
49 { TYPE_OLDRAR : self.__extract_rar,
50 TYPE_NEWRAR : self.__extract_rar,
51 TYPE_ZIP : self.__extract_zip,
52 TYPE_NOEXTRACT : self.__extract_noextract }[self.type]
54 # Call the extraction function on each head
57 # Run in the head's directory
58 ret = extraction_func (h, os.path.dirname (h))
60 ret = extraction_func (h, todir)
62 logger.addMessage ('Extraction Function returned: %d' % ret, RarslaveLogger.MessageType.Debug)
66 logger.addMessage ('Failed extracting: %s' % h, RarslaveLogger.MessageType.Fatal)
71 def __extract_rar (self, file, todir):
72 assert os.path.isfile (file)
73 assert os.path.isdir (todir)
75 RAR_CMD = config.get_value ('commands', 'unrar')
77 cmd = '%s \"%s\"' % (RAR_CMD, file)
78 ret = run_command (cmd, todir)
86 def __extract_zip (self, file, todir):
87 ZIP_CMD = config.get_value ('commands', 'unzip')
89 cmd = ZIP_CMD % (file, todir)
90 ret = run_command (cmd)
98 def __extract_noextract (self, file, todir):
99 # Just move this file to the $todir, since no extraction is needed
100 # FIXME: NOTE: mv will fail by itself if you're moving to the same dir!
101 NOEXTRACT_CMD = config.get_value ('commands', 'noextract')
103 # Make sure that both files are not the same file. If they are, don't run at all.
104 if os.path.samefile (file, os.path.join (todir, file)):
107 cmd = NOEXTRACT_CMD % (file, todir)
108 ret = run_command (cmd)
118 class RarslaveRepairer (object):
119 # Verify (and repair) the set
120 # Make sure it worked, otherwise clean up and return failure
122 def __init__ (self, dir, file, join=False):
123 self.dir = dir # the directory containing the par2 file
124 self.file = file # the par2 file
125 self.join = join # True if the par2 set is 001 002 ...
127 assert os.path.isdir (dir)
128 assert os.path.isfile (os.path.join (dir, file))
130 def checkAndRepair (self):
132 # par2repair -- PAR2 PAR2_EXTRA [JOIN_FILES]
133 PAR2_CMD = config.get_value ('commands', 'par2repair')
136 basename = get_basename (self.file)
137 all_files = find_likely_files (basename, self.dir)
139 par2_files = find_par2_files (all_files)
141 # assemble the command
142 command = "%s \"%s\" " % (PAR2_CMD, self.file)
146 command += "\"%s\" " % os.path.split (f)[1]
150 if f not in par2_files:
151 command += "\"%s\" " % os.path.split (f)[1]
154 ret = run_command (command, self.dir)
158 logger.addMessage ('PAR2 Check / Repair failed: %s' % self.file, RarslaveLogger.MessageType.Fatal)
163 def run_command (cmd, indir=None):
164 # Runs the specified command-line in the directory given (or, in the current directory
165 # if none is given). It returns the status code given by the application.
170 assert os.path.isdir (indir) # MUST be a directory!
173 ret = os.system (cmd)
177 def full_abspath (p):
178 return os.path.abspath (os.path.expanduser (p))
180 def get_basename (name):
181 """Strips most kinds of endings from a filename"""
183 regex = config.get_value ('regular expressions', 'basename_regex')
184 r = re.compile (regex, re.IGNORECASE)
191 g = r.match (name).groups()
197 def find_likely_files (name, dir):
198 """Finds files which are likely to be part of the set corresponding
199 to $name in the directory $dir"""
201 if not os.path.isdir (os.path.abspath (dir)):
202 raise ValueError # bad directory given
204 dir = os.path.abspath (dir)
205 ename = re.escape (name)
206 regex = re.compile ('^%s.*$' % (ename, ))
208 return [f for f in os.listdir (dir) if regex.match (f)]
210 def find_par2_files (files):
211 """Find all par2 files in the list $files"""
213 PAR2_REGEX = config.get_value ('regular expressions', 'par2_regex')
214 regex = re.compile (PAR2_REGEX, re.IGNORECASE)
215 return [f for f in files if regex.match (f)]
217 def find_all_par2_files (dir):
218 """Finds all par2 files in a directory"""
219 # NOTE: does NOT return absolute paths
221 if not os.path.isdir (os.path.abspath (dir)):
222 raise ValueError # bad directory given
224 dir = os.path.abspath (dir)
225 files = os.listdir (dir)
227 return find_par2_files (files)
229 def has_extension (f, ext):
230 """Checks if f has the extension ext"""
235 ext = re.escape (ext)
236 regex = re.compile ('^.*%s$' % (ext, ), re.IGNORECASE)
237 return regex.match (f)
239 def find_extraction_heads (dir, files):
240 """Takes a list of possible files and finds likely heads of
243 # NOTE: perhaps this should happen AFTER repair is
244 # NOTE: successful. That way all files would already exist
246 # According to various sources online:
247 # 1) pre rar-3.0: .rar .r00 .r01 ...
248 # 2) post rar-3.0: .part01.rar .part02.rar
249 # 3) zip all ver: .zip
252 p2files = find_par2_files (files)
254 # Old RAR type, find all files ending in .rar
255 if is_oldrar (files):
256 extractor = RarslaveExtractor (TYPE_OLDRAR)
257 regex = re.compile ('^.*\.rar$', re.IGNORECASE)
260 extractor.addHead (dir, f)
262 if is_newrar (files):
263 extractor = RarslaveExtractor (TYPE_NEWRAR)
264 regex = re.compile ('^.*\.part01.rar$', re.IGNORECASE)
267 extractor.addHead (dir, f)
270 extractor = RarslaveExtractor (TYPE_ZIP)
271 regex = re.compile ('^.*\.zip$', re.IGNORECASE)
274 extractor.addHead (dir, f)
276 if is_noextract (files):
277 # Use the Par2 Parser (from cfv) here to find out what files are protected.
278 # Since these are not being extracted, they will be mv'd to another directory
280 extractor = RarslaveExtractor (TYPE_NOEXTRACT)
285 prot_files = par2parser.get_protected_files (dir, f)
287 except: #FIXME: add the actual exceptions
288 logger.addMessage ('Error parsing PAR2 file: %s', f)
296 extractor.addHead (dir, f)
298 logger.addMessage ('Error parsing all PAR2 files in this set ...')
300 # Make sure we found the type
301 if extractor == None:
302 logger.addMessage ('Not able to find an extractor for this type of set: %s' % p2files[0],
303 RarslaveLogger.MessageType.Fatal)
305 # No-heads here, but it's better than failing completely
306 extractor = RarslaveExtractor (TYPE_NOEXTRACT)
310 def is_oldrar (files):
312 if has_extension (f, '.r00'):
317 def is_newrar (files):
319 if has_extension (f, '.part01.rar'):
326 if has_extension (f, '.zip'):
331 def is_noextract (files):
332 # Type that needs no extraction.
333 # TODO: Add others ???
335 if has_extension (f, '.001'):
340 def find_deleteable_files (files):
341 # Deleteable types regex should come from the config
343 DELETE_REGEX = config.get_value ('regular expressions', 'delete_regex')
344 dregex = re.compile (DELETE_REGEX, re.IGNORECASE)
346 return [f for f in files if dregex.match (f)]
352 class PAR2Set (object):
358 def __init__ (self, dir, file):
359 assert os.path.isdir (dir)
360 assert os.path.isfile (os.path.join (dir, file))
365 basename = get_basename (file)
366 self.likely_files = find_likely_files (basename, dir)
368 def __list_eq (self, l1, l2):
370 if len(l1) != len(l2):
379 def __eq__ (self, rhs):
380 return self.__list_eq (self.likely_files, rhs.likely_files)
383 par2files = find_par2_files (self.likely_files)
384 par2head = par2files[0]
386 join = is_noextract (self.likely_files)
389 repairer = RarslaveRepairer (self.dir, par2head, join)
390 ret = repairer.checkAndRepair ()
393 logger.addMessage ('Repair stage failed for: %s' % par2head, RarslaveLogger.MessageType.Fatal)
397 EXTRACT_DIR = options.extract_dir
398 extractor = find_extraction_heads (self.dir, self.likely_files)
399 ret = extractor.extract (EXTRACT_DIR)
402 logger.addMessage ('Extraction stage failed for: %s' % par2head, RarslaveLogger.MessageType.Fatal)
406 DELETE_INTERACTIVE = options.interactive
407 deleteable_files = find_deleteable_files (self.likely_files)
408 ret = delete_list (self.dir, deleteable_files, DELETE_INTERACTIVE)
411 logger.addMessage ('Deletion stage failed for: %s' % par2head, RarslaveLogger.MessageType.Fatal)
414 logger.addMessage ('Successfully completed: %s' % par2head)
417 def delete_list (dir, files, interactive=False):
418 # Delete a list of files
420 assert os.path.isdir (dir)
423 valid_y = ['Y', 'YES']
424 valid_n = ['N', 'NO']
428 print 'Do you want to delete the following?:'
429 s = raw_input ('Delete [y/N]: ').upper()
431 if s in valid_y + valid_n:
438 os.remove (os.path.join (dir, f))
443 def generate_all_parsets (dir):
444 # Generate all parsets in the given directory.
446 assert os.path.isdir (dir) # Directory MUST be valid
449 p2files = find_all_par2_files (dir)
458 def check_required_progs():
459 """Check if the required programs are installed"""
461 shell_not_found = 32512
464 if run_command ('par2repair --help > /dev/null 2>&1') == shell_not_found:
465 needed.append ('par2repair')
467 if run_command ('unrar --help > /dev/null 2>&1') == shell_not_found:
468 needed.append ('unrar')
470 if run_command ('unzip --help > /dev/null 2>&1') == shell_not_found:
471 needed.append ('unzip')
475 print 'Needed program "%s" not found in $PATH' % (n, )
479 def run_options (options):
482 options.work_dir = full_abspath (options.work_dir)
484 # Make sure that the directory is valid
485 if not os.path.isdir (options.work_dir):
486 sys.stderr.write ('\"%s\" is not a valid directory. Use the \"-d\"\n' % options.work_dir)
487 sys.stderr.write ('option to override the working directory temporarily, or edit the\n')
488 sys.stderr.write ('configuration file to override the working directory permanently.\n')
491 if options.extract_dir != None:
492 options.extract_dir = full_abspath (options.extract_dir)
495 print PROGRAM + ' - ' + VERSION
497 print 'Copyright (c) 2005,2006 Ira W. Snyder (devel@irasnyder.com)'
499 print 'This program comes with ABSOLUTELY NO WARRANTY.'
500 print 'This is free software, and you are welcome to redistribute it'
501 print 'under certain conditions. See the file COPYING for details.'
504 if options.check_progs:
505 check_required_progs ()
507 if options.write_def_config:
508 config.write_config (default=True)
510 if options.write_config:
511 config.write_config ()
513 def find_loglevel (options):
515 loglevel = options.verbose - options.quiet
517 if loglevel < RarslaveLogger.MessageType.Fatal:
518 loglevel = RarslaveLogger.MessageType.Fatal
520 if loglevel > RarslaveLogger.MessageType.Debug:
521 loglevel = RarslaveLogger.MessageType.Debug
525 def printMessageTable (loglevel):
527 if logger.hasFatalMessages ():
528 print '\nFatal Messages\n' + '=' * 80
529 logger.printLoglevel (RarslaveLogger.MessageType.Fatal)
531 if loglevel == RarslaveLogger.MessageType.Fatal:
534 if logger.hasNormalMessages ():
535 print '\nNormal Messages\n' + '=' * 80
536 logger.printLoglevel (RarslaveLogger.MessageType.Normal)
538 if loglevel == RarslaveLogger.MessageType.Normal:
541 if logger.hasVerboseMessages ():
542 print '\nVerbose Messages\n' + '=' * 80
543 logger.printLoglevel (RarslaveLogger.MessageType.Verbose)
545 if loglevel == RarslaveLogger.MessageType.Verbose:
548 if logger.hasDebugMessages ():
549 print '\nDebug Messages\n' + '=' * 80
550 logger.printLoglevel (RarslaveLogger.MessageType.Debug)
556 # Build the OptionParser
557 parser = optparse.OptionParser()
558 parser.add_option('-n', '--not-recursive',
559 action='store_false', dest='recursive',
560 default=config.get_value('options', 'recursive'),
561 help="Don't run recursively")
563 parser.add_option('-d', '--work-dir',
564 dest='work_dir', type='string',
565 default=config.get_value('directories', 'working_directory'),
566 help="Start running at DIR", metavar='DIR')
568 parser.add_option('-e', '--extract-dir',
569 dest='extract_dir', type='string',
570 default=config.get_value('directories', 'extract_directory'),
571 help="Extract to DIR", metavar='DIR')
573 parser.add_option('-p', '--check-required-programs',
574 action='store_true', dest='check_progs',
576 help="Check for required programs")
578 parser.add_option('-f', '--write-default-config',
579 action='store_true', dest='write_def_config',
580 default=False, help="Write out a new default config")
582 parser.add_option('-c', '--write-new-config',
583 action='store_true', dest='write_config',
584 default=False, help="Write out the current config")
586 parser.add_option('-i', '--interactive', dest='interactive', action='store_true',
587 default=config.get_value('options', 'interactive'),
588 help="Confirm before removing files")
590 parser.add_option('-q', '--quiet', dest='quiet', action='count',
591 default=0, help="Output fatal messages only")
593 parser.add_option('-v', '--verbose', dest='verbose', action='count',
594 default=0, help="Output extra information")
596 parser.add_option('-V', '--version', dest='version', action='store_true',
597 default=False, help="Output version information")
599 parser.version = VERSION
601 # Parse the given options
603 (options, args) = parser.parse_args()
605 # Run any special actions that are needed on these options
606 run_options (options)
608 # Find the loglevel using the options given
609 loglevel = find_loglevel (options)
612 if options.recursive:
613 for (dir, subdirs, files) in os.walk (options.work_dir):
614 parsets = generate_all_parsets (dir)
620 parsets = generate_all_parsets (options.work_dir)
625 printMessageTable (loglevel)
630 if __name__ == '__main__':