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)
116 def run_command (cmd, indir=None):
117 # Runs the specified command-line in the directory given (or, in the current directory
118 # if none is given). It returns the status code given by the application.
123 assert os.path.isdir (indir) # MUST be a directory!
126 ret = os.system (cmd)
130 def full_abspath (p):
131 return os.path.abspath (os.path.expanduser (p))
133 def get_basename (name):
134 """Strips most kinds of endings from a filename"""
136 regex = config.get_value ('regular expressions', 'basename_regex')
137 r = re.compile (regex, re.IGNORECASE)
144 g = r.match (name).groups()
150 def find_par2_files (files):
151 """Find all par2 files in the list $files"""
153 PAR2_REGEX = config.get_value ('regular expressions', 'par2_regex')
154 regex = re.compile (PAR2_REGEX, re.IGNORECASE)
155 return [f for f in files if regex.match (f)]
157 def find_all_par2_files (dir):
158 """Finds all par2 files in a directory"""
159 # NOTE: does NOT return absolute paths
161 if not os.path.isdir (os.path.abspath (dir)):
162 raise ValueError # bad directory given
164 dir = os.path.abspath (dir)
165 files = os.listdir (dir)
167 return find_par2_files (files)
169 def find_extraction_heads (dir, files):
170 """Takes a list of possible files and finds likely heads of
173 # NOTE: perhaps this should happen AFTER repair is
174 # NOTE: successful. That way all files would already exist
176 # According to various sources online:
177 # 1) pre rar-3.0: .rar .r00 .r01 ...
178 # 2) post rar-3.0: .part01.rar .part02.rar
179 # 3) zip all ver: .zip
182 p2files = find_par2_files (files)
184 # Old RAR type, find all files ending in .rar
185 if is_oldrar (files):
186 extractor = RarslaveExtractor (TYPE_OLDRAR)
187 regex = re.compile ('^.*\.rar$', re.IGNORECASE)
190 extractor.addHead (dir, f)
192 if is_newrar (files):
193 extractor = RarslaveExtractor (TYPE_NEWRAR)
194 regex = re.compile ('^.*\.part0*1.rar$', re.IGNORECASE)
197 extractor.addHead (dir, f)
200 extractor = RarslaveExtractor (TYPE_ZIP)
201 regex = re.compile ('^.*\.zip$', re.IGNORECASE)
204 extractor.addHead (dir, f)
206 if is_noextract (files):
207 # Use the Par2 Parser (from cfv) here to find out what files are protected.
208 # Since these are not being extracted, they will be mv'd to another directory
210 extractor = RarslaveExtractor (TYPE_NOEXTRACT)
215 prot_files = Par2Parser.get_protected_files (dir, f)
217 except (EnvironmentError, OverflowError, OSError):
218 logger.addMessage ('Error parsing PAR2 file: %s', f)
226 extractor.addHead (dir, f)
228 logger.addMessage ('Error parsing all PAR2 files in this set ...')
230 # Make sure we found the type
231 if extractor == None:
232 logger.addMessage ('Not able to find an extractor for this type of set: %s' % p2files[0],
233 RarslaveLogger.MessageType.Verbose)
235 # No-heads here, but it's better than failing completely
236 extractor = RarslaveExtractor (TYPE_NOEXTRACT)
240 def generic_matcher (files, regex, nocase=False):
241 """Run the regex over the files, and see if one matches or not.
242 NOTE: this does not return the matches, just if a match occurred."""
245 cregex = re.compile (regex, re.IGNORECASE)
247 cregex = re.compile (regex)
255 def is_oldrar (files):
256 return generic_matcher (files, '^.*\.r00$')
258 def is_newrar (files):
259 return generic_matcher (files, '^.*\.part0*1\.rar$')
262 return generic_matcher (files, '^.*\.zip$')
264 def is_noextract (files):
265 # Type that needs no extraction.
266 # TODO: Add others ???
267 return generic_matcher (files, '^.*\.001$')
273 class PAR2Set (object):
276 p2file = None # The starting par2
277 basename = None # The p2file's basename
279 name_matched_files = [] # Files that match by basename of the p2file
280 prot_matched_files = [] # Files that match by being protected members
282 def __init__ (self, dir, p2file):
283 assert os.path.isdir (dir)
284 assert os.path.isfile (os.path.join (dir, p2file))
288 self.basename = get_basename (p2file)
290 # Find files that match by name only
291 self.name_matched_files = self.__find_name_matches (self.dir, self.basename)
293 # Find all par2 files for this set using name matches
294 self.all_p2files = find_par2_files (self.name_matched_files)
296 # Try to get the protected files for this set
297 self.prot_matched_files = self.__parse_all_par2 ()
299 def __list_eq (self, l1, l2):
301 if len(l1) != len(l2):
310 def __eq__ (self, rhs):
311 return (self.dir == rhs.dir) and (self.basename == rhs.basename) and \
312 self.__list_eq (self.name_matched_files, rhs.name_matched_files) and \
313 self.__list_eq (self.prot_matched_files, rhs.prot_matched_files)
315 def __parse_all_par2 (self):
316 """Searches though self.all_p2files and tries to parse at least one of them"""
320 for f in self.all_p2files:
322 # Exit early if we've found a good file
327 files = Par2Parser.get_protected_files (self.dir, f)
329 except (EnvironmentError, OSError, OverflowError):
330 logger.addMessage ('Corrupt PAR2 file: %s' % f, RarslaveLogger.MessageType.Fatal)
332 # Now that we're out of the loop, check if we really finished
334 logger.addMessage ('All PAR2 files corrupt for: %s' % self.p2file, RarslaveLogger.MessageType.Fatal)
336 # Return whatever we've got, empty or not
339 def __find_name_matches (self, dir, basename):
340 """Finds files which are likely to be part of the set corresponding
341 to $name in the directory $dir"""
343 assert os.path.isdir (dir)
345 ename = re.escape (basename)
346 regex = re.compile ('^%s.*$' % (ename, ))
348 return [f for f in os.listdir (dir) if regex.match (f)]
350 def __update_name_matches (self):
351 """Updates the self.name_matched_files variable with the most current information.
352 This should be called after the directory contents are likely to change."""
354 self.name_matched_files = self.__find_name_matches (self.dir, self.basename)
356 def runCheckAndRepair (self):
357 PAR2_CMD = config.get_value ('commands', 'par2repair')
360 all_files = self.name_matched_files + self.prot_matched_files
361 join = is_noextract (all_files)
363 # assemble the command
364 # par2repair -- PAR2 PAR2_EXTRA [JOIN_FILES]
365 command = "%s \"%s\" " % (PAR2_CMD, self.p2file)
367 for f in self.all_p2files:
369 command += "\"%s\" " % os.path.split (f)[1]
373 if f not in self.p2files:
374 command += "\"%s\" " % os.path.split (f)[1]
377 ret = run_command (command, self.dir)
381 logger.addMessage ('PAR2 Check / Repair failed: %s' % self.p2file, RarslaveLogger.MessageType.Fatal)
386 def __find_deleteable_files (self):
387 all_files = self.name_matched_files + self.prot_matched_files
388 DELETE_REGEX = config.get_value ('regular expressions', 'delete_regex')
389 dregex = re.compile (DELETE_REGEX, re.IGNORECASE)
391 dfiles = [f for f in all_files if dregex.match (f)]
392 dset = set(dfiles) # to eliminate dupes
395 def __delete_list_of_files (self, dir, files, interactive=False):
396 # Delete a list of files
398 assert os.path.isdir (dir)
401 valid_y = ['Y', 'YES']
402 valid_n = ['N', 'NO', '']
406 print 'Do you want to delete the following?:'
408 s = raw_input ('Delete [y/N]: ').upper()
410 if s in valid_y + valid_n:
418 os.remove (os.path.join (dir, f))
419 logger.addMessage ('Deleteing: %s' % os.path.join (dir, f), RarslaveLogger.MessageType.Debug)
421 logger.addMessage ('Failed to delete: %s' % os.path.join (dir, f),
422 RarslaveLogger.MessageType.Fatal)
427 def runDelete (self):
428 deleteable_files = self.__find_deleteable_files ()
429 ret = self.__delete_list_of_files (self.dir, deleteable_files, options.interactive)
434 all_files = self.name_matched_files + self.prot_matched_files
437 ret = self.runCheckAndRepair ()
440 logger.addMessage ('Repair stage failed for: %s' % self.p2file, RarslaveLogger.MessageType.Fatal)
443 self.__update_name_matches ()
444 all_files = self.name_matched_files + self.prot_matched_files
447 EXTRACT_DIR = options.extract_dir
448 extractor = find_extraction_heads (self.dir, all_files)
449 ret = extractor.extract (EXTRACT_DIR)
452 logger.addMessage ('Extraction stage failed for: %s' % self.p2file, RarslaveLogger.MessageType.Fatal)
455 self.__update_name_matches ()
456 all_files = self.name_matched_files + self.prot_matched_files
459 ret = self.runDelete ()
462 logger.addMessage ('Deletion stage failed for: %s' % self.p2file, RarslaveLogger.MessageType.Fatal)
465 logger.addMessage ('Successfully completed: %s' % self.p2file)
470 def generate_all_parsets (dir):
471 # Generate all parsets in the given directory.
473 assert os.path.isdir (dir) # Directory MUST be valid
476 p2files = find_all_par2_files (dir)
485 def check_required_progs():
486 """Check if the required programs are installed"""
488 shell_not_found = 32512
491 if run_command ('par2repair --help > /dev/null 2>&1') == shell_not_found:
492 needed.append ('par2repair')
494 if run_command ('unrar --help > /dev/null 2>&1') == shell_not_found:
495 needed.append ('unrar')
497 if run_command ('unzip --help > /dev/null 2>&1') == shell_not_found:
498 needed.append ('unzip')
502 print 'Needed program "%s" not found in $PATH' % (n, )
506 def run_options (options):
509 options.work_dir = full_abspath (options.work_dir)
511 # Make sure that the directory is valid
512 if not os.path.isdir (options.work_dir):
513 sys.stderr.write ('\"%s\" is not a valid directory. Use the \"-d\"\n' % options.work_dir)
514 sys.stderr.write ('option to override the working directory temporarily, or edit the\n')
515 sys.stderr.write ('configuration file to override the working directory permanently.\n')
518 if options.extract_dir != None:
519 options.extract_dir = full_abspath (options.extract_dir)
522 print PROGRAM + ' - ' + VERSION
524 print 'Copyright (c) 2005,2006 Ira W. Snyder (devel@irasnyder.com)'
526 print 'This program comes with ABSOLUTELY NO WARRANTY.'
527 print 'This is free software, and you are welcome to redistribute it'
528 print 'under certain conditions. See the file COPYING for details.'
531 if options.check_progs:
532 check_required_progs ()
534 if options.write_def_config:
535 config.write_config (default=True)
537 if options.write_config:
538 config.write_config ()
540 def find_loglevel (options):
542 loglevel = options.verbose - options.quiet
544 if loglevel < RarslaveLogger.MessageType.Fatal:
545 loglevel = RarslaveLogger.MessageType.Fatal
547 if loglevel > RarslaveLogger.MessageType.Debug:
548 loglevel = RarslaveLogger.MessageType.Debug
552 def printMessageTable (loglevel):
554 if logger.hasFatalMessages ():
555 print '\nFatal Messages\n' + '=' * 80
556 logger.printLoglevel (RarslaveLogger.MessageType.Fatal)
558 if loglevel == RarslaveLogger.MessageType.Fatal:
561 if logger.hasNormalMessages ():
562 print '\nNormal Messages\n' + '=' * 80
563 logger.printLoglevel (RarslaveLogger.MessageType.Normal)
565 if loglevel == RarslaveLogger.MessageType.Normal:
568 if logger.hasVerboseMessages ():
569 print '\nVerbose Messages\n' + '=' * 80
570 logger.printLoglevel (RarslaveLogger.MessageType.Verbose)
572 if loglevel == RarslaveLogger.MessageType.Verbose:
575 if logger.hasDebugMessages ():
576 print '\nDebug Messages\n' + '=' * 80
577 logger.printLoglevel (RarslaveLogger.MessageType.Debug)
583 # Build the OptionParser
584 parser = optparse.OptionParser()
585 parser.add_option('-n', '--not-recursive',
586 action='store_false', dest='recursive',
587 default=config.get_value('options', 'recursive'),
588 help="Don't run recursively")
590 parser.add_option('-d', '--work-dir',
591 dest='work_dir', type='string',
592 default=config.get_value('directories', 'working_directory'),
593 help="Start running at DIR", metavar='DIR')
595 parser.add_option('-e', '--extract-dir',
596 dest='extract_dir', type='string',
597 default=config.get_value('directories', 'extract_directory'),
598 help="Extract to DIR", metavar='DIR')
600 parser.add_option('-p', '--check-required-programs',
601 action='store_true', dest='check_progs',
603 help="Check for required programs")
605 parser.add_option('-f', '--write-default-config',
606 action='store_true', dest='write_def_config',
607 default=False, help="Write out a new default config")
609 parser.add_option('-c', '--write-new-config',
610 action='store_true', dest='write_config',
611 default=False, help="Write out the current config")
613 parser.add_option('-i', '--interactive', dest='interactive', action='store_true',
614 default=config.get_value('options', 'interactive'),
615 help="Confirm before removing files")
617 parser.add_option('-q', '--quiet', dest='quiet', action='count',
618 default=0, help="Output fatal messages only")
620 parser.add_option('-v', '--verbose', dest='verbose', action='count',
621 default=0, help="Output extra information")
623 parser.add_option('-V', '--version', dest='version', action='store_true',
624 default=False, help="Output version information")
626 parser.version = VERSION
628 # Parse the given options
630 (options, args) = parser.parse_args()
632 # Run any special actions that are needed on these options
633 run_options (options)
635 # Find the loglevel using the options given
636 loglevel = find_loglevel (options)
639 if options.recursive:
640 for (dir, subdirs, files) in os.walk (options.work_dir):
641 parsets = generate_all_parsets (dir)
647 parsets = generate_all_parsets (options.work_dir)
652 printMessageTable (loglevel)
657 if __name__ == '__main__':