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 __is_joinfile (self, filename):
325 regex = re.compile ('^.*\.\d\d\d$', re.IGNORECASE)
326 if regex.match (filename):
331 def __should_be_joined (self, files):
333 if self.__is_joinfile (f):
336 def runCheckAndRepair (self):
337 PAR2_CMD = config.get_value ('commands', 'par2repair')
340 all_files = no_duplicates (self.name_matched_files + self.prot_matched_files)
341 join = self.__should_be_joined (all_files)
343 # assemble the command
344 # par2repair -- PAR2 PAR2_EXTRA [JOIN_FILES]
345 command = "%s \"%s\" " % (PAR2_CMD, self.p2file)
347 for f in self.all_p2files:
349 command += "\"%s\" " % os.path.split (f)[1]
351 # Only needed when using par2 to join
354 if self.__is_joinfile (f):
355 command += "\"%s\" " % os.path.split (f)[1]
358 ret = run_command (command, self.dir)
362 logger.addMessage ('PAR2 Check / Repair failed: %s' % self.p2file, RarslaveLogger.MessageType.Fatal)
367 def __find_deleteable_files (self):
368 all_files = no_duplicates (self.name_matched_files + self.prot_matched_files)
369 DELETE_REGEX = config.get_value ('regular expressions', 'delete_regex')
370 dregex = re.compile (DELETE_REGEX, re.IGNORECASE)
372 return [f for f in all_files if dregex.match (f)]
374 def __delete_list_of_files (self, dir, files, interactive=False):
375 # Delete a list of files
377 assert os.path.isdir (dir)
380 valid_y = ['Y', 'YES']
381 valid_n = ['N', 'NO', '']
385 print 'Do you want to delete the following?:'
388 s = raw_input ('Delete [y/N]: ').upper()
390 if s in valid_y + valid_n:
398 os.remove (os.path.join (dir, f))
399 logger.addMessage ('Deleteing: %s' % os.path.join (dir, f), RarslaveLogger.MessageType.Debug)
401 logger.addMessage ('Failed to delete: %s' % os.path.join (dir, f),
402 RarslaveLogger.MessageType.Fatal)
407 def runDelete (self):
408 deleteable_files = self.__find_deleteable_files ()
409 ret = self.__delete_list_of_files (self.dir, deleteable_files, options.interactive)
414 all_files = no_duplicates (self.name_matched_files + self.prot_matched_files)
417 ret = self.runCheckAndRepair ()
420 logger.addMessage ('Repair stage failed for: %s' % self.p2file, RarslaveLogger.MessageType.Fatal)
423 self.__update_name_matches ()
424 all_files = no_duplicates (self.name_matched_files + self.prot_matched_files)
427 extractor = RarslaveExtractor (self.dir, self.all_p2files, \
428 self.name_matched_files, self.prot_matched_files)
429 ret = extractor.runExtract (options.extract_dir)
432 logger.addMessage ('Extraction stage failed for: %s' % self.p2file, RarslaveLogger.MessageType.Fatal)
435 self.__update_name_matches ()
436 all_files = no_duplicates (self.name_matched_files + self.prot_matched_files)
439 ret = self.runDelete ()
442 logger.addMessage ('Deletion stage failed for: %s' % self.p2file, RarslaveLogger.MessageType.Fatal)
445 logger.addMessage ('Successfully completed: %s' % self.p2file)
448 def run_command (cmd, indir=None):
449 # Runs the specified command-line in the directory given (or, in the current directory
450 # if none is given). It returns the status code given by the application.
455 assert os.path.isdir (indir) # MUST be a directory!
458 print 'RUNNING (%s): %s' % (indir, cmd)
459 ret = os.system (cmd)
463 def full_abspath (p):
464 return os.path.abspath (os.path.expanduser (p))
466 def find_par2_files (files):
467 """Find all par2 files in the list $files"""
469 PAR2_REGEX = config.get_value ('regular expressions', 'par2_regex')
470 regex = re.compile (PAR2_REGEX, re.IGNORECASE)
471 return [f for f in files if regex.match (f)]
473 def find_all_par2_files (dir):
474 """Finds all par2 files in a directory"""
475 # NOTE: does NOT return absolute paths
477 if not os.path.isdir (os.path.abspath (dir)):
478 raise ValueError # bad directory given
480 dir = os.path.abspath (dir)
481 files = os.listdir (dir)
483 return find_par2_files (files)
485 def no_duplicates (li):
486 """Removes all duplicates from a list"""
489 def generate_all_parsets (dir):
490 # Generate all parsets in the given directory.
492 assert os.path.isdir (dir) # Directory MUST be valid
495 p2files = find_all_par2_files (dir)
504 def check_required_progs():
505 """Check if the required programs are installed"""
507 shell_not_found = 32512
510 if run_command ('par2repair --help > /dev/null 2>&1') == shell_not_found:
511 needed.append ('par2repair')
513 if run_command ('unrar --help > /dev/null 2>&1') == shell_not_found:
514 needed.append ('unrar')
516 if run_command ('unzip --help > /dev/null 2>&1') == shell_not_found:
517 needed.append ('unzip')
521 print 'Needed program "%s" not found in $PATH' % (n, )
525 def run_options (options):
528 options.work_dir = full_abspath (options.work_dir)
530 # Make sure that the directory is valid
531 if not os.path.isdir (options.work_dir):
532 sys.stderr.write ('\"%s\" is not a valid directory. Use the \"-d\"\n' % options.work_dir)
533 sys.stderr.write ('option to override the working directory temporarily, or edit the\n')
534 sys.stderr.write ('configuration file to override the working directory permanently.\n')
537 if options.extract_dir != None:
538 options.extract_dir = full_abspath (options.extract_dir)
541 print PROGRAM + ' - ' + VERSION
543 print 'Copyright (c) 2005,2006 Ira W. Snyder (devel@irasnyder.com)'
545 print 'This program comes with ABSOLUTELY NO WARRANTY.'
546 print 'This is free software, and you are welcome to redistribute it'
547 print 'under certain conditions. See the file COPYING for details.'
550 if options.check_progs:
551 check_required_progs ()
553 if options.write_def_config:
554 config.write_config (default=True)
556 if options.write_config:
557 config.write_config ()
559 def find_loglevel (options):
561 loglevel = options.verbose - options.quiet
563 if loglevel < RarslaveLogger.MessageType.Fatal:
564 loglevel = RarslaveLogger.MessageType.Fatal
566 if loglevel > RarslaveLogger.MessageType.Debug:
567 loglevel = RarslaveLogger.MessageType.Debug
571 def printMessageTable (loglevel):
573 if logger.hasFatalMessages ():
574 print '\nFatal Messages\n' + '=' * 80
575 logger.printLoglevel (RarslaveLogger.MessageType.Fatal)
577 if loglevel == RarslaveLogger.MessageType.Fatal:
580 if logger.hasNormalMessages ():
581 print '\nNormal Messages\n' + '=' * 80
582 logger.printLoglevel (RarslaveLogger.MessageType.Normal)
584 if loglevel == RarslaveLogger.MessageType.Normal:
587 if logger.hasVerboseMessages ():
588 print '\nVerbose Messages\n' + '=' * 80
589 logger.printLoglevel (RarslaveLogger.MessageType.Verbose)
591 if loglevel == RarslaveLogger.MessageType.Verbose:
594 if logger.hasDebugMessages ():
595 print '\nDebug Messages\n' + '=' * 80
596 logger.printLoglevel (RarslaveLogger.MessageType.Debug)
602 # Build the OptionParser
603 parser = optparse.OptionParser()
604 parser.add_option('-n', '--not-recursive',
605 action='store_false', dest='recursive',
606 default=config.get_value('options', 'recursive'),
607 help="Don't run recursively")
609 parser.add_option('-d', '--work-dir',
610 dest='work_dir', type='string',
611 default=config.get_value('directories', 'working_directory'),
612 help="Start running at DIR", metavar='DIR')
614 parser.add_option('-e', '--extract-dir',
615 dest='extract_dir', type='string',
616 default=config.get_value('directories', 'extract_directory'),
617 help="Extract to DIR", metavar='DIR')
619 parser.add_option('-p', '--check-required-programs',
620 action='store_true', dest='check_progs',
622 help="Check for required programs")
624 parser.add_option('-f', '--write-default-config',
625 action='store_true', dest='write_def_config',
626 default=False, help="Write out a new default config")
628 parser.add_option('-c', '--write-new-config',
629 action='store_true', dest='write_config',
630 default=False, help="Write out the current config")
632 parser.add_option('-i', '--interactive', dest='interactive', action='store_true',
633 default=config.get_value('options', 'interactive'),
634 help="Confirm before removing files")
636 parser.add_option('-q', '--quiet', dest='quiet', action='count',
637 default=0, help="Output fatal messages only")
639 parser.add_option('-v', '--verbose', dest='verbose', action='count',
640 default=0, help="Output extra information")
642 parser.add_option('-V', '--version', dest='version', action='store_true',
643 default=False, help="Output version information")
645 parser.version = VERSION
647 # Parse the given options
649 (options, args) = parser.parse_args()
651 # Run any special actions that are needed on these options
652 run_options (options)
654 # Find the loglevel using the options given
655 loglevel = find_loglevel (options)
658 if options.recursive:
659 for (dir, subdirs, files) in os.walk (options.work_dir):
660 parsets = generate_all_parsets (dir)
666 parsets = generate_all_parsets (options.work_dir)
671 printMessageTable (loglevel)
676 if __name__ == '__main__':