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, TYPE_UNKNOWN) = range (5)
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):
26 def __init__ (self, dir, p2files, name_files, prot_files):
29 self.p2files = p2files
30 self.name_matched_files = name_files
31 self.prot_matched_files = prot_files
34 self.type = self.__find_type ()
36 logger.addMessage ('Detected set of type: %s' % self, RarslaveLogger.MessageType.Debug)
39 self.heads = self.__find_heads ()
42 logger.addMessage ('Adding extraction head: %s' % h, RarslaveLogger.MessageType.Debug)
46 { TYPE_OLDRAR : 'Old RAR',
47 TYPE_NEWRAR : 'New RAR',
49 TYPE_NOEXTRACT : 'No Extract',
50 TYPE_UNKNOWN : 'Unknown' } [self.type]
52 def __find_type (self):
54 all_files = no_duplicates (self.name_matched_files + self.prot_matched_files)
56 if self.is_oldrar (all_files):
58 elif self.is_newrar (all_files):
60 elif self.is_zip (all_files):
62 elif self.is_noextract (all_files):
67 def __generic_find_heads (self, regex, ignorecase=True):
72 cregex = re.compile (regex, re.IGNORECASE)
74 cregex = re.compile (regex)
76 all_files = no_duplicates (self.name_matched_files + self.prot_matched_files)
84 def __find_heads (self):
86 if self.type == TYPE_OLDRAR:
87 return self.__generic_find_heads ('^.*\.rar$')
88 elif self.type == TYPE_NEWRAR:
89 return self.__generic_find_heads ('^.*\.part0*1\.rar$')
90 elif self.type == TYPE_ZIP:
91 return self.__generic_find_heads ('^.*\.zip$')
92 elif self.type == TYPE_NOEXTRACT:
93 return self.prot_matched_files
97 def __create_directory (self, dir):
101 if os.path.isdir (dir):
106 logger.addMessage ('Created directory: %s' % dir, RarslaveLogger.MessageType.Verbose)
108 logger.addMessage ('FAILED to create directory: %s' % dir, RarslaveLogger.MessageType.Fatal)
113 def runExtract (self, todir=None):
114 # Extract all heads of this set
116 # Extract to the head's dir if we don't care where to extract
120 # Create the directory $todir if it doesn't exist
121 ret = self.__create_directory (todir)
128 { TYPE_OLDRAR : self.__extract_rar,
129 TYPE_NEWRAR : self.__extract_rar,
130 TYPE_ZIP : self.__extract_zip,
131 TYPE_NOEXTRACT : self.__extract_noextract,
132 TYPE_UNKNOWN : self.__extract_unknown }[self.type]
134 # Call the extraction function on each head
136 full_head = full_abspath (h)
137 ret = extraction_func (full_head, todir)
138 logger.addMessage ('Extraction Function returned: %d' % ret, RarslaveLogger.MessageType.Debug)
142 logger.addMessage ('Failed extracting: %s' % h, RarslaveLogger.MessageType.Fatal)
147 def __extract_rar (self, file, todir):
148 assert os.path.isfile (file)
149 assert os.path.isdir (todir)
151 RAR_CMD = config.get_value ('commands', 'unrar')
153 cmd = '%s \"%s\"' % (RAR_CMD, file)
154 ret = run_command (cmd, todir)
162 def __extract_zip (self, file, todir):
163 ZIP_CMD = config.get_value ('commands', 'unzip')
165 cmd = ZIP_CMD % (file, todir)
166 ret = run_command (cmd)
174 def __extract_noextract (self, file, todir):
175 # Just move this file to the $todir, since no extraction is needed
176 # FIXME: NOTE: mv will fail by itself if you're moving to the same dir!
177 NOEXTRACT_CMD = config.get_value ('commands', 'noextract')
179 # Make sure that both files are not the same file. If they are, don't run at all.
180 if os.path.samefile (file, os.path.join (todir, file)):
183 cmd = NOEXTRACT_CMD % (file, todir)
184 ret = run_command (cmd)
192 def __extract_unknown (self, file, todir):
195 def __generic_matcher (self, files, regex, nocase=False):
196 """Run the regex over the files, and see if one matches or not.
197 NOTE: this does not return the matches, just if a match occurred."""
200 cregex = re.compile (regex, re.IGNORECASE)
202 cregex = re.compile (regex)
210 def is_oldrar (self, files):
211 return self.__generic_matcher (files, '^.*\.r00$')
213 def is_newrar (self, files):
214 return self.__generic_matcher (files, '^.*\.part0*1\.rar$')
216 def is_zip (self, files):
217 return self.__generic_matcher (files, '^.*\.zip$')
219 def is_noextract (self, files):
220 # Type that needs no extraction.
221 # TODO: Add others ???
222 return self.__generic_matcher (files, '^.*\.001$')
224 class PAR2Set (object):
227 p2file = None # The starting par2
228 basename = None # The p2file's basename
230 name_matched_files = [] # Files that match by basename of the p2file
231 prot_matched_files = [] # Files that match by being protected members
233 def __init__ (self, dir, p2file):
234 assert os.path.isdir (dir)
235 assert os.path.isfile (os.path.join (dir, p2file))
239 self.basename = self.__get_basename (p2file)
241 # Find files that match by name only
242 self.name_matched_files = self.__find_name_matches (self.dir, self.basename)
244 # Find all par2 files for this set using name matches
245 self.all_p2files = find_par2_files (self.name_matched_files)
247 # Try to get the protected files for this set
248 self.prot_matched_files = self.__parse_all_par2 ()
250 def __list_eq (self, l1, l2):
252 if len(l1) != len(l2):
261 def __eq__ (self, rhs):
262 return (self.dir == rhs.dir) and (self.basename == rhs.basename) and \
263 self.__list_eq (self.name_matched_files, rhs.name_matched_files) and \
264 self.__list_eq (self.prot_matched_files, rhs.prot_matched_files)
266 def __get_basename (self, name):
267 """Strips most kinds of endings from a filename"""
269 regex = config.get_value ('regular expressions', 'basename_regex')
270 r = re.compile (regex, re.IGNORECASE)
277 g = r.match (name).groups()
283 def __parse_all_par2 (self):
284 """Searches though self.all_p2files and tries to parse at least one of them"""
288 for f in self.all_p2files:
290 # Exit early if we've found a good file
295 files = Par2Parser.get_protected_files (self.dir, f)
297 except (EnvironmentError, OSError, OverflowError):
298 logger.addMessage ('Corrupt PAR2 file: %s' % f, RarslaveLogger.MessageType.Fatal)
300 # Now that we're out of the loop, check if we really finished
302 logger.addMessage ('All PAR2 files corrupt for: %s' % self.p2file, RarslaveLogger.MessageType.Fatal)
304 # Return whatever we've got, empty or not
307 def __find_name_matches (self, dir, basename):
308 """Finds files which are likely to be part of the set corresponding
309 to $name in the directory $dir"""
311 assert os.path.isdir (dir)
313 ename = re.escape (basename)
314 regex = re.compile ('^%s.*$' % (ename, ))
316 return [f for f in os.listdir (dir) if regex.match (f)]
318 def __update_name_matches (self):
319 """Updates the self.name_matched_files variable with the most current information.
320 This should be called after the directory contents are likely to change."""
322 self.name_matched_files = self.__find_name_matches (self.dir, self.basename)
324 def __should_be_joined (self, files):
325 regex = re.compile ('^.*\.001$', re.IGNORECASE)
330 def runCheckAndRepair (self):
331 PAR2_CMD = config.get_value ('commands', 'par2repair')
334 all_files = no_duplicates (self.name_matched_files + self.prot_matched_files)
335 join = self.__should_be_joined (all_files)
337 # assemble the command
338 # par2repair -- PAR2 PAR2_EXTRA [JOIN_FILES]
339 command = "%s \"%s\" " % (PAR2_CMD, self.p2file)
341 for f in self.all_p2files:
343 command += "\"%s\" " % os.path.split (f)[1]
347 if f not in self.p2files:
348 command += "\"%s\" " % os.path.split (f)[1]
351 ret = run_command (command, self.dir)
355 logger.addMessage ('PAR2 Check / Repair failed: %s' % self.p2file, RarslaveLogger.MessageType.Fatal)
360 def __find_deleteable_files (self):
361 all_files = no_duplicates (self.name_matched_files + self.prot_matched_files)
362 DELETE_REGEX = config.get_value ('regular expressions', 'delete_regex')
363 dregex = re.compile (DELETE_REGEX, re.IGNORECASE)
365 return [f for f in all_files if dregex.match (f)]
367 def __delete_list_of_files (self, dir, files, interactive=False):
368 # Delete a list of files
370 assert os.path.isdir (dir)
373 valid_y = ['Y', 'YES']
374 valid_n = ['N', 'NO', '']
378 print 'Do you want to delete the following?:'
381 s = raw_input ('Delete [y/N]: ').upper()
383 if s in valid_y + valid_n:
391 os.remove (os.path.join (dir, f))
392 logger.addMessage ('Deleteing: %s' % os.path.join (dir, f), RarslaveLogger.MessageType.Debug)
394 logger.addMessage ('Failed to delete: %s' % os.path.join (dir, f),
395 RarslaveLogger.MessageType.Fatal)
400 def runDelete (self):
401 deleteable_files = self.__find_deleteable_files ()
402 ret = self.__delete_list_of_files (self.dir, deleteable_files, options.interactive)
407 all_files = no_duplicates (self.name_matched_files + self.prot_matched_files)
410 ret = self.runCheckAndRepair ()
413 logger.addMessage ('Repair stage failed for: %s' % self.p2file, RarslaveLogger.MessageType.Fatal)
416 self.__update_name_matches ()
417 all_files = no_duplicates (self.name_matched_files + self.prot_matched_files)
420 extractor = RarslaveExtractor (self.dir, self.all_p2files, \
421 self.name_matched_files, self.prot_matched_files)
422 ret = extractor.runExtract (options.extract_dir)
425 logger.addMessage ('Extraction stage failed for: %s' % self.p2file, RarslaveLogger.MessageType.Fatal)
428 self.__update_name_matches ()
429 all_files = no_duplicates (self.name_matched_files + self.prot_matched_files)
432 ret = self.runDelete ()
435 logger.addMessage ('Deletion stage failed for: %s' % self.p2file, RarslaveLogger.MessageType.Fatal)
438 logger.addMessage ('Successfully completed: %s' % self.p2file)
441 def run_command (cmd, indir=None):
442 # Runs the specified command-line in the directory given (or, in the current directory
443 # if none is given). It returns the status code given by the application.
448 assert os.path.isdir (indir) # MUST be a directory!
451 print 'RUNNING (%s): %s' % (indir, cmd)
452 ret = os.system (cmd)
456 def full_abspath (p):
457 return os.path.abspath (os.path.expanduser (p))
459 def find_par2_files (files):
460 """Find all par2 files in the list $files"""
462 PAR2_REGEX = config.get_value ('regular expressions', 'par2_regex')
463 regex = re.compile (PAR2_REGEX, re.IGNORECASE)
464 return [f for f in files if regex.match (f)]
466 def find_all_par2_files (dir):
467 """Finds all par2 files in a directory"""
468 # NOTE: does NOT return absolute paths
470 if not os.path.isdir (os.path.abspath (dir)):
471 raise ValueError # bad directory given
473 dir = os.path.abspath (dir)
474 files = os.listdir (dir)
476 return find_par2_files (files)
478 def no_duplicates (li):
479 """Removes all duplicates from a list"""
482 def generate_all_parsets (dir):
483 # Generate all parsets in the given directory.
485 assert os.path.isdir (dir) # Directory MUST be valid
488 p2files = find_all_par2_files (dir)
497 def check_required_progs():
498 """Check if the required programs are installed"""
500 shell_not_found = 32512
503 if run_command ('par2repair --help > /dev/null 2>&1') == shell_not_found:
504 needed.append ('par2repair')
506 if run_command ('unrar --help > /dev/null 2>&1') == shell_not_found:
507 needed.append ('unrar')
509 if run_command ('unzip --help > /dev/null 2>&1') == shell_not_found:
510 needed.append ('unzip')
514 print 'Needed program "%s" not found in $PATH' % (n, )
518 def run_options (options):
521 options.work_dir = full_abspath (options.work_dir)
523 # Make sure that the directory is valid
524 if not os.path.isdir (options.work_dir):
525 sys.stderr.write ('\"%s\" is not a valid directory. Use the \"-d\"\n' % options.work_dir)
526 sys.stderr.write ('option to override the working directory temporarily, or edit the\n')
527 sys.stderr.write ('configuration file to override the working directory permanently.\n')
530 if options.extract_dir != None:
531 options.extract_dir = full_abspath (options.extract_dir)
534 print PROGRAM + ' - ' + VERSION
536 print 'Copyright (c) 2005,2006 Ira W. Snyder (devel@irasnyder.com)'
538 print 'This program comes with ABSOLUTELY NO WARRANTY.'
539 print 'This is free software, and you are welcome to redistribute it'
540 print 'under certain conditions. See the file COPYING for details.'
543 if options.check_progs:
544 check_required_progs ()
546 if options.write_def_config:
547 config.write_config (default=True)
549 if options.write_config:
550 config.write_config ()
552 def find_loglevel (options):
554 loglevel = options.verbose - options.quiet
556 if loglevel < RarslaveLogger.MessageType.Fatal:
557 loglevel = RarslaveLogger.MessageType.Fatal
559 if loglevel > RarslaveLogger.MessageType.Debug:
560 loglevel = RarslaveLogger.MessageType.Debug
564 def printMessageTable (loglevel):
566 if logger.hasFatalMessages ():
567 print '\nFatal Messages\n' + '=' * 80
568 logger.printLoglevel (RarslaveLogger.MessageType.Fatal)
570 if loglevel == RarslaveLogger.MessageType.Fatal:
573 if logger.hasNormalMessages ():
574 print '\nNormal Messages\n' + '=' * 80
575 logger.printLoglevel (RarslaveLogger.MessageType.Normal)
577 if loglevel == RarslaveLogger.MessageType.Normal:
580 if logger.hasVerboseMessages ():
581 print '\nVerbose Messages\n' + '=' * 80
582 logger.printLoglevel (RarslaveLogger.MessageType.Verbose)
584 if loglevel == RarslaveLogger.MessageType.Verbose:
587 if logger.hasDebugMessages ():
588 print '\nDebug Messages\n' + '=' * 80
589 logger.printLoglevel (RarslaveLogger.MessageType.Debug)
595 # Build the OptionParser
596 parser = optparse.OptionParser()
597 parser.add_option('-n', '--not-recursive',
598 action='store_false', dest='recursive',
599 default=config.get_value('options', 'recursive'),
600 help="Don't run recursively")
602 parser.add_option('-d', '--work-dir',
603 dest='work_dir', type='string',
604 default=config.get_value('directories', 'working_directory'),
605 help="Start running at DIR", metavar='DIR')
607 parser.add_option('-e', '--extract-dir',
608 dest='extract_dir', type='string',
609 default=config.get_value('directories', 'extract_directory'),
610 help="Extract to DIR", metavar='DIR')
612 parser.add_option('-p', '--check-required-programs',
613 action='store_true', dest='check_progs',
615 help="Check for required programs")
617 parser.add_option('-f', '--write-default-config',
618 action='store_true', dest='write_def_config',
619 default=False, help="Write out a new default config")
621 parser.add_option('-c', '--write-new-config',
622 action='store_true', dest='write_config',
623 default=False, help="Write out the current config")
625 parser.add_option('-i', '--interactive', dest='interactive', action='store_true',
626 default=config.get_value('options', 'interactive'),
627 help="Confirm before removing files")
629 parser.add_option('-q', '--quiet', dest='quiet', action='count',
630 default=0, help="Output fatal messages only")
632 parser.add_option('-v', '--verbose', dest='verbose', action='count',
633 default=0, help="Output extra information")
635 parser.add_option('-V', '--version', dest='version', action='store_true',
636 default=False, help="Output version information")
638 parser.version = VERSION
640 # Parse the given options
642 (options, args) = parser.parse_args()
644 # Run any special actions that are needed on these options
645 run_options (options)
647 # Find the loglevel using the options given
648 loglevel = find_loglevel (options)
651 if options.recursive:
652 for (dir, subdirs, files) in os.walk (options.work_dir):
653 parsets = generate_all_parsets (dir)
659 parsets = generate_all_parsets (options.work_dir)
664 printMessageTable (loglevel)
669 if __name__ == '__main__':