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 ret = os.system (cmd)
462 def full_abspath (p):
463 return os.path.abspath (os.path.expanduser (p))
465 def find_par2_files (files):
466 """Find all par2 files in the list $files"""
468 PAR2_REGEX = config.get_value ('regular expressions', 'par2_regex')
469 regex = re.compile (PAR2_REGEX, re.IGNORECASE)
470 return [f for f in files if regex.match (f)]
472 def find_all_par2_files (dir):
473 """Finds all par2 files in a directory"""
474 # NOTE: does NOT return absolute paths
476 if not os.path.isdir (os.path.abspath (dir)):
477 raise ValueError # bad directory given
479 dir = os.path.abspath (dir)
480 files = os.listdir (dir)
482 return find_par2_files (files)
484 def no_duplicates (li):
485 """Removes all duplicates from a list"""
488 def generate_all_parsets (dir):
489 # Generate all parsets in the given directory.
491 assert os.path.isdir (dir) # Directory MUST be valid
494 p2files = find_all_par2_files (dir)
503 def check_required_progs():
504 """Check if the required programs are installed"""
506 shell_not_found = 32512
509 if run_command ('par2repair --help > /dev/null 2>&1') == shell_not_found:
510 needed.append ('par2repair')
512 if run_command ('unrar --help > /dev/null 2>&1') == shell_not_found:
513 needed.append ('unrar')
515 if run_command ('unzip --help > /dev/null 2>&1') == shell_not_found:
516 needed.append ('unzip')
520 print 'Needed program "%s" not found in $PATH' % (n, )
524 def run_options (options):
527 options.work_dir = full_abspath (options.work_dir)
529 # Make sure that the directory is valid
530 if not os.path.isdir (options.work_dir):
531 sys.stderr.write ('\"%s\" is not a valid directory. Use the \"-d\"\n' % options.work_dir)
532 sys.stderr.write ('option to override the working directory temporarily, or edit the\n')
533 sys.stderr.write ('configuration file to override the working directory permanently.\n')
536 if options.extract_dir != None:
537 options.extract_dir = full_abspath (options.extract_dir)
540 print PROGRAM + ' - ' + VERSION
542 print 'Copyright (c) 2005,2006 Ira W. Snyder (devel@irasnyder.com)'
544 print 'This program comes with ABSOLUTELY NO WARRANTY.'
545 print 'This is free software, and you are welcome to redistribute it'
546 print 'under certain conditions. See the file COPYING for details.'
549 if options.check_progs:
550 check_required_progs ()
552 if options.write_def_config:
553 config.write_config (default=True)
555 if options.write_config:
556 config.write_config ()
558 def find_loglevel (options):
560 loglevel = options.verbose - options.quiet
562 if loglevel < RarslaveLogger.MessageType.Fatal:
563 loglevel = RarslaveLogger.MessageType.Fatal
565 if loglevel > RarslaveLogger.MessageType.Debug:
566 loglevel = RarslaveLogger.MessageType.Debug
570 def printMessageTable (loglevel):
572 if logger.hasFatalMessages ():
573 print '\nFatal Messages\n' + '=' * 80
574 logger.printLoglevel (RarslaveLogger.MessageType.Fatal)
576 if loglevel == RarslaveLogger.MessageType.Fatal:
579 if logger.hasNormalMessages ():
580 print '\nNormal Messages\n' + '=' * 80
581 logger.printLoglevel (RarslaveLogger.MessageType.Normal)
583 if loglevel == RarslaveLogger.MessageType.Normal:
586 if logger.hasVerboseMessages ():
587 print '\nVerbose Messages\n' + '=' * 80
588 logger.printLoglevel (RarslaveLogger.MessageType.Verbose)
590 if loglevel == RarslaveLogger.MessageType.Verbose:
593 if logger.hasDebugMessages ():
594 print '\nDebug Messages\n' + '=' * 80
595 logger.printLoglevel (RarslaveLogger.MessageType.Debug)
601 # Build the OptionParser
602 parser = optparse.OptionParser()
603 parser.add_option('-n', '--not-recursive',
604 action='store_false', dest='recursive',
605 default=config.get_value('options', 'recursive'),
606 help="Don't run recursively")
608 parser.add_option('-d', '--work-dir',
609 dest='work_dir', type='string',
610 default=config.get_value('directories', 'working_directory'),
611 help="Start running at DIR", metavar='DIR')
613 parser.add_option('-e', '--extract-dir',
614 dest='extract_dir', type='string',
615 default=config.get_value('directories', 'extract_directory'),
616 help="Extract to DIR", metavar='DIR')
618 parser.add_option('-p', '--check-required-programs',
619 action='store_true', dest='check_progs',
621 help="Check for required programs")
623 parser.add_option('-f', '--write-default-config',
624 action='store_true', dest='write_def_config',
625 default=False, help="Write out a new default config")
627 parser.add_option('-c', '--write-new-config',
628 action='store_true', dest='write_config',
629 default=False, help="Write out the current config")
631 parser.add_option('-i', '--interactive', dest='interactive', action='store_true',
632 default=config.get_value('options', 'interactive'),
633 help="Confirm before removing files")
635 parser.add_option('-q', '--quiet', dest='quiet', action='count',
636 default=0, help="Output fatal messages only")
638 parser.add_option('-v', '--verbose', dest='verbose', action='count',
639 default=0, help="Output extra information")
641 parser.add_option('-V', '--version', dest='version', action='store_true',
642 default=False, help="Output version information")
644 parser.version = VERSION
646 # Parse the given options
648 (options, args) = parser.parse_args()
650 # Run any special actions that are needed on these options
651 run_options (options)
653 # Find the loglevel using the options given
654 loglevel = find_loglevel (options)
657 if options.recursive:
658 for (dir, subdirs, files) in os.walk (options.work_dir):
659 parsets = generate_all_parsets (dir)
665 parsets = generate_all_parsets (options.work_dir)
670 printMessageTable (loglevel)
675 if __name__ == '__main__':