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):
24 # ==========================================================================
25 # dir -- The directory in which this set lives
26 # p2files -- All PAR2 files in this set
27 # name_matched_files -- Files in this set, matched by name only
28 # prot_matched_files -- Files in this set, matched by parsing PAR2 files only
29 # type -- This set's type
30 # heads -- The heads to be extracted
32 def __init__ (self, dir, p2files, name_files, prot_files):
35 self.p2files = p2files
36 self.name_matched_files = name_files
37 self.prot_matched_files = prot_files
40 self.type = self.__find_type ()
42 logger.addMessage ('Detected set of type: %s' % self, RarslaveLogger.MessageType.Debug)
45 self.heads = self.__find_heads ()
48 logger.addMessage ('Adding extraction head: %s' % h, RarslaveLogger.MessageType.Debug)
52 { TYPE_OLDRAR : 'Old RAR',
53 TYPE_NEWRAR : 'New RAR',
55 TYPE_NOEXTRACT : 'No Extract',
56 TYPE_UNKNOWN : 'Unknown' } [self.type]
58 def __find_type (self):
60 all_files = no_duplicates (self.name_matched_files + self.prot_matched_files)
62 if self.is_oldrar (all_files):
64 elif self.is_newrar (all_files):
66 elif self.is_zip (all_files):
68 elif self.is_noextract (all_files):
73 def __generic_find_heads (self, regex, ignorecase=True):
78 cregex = re.compile (regex, re.IGNORECASE)
80 cregex = re.compile (regex)
82 all_files = no_duplicates (self.name_matched_files + self.prot_matched_files)
90 def __find_heads (self):
92 if self.type == TYPE_OLDRAR:
93 return self.__generic_find_heads ('^.*\.rar$')
94 elif self.type == TYPE_NEWRAR:
95 return self.__generic_find_heads ('^.*\.part0*1\.rar$')
96 elif self.type == TYPE_ZIP:
97 return self.__generic_find_heads ('^.*\.zip$')
98 elif self.type == TYPE_NOEXTRACT:
99 return self.prot_matched_files
103 def __create_directory (self, dir):
107 if os.path.isdir (dir):
112 logger.addMessage ('Created directory: %s' % dir, RarslaveLogger.MessageType.Verbose)
114 logger.addMessage ('FAILED to create directory: %s' % dir, RarslaveLogger.MessageType.Fatal)
119 def runExtract (self, todir=None):
120 # Extract all heads of this set
122 # Extract to the head's dir if we don't care where to extract
126 # Create the directory $todir if it doesn't exist
127 ret = self.__create_directory (todir)
134 { TYPE_OLDRAR : self.__extract_rar,
135 TYPE_NEWRAR : self.__extract_rar,
136 TYPE_ZIP : self.__extract_zip,
137 TYPE_NOEXTRACT : self.__extract_noextract,
138 TYPE_UNKNOWN : self.__extract_unknown }[self.type]
140 # Call the extraction function on each head
142 full_head = full_abspath (h)
143 ret = extraction_func (full_head, todir)
144 logger.addMessage ('Extraction Function returned: %d' % ret, RarslaveLogger.MessageType.Debug)
148 logger.addMessage ('Failed extracting: %s' % h, RarslaveLogger.MessageType.Fatal)
153 def __extract_rar (self, file, todir):
154 assert os.path.isfile (file)
155 assert os.path.isdir (todir)
157 RAR_CMD = config.get_value ('commands', 'unrar')
159 cmd = '%s \"%s\"' % (RAR_CMD, file)
160 ret = run_command (cmd, todir)
168 def __extract_zip (self, file, todir):
169 ZIP_CMD = config.get_value ('commands', 'unzip')
171 cmd = ZIP_CMD % (file, todir)
172 ret = run_command (cmd)
180 def __extract_noextract (self, file, todir):
181 # Just move this file to the $todir, since no extraction is needed
182 # FIXME: NOTE: mv will fail by itself if you're moving to the same dir!
183 NOEXTRACT_CMD = config.get_value ('commands', 'noextract')
185 # Make sure that both files are not the same file. If they are, don't run at all.
186 if os.path.samefile (file, os.path.join (todir, file)):
189 cmd = NOEXTRACT_CMD % (file, todir)
190 ret = run_command (cmd)
198 def __extract_unknown (self, file, todir):
201 def __generic_matcher (self, files, regex, nocase=False):
202 """Run the regex over the files, and see if one matches or not.
203 NOTE: this does not return the matches, just if a match occurred."""
206 cregex = re.compile (regex, re.IGNORECASE)
208 cregex = re.compile (regex)
216 def is_oldrar (self, files):
217 return self.__generic_matcher (files, '^.*\.r00$')
219 def is_newrar (self, files):
220 return self.__generic_matcher (files, '^.*\.part0*1\.rar$')
222 def is_zip (self, files):
223 return self.__generic_matcher (files, '^.*\.zip$')
225 def is_noextract (self, files):
226 # Type that needs no extraction.
227 # TODO: Add others ???
228 return self.__generic_matcher (files, '^.*\.001$')
230 class PAR2Set (object):
233 # ==========================================================================
234 # dir -- The directory this set lives in
235 # p2file -- The starting PAR2 file
236 # basename -- The basename of the set, guessed from the PAR2 file
237 # all_p2files -- All PAR2 files of the set, guessed from the PAR2 file name only
238 # name_matched_files -- Files in this set, guessed by name only
239 # prot_matched_files -- Files in this set, guessed by parsing the PAR2 only
241 def __init__ (self, dir, p2file):
242 assert os.path.isdir (dir)
243 assert os.path.isfile (os.path.join (dir, p2file))
247 self.basename = self.__get_basename (p2file)
249 # Find files that match by name only
250 self.name_matched_files = self.__find_name_matches (self.dir, self.basename)
252 # Find all par2 files for this set using name matches
253 self.all_p2files = find_par2_files (self.name_matched_files)
255 # Try to get the protected files for this set
256 self.prot_matched_files = self.__parse_all_par2 ()
258 def __list_eq (self, l1, l2):
260 if len(l1) != len(l2):
269 def __eq__ (self, rhs):
270 return (self.dir == rhs.dir) and (self.basename == rhs.basename) and \
271 self.__list_eq (self.name_matched_files, rhs.name_matched_files) and \
272 self.__list_eq (self.prot_matched_files, rhs.prot_matched_files)
274 def __get_basename (self, name):
275 """Strips most kinds of endings from a filename"""
277 regex = config.get_value ('regular expressions', 'basename_regex')
278 r = re.compile (regex, re.IGNORECASE)
285 g = r.match (name).groups()
291 def __parse_all_par2 (self):
292 """Searches though self.all_p2files and tries to parse at least one of them"""
296 for f in self.all_p2files:
298 # Exit early if we've found a good file
303 files = Par2Parser.get_protected_files (self.dir, f)
305 except (EnvironmentError, OSError, OverflowError):
306 logger.addMessage ('Corrupt PAR2 file: %s' % f, RarslaveLogger.MessageType.Fatal)
308 # Now that we're out of the loop, check if we really finished
310 logger.addMessage ('All PAR2 files corrupt for: %s' % self.p2file, RarslaveLogger.MessageType.Fatal)
312 # Return whatever we've got, empty or not
315 def __find_name_matches (self, dir, basename):
316 """Finds files which are likely to be part of the set corresponding
317 to $name in the directory $dir"""
319 assert os.path.isdir (dir)
321 ename = re.escape (basename)
322 regex = re.compile ('^%s.*$' % (ename, ))
324 return [f for f in os.listdir (dir) if regex.match (f)]
326 def __update_name_matches (self):
327 """Updates the self.name_matched_files variable with the most current information.
328 This should be called after the directory contents are likely to change."""
330 self.name_matched_files = self.__find_name_matches (self.dir, self.basename)
332 def __is_joinfile (self, filename):
333 regex = re.compile ('^.*\.\d\d\d$', re.IGNORECASE)
334 if regex.match (filename):
339 def __should_be_joined (self, files):
341 if self.__is_joinfile (f):
344 def runCheckAndRepair (self):
345 PAR2_CMD = config.get_value ('commands', 'par2repair')
348 all_files = no_duplicates (self.name_matched_files + self.prot_matched_files)
349 join = self.__should_be_joined (all_files)
351 # assemble the command
352 # par2repair -- PAR2 PAR2_EXTRA [JOIN_FILES]
353 command = "%s \"%s\" " % (PAR2_CMD, self.p2file)
355 for f in self.all_p2files:
357 command += "\"%s\" " % os.path.split (f)[1]
359 # Only needed when using par2 to join
362 if self.__is_joinfile (f):
363 command += "\"%s\" " % os.path.split (f)[1]
366 ret = run_command (command, self.dir)
370 logger.addMessage ('PAR2 Check / Repair failed: %s' % self.p2file, RarslaveLogger.MessageType.Fatal)
375 def __find_deleteable_files (self):
376 all_files = no_duplicates (self.name_matched_files + self.prot_matched_files)
377 DELETE_REGEX = config.get_value ('regular expressions', 'delete_regex')
378 dregex = re.compile (DELETE_REGEX, re.IGNORECASE)
380 return [f for f in all_files if dregex.match (f)]
382 def __delete_list_of_files (self, dir, files, interactive=False):
383 # Delete a list of files
385 assert os.path.isdir (dir)
388 valid_y = ['Y', 'YES']
389 valid_n = ['N', 'NO', '']
393 print 'Do you want to delete the following?:'
396 s = raw_input ('Delete [y/N]: ').upper()
398 if s in valid_y + valid_n:
406 os.remove (os.path.join (dir, f))
407 logger.addMessage ('Deleteing: %s' % os.path.join (dir, f), RarslaveLogger.MessageType.Debug)
409 logger.addMessage ('Failed to delete: %s' % os.path.join (dir, f),
410 RarslaveLogger.MessageType.Fatal)
415 def runDelete (self):
416 deleteable_files = self.__find_deleteable_files ()
417 ret = self.__delete_list_of_files (self.dir, deleteable_files, options.interactive)
422 all_files = no_duplicates (self.name_matched_files + self.prot_matched_files)
425 ret = self.runCheckAndRepair ()
428 logger.addMessage ('Repair stage failed for: %s' % self.p2file, RarslaveLogger.MessageType.Fatal)
431 self.__update_name_matches ()
432 all_files = no_duplicates (self.name_matched_files + self.prot_matched_files)
435 extractor = RarslaveExtractor (self.dir, self.all_p2files, \
436 self.name_matched_files, self.prot_matched_files)
437 ret = extractor.runExtract (options.extract_dir)
440 logger.addMessage ('Extraction stage failed for: %s' % self.p2file, RarslaveLogger.MessageType.Fatal)
443 self.__update_name_matches ()
444 all_files = no_duplicates (self.name_matched_files + self.prot_matched_files)
447 ret = self.runDelete ()
450 logger.addMessage ('Deletion stage failed for: %s' % self.p2file, RarslaveLogger.MessageType.Fatal)
453 logger.addMessage ('Successfully completed: %s' % self.p2file)
456 def run_command (cmd, indir=None):
457 # Runs the specified command-line in the directory given (or, in the current directory
458 # if none is given). It returns the status code given by the application.
463 assert os.path.isdir (indir) # MUST be a directory!
466 ret = os.system (cmd)
470 def full_abspath (p):
471 return os.path.abspath (os.path.expanduser (p))
473 def find_par2_files (files):
474 """Find all par2 files in the list $files"""
476 PAR2_REGEX = config.get_value ('regular expressions', 'par2_regex')
477 regex = re.compile (PAR2_REGEX, re.IGNORECASE)
478 return [f for f in files if regex.match (f)]
480 def find_all_par2_files (dir):
481 """Finds all par2 files in a directory"""
482 # NOTE: does NOT return absolute paths
484 if not os.path.isdir (os.path.abspath (dir)):
485 raise ValueError # bad directory given
487 dir = os.path.abspath (dir)
488 files = os.listdir (dir)
490 return find_par2_files (files)
492 def no_duplicates (li):
493 """Removes all duplicates from a list"""
496 def generate_all_parsets (dir):
497 # Generate all parsets in the given directory.
499 assert os.path.isdir (dir) # Directory MUST be valid
502 p2files = find_all_par2_files (dir)
511 def check_required_progs():
512 """Check if the required programs are installed"""
514 shell_not_found = 32512
517 if run_command ('par2repair --help > /dev/null 2>&1') == shell_not_found:
518 needed.append ('par2repair')
520 if run_command ('unrar --help > /dev/null 2>&1') == shell_not_found:
521 needed.append ('unrar')
523 if run_command ('unzip --help > /dev/null 2>&1') == shell_not_found:
524 needed.append ('unzip')
528 print 'Needed program "%s" not found in $PATH' % (n, )
532 def run_options (options):
535 options.work_dir = full_abspath (options.work_dir)
537 # Make sure that the directory is valid
538 if not os.path.isdir (options.work_dir):
539 sys.stderr.write ('\"%s\" is not a valid directory. Use the \"-d\"\n' % options.work_dir)
540 sys.stderr.write ('option to override the working directory temporarily, or edit the\n')
541 sys.stderr.write ('configuration file to override the working directory permanently.\n')
544 if options.extract_dir != None:
545 options.extract_dir = full_abspath (options.extract_dir)
548 print PROGRAM + ' - ' + VERSION
550 print 'Copyright (c) 2005,2006 Ira W. Snyder (devel@irasnyder.com)'
552 print 'This program comes with ABSOLUTELY NO WARRANTY.'
553 print 'This is free software, and you are welcome to redistribute it'
554 print 'under certain conditions. See the file COPYING for details.'
557 if options.check_progs:
558 check_required_progs ()
560 if options.write_def_config:
561 config.write_config (default=True)
563 if options.write_config:
564 config.write_config ()
566 def find_loglevel (options):
568 loglevel = options.verbose - options.quiet
570 if loglevel < RarslaveLogger.MessageType.Fatal:
571 loglevel = RarslaveLogger.MessageType.Fatal
573 if loglevel > RarslaveLogger.MessageType.Debug:
574 loglevel = RarslaveLogger.MessageType.Debug
578 def printMessageTable (loglevel):
580 if logger.hasFatalMessages ():
581 print '\nFatal Messages\n' + '=' * 80
582 logger.printLoglevel (RarslaveLogger.MessageType.Fatal)
584 if loglevel == RarslaveLogger.MessageType.Fatal:
587 if logger.hasNormalMessages ():
588 print '\nNormal Messages\n' + '=' * 80
589 logger.printLoglevel (RarslaveLogger.MessageType.Normal)
591 if loglevel == RarslaveLogger.MessageType.Normal:
594 if logger.hasVerboseMessages ():
595 print '\nVerbose Messages\n' + '=' * 80
596 logger.printLoglevel (RarslaveLogger.MessageType.Verbose)
598 if loglevel == RarslaveLogger.MessageType.Verbose:
601 if logger.hasDebugMessages ():
602 print '\nDebug Messages\n' + '=' * 80
603 logger.printLoglevel (RarslaveLogger.MessageType.Debug)
609 # Build the OptionParser
610 parser = optparse.OptionParser()
611 parser.add_option('-n', '--not-recursive',
612 action='store_false', dest='recursive',
613 default=config.get_value('options', 'recursive'),
614 help="Don't run recursively")
616 parser.add_option('-d', '--work-dir',
617 dest='work_dir', type='string',
618 default=config.get_value('directories', 'working_directory'),
619 help="Start running at DIR", metavar='DIR')
621 parser.add_option('-e', '--extract-dir',
622 dest='extract_dir', type='string',
623 default=config.get_value('directories', 'extract_directory'),
624 help="Extract to DIR", metavar='DIR')
626 parser.add_option('-p', '--check-required-programs',
627 action='store_true', dest='check_progs',
629 help="Check for required programs")
631 parser.add_option('-f', '--write-default-config',
632 action='store_true', dest='write_def_config',
633 default=False, help="Write out a new default config")
635 parser.add_option('-c', '--write-new-config',
636 action='store_true', dest='write_config',
637 default=False, help="Write out the current config")
639 parser.add_option('-i', '--interactive', dest='interactive', action='store_true',
640 default=config.get_value('options', 'interactive'),
641 help="Confirm before removing files")
643 parser.add_option('-q', '--quiet', dest='quiet', action='count',
644 default=0, help="Output fatal messages only")
646 parser.add_option('-v', '--verbose', dest='verbose', action='count',
647 default=0, help="Output extra information")
649 parser.add_option('-V', '--version', dest='version', action='store_true',
650 default=False, help="Output version information")
652 parser.version = VERSION
654 # Parse the given options
656 (options, args) = parser.parse_args()
658 # Run any special actions that are needed on these options
659 run_options (options)
661 # Find the loglevel using the options given
662 loglevel = find_loglevel (options)
665 if options.recursive:
666 for (dir, subdirs, files) in os.walk (options.work_dir):
667 parsets = generate_all_parsets (dir)
673 parsets = generate_all_parsets (options.work_dir)
678 printMessageTable (loglevel)
683 if __name__ == '__main__':